From 3d911d2dc98222c4b418f847522c0b9384fcda1b Mon Sep 17 00:00:00 2001 From: chihlasm Date: Fri, 20 Mar 2026 14:22:50 +0000 Subject: [PATCH] feat(dashboard): FlowPilot cockpit dashboard + sidebar redesign - Replace QuickStartPage with FlowPilot-centric dashboard - Add StartSessionInput with Guided/Chat mode toggle - Add PendingEscalations, ActiveFlowPilotSessions, PerformanceCards - Add KnowledgeBaseCards, TeamSummary, RecentFlowPilotSessions - Every number/card is a portal to its detail page - Restructure sidebar: Resolve/Knowledge/Insights sections - Remove redundant nav items (FlowPilot, Flow Editor, Flow Assist, etc.) - Wire prefill from dashboard input to FlowPilot intake - Update mobile nav to match new sidebar structure Co-Authored-By: Claude Opus 4.6 --- ...20-flowpilot-dashboard-sidebar-redesign.md | 1043 +++++++++++++++++ .../dashboard/ActiveFlowPilotSessions.tsx | 111 ++ .../dashboard/KnowledgeBaseCards.tsx | 53 + .../dashboard/PendingEscalations.tsx | 87 ++ .../components/dashboard/PerformanceCards.tsx | 95 ++ .../dashboard/RecentFlowPilotSessions.tsx | 85 ++ .../dashboard/StartSessionInput.tsx | 88 ++ .../src/components/dashboard/TeamSummary.tsx | 58 + .../components/flowpilot/FlowPilotIntake.tsx | 5 +- frontend/src/components/layout/AppLayout.tsx | 14 +- frontend/src/components/layout/Sidebar.tsx | 69 +- frontend/src/pages/FlowPilotSessionPage.tsx | 6 +- frontend/src/pages/QuickStartPage.tsx | 623 +--------- 13 files changed, 1704 insertions(+), 633 deletions(-) create mode 100644 docs/plans/2026-03-20-flowpilot-dashboard-sidebar-redesign.md create mode 100644 frontend/src/components/dashboard/ActiveFlowPilotSessions.tsx create mode 100644 frontend/src/components/dashboard/KnowledgeBaseCards.tsx create mode 100644 frontend/src/components/dashboard/PendingEscalations.tsx create mode 100644 frontend/src/components/dashboard/PerformanceCards.tsx create mode 100644 frontend/src/components/dashboard/RecentFlowPilotSessions.tsx create mode 100644 frontend/src/components/dashboard/StartSessionInput.tsx create mode 100644 frontend/src/components/dashboard/TeamSummary.tsx diff --git a/docs/plans/2026-03-20-flowpilot-dashboard-sidebar-redesign.md b/docs/plans/2026-03-20-flowpilot-dashboard-sidebar-redesign.md new file mode 100644 index 00000000..0cf64850 --- /dev/null +++ b/docs/plans/2026-03-20-flowpilot-dashboard-sidebar-redesign.md @@ -0,0 +1,1043 @@ +# FlowPilot Dashboard & Sidebar Redesign — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Transform the dashboard into a FlowPilot-centric cockpit where engineers land, see their active work, start new sessions, and navigate to deeper pages — with every number acting as a portal. + +**Architecture:** Replace `QuickStartPage` with a new `FlowPilotDashboardPage` composed of section cards that each call existing APIs (sidebar stats, AI sessions, escalation queue, flowpilot analytics). Restructure the sidebar from 3 sections (Resolve/Build/Insights) to a cleaner nav (Resolve/Knowledge/Insights) with stats moved to the dashboard. No backend changes needed. + +**Tech Stack:** React 19, TypeScript, Tailwind CSS v4, Zustand, Lucide React, existing API clients + +--- + +## Task 1: Create StartSessionInput Component + +**Files:** +- Create: `frontend/src/components/dashboard/StartSessionInput.tsx` + +**What it does:** A prominent text input with Guided | Chat mode toggle. Pressing Enter navigates to `/pilot` (Guided) or `/assistant` (Chat) with the problem text pre-filled via router state. + +**Step 1: Create the component** + +```tsx +// frontend/src/components/dashboard/StartSessionInput.tsx +import { useState, useRef, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { Sparkles, MessageCircle } from 'lucide-react' +import { cn } from '@/lib/utils' + +type SessionMode = 'guided' | 'chat' + +export function StartSessionInput() { + const [mode, setMode] = useState('guided') + const [value, setValue] = useState('') + const navigate = useNavigate() + const inputRef = useRef(null) + + // Auto-focus on mount + useEffect(() => { inputRef.current?.focus() }, []) + + const handleSubmit = () => { + const trimmed = value.trim() + if (!trimmed) return + + if (mode === 'guided') { + navigate('/pilot', { state: { prefill: trimmed } }) + } else { + navigate('/assistant', { state: { prefill: trimmed } }) + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleSubmit() + } + } + + return ( +
+
+
+ + setValue(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="What are you troubleshooting?" + className="w-full rounded-xl border border-border bg-background py-3.5 pl-11 pr-4 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20" + /> +
+
+
+ + +
+ + Press Enter to start + +
+
+
+ ) +} +``` + +**Step 2: Verify build** + +Run: `cd frontend && PATH="/home/michaelchihlas/.nvm/versions/node/v20.19.0/bin:$PATH" npm run build 2>&1 | tail -5` +Expected: Build succeeds (component isn't imported yet, just verifying no syntax errors via tree-shaking) + +**Step 3: Commit** + +```bash +git add frontend/src/components/dashboard/StartSessionInput.tsx +git commit -m "feat(dashboard): add StartSessionInput with guided/chat mode toggle" +``` + +--- + +## Task 2: Create PendingEscalations Component + +**Files:** +- Create: `frontend/src/components/dashboard/PendingEscalations.tsx` + +**What it does:** Shows unacknowledged escalations with "Pick up" buttons. Only renders if there are pending escalations. Team leads see all team escalations; engineers see their own. + +**Step 1: Create the component** + +```tsx +// frontend/src/components/dashboard/PendingEscalations.tsx +import { useState, useEffect } from 'react' +import { Link, useNavigate } from 'react-router-dom' +import { AlertTriangle } from 'lucide-react' +import { aiSessionsApi } from '@/api/aiSessions' +import type { AISessionSummary } from '@/types/ai-session' + +function timeAgo(dateStr: string): string { + const diffMs = Date.now() - new Date(dateStr).getTime() + const minutes = Math.floor(diffMs / 60000) + if (minutes < 1) return 'just now' + if (minutes < 60) return `${minutes}m ago` + const hours = Math.floor(minutes / 60) + if (hours < 24) return `${hours}h ago` + return `${Math.floor(hours / 24)}d ago` +} + +export function PendingEscalations() { + const [escalations, setEscalations] = useState([]) + const navigate = useNavigate() + + useEffect(() => { + aiSessionsApi.getEscalationQueue() + .then(setEscalations) + .catch(() => {}) + }, []) + + if (escalations.length === 0) return null + + return ( +
+
+
+ +

+ Pending Escalations + + {escalations.length} + +

+
+ + View all + +
+
+ {escalations.slice(0, 3).map((esc, i) => ( +
+ +
+
+ {esc.problem_summary || 'Escalated session'} +
+
+ {esc.problem_domain || 'General'} + · + {timeAgo(esc.created_at)} +
+
+ +
+ ))} +
+
+ ) +} +``` + +**Step 2: Commit** + +```bash +git add frontend/src/components/dashboard/PendingEscalations.tsx +git commit -m "feat(dashboard): add PendingEscalations component with pickup action" +``` + +--- + +## Task 3: Create ActiveFlowPilotSessions Component + +**Files:** +- Create: `frontend/src/components/dashboard/ActiveFlowPilotSessions.tsx` + +**What it does:** Shows the user's active FlowPilot (AI) sessions as cards. Click any card to resume at `/pilot/{id}`. + +**Step 1: Create the component** + +```tsx +// frontend/src/components/dashboard/ActiveFlowPilotSessions.tsx +import { useState, useEffect } from 'react' +import { Link, useNavigate } from 'react-router-dom' +import { Sparkles, Clock, ArrowRight } from 'lucide-react' +import { aiSessionsApi } from '@/api/aiSessions' +import type { AISessionSummary } from '@/types/ai-session' +import { cn } from '@/lib/utils' + +function timeAgo(dateStr: string): string { + const diffMs = Date.now() - new Date(dateStr).getTime() + const minutes = Math.floor(diffMs / 60000) + if (minutes < 1) return 'just now' + if (minutes < 60) return `${minutes}m ago` + const hours = Math.floor(minutes / 60) + if (hours < 24) return `${hours}h ago` + return `${Math.floor(hours / 24)}d ago` +} + +export function ActiveFlowPilotSessions() { + const [sessions, setSessions] = useState([]) + const [loading, setLoading] = useState(true) + const navigate = useNavigate() + + useEffect(() => { + aiSessionsApi.listSessions({ status: 'active', limit: 6 }) + .then(setSessions) + .catch(() => {}) + .finally(() => setLoading(false)) + }, []) + + if (loading) { + return ( +
+
+

Active Sessions

+
+
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ ))} +
+
+ ) + } + + return ( +
+
+
+

Active Sessions

+ {sessions.length > 0 && ( + + {sessions.length} + + )} +
+ + View all + +
+ + {sessions.length === 0 ? ( +
+

No active sessions

+

Start typing above to begin troubleshooting

+
+ ) : ( +
+ {sessions.map((session) => ( + + ))} +
+ )} +
+ ) +} +``` + +**Step 2: Commit** + +```bash +git add frontend/src/components/dashboard/ActiveFlowPilotSessions.tsx +git commit -m "feat(dashboard): add ActiveFlowPilotSessions card grid" +``` + +--- + +## Task 4: Create PerformanceCards Component + +**Files:** +- Create: `frontend/src/components/dashboard/PerformanceCards.tsx` + +**What it does:** 4 stat cards (Resolved Today, Avg MTTR, Success Rate, Escalated) — each clickable to navigate to analytics. + +**Step 1: Create the component** + +```tsx +// frontend/src/components/dashboard/PerformanceCards.tsx +import { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { CheckCircle, Clock, TrendingUp, AlertTriangle } from 'lucide-react' +import type { LucideIcon } from 'lucide-react' +import { sidebarApi } from '@/api' +import { cn } from '@/lib/utils' + +interface StatCard { + label: string + value: string | number + icon: LucideIcon + iconColor: string + href: string + highlight?: boolean +} + +export function PerformanceCards() { + const navigate = useNavigate() + const [resolved, setResolved] = useState(0) + const [active, setActive] = useState(0) + const [totalMinutes, setTotalMinutes] = useState(0) + + useEffect(() => { + sidebarApi.getStats() + .then((stats) => { + setResolved(stats.resolved_today) + setActive(stats.active_count) + setTotalMinutes(stats.total_session_minutes_today) + }) + .catch(() => {}) + }, []) + + const avgMttr = resolved > 0 ? Math.round(totalMinutes / resolved) : 0 + + const cards: StatCard[] = [ + { + label: 'Resolved Today', + value: resolved, + icon: CheckCircle, + iconColor: '#34d399', + href: '/analytics', + highlight: true, + }, + { + label: 'Avg Resolution', + value: avgMttr > 0 ? `${avgMttr}m` : '—', + icon: Clock, + iconColor: '#22d3ee', + href: '/analytics', + }, + { + label: 'Active Now', + value: active, + icon: TrendingUp, + iconColor: '#38bdf8', + href: '/sessions?filter=active', + }, + { + label: 'Session Time', + value: totalMinutes > 0 ? `${totalMinutes}m` : '—', + icon: AlertTriangle, + iconColor: '#fbbf24', + href: '/analytics', + }, + ] + + return ( +
+ {cards.map((card, i) => ( + + ))} +
+ ) +} +``` + +**Step 2: Commit** + +```bash +git add frontend/src/components/dashboard/PerformanceCards.tsx +git commit -m "feat(dashboard): add PerformanceCards with clickable stat cards" +``` + +--- + +## Task 5: Create KnowledgeBaseCards Component + +**Files:** +- Create: `frontend/src/components/dashboard/KnowledgeBaseCards.tsx` + +**What it does:** Shows counts (Flows, Scripts, Pending Review) — each clickable to navigate to the relevant page. + +**Step 1: Create the component** + +```tsx +// frontend/src/components/dashboard/KnowledgeBaseCards.tsx +import { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { Network, Code2, ListChecks, ArrowRight } from 'lucide-react' +import { sidebarApi } from '@/api' + +export function KnowledgeBaseCards() { + const navigate = useNavigate() + const [flowCount, setFlowCount] = useState(0) + + useEffect(() => { + sidebarApi.getStats() + .then((stats) => { + setFlowCount(stats.tree_counts.total) + }) + .catch(() => {}) + }, []) + + const items = [ + { label: 'Flows', value: flowCount, icon: Network, color: '#a78bfa', href: '/trees' }, + { label: 'Scripts', value: '—', icon: Code2, color: '#2dd4bf', href: '/scripts' }, + { label: 'Pending Review', value: '—', icon: ListChecks, color: '#fbbf24', href: '/review-queue' }, + ] + + return ( +
+
+

Knowledge Base

+ +
+
+ {items.map((item) => ( + + ))} +
+
+ ) +} +``` + +**Step 2: Commit** + +```bash +git add frontend/src/components/dashboard/KnowledgeBaseCards.tsx +git commit -m "feat(dashboard): add KnowledgeBaseCards with portal navigation" +``` + +--- + +## Task 6: Create TeamSummary Component + +**Files:** +- Create: `frontend/src/components/dashboard/TeamSummary.tsx` + +**What it does:** Admin/team lead only section showing team-wide metrics. Each number navigates to its detail page. + +**Step 1: Create the component** + +```tsx +// frontend/src/components/dashboard/TeamSummary.tsx +import { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { Users, AlertTriangle, Activity, ArrowRight } from 'lucide-react' +import { usePermissions } from '@/hooks/usePermissions' +import { aiSessionsApi } from '@/api/aiSessions' + +export function TeamSummary() { + const { isAccountOwner } = usePermissions() + const navigate = useNavigate() + const [escalationCount, setEscalationCount] = useState(0) + + useEffect(() => { + if (!isAccountOwner) return + aiSessionsApi.getEscalationQueue() + .then((esc) => setEscalationCount(esc.length)) + .catch(() => {}) + }, [isAccountOwner]) + + if (!isAccountOwner) return null + + const items = [ + { label: 'Escalations', value: escalationCount, icon: AlertTriangle, color: '#fbbf24', href: '/escalations' }, + { label: 'Team Activity', value: '—', icon: Activity, color: '#22d3ee', href: '/analytics' }, + { label: 'Members', value: '—', icon: Users, color: '#a78bfa', href: '/account' }, + ] + + return ( +
+
+

Team Summary

+ +
+
+ {items.map((item) => ( + + ))} +
+
+ ) +} +``` + +**Step 2: Commit** + +```bash +git add frontend/src/components/dashboard/TeamSummary.tsx +git commit -m "feat(dashboard): add TeamSummary component (admin/lead only)" +``` + +--- + +## Task 7: Create RecentFlowPilotSessions Component + +**Files:** +- Create: `frontend/src/components/dashboard/RecentFlowPilotSessions.tsx` + +**What it does:** Shows last 5 completed/escalated AI sessions with status indicators and links to history. + +**Step 1: Create the component** + +```tsx +// frontend/src/components/dashboard/RecentFlowPilotSessions.tsx +import { useState, useEffect } from 'react' +import { Link, useNavigate } from 'react-router-dom' +import { CheckCircle, AlertTriangle, XCircle, ArrowRight } from 'lucide-react' +import { aiSessionsApi } from '@/api/aiSessions' +import type { AISessionSummary } from '@/types/ai-session' + +function timeAgo(dateStr: string): string { + const diffMs = Date.now() - new Date(dateStr).getTime() + const minutes = Math.floor(diffMs / 60000) + if (minutes < 1) return 'just now' + if (minutes < 60) return `${minutes}m ago` + const hours = Math.floor(minutes / 60) + if (hours < 24) return `${hours}h ago` + const days = Math.floor(hours / 24) + if (days === 1) return 'yesterday' + return `${days}d ago` +} + +const STATUS_CONFIG: Record = { + resolved: { icon: CheckCircle, color: '#34d399', label: 'Resolved' }, + escalated: { icon: AlertTriangle, color: '#fbbf24', label: 'Escalated' }, + abandoned: { icon: XCircle, color: '#8891a0', label: 'Abandoned' }, +} + +export function RecentFlowPilotSessions() { + const [sessions, setSessions] = useState([]) + const navigate = useNavigate() + + useEffect(() => { + // Fetch recent completed sessions (resolved, escalated, abandoned) + Promise.all([ + aiSessionsApi.listSessions({ status: 'resolved', limit: 5 }).catch(() => []), + aiSessionsApi.listSessions({ status: 'escalated', limit: 3 }).catch(() => []), + ]).then(([resolved, escalated]) => { + const all = [...resolved, ...escalated] + .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) + .slice(0, 5) + setSessions(all) + }) + }, []) + + if (sessions.length === 0) return null + + return ( +
+
+

Recent Sessions

+ + History + +
+
+ {sessions.map((session, i) => { + const config = STATUS_CONFIG[session.status] || STATUS_CONFIG.abandoned + const StatusIcon = config.icon + return ( + + ) + })} +
+
+ ) +} +``` + +**Step 2: Commit** + +```bash +git add frontend/src/components/dashboard/RecentFlowPilotSessions.tsx +git commit -m "feat(dashboard): add RecentFlowPilotSessions with status indicators" +``` + +--- + +## Task 8: Assemble FlowPilotDashboardPage + +**Files:** +- Modify: `frontend/src/pages/QuickStartPage.tsx` (rewrite) + +**What it does:** Replace the current QuickStartPage content with the new FlowPilot cockpit layout, composing all the new section components. + +**Step 1: Rewrite QuickStartPage** + +Replace the entire file content with: + +```tsx +// frontend/src/pages/QuickStartPage.tsx +import { PageMeta } from '@/components/common/PageMeta' +import { useAuthStore } from '@/store/authStore' +import { StartSessionInput } from '@/components/dashboard/StartSessionInput' +import { PendingEscalations } from '@/components/dashboard/PendingEscalations' +import { ActiveFlowPilotSessions } from '@/components/dashboard/ActiveFlowPilotSessions' +import { PerformanceCards } from '@/components/dashboard/PerformanceCards' +import { KnowledgeBaseCards } from '@/components/dashboard/KnowledgeBaseCards' +import { TeamSummary } from '@/components/dashboard/TeamSummary' +import { RecentFlowPilotSessions } from '@/components/dashboard/RecentFlowPilotSessions' +import { OnboardingChecklist } from '@/components/dashboard/OnboardingChecklist' + +export function QuickStartPage() { + const user = useAuthStore((s) => s.user) + + return ( +
+ +
+ {/* Greeting */} +
+

+ Good{' '} + {new Date().getHours() < 12 + ? 'morning' + : new Date().getHours() < 18 + ? 'afternoon' + : 'evening'} + , {user?.name?.split(' ')[0] || 'there'} +

+

+ {new Date().toLocaleDateString('en-US', { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + })} +

+
+ + {/* Onboarding */} + + + {/* 1. Start Session Input */} +
+ +
+ + {/* 2. Pending Escalations (auto-hides if none) */} + + + {/* 3. Active Sessions */} + + + {/* 4. Performance Stats */} + + + {/* 5 + 6. Knowledge Base + Team Summary side by side on desktop */} +
+ + +
+ + {/* 7. Recent Sessions */} + +
+
+ ) +} + +export default QuickStartPage +``` + +**Step 2: Verify build** + +Run: `cd frontend && PATH="/home/michaelchihlas/.nvm/versions/node/v20.19.0/bin:$PATH" npm run build 2>&1 | tail -5` +Expected: Build succeeds + +**Step 3: Commit** + +```bash +git add frontend/src/pages/QuickStartPage.tsx +git commit -m "feat(dashboard): replace QuickStartPage with FlowPilot cockpit layout" +``` + +--- + +## Task 9: Wire Prefill into FlowPilotSessionPage and AssistantChatPage + +**Files:** +- Modify: `frontend/src/pages/FlowPilotSessionPage.tsx` — read `location.state.prefill` and pass to `FlowPilotIntake` +- Modify: `frontend/src/pages/AssistantChatPage.tsx` — read `location.state.prefill` and auto-send first message + +**Step 1: Update FlowPilotSessionPage** + +In `FlowPilotSessionPage.tsx`, add after the existing `useParams`/`useSearchParams`: + +```tsx +const location = useLocation() +const prefill = (location.state as { prefill?: string })?.prefill || '' +``` + +Pass `prefill` to `FlowPilotIntake` as a `defaultProblem` prop. In the `FlowPilotIntake` component, initialize the problem field with this value. + +**Step 2: Update AssistantChatPage** + +In `AssistantChatPage.tsx`, read `location.state.prefill`. If present and no existing chat is loaded, auto-populate the input field and optionally auto-send. + +**Step 3: Verify build** + +Run: `cd frontend && PATH="/home/michaelchihlas/.nvm/versions/node/v20.19.0/bin:$PATH" npm run build 2>&1 | tail -5` + +**Step 4: Commit** + +```bash +git add frontend/src/pages/FlowPilotSessionPage.tsx frontend/src/pages/AssistantChatPage.tsx +git commit -m "feat(dashboard): wire prefill from dashboard input to FlowPilot and Chat" +``` + +--- + +## Task 10: Restructure Sidebar Navigation + +**Files:** +- Modify: `frontend/src/components/layout/Sidebar.tsx` + +**What it does:** Replace the 3-section nav (Resolve/Build/Insights) with the new structure: Dashboard → Resolve (Active Sessions, History, Escalations) → Knowledge (Flows, Step Library, Scripts, Review Queue) → Insights (Analytics, FlowPilot Analytics). Remove SidebarStatsBar, SidebarActivityFeed, and "New Session" CTA. + +**Step 1: Update expanded sidebar nav** + +Replace lines 106-175 (the expanded mode content) with: + +```tsx +{/* Navigation — no stats bar, no activity feed, no New Session CTA */} +
+ {/* Dashboard */} + + + {/* Resolve */} +
+ Resolve +
+ + + + + {/* Knowledge */} +
+ Knowledge +
+ + + + + + {/* Insights */} +
+ Insights +
+ + + +
+``` + +**Step 2: Update collapsed sidebar** + +Update the collapsed icon list to match (remove New Session, FlowPilot, Flow Editor, Flow Assist, KB Accelerator). + +**Step 3: Remove unused imports** + +Remove `SidebarStatsBar` and `SidebarActivityFeed` imports (they're no longer rendered). + +**Step 4: Verify build** + +Run: `cd frontend && PATH="/home/michaelchihlas/.nvm/versions/node/v20.19.0/bin:$PATH" npm run build 2>&1 | tail -5` + +**Step 5: Commit** + +```bash +git add frontend/src/components/layout/Sidebar.tsx +git commit -m "refactor(sidebar): restructure nav to Resolve/Knowledge/Insights sections" +``` + +--- + +## Task 11: Update Mobile Nav in AppLayout + +**Files:** +- Modify: `frontend/src/components/layout/AppLayout.tsx` + +**What it does:** Update the mobile hamburger menu items to match the new sidebar structure. + +**Step 1: Update mobile nav items** + +Find the mobile nav items array and replace with: + +```tsx +const mobileNavItems = [ + { href: '/', label: 'Dashboard', icon: LayoutGrid }, + { href: '/sessions?filter=active', label: 'Active Sessions', icon: Clock }, + { href: '/sessions', label: 'History', icon: Clock }, + { href: '/escalations', label: 'Escalations', icon: AlertTriangle }, + { href: '/trees', label: 'Flows', icon: Network }, + { href: '/step-library', label: 'Step Library', icon: Library }, + { href: '/scripts', label: 'Scripts', icon: Code2 }, + { href: '/analytics', label: 'Analytics', icon: BarChart3 }, + { href: '/account', label: 'Account', icon: Settings }, +] +``` + +**Step 2: Verify build** + +Run: `cd frontend && PATH="/home/michaelchihlas/.nvm/versions/node/v20.19.0/bin:$PATH" npm run build 2>&1 | tail -5` + +**Step 3: Commit** + +```bash +git add frontend/src/components/layout/AppLayout.tsx +git commit -m "refactor(mobile): update mobile nav to match new sidebar structure" +``` + +--- + +## Task 12: Final Build Verification + Cleanup + +**Step 1: Full frontend build** + +Run: `cd frontend && PATH="/home/michaelchihlas/.nvm/versions/node/v20.19.0/bin:$PATH" npm run build` +Expected: Clean build with no errors + +**Step 2: Backend tests** + +Run: `cd backend && source venv/bin/activate && python -m pytest tests/ -x -q --override-ini="addopts=" 2>&1 | tail -10` +Expected: All tests pass (no backend changes, so this is a sanity check) + +**Step 3: Verify all routes still work** + +Manual spot-check that these routes still load: +- `/` — new dashboard +- `/pilot` — FlowPilot (still works) +- `/assistant` — Chat (still works) +- `/sessions` — Session history +- `/escalations` — Escalation queue +- `/trees` — Flow library +- `/analytics` — Team analytics +- `/my-trees` — Flow editor (deep link still works even though removed from nav) + +**Step 4: Commit and push** + +```bash +git add -A +git commit -m "chore: final build verification for dashboard/sidebar redesign" +git push +``` + +--- + +## Removed Items — Routes Preserved + +These pages are removed from nav but their routes are NOT deleted (deep links still work): +- `/assistant` — FlowPilot Chat (accessible via dashboard Chat mode) +- `/my-trees` — Flow Editor (accessible via Flows sub-navigation or direct link) +- `/flow-assist` — Flow Assist (accessible from within flow editor) +- `/kb-accelerator` — KB Accelerator (accessible from review queue) + +No routes are deleted from `router.tsx` — only sidebar nav links are reorganized. diff --git a/frontend/src/components/dashboard/ActiveFlowPilotSessions.tsx b/frontend/src/components/dashboard/ActiveFlowPilotSessions.tsx new file mode 100644 index 00000000..1a74c625 --- /dev/null +++ b/frontend/src/components/dashboard/ActiveFlowPilotSessions.tsx @@ -0,0 +1,111 @@ +import { useState, useEffect } from 'react' +import { Link, useNavigate } from 'react-router-dom' +import { Sparkles, Clock, ArrowRight } from 'lucide-react' +import { aiSessionsApi } from '@/api/aiSessions' +import type { AISessionSummary } from '@/types/ai-session' +import { cn } from '@/lib/utils' + +function timeAgo(dateStr: string): string { + const diffMs = Date.now() - new Date(dateStr).getTime() + const minutes = Math.floor(diffMs / 60000) + if (minutes < 1) return 'just now' + if (minutes < 60) return `${minutes}m ago` + const hours = Math.floor(minutes / 60) + if (hours < 24) return `${hours}h ago` + return `${Math.floor(hours / 24)}d ago` +} + +export function ActiveFlowPilotSessions() { + const [sessions, setSessions] = useState([]) + const [loading, setLoading] = useState(true) + const navigate = useNavigate() + + useEffect(() => { + aiSessionsApi.listSessions({ status: 'active', limit: 6 }) + .then(setSessions) + .catch(() => {}) + .finally(() => setLoading(false)) + }, []) + + if (loading) { + return ( +
+
+

Active Sessions

+
+
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ ))} +
+
+ ) + } + + return ( +
+
+
+

Active Sessions

+ {sessions.length > 0 && ( + + {sessions.length} + + )} +
+ + View all + +
+ + {sessions.length === 0 ? ( +
+

No active sessions

+

Start typing above to begin troubleshooting

+
+ ) : ( +
+ {sessions.map((session) => ( + + ))} +
+ )} +
+ ) +} diff --git a/frontend/src/components/dashboard/KnowledgeBaseCards.tsx b/frontend/src/components/dashboard/KnowledgeBaseCards.tsx new file mode 100644 index 00000000..303bd82d --- /dev/null +++ b/frontend/src/components/dashboard/KnowledgeBaseCards.tsx @@ -0,0 +1,53 @@ +import { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { Network, Code2, ListChecks, ArrowRight } from 'lucide-react' +import { sidebarApi } from '@/api' + +export function KnowledgeBaseCards() { + const navigate = useNavigate() + const [flowCount, setFlowCount] = useState(0) + + useEffect(() => { + sidebarApi.getStats() + .then((stats) => setFlowCount(stats.tree_counts.total)) + .catch(() => {}) + }, []) + + const items = [ + { label: 'Flows', value: flowCount, icon: Network, color: '#a78bfa', href: '/trees' }, + { label: 'Scripts', value: '\u2014', icon: Code2, color: '#2dd4bf', href: '/scripts' }, + { label: 'Pending Review', value: '\u2014', icon: ListChecks, color: '#fbbf24', href: '/review-queue' }, + ] + + return ( +
+
+

Knowledge Base

+ +
+
+ {items.map((item) => ( + + ))} +
+
+ ) +} diff --git a/frontend/src/components/dashboard/PendingEscalations.tsx b/frontend/src/components/dashboard/PendingEscalations.tsx new file mode 100644 index 00000000..f5f4fe07 --- /dev/null +++ b/frontend/src/components/dashboard/PendingEscalations.tsx @@ -0,0 +1,87 @@ +import { useState, useEffect } from 'react' +import { Link, useNavigate } from 'react-router-dom' +import { AlertTriangle } from 'lucide-react' +import { aiSessionsApi } from '@/api/aiSessions' +import type { AISessionSummary } from '@/types/ai-session' + +function timeAgo(dateStr: string): string { + const diffMs = Date.now() - new Date(dateStr).getTime() + const minutes = Math.floor(diffMs / 60000) + if (minutes < 1) return 'just now' + if (minutes < 60) return `${minutes}m ago` + const hours = Math.floor(minutes / 60) + if (hours < 24) return `${hours}h ago` + return `${Math.floor(hours / 24)}d ago` +} + +export function PendingEscalations() { + const [escalations, setEscalations] = useState([]) + const navigate = useNavigate() + + useEffect(() => { + aiSessionsApi.getEscalationQueue() + .then(setEscalations) + .catch(() => {}) + }, []) + + if (escalations.length === 0) return null + + return ( +
+
+
+ +

+ Pending Escalations + + {escalations.length} + +

+
+ + View all + +
+
+ {escalations.slice(0, 3).map((esc, i) => ( +
+ +
+
+ {esc.problem_summary || 'Escalated session'} +
+
+ {esc.problem_domain || 'General'} + · + {timeAgo(esc.created_at)} +
+
+ +
+ ))} +
+
+ ) +} diff --git a/frontend/src/components/dashboard/PerformanceCards.tsx b/frontend/src/components/dashboard/PerformanceCards.tsx new file mode 100644 index 00000000..2dd14549 --- /dev/null +++ b/frontend/src/components/dashboard/PerformanceCards.tsx @@ -0,0 +1,95 @@ +import { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { CheckCircle, Clock, TrendingUp, Timer } from 'lucide-react' +import type { LucideIcon } from 'lucide-react' +import { sidebarApi } from '@/api' +import { cn } from '@/lib/utils' + +interface StatCard { + label: string + value: string | number + icon: LucideIcon + iconColor: string + href: string + highlight?: boolean +} + +export function PerformanceCards() { + const navigate = useNavigate() + const [resolved, setResolved] = useState(0) + const [active, setActive] = useState(0) + const [totalMinutes, setTotalMinutes] = useState(0) + + useEffect(() => { + sidebarApi.getStats() + .then((stats) => { + setResolved(stats.resolved_today) + setActive(stats.active_count) + setTotalMinutes(stats.total_session_minutes_today) + }) + .catch(() => {}) + }, []) + + const avgMttr = resolved > 0 ? Math.round(totalMinutes / resolved) : 0 + + const cards: StatCard[] = [ + { + label: 'Resolved Today', + value: resolved, + icon: CheckCircle, + iconColor: '#34d399', + href: '/analytics', + highlight: true, + }, + { + label: 'Avg Resolution', + value: avgMttr > 0 ? `${avgMttr}m` : '\u2014', + icon: Clock, + iconColor: '#22d3ee', + href: '/analytics', + }, + { + label: 'Active Now', + value: active, + icon: TrendingUp, + iconColor: '#38bdf8', + href: '/sessions?filter=active', + }, + { + label: 'Session Time', + value: totalMinutes > 0 ? `${totalMinutes}m` : '\u2014', + icon: Timer, + iconColor: '#fbbf24', + href: '/analytics', + }, + ] + + return ( +
+ {cards.map((card, i) => ( + + ))} +
+ ) +} diff --git a/frontend/src/components/dashboard/RecentFlowPilotSessions.tsx b/frontend/src/components/dashboard/RecentFlowPilotSessions.tsx new file mode 100644 index 00000000..d757f84e --- /dev/null +++ b/frontend/src/components/dashboard/RecentFlowPilotSessions.tsx @@ -0,0 +1,85 @@ +import { useState, useEffect } from 'react' +import { Link, useNavigate } from 'react-router-dom' +import { CheckCircle, AlertTriangle, XCircle, ArrowRight } from 'lucide-react' +import { aiSessionsApi } from '@/api/aiSessions' +import type { AISessionSummary } from '@/types/ai-session' + +function timeAgo(dateStr: string): string { + const diffMs = Date.now() - new Date(dateStr).getTime() + const minutes = Math.floor(diffMs / 60000) + if (minutes < 1) return 'just now' + if (minutes < 60) return `${minutes}m ago` + const hours = Math.floor(minutes / 60) + if (hours < 24) return `${hours}h ago` + const days = Math.floor(hours / 24) + if (days === 1) return 'yesterday' + return `${days}d ago` +} + +const STATUS_CONFIG: Record = { + resolved: { icon: CheckCircle, color: '#34d399' }, + escalated: { icon: AlertTriangle, color: '#fbbf24' }, + abandoned: { icon: XCircle, color: '#8891a0' }, +} + +export function RecentFlowPilotSessions() { + const [sessions, setSessions] = useState([]) + const navigate = useNavigate() + + useEffect(() => { + Promise.all([ + aiSessionsApi.listSessions({ status: 'resolved', limit: 5 }).catch(() => []), + aiSessionsApi.listSessions({ status: 'escalated', limit: 3 }).catch(() => []), + ]).then(([resolved, escalated]) => { + const all = [...resolved, ...escalated] + .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) + .slice(0, 5) + setSessions(all) + }) + }, []) + + if (sessions.length === 0) return null + + return ( +
+
+

Recent Sessions

+ + History + +
+
+ {sessions.map((session, i) => { + const config = STATUS_CONFIG[session.status] || STATUS_CONFIG.abandoned + const StatusIcon = config.icon + return ( + + ) + })} +
+
+ ) +} diff --git a/frontend/src/components/dashboard/StartSessionInput.tsx b/frontend/src/components/dashboard/StartSessionInput.tsx new file mode 100644 index 00000000..d57f8b12 --- /dev/null +++ b/frontend/src/components/dashboard/StartSessionInput.tsx @@ -0,0 +1,88 @@ +import { useState, useRef, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { Sparkles, MessageCircle } from 'lucide-react' +import { cn } from '@/lib/utils' + +type SessionMode = 'guided' | 'chat' + +export function StartSessionInput() { + const [mode, setMode] = useState('guided') + const [value, setValue] = useState('') + const navigate = useNavigate() + const inputRef = useRef(null) + + useEffect(() => { inputRef.current?.focus() }, []) + + const handleSubmit = () => { + const trimmed = value.trim() + if (!trimmed) return + + if (mode === 'guided') { + navigate('/pilot', { state: { prefill: trimmed } }) + } else { + navigate('/assistant', { state: { prefill: trimmed } }) + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleSubmit() + } + } + + return ( +
+
+
+ + setValue(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="What are you troubleshooting?" + className="w-full rounded-xl border border-border bg-background py-3.5 pl-11 pr-4 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20" + /> +
+
+
+ + +
+ + Press Enter to start + +
+
+
+ ) +} diff --git a/frontend/src/components/dashboard/TeamSummary.tsx b/frontend/src/components/dashboard/TeamSummary.tsx new file mode 100644 index 00000000..a98c814c --- /dev/null +++ b/frontend/src/components/dashboard/TeamSummary.tsx @@ -0,0 +1,58 @@ +import { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { Users, AlertTriangle, Activity, ArrowRight } from 'lucide-react' +import { usePermissions } from '@/hooks/usePermissions' +import { aiSessionsApi } from '@/api/aiSessions' + +export function TeamSummary() { + const { isAccountOwner } = usePermissions() + const navigate = useNavigate() + const [escalationCount, setEscalationCount] = useState(0) + + useEffect(() => { + if (!isAccountOwner) return + aiSessionsApi.getEscalationQueue() + .then((esc) => setEscalationCount(esc.length)) + .catch(() => {}) + }, [isAccountOwner]) + + if (!isAccountOwner) return null + + const items = [ + { label: 'Escalations', value: escalationCount, icon: AlertTriangle, color: '#fbbf24', href: '/escalations' }, + { label: 'Team Activity', value: '\u2014', icon: Activity, color: '#22d3ee', href: '/analytics' }, + { label: 'Members', value: '\u2014', icon: Users, color: '#a78bfa', href: '/account' }, + ] + + return ( +
+
+

Team Summary

+ +
+
+ {items.map((item) => ( + + ))} +
+
+ ) +} diff --git a/frontend/src/components/flowpilot/FlowPilotIntake.tsx b/frontend/src/components/flowpilot/FlowPilotIntake.tsx index c263f52b..cfad60ac 100644 --- a/frontend/src/components/flowpilot/FlowPilotIntake.tsx +++ b/frontend/src/components/flowpilot/FlowPilotIntake.tsx @@ -10,10 +10,11 @@ import type { FileUploadResponse } from '@/types/upload' interface FlowPilotIntakeProps { onSubmit: (request: AISessionCreateRequest) => void isLoading: boolean + defaultProblem?: string } -export function FlowPilotIntake({ onSubmit, isLoading }: FlowPilotIntakeProps) { - const [text, setText] = useState('') +export function FlowPilotIntake({ onSubmit, isLoading, defaultProblem }: FlowPilotIntakeProps) { + const [text, setText] = useState(defaultProblem || '') const [showLogs, setShowLogs] = useState(false) const [logContent, setLogContent] = useState('') const [showTicketPicker, setShowTicketPicker] = useState(false) diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/layout/AppLayout.tsx index dab0596a..dd813cf9 100644 --- a/frontend/src/components/layout/AppLayout.tsx +++ b/frontend/src/components/layout/AppLayout.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, useCallback } from 'react' import { useLocation, useNavigate, Link } from 'react-router-dom' -import { Menu, X, LayoutGrid, Box, PenLine, Clock, FileText, Bookmark, Settings, LogOut, Shield, Terminal } from 'lucide-react' +import { Menu, X, LayoutGrid, Clock, Network, AlertTriangle, Code2, BarChart3, Settings, LogOut, Shield, Library } from 'lucide-react' import { useAuthStore } from '@/store/authStore' import { usePermissions } from '@/hooks/usePermissions' import { useUserPreferencesStore } from '@/store/userPreferencesStore' @@ -52,12 +52,12 @@ export function AppLayout() { const mobileNavItems = [ { path: '/', label: 'Dashboard', icon: LayoutGrid }, - { path: '/trees', label: 'All Flows', icon: Box }, - { path: '/my-trees', label: 'My Flows', icon: PenLine }, - { path: '/sessions', label: 'Sessions', icon: Clock }, - { path: '/shares', label: 'Exports', icon: FileText }, - { path: '/step-library', label: 'Step Library', icon: Bookmark }, - { path: '/scripts', label: 'Script Library', icon: Terminal }, + { path: '/sessions', label: 'Active Sessions', icon: Clock }, + { path: '/escalations', label: 'Escalations', icon: AlertTriangle }, + { path: '/trees', label: 'Flows', icon: Network }, + { path: '/step-library', label: 'Step Library', icon: Library }, + { path: '/scripts', label: 'Scripts', icon: Code2 }, + { path: '/analytics', label: 'Analytics', icon: BarChart3 }, { path: '/account', label: 'Account', icon: Settings }, ] diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 3ba63123..674bf13c 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -1,30 +1,24 @@ import { useCallback, useEffect, useState } from 'react' import { useLocation } from 'react-router-dom' import { - LayoutGrid, Network, Wrench, Clock, FileOutput, BarChart3, TrendingUp, + LayoutGrid, Network, Clock, FileOutput, BarChart3, TrendingUp, Settings, PanelLeftClose, PanelLeftOpen, MessageSquareText, ListChecks, - BookOpen, Lightbulb, Code2, Library, Brain, WandSparkles, Sparkles, AlertTriangle, + BookOpen, Code2, Library, AlertTriangle, } from 'lucide-react' import { cn } from '@/lib/utils' import { useUserPreferencesStore } from '@/store/userPreferencesStore' import { sidebarApi } from '@/api' import type { SidebarStatsResponse } from '@/api/sidebar' -import { SidebarStatsBar } from '@/components/sidebar/SidebarStatsBar' -import { SidebarActivityFeed } from '@/components/sidebar/SidebarActivityFeed' import { NavItem } from './NavItem' // Semantic icon colors — each nav item gets a unique color for visual landmarks const NAV_COLORS = { dashboard: '#22d3ee', // cyan-400 flows: '#a78bfa', // violet-400 - editor: '#f59e0b', // amber-500 sessions: '#34d399', // emerald-400 exports: '#60a5fa', // blue-400 - flowPilot: '#e879f9', // fuchsia-400 - flowAssist:'#f472b6', // pink-400 stepLib: '#fb923c', // orange-400 scripts: '#2dd4bf', // teal-400 - kb: '#fb7185', // rose-400 analytics: '#38bdf8', // sky-400 guides: '#a3e635', // lime-400 feedback: '#818cf8', // indigo-400 @@ -83,17 +77,12 @@ export function Sidebar() { <> {/* Collapsed: icon-only nav */}
- - + - - - - - + - + @@ -104,65 +93,37 @@ export function Sidebar() { ) : ( <> - {/* Stats Bar */} - s.started_at) ?? []} - /> - - {/* Activity Feed */} - - -
- - {/* New Session CTA */} -
- -
- -
- {/* Navigation */}
- {/* Dashboard (standalone) */} + {/* Dashboard */} {/* Resolve */}
Resolve
- + + + {/* Knowledge */} +
+ Knowledge +
- - - - {/* Build */} -
- Build -
- - - + {/* Insights */} diff --git a/frontend/src/pages/FlowPilotSessionPage.tsx b/frontend/src/pages/FlowPilotSessionPage.tsx index 1e138719..469ddacf 100644 --- a/frontend/src/pages/FlowPilotSessionPage.tsx +++ b/frontend/src/pages/FlowPilotSessionPage.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { useParams, useSearchParams } from 'react-router-dom' +import { useParams, useSearchParams, useLocation } from 'react-router-dom' import { Sparkles, Loader2 } from 'lucide-react' import { useFlowPilotSession } from '@/hooks/useFlowPilotSession' import { FlowPilotIntake, FlowPilotSession, SessionBriefing } from '@/components/flowpilot' @@ -9,6 +9,8 @@ import { toast } from '@/lib/toast' export default function FlowPilotSessionPage() { const { sessionId } = useParams<{ sessionId?: string }>() const [searchParams, setSearchParams] = useSearchParams() + const location = useLocation() + const prefill = (location.state as { prefill?: string } | null)?.prefill || '' const isPickup = searchParams.get('pickup') === 'true' const fp = useFlowPilotSession() const [pickingUp, setPickingUp] = useState(false) @@ -84,7 +86,7 @@ export default function FlowPilotSessionPage() { if (!fp.session) { return (
- +
) } diff --git a/frontend/src/pages/QuickStartPage.tsx b/frontend/src/pages/QuickStartPage.tsx index 26278446..9232a922 100644 --- a/frontend/src/pages/QuickStartPage.tsx +++ b/frontend/src/pages/QuickStartPage.tsx @@ -1,581 +1,68 @@ -import { useState, useEffect, useRef, useCallback } from 'react' -import { useNavigate } from 'react-router-dom' -import { Search, Loader2, ChevronLeft, ChevronRight, GitBranch } from 'lucide-react' import { PageMeta } from '@/components/common/PageMeta' -import { treesApi } from '@/api/trees' -import { sessionsApi } from '@/api/sessions' -import type { TreeListItem, TreeFilters } from '@/types' -import type { Session } from '@/types/session' -import { getTreeNavigatePath } from '@/lib/routing' -import { usePermissions } from '@/hooks/usePermissions' import { useAuthStore } from '@/store/authStore' -import { useUserPreferencesStore } from '@/store/userPreferencesStore' -import { usePaginationParams } from '@/hooks/usePaginationParams' -import { useCachedQuota } from '@/hooks/useCachedQuota' -// QuickStats and SessionsPanel replaced by new dashboard panels -import { TreeGridView } from '@/components/library/TreeGridView' -import { TreeListView } from '@/components/library/TreeListView' -import { TreeTableView } from '@/components/library/TreeTableView' -import { ViewToggle } from '@/components/library/ViewToggle' - -import { CreateFlowDropdown } from '@/components/common/CreateFlowDropdown' -import { cn } from '@/lib/utils' -import { toast } from '@/lib/toast' -import { WeeklyCalendar } from '@/components/dashboard/WeeklyCalendar' -import { QuickActions } from '@/components/dashboard/QuickActions' -import { OpenSessions } from '@/components/dashboard/OpenSessions' -import { RecentActivity } from '@/components/dashboard/RecentActivity' -import { PreparedSessions } from '@/components/dashboard/PreparedSessions' +import { StartSessionInput } from '@/components/dashboard/StartSessionInput' +import { PendingEscalations } from '@/components/dashboard/PendingEscalations' +import { ActiveFlowPilotSessions } from '@/components/dashboard/ActiveFlowPilotSessions' +import { PerformanceCards } from '@/components/dashboard/PerformanceCards' +import { KnowledgeBaseCards } from '@/components/dashboard/KnowledgeBaseCards' +import { TeamSummary } from '@/components/dashboard/TeamSummary' +import { RecentFlowPilotSessions } from '@/components/dashboard/RecentFlowPilotSessions' import { OnboardingChecklist } from '@/components/dashboard/OnboardingChecklist' -function timeAgo(dateStr: string): string { - const now = Date.now() - const then = new Date(dateStr).getTime() - const diffMs = now - then - const minutes = Math.floor(diffMs / 60000) - if (minutes < 1) return 'just now' - if (minutes < 60) return `${minutes}m ago` - const hours = Math.floor(minutes / 60) - if (hours < 24) return `${hours}h ago` - const days = Math.floor(hours / 24) - if (days === 1) return 'Yesterday' - return `${days}d ago` -} - export function QuickStartPage() { - const navigate = useNavigate() - const { canCreateTrees } = usePermissions() const user = useAuthStore((s) => s.user) - // Search state - const [query, setQuery] = useState('') - const [searchResults, setSearchResults] = useState([]) - const [isSearching, setIsSearching] = useState(false) - const [showResults, setShowResults] = useState(false) - const searchRef = useRef(null) - const debounceRef = useRef | null>(null) - - // Sessions state - const [activeSessions, setActiveSessions] = useState([]) - const [allSessions, setAllSessions] = useState([]) - - // My Flows state - const [myFlows, setMyFlows] = useState([]) - const [isLoadingFlows, setIsLoadingFlows] = useState(true) - const [hasNextPage, setHasNextPage] = useState(false) - const [allFlowsCeiling, setAllFlowsCeiling] = useState(false) - - // Favorites state - - - // AI Builder - const { aiEnabled } = useCachedQuota() - - // Tab state - type Tab = 'mine' | 'team' | 'public' | 'all' - const hasTeam = Boolean(user?.account_id) - const [activeTab, setActiveTab] = useState('mine') - - // Fork modal state - const [forkTarget, setForkTarget] = useState(null) - const [forkReason, setForkReason] = useState('') - const [isForking, setIsForking] = useState(false) - - // Preferences - const { dashboardMyFlowsView, setDashboardMyFlowsView } = useUserPreferencesStore() - - // Pagination - const { page, pageSize, setPage, setPageSize } = usePaginationParams({ - defaultPageSize: 10, - allowedPageSizes: [10, 25, 50, 'all'], - }) - - - - // Load sessions on mount - useEffect(() => { - Promise.all([ - sessionsApi.list({ completed: false, size: 5 }).catch(() => []), - sessionsApi.list({ size: 10 }).catch(() => []), - ]).then(([active, recent]) => { - setActiveSessions(active) - setAllSessions(recent) - }) - }, []) - - // Load flows — tab-aware - const loadFlows = useCallback(async () => { - if (!user?.id) return - setIsLoadingFlows(true) - setAllFlowsCeiling(false) - - try { - if (pageSize === 'all') { - let allItems: TreeListItem[] = [] - let skip = 0 - const CHUNK = 100 - const MAX = 500 - - while (true) { - const params: TreeFilters = { sort_by: 'updated_at', limit: CHUNK, skip } - if (activeTab === 'mine') params.author_id = user.id - if (activeTab === 'team') params.visibility = 'team' - if (activeTab === 'public') { params.visibility = 'public'; params.sort_by = 'usage_count' } - - const chunk = await treesApi.list(params) - allItems = [...allItems, ...chunk] - if (chunk.length < CHUNK || allItems.length >= MAX) { - if (allItems.length >= MAX) { allItems = allItems.slice(0, MAX); setAllFlowsCeiling(true) } - break - } - skip += CHUNK - } - setMyFlows(allItems) - setHasNextPage(false) - } else { - const numSize = pageSize as number - const params: TreeFilters = { - sort_by: activeTab === 'public' ? 'usage_count' : 'updated_at', - limit: numSize + 1, - skip: (page - 1) * numSize, - } - if (activeTab === 'mine') params.author_id = user.id - if (activeTab === 'team') params.visibility = 'team' - if (activeTab === 'public') params.visibility = 'public' - - const response = await treesApi.list(params) - setHasNextPage(response.length > numSize) - setMyFlows(response.slice(0, numSize)) - } - } catch { - // silently fail - } finally { - setIsLoadingFlows(false) - } - }, [user?.id, page, pageSize, activeTab]) - - useEffect(() => { loadFlows() }, [loadFlows]) - - // Reload on window focus (fixes stale data after returning from editor) - useEffect(() => { - const onFocus = () => loadFlows() - window.addEventListener('focus', onFocus) - return () => window.removeEventListener('focus', onFocus) - }, [loadFlows]) - - // Debounced search with staleness guard - const searchRequestId = useRef(0) - useEffect(() => { - if (debounceRef.current) clearTimeout(debounceRef.current) - if (query.length < 2) { - setSearchResults([]) - setShowResults(false) - setIsSearching(false) - return - } - setIsSearching(true) - setShowResults(true) - debounceRef.current = setTimeout(async () => { - const requestId = ++searchRequestId.current - try { - const results = await treesApi.search(query, 8) - if (requestId !== searchRequestId.current) return - setSearchResults(results) - } catch { - if (requestId !== searchRequestId.current) return - setSearchResults([]) - } finally { - if (requestId === searchRequestId.current) setIsSearching(false) - } - }, 300) - return () => { if (debounceRef.current) clearTimeout(debounceRef.current) } - }, [query]) - - // Close search dropdown on outside click - useEffect(() => { - function handleClick(e: MouseEvent) { - if (searchRef.current && !searchRef.current.contains(e.target as Node)) { - setShowResults(false) - } - } - document.addEventListener('mousedown', handleClick) - return () => document.removeEventListener('mousedown', handleClick) - }, []) - - // Stats - const openSessions = activeSessions.length - const todaySessions = allSessions.filter(s => { - if (!s.started_at) return false - const d = new Date(s.started_at) - const now = new Date() - return d.toDateString() === now.toDateString() - }).length - // completedSessions removed — no longer displayed in new layout - - // Open sessions for the new panel (3 oldest) - const openSessionItems = activeSessions - .filter(s => s.started_at) // Exclude prepared sessions (started_at is null) - .sort((a, b) => new Date(a.started_at!).getTime() - new Date(b.started_at!).getTime()) - .slice(0, 3) - .map(s => ({ - id: s.id, - treeName: s.tree_snapshot?.name || 'Unknown', - treeId: s.tree_id, - treeType: (s.tree_snapshot as unknown as Record)?.tree_type as string | undefined, - timeAgo: timeAgo(s.started_at!), - })) - - // recentSessionItems removed — replaced by RecentActivity component - - // Handlers - const handleStartSession = (treeId: string, treeType?: string) => { - navigate(getTreeNavigatePath(treeId, treeType)) - } - - const handleDeleteTree = () => {} // Not used on dashboard - const handleTagClick = () => {} // Not used on dashboard - const handleFolderCreated = () => {} // Not used on dashboard - - const handleFork = async () => { - if (!forkTarget) return - setIsForking(true) - try { - const forked = await treesApi.fork(forkTarget.id, { - fork_reason: forkReason.trim() || undefined, - }) - toast.success(`"${forked.name}" added to your flows`) - setForkTarget(null) - setForkReason('') - setActiveTab('mine') - } catch { - toast.error('Failed to fork flow') - } finally { - setIsForking(false) - } - } - - // Page size options - const pageSizeOptions: (number | 'all')[] = [10, 25, 50, 'all'] - - // Tabs - const tabs: { id: Tab; label: string }[] = [ - { id: 'mine', label: 'My Flows' }, - ...(hasTeam ? [{ id: 'team' as Tab, label: 'My Team' }] : []), - { id: 'public', label: 'Public' }, - { id: 'all', label: 'All' }, - ] - return (
- -
- {/* Greeting */} -
-

- Good {new Date().getHours() < 12 ? 'morning' : new Date().getHours() < 18 ? 'afternoon' : 'evening'}, {user?.name?.split(' ')[0] || 'there'} -

-

- {new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })} -

+ +
+ {/* Greeting */} +
+

+ Good{' '} + {new Date().getHours() < 12 + ? 'morning' + : new Date().getHours() < 18 + ? 'afternoon' + : 'evening'} + , {user?.name?.split(' ')[0] || 'there'} +

+

+ {new Date().toLocaleDateString('en-US', { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + })} +

+
+ + {/* Onboarding */} + + + {/* 1. Start Session Input */} +
+ +
+ + {/* 2. Pending Escalations (auto-hides if none) */} + + + {/* 3. Active Sessions */} + + + {/* 4. Performance Stats */} + + + {/* 5 + 6. Knowledge Base + Team Summary side by side on desktop */} +
+ + +
+ + {/* 7. Recent Sessions */} +
- - {/* Onboarding Checklist */} - - - {/* Row 1: Calendar + Quick Actions */} -
-
- -
-
- -
-
- - {/* Row 2: Open Sessions + Stats 2x2 */} -
-
- -
-
-
- {[ - { label: 'Active Flows', value: myFlows.length, gradient: true, glow: true }, - { label: 'This Week', value: todaySessions }, - { label: 'Open Sessions', value: openSessions }, - ].map((stat, i) => ( -
-

- {stat.label} -

-

- {stat.value} -

-
- ))} -
-
-
- - {/* Row 3: Prepared Sessions (only visible when sessions exist) */} - - - {/* Row 4: Recent Activity */} - - - {/* ── Existing content below ── */} -
- - {/* Search */} -
- - setQuery(e.target.value)} - onFocus={() => query.length >= 2 && setShowResults(true)} - placeholder="Search flows, sessions, tags…" - className="w-full rounded-lg border border-border bg-card py-2.5 pl-9 pr-4 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20" - /> - {showResults && ( -
- {isSearching ? ( -
- -
- ) : searchResults.length === 0 ? ( -
No results found
- ) : ( -
    - {searchResults.map((tree) => ( -
  • - -
  • - ))} -
- )} -
- )} -
- - {/* My Flows Section — tabbed */} -
-
- {tabs.map((tab) => ( - - ))} -
- {activeTab === 'mine' && canCreateTrees && ( - - )} - -
-
- - {isLoadingFlows ? ( -
- {Array.from({ length: 6 }).map((_, i) => ( -
- ))} -
- ) : myFlows.length === 0 ? ( -
-

- {activeTab === 'mine' - ? "You haven't created any flows yet." - : activeTab === 'team' - ? 'No team flows found.' - : activeTab === 'public' - ? 'No public flows found.' - : 'No flows found.'} -

- {activeTab === 'mine' && canCreateTrees && ( - - )} -
- ) : ( - <> - {allFlowsCeiling && ( -

- Showing first 500 flows. Use search or filters to find specific flows. -

- )} - - {dashboardMyFlowsView === 'grid' && ( - - )} - {dashboardMyFlowsView === 'list' && ( - - )} - {dashboardMyFlowsView === 'table' && ( - - )} - - {/* Pagination controls */} - {pageSize !== 'all' && ( -
-
- - Page {page} - -
-
- Show: - -
-
- )} - {pageSize === 'all' && ( -
-
- Show: - -
-
- )} - - )} -
-
- - {/* Fork Modal */} - {forkTarget && ( -
-
-

Fork this flow?

-

- Creates a copy of “{forkTarget.name}” under your account that you can edit freely. -

- - setForkReason(e.target.value)} - placeholder="e.g. Adding Cisco Meraki steps for our network" - maxLength={255} - className="mb-4 w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20" - onKeyDown={(e) => e.key === 'Enter' && handleFork()} - /> -
- - -
-
-
- )} - -
) }