Design spec covers: activity zone with daily stats + session feed, nav grouping (Resolve/Build/Insights), AI split (FlowPilot + Flow Assist), pinned flows removal. Implementation plan has 5 chunks, 12 tasks, 39 steps. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1270 lines
41 KiB
Markdown
1270 lines
41 KiB
Markdown
# Sidebar Redesign Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Replace the sidebar's pinned flows section with a daily stats bar + activity feed, reorganize nav into Resolve/Build/Insights groups, and split AI Assistant into FlowPilot + Flow Assist.
|
|
|
|
**Architecture:** New lightweight backend endpoint (`/sessions/sidebar-stats`) returns daily stats and active/recent session data in a single call. Frontend restructures `Sidebar.tsx` to render stats bar, activity feed, and grouped nav. Pinned flows system removed from frontend only (backend tables/endpoints left for future cleanup).
|
|
|
|
**Tech Stack:** Python FastAPI + SQLAlchemy async (backend), React + TypeScript + Zustand + Tailwind (frontend), Lucide React icons.
|
|
|
|
**Spec:** `docs/superpowers/specs/2026-03-15-sidebar-redesign-design.md`
|
|
|
|
---
|
|
|
|
## File Map
|
|
|
|
### Backend (new)
|
|
- `backend/app/schemas/sidebar.py` — Pydantic schemas for sidebar stats response
|
|
- `backend/app/api/endpoints/sidebar.py` — `GET /sessions/sidebar-stats` endpoint
|
|
|
|
### Backend (modify)
|
|
- `backend/app/api/router.py` — register new sidebar router
|
|
|
|
### Frontend (new)
|
|
- `frontend/src/components/sidebar/SidebarStatsBar.tsx` — three-stat row component
|
|
- `frontend/src/components/sidebar/SidebarActivityFeed.tsx` — activity feed (active + recents)
|
|
- `frontend/src/components/sidebar/ActivityItem.tsx` — single activity row
|
|
- `frontend/src/api/sidebar.ts` — API client for sidebar stats
|
|
- `frontend/src/pages/FlowAssistPage.tsx` — standalone flow builder AI page
|
|
|
|
### Frontend (modify)
|
|
- `frontend/src/components/layout/Sidebar.tsx` — major restructure
|
|
- `frontend/src/router.tsx` — add `/flow-assist` route
|
|
- `frontend/src/api/index.ts` — export new sidebar API
|
|
- `frontend/src/pages/TreeLibraryPage.tsx` — remove pin integration
|
|
- `frontend/src/components/library/TreeGridView.tsx` — remove pin button
|
|
- `frontend/src/components/library/TreeListView.tsx` — remove pin button
|
|
- `frontend/src/components/library/TreeTableView.tsx` — remove pin button
|
|
|
|
### Frontend (delete)
|
|
- `frontend/src/components/sidebar/PinnedFlowsSection.tsx`
|
|
- `frontend/src/store/pinnedFlowsStore.ts`
|
|
- `frontend/src/api/pinnedFlows.ts`
|
|
|
|
### Tests (new)
|
|
- `backend/tests/test_sidebar_stats.py` — integration tests for sidebar endpoint
|
|
|
|
---
|
|
|
|
## Chunk 1: Backend — Sidebar Stats Endpoint
|
|
|
|
### Task 1: Sidebar Stats Schema
|
|
|
|
**Files:**
|
|
- Create: `backend/app/schemas/sidebar.py`
|
|
|
|
- [ ] **Step 1: Create the Pydantic schemas**
|
|
|
|
```python
|
|
"""Schemas for sidebar stats endpoint."""
|
|
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
from uuid import UUID
|
|
|
|
from pydantic import BaseModel
|
|
|
|
|
|
class SidebarActiveSession(BaseModel):
|
|
"""An active or paused session for the activity feed."""
|
|
session_id: UUID
|
|
tree_name: str
|
|
tree_id: UUID
|
|
tree_type: str
|
|
started_at: datetime
|
|
ticket_number: Optional[str] = None
|
|
psa_ticket_id: Optional[str] = None
|
|
|
|
|
|
class SidebarRecentSession(BaseModel):
|
|
"""A recently completed session for the activity feed."""
|
|
session_id: UUID
|
|
tree_name: str
|
|
tree_id: UUID
|
|
tree_type: str
|
|
completed_at: datetime
|
|
|
|
|
|
class SidebarTreeCounts(BaseModel):
|
|
"""Tree counts for All Flows sub-items."""
|
|
total: int
|
|
troubleshooting: int
|
|
procedural: int
|
|
maintenance: int
|
|
|
|
|
|
class SidebarStatsResponse(BaseModel):
|
|
"""Response for GET /sessions/sidebar-stats."""
|
|
resolved_today: int
|
|
active_count: int
|
|
total_session_minutes_today: int
|
|
tree_counts: SidebarTreeCounts
|
|
active_sessions: list[SidebarActiveSession]
|
|
recent_completions: list[SidebarRecentSession]
|
|
```
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
git add backend/app/schemas/sidebar.py
|
|
git commit -m "feat: add sidebar stats Pydantic schemas"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: Sidebar Stats Endpoint — Tests First
|
|
|
|
**Files:**
|
|
- Create: `backend/tests/test_sidebar_stats.py`
|
|
|
|
- [ ] **Step 3: Write failing tests**
|
|
|
|
```python
|
|
"""Integration tests for sidebar stats endpoint."""
|
|
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
|
|
class TestSidebarStats:
|
|
"""Tests for GET /sessions/sidebar-stats."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sidebar_stats_no_sessions(
|
|
self, client: AsyncClient, auth_headers: dict
|
|
):
|
|
"""Empty stats when user has no sessions."""
|
|
response = await client.get(
|
|
"/api/v1/sessions/sidebar-stats?tz_offset=0",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["resolved_today"] == 0
|
|
assert data["active_count"] == 0
|
|
assert data["total_session_minutes_today"] == 0
|
|
assert data["active_sessions"] == []
|
|
assert data["recent_completions"] == []
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sidebar_stats_with_active_session(
|
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
|
):
|
|
"""Active session appears in activity feed."""
|
|
# Create a session (creates as active with started_at set)
|
|
create_resp = await client.post(
|
|
"/api/v1/sessions",
|
|
json={"tree_id": test_tree["id"], "ticket_number": "TK-100"},
|
|
headers=auth_headers,
|
|
)
|
|
assert create_resp.status_code == 201
|
|
|
|
response = await client.get(
|
|
"/api/v1/sessions/sidebar-stats?tz_offset=0",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["active_count"] == 1
|
|
assert len(data["active_sessions"]) == 1
|
|
assert data["active_sessions"][0]["ticket_number"] == "TK-100"
|
|
assert data["active_sessions"][0]["tree_id"] == test_tree["id"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sidebar_stats_resolved_today(
|
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
|
):
|
|
"""Resolved session counts in resolved_today."""
|
|
# Create and complete a session
|
|
create_resp = await client.post(
|
|
"/api/v1/sessions",
|
|
json={"tree_id": test_tree["id"]},
|
|
headers=auth_headers,
|
|
)
|
|
session_id = create_resp.json()["id"]
|
|
|
|
# Complete with resolved outcome
|
|
await client.post(
|
|
f"/api/v1/sessions/{session_id}/complete",
|
|
json={"outcome": "resolved", "outcome_notes": "Fixed it"},
|
|
headers=auth_headers,
|
|
)
|
|
|
|
response = await client.get(
|
|
"/api/v1/sessions/sidebar-stats?tz_offset=0",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["resolved_today"] >= 1
|
|
assert data["active_count"] == 0
|
|
assert len(data["recent_completions"]) >= 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sidebar_stats_max_active_sessions(
|
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
|
):
|
|
"""Active sessions capped at 5."""
|
|
# Create 7 active sessions
|
|
for i in range(7):
|
|
await client.post(
|
|
"/api/v1/sessions",
|
|
json={"tree_id": test_tree["id"], "ticket_number": f"TK-{i}"},
|
|
headers=auth_headers,
|
|
)
|
|
|
|
response = await client.get(
|
|
"/api/v1/sessions/sidebar-stats?tz_offset=0",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["active_count"] == 7 # Total count is accurate
|
|
assert len(data["active_sessions"]) == 5 # But list capped at 5
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sidebar_stats_recent_completions_max_3(
|
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
|
):
|
|
"""Recent completions capped at 3."""
|
|
# Create and complete 5 sessions
|
|
for i in range(5):
|
|
create_resp = await client.post(
|
|
"/api/v1/sessions",
|
|
json={"tree_id": test_tree["id"]},
|
|
headers=auth_headers,
|
|
)
|
|
session_id = create_resp.json()["id"]
|
|
await client.post(
|
|
f"/api/v1/sessions/{session_id}/complete",
|
|
json={"outcome": "resolved", "outcome_notes": "Done"},
|
|
headers=auth_headers,
|
|
)
|
|
|
|
response = await client.get(
|
|
"/api/v1/sessions/sidebar-stats?tz_offset=0",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data["recent_completions"]) == 3
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sidebar_stats_requires_auth(self, client: AsyncClient):
|
|
"""Endpoint requires authentication."""
|
|
response = await client.get("/api/v1/sessions/sidebar-stats?tz_offset=0")
|
|
assert response.status_code == 401
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sidebar_stats_requires_tz_offset(
|
|
self, client: AsyncClient, auth_headers: dict
|
|
):
|
|
"""tz_offset query param is required."""
|
|
response = await client.get(
|
|
"/api/v1/sessions/sidebar-stats",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 422
|
|
```
|
|
|
|
- [ ] **Step 4: Run tests to verify they fail**
|
|
|
|
```bash
|
|
cd backend && python -m pytest tests/test_sidebar_stats.py -v --override-ini="addopts="
|
|
```
|
|
|
|
Expected: All tests FAIL (404 — endpoint doesn't exist yet).
|
|
|
|
- [ ] **Step 5: Commit failing tests**
|
|
|
|
```bash
|
|
git add backend/tests/test_sidebar_stats.py
|
|
git commit -m "test: add failing tests for sidebar stats endpoint"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3: Sidebar Stats Endpoint — Implementation
|
|
|
|
**Files:**
|
|
- Create: `backend/app/api/endpoints/sidebar.py`
|
|
- Modify: `backend/app/api/router.py`
|
|
|
|
- [ ] **Step 6: Implement the sidebar stats endpoint**
|
|
|
|
Create `backend/app/api/endpoints/sidebar.py`:
|
|
|
|
```python
|
|
"""Sidebar stats and activity feed endpoint."""
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
from sqlalchemy import func, select, and_, case
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.database import get_db
|
|
from app.api.deps import get_current_active_user
|
|
from app.models.session import Session
|
|
from app.models.tree import Tree
|
|
from app.models.user import User
|
|
from app.schemas.sidebar import (
|
|
SidebarActiveSession,
|
|
SidebarRecentSession,
|
|
SidebarStatsResponse,
|
|
SidebarTreeCounts,
|
|
)
|
|
|
|
router = APIRouter(prefix="/sessions", tags=["sidebar"])
|
|
|
|
|
|
@router.get("/sidebar-stats", response_model=SidebarStatsResponse)
|
|
async def get_sidebar_stats(
|
|
tz_offset: int = Query(..., description="Client UTC offset in minutes (e.g. -300 for EST)"),
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> SidebarStatsResponse:
|
|
"""Get sidebar stats and activity feed for the current user.
|
|
|
|
Returns daily stats (resolved count, active count, time in session)
|
|
and lists of active + recently completed sessions.
|
|
"""
|
|
# Compute "today" start in user's timezone, then convert to UTC
|
|
now_utc = datetime.now(timezone.utc)
|
|
user_tz = timezone(timedelta(minutes=-tz_offset))
|
|
now_local = now_utc.astimezone(user_tz)
|
|
today_start_local = now_local.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
today_start_utc = today_start_local.astimezone(timezone.utc)
|
|
|
|
user_filter = Session.user_id == current_user.id
|
|
|
|
# --- Resolved today ---
|
|
resolved_result = await db.execute(
|
|
select(func.count()).where(
|
|
and_(
|
|
user_filter,
|
|
Session.completed_at >= today_start_utc,
|
|
Session.outcome == "resolved",
|
|
)
|
|
)
|
|
)
|
|
resolved_today = resolved_result.scalar() or 0
|
|
|
|
# --- Active count (all time, not just today) ---
|
|
active_result = await db.execute(
|
|
select(func.count()).where(
|
|
and_(
|
|
user_filter,
|
|
Session.started_at.isnot(None),
|
|
Session.completed_at.is_(None),
|
|
)
|
|
)
|
|
)
|
|
active_count = active_result.scalar() or 0
|
|
|
|
# --- Total session minutes today ---
|
|
# Sum of (completed_at or now) - started_at for sessions started today
|
|
duration_expr = func.extract(
|
|
"epoch",
|
|
func.coalesce(Session.completed_at, now_utc) - Session.started_at,
|
|
) / 60.0
|
|
duration_result = await db.execute(
|
|
select(func.coalesce(func.sum(duration_expr), 0)).where(
|
|
and_(
|
|
user_filter,
|
|
Session.started_at.isnot(None),
|
|
Session.started_at >= today_start_utc,
|
|
)
|
|
)
|
|
)
|
|
total_minutes = int(duration_result.scalar() or 0)
|
|
|
|
# --- Active sessions (max 5, most recent first) ---
|
|
active_sessions_result = await db.execute(
|
|
select(
|
|
Session.id,
|
|
Session.tree_id,
|
|
Session.started_at,
|
|
Session.ticket_number,
|
|
Session.psa_ticket_id,
|
|
Session.tree_snapshot["name"].as_string().label("tree_name"),
|
|
Session.tree_snapshot["tree_type"].as_string().label("tree_type"),
|
|
)
|
|
.where(
|
|
and_(
|
|
user_filter,
|
|
Session.started_at.isnot(None),
|
|
Session.completed_at.is_(None),
|
|
)
|
|
)
|
|
.order_by(Session.started_at.desc())
|
|
.limit(5)
|
|
)
|
|
active_sessions = [
|
|
SidebarActiveSession(
|
|
session_id=row.id,
|
|
tree_name=row.tree_name or "Unknown Flow",
|
|
tree_id=row.tree_id,
|
|
tree_type=row.tree_type or "troubleshooting",
|
|
started_at=row.started_at,
|
|
ticket_number=row.ticket_number,
|
|
psa_ticket_id=row.psa_ticket_id,
|
|
)
|
|
for row in active_sessions_result.all()
|
|
]
|
|
|
|
# --- Recent completions (max 3, completed today, most recent first) ---
|
|
recent_result = await db.execute(
|
|
select(
|
|
Session.id,
|
|
Session.tree_id,
|
|
Session.completed_at,
|
|
Session.tree_snapshot["name"].as_string().label("tree_name"),
|
|
Session.tree_snapshot["tree_type"].as_string().label("tree_type"),
|
|
)
|
|
.where(
|
|
and_(
|
|
user_filter,
|
|
Session.completed_at.isnot(None),
|
|
Session.completed_at >= today_start_utc,
|
|
)
|
|
)
|
|
.order_by(Session.completed_at.desc())
|
|
.limit(3)
|
|
)
|
|
recent_completions = [
|
|
SidebarRecentSession(
|
|
session_id=row.id,
|
|
tree_name=row.tree_name or "Unknown Flow",
|
|
tree_id=row.tree_id,
|
|
tree_type=row.tree_type or "troubleshooting",
|
|
completed_at=row.completed_at,
|
|
)
|
|
for row in recent_result.all()
|
|
]
|
|
|
|
# --- Tree counts (for All Flows sub-items) ---
|
|
tree_counts_result = await db.execute(
|
|
select(
|
|
func.count().label("total"),
|
|
func.count().filter(Tree.tree_type == "troubleshooting").label("troubleshooting"),
|
|
func.count().filter(Tree.tree_type == "procedural").label("procedural"),
|
|
func.count().filter(Tree.tree_type == "maintenance").label("maintenance"),
|
|
).where(
|
|
and_(
|
|
Tree.account_id == current_user.account_id,
|
|
Tree.is_active.is_(True),
|
|
Tree.deleted_at.is_(None),
|
|
)
|
|
)
|
|
)
|
|
tc = tree_counts_result.one()
|
|
|
|
return SidebarStatsResponse(
|
|
resolved_today=resolved_today,
|
|
active_count=active_count,
|
|
total_session_minutes_today=total_minutes,
|
|
tree_counts=SidebarTreeCounts(
|
|
total=tc.total,
|
|
troubleshooting=tc.troubleshooting,
|
|
procedural=tc.procedural,
|
|
maintenance=tc.maintenance,
|
|
),
|
|
active_sessions=active_sessions,
|
|
recent_completions=recent_completions,
|
|
)
|
|
```
|
|
|
|
- [ ] **Step 7: Register the router in `router.py`**
|
|
|
|
In `backend/app/api/router.py`, add the import and registration. The sidebar router must be registered BEFORE the sessions router since both use `/sessions` prefix — FastAPI matches routes in registration order, and `/sessions/sidebar-stats` needs to match before `/sessions/{session_id}`.
|
|
|
|
```python
|
|
from app.api.endpoints import sidebar
|
|
# ... existing imports ...
|
|
|
|
# Add BEFORE sessions router registration:
|
|
api_router.include_router(sidebar.router)
|
|
```
|
|
|
|
- [ ] **Step 8: Run tests to verify they pass**
|
|
|
|
```bash
|
|
cd backend && python -m pytest tests/test_sidebar_stats.py -v --override-ini="addopts="
|
|
```
|
|
|
|
Expected: All 7 tests PASS.
|
|
|
|
- [ ] **Step 9: Commit**
|
|
|
|
```bash
|
|
git add backend/app/api/endpoints/sidebar.py backend/app/api/router.py backend/app/schemas/sidebar.py
|
|
git commit -m "feat: add sidebar stats endpoint with daily stats and activity feed"
|
|
```
|
|
|
|
---
|
|
|
|
## Chunk 2: Frontend — Sidebar API Client & Activity Components
|
|
|
|
### Task 4: Sidebar API Client
|
|
|
|
**Files:**
|
|
- Create: `frontend/src/api/sidebar.ts`
|
|
- Modify: `frontend/src/api/index.ts`
|
|
|
|
- [ ] **Step 10: Create the sidebar API client**
|
|
|
|
```typescript
|
|
import { apiClient } from './client'
|
|
|
|
export interface SidebarActiveSession {
|
|
session_id: string
|
|
tree_name: string
|
|
tree_id: string
|
|
tree_type: 'troubleshooting' | 'procedural' | 'maintenance'
|
|
started_at: string
|
|
ticket_number: string | null
|
|
psa_ticket_id: string | null
|
|
}
|
|
|
|
export interface SidebarRecentSession {
|
|
session_id: string
|
|
tree_name: string
|
|
tree_id: string
|
|
tree_type: 'troubleshooting' | 'procedural' | 'maintenance'
|
|
completed_at: string
|
|
}
|
|
|
|
export interface SidebarTreeCounts {
|
|
total: number
|
|
troubleshooting: number
|
|
procedural: number
|
|
maintenance: number
|
|
}
|
|
|
|
export interface SidebarStatsResponse {
|
|
resolved_today: number
|
|
active_count: number
|
|
total_session_minutes_today: number
|
|
tree_counts: SidebarTreeCounts
|
|
active_sessions: SidebarActiveSession[]
|
|
recent_completions: SidebarRecentSession[]
|
|
}
|
|
|
|
export const sidebarApi = {
|
|
getStats: async (): Promise<SidebarStatsResponse> => {
|
|
const tzOffset = new Date().getTimezoneOffset()
|
|
const response = await apiClient.get<SidebarStatsResponse>(
|
|
`/sessions/sidebar-stats?tz_offset=${tzOffset}`
|
|
)
|
|
return response.data
|
|
},
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 11: Export from api/index.ts**
|
|
|
|
Add to `frontend/src/api/index.ts`:
|
|
|
|
```typescript
|
|
export { sidebarApi } from './sidebar'
|
|
```
|
|
|
|
- [ ] **Step 12: Commit**
|
|
|
|
```bash
|
|
git add frontend/src/api/sidebar.ts frontend/src/api/index.ts
|
|
git commit -m "feat: add sidebar stats API client"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5: ActivityItem Component
|
|
|
|
**Files:**
|
|
- Create: `frontend/src/components/sidebar/ActivityItem.tsx`
|
|
|
|
- [ ] **Step 13: Create the ActivityItem component**
|
|
|
|
```typescript
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { getTreeNavigatePath } from '@/lib/routing'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
interface ActivityItemProps {
|
|
sessionId: string
|
|
treeName: string
|
|
treeId: string
|
|
treeType: 'troubleshooting' | 'procedural' | 'maintenance'
|
|
status: 'active' | 'paused' | 'recent'
|
|
ticketNumber?: string | null
|
|
timestamp?: string | null
|
|
}
|
|
|
|
function formatRelativeTime(dateString: string): string {
|
|
const now = Date.now()
|
|
const then = new Date(dateString).getTime()
|
|
const diffMinutes = Math.floor((now - then) / 60000)
|
|
|
|
if (diffMinutes < 1) return 'just now'
|
|
if (diffMinutes < 60) return `${diffMinutes}m ago`
|
|
const diffHours = Math.floor(diffMinutes / 60)
|
|
if (diffHours < 24) return `${diffHours}h ago`
|
|
return 'yesterday'
|
|
}
|
|
|
|
export function ActivityItem({
|
|
sessionId,
|
|
treeName,
|
|
treeId,
|
|
treeType,
|
|
status,
|
|
ticketNumber,
|
|
timestamp,
|
|
}: ActivityItemProps) {
|
|
const navigate = useNavigate()
|
|
|
|
const handleClick = () => {
|
|
navigate(getTreeNavigatePath(treeId, treeType), {
|
|
state: { sessionId },
|
|
})
|
|
}
|
|
|
|
const isRecent = status === 'recent'
|
|
|
|
return (
|
|
<button
|
|
onClick={handleClick}
|
|
className={cn(
|
|
'flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-left transition-colors',
|
|
'hover:bg-[rgba(255,255,255,0.03)]',
|
|
isRecent ? 'text-[#6b7280] text-[0.72rem]' : 'text-[#e2e8f0] text-[0.8rem]'
|
|
)}
|
|
title={`${treeName}${ticketNumber ? ` (${ticketNumber})` : ''} — click to resume`}
|
|
aria-label={
|
|
status === 'active'
|
|
? `Active session: ${treeName}`
|
|
: status === 'paused'
|
|
? `Paused session: ${treeName}`
|
|
: `Recent session: ${treeName}`
|
|
}
|
|
>
|
|
{/* Status dot */}
|
|
{status === 'active' && (
|
|
<span
|
|
className="h-[7px] w-[7px] shrink-0 rounded-full"
|
|
style={{
|
|
background: '#34d399',
|
|
boxShadow: '0 0 6px rgba(52,211,153,0.5)',
|
|
animation: 'pulse-dot 2s ease-in-out infinite',
|
|
}}
|
|
aria-label="Active session"
|
|
/>
|
|
)}
|
|
{status === 'paused' && (
|
|
<span
|
|
className="h-[7px] w-[7px] shrink-0 rounded-full"
|
|
style={{
|
|
background: '#f59e0b',
|
|
boxShadow: '0 0 4px rgba(245,158,11,0.3)',
|
|
}}
|
|
aria-label="Paused session"
|
|
/>
|
|
)}
|
|
{status === 'recent' && (
|
|
<span className="h-1 w-1 shrink-0 rounded-full bg-[#3d4350]" />
|
|
)}
|
|
|
|
{/* Flow name */}
|
|
<span className="flex-1 truncate">{treeName}</span>
|
|
|
|
{/* Ticket number or timestamp */}
|
|
{ticketNumber && !isRecent && (
|
|
<span className="shrink-0 font-label text-[0.5625rem] text-[#60a5fa]">
|
|
{ticketNumber}
|
|
</span>
|
|
)}
|
|
{isRecent && timestamp && (
|
|
<span className="shrink-0 font-label text-[0.5625rem] text-[#5a6170]">
|
|
{formatRelativeTime(timestamp)}
|
|
</span>
|
|
)}
|
|
</button>
|
|
)
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 14: Commit**
|
|
|
|
```bash
|
|
git add frontend/src/components/sidebar/ActivityItem.tsx
|
|
git commit -m "feat: add ActivityItem component for sidebar activity feed"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6: SidebarStatsBar Component
|
|
|
|
**Files:**
|
|
- Create: `frontend/src/components/sidebar/SidebarStatsBar.tsx`
|
|
|
|
- [ ] **Step 15: Create the stats bar component**
|
|
|
|
```typescript
|
|
interface SidebarStatsBarProps {
|
|
resolved: number
|
|
active: number
|
|
sessionMinutes: number
|
|
}
|
|
|
|
function formatDuration(minutes: number): string {
|
|
if (minutes < 60) return `${minutes}m`
|
|
const h = Math.floor(minutes / 60)
|
|
const m = minutes % 60
|
|
return m > 0 ? `${h}h ${m}m` : `${h}h`
|
|
}
|
|
|
|
export function SidebarStatsBar({ resolved, active, sessionMinutes }: SidebarStatsBarProps) {
|
|
return (
|
|
<div
|
|
className="flex gap-0.5 px-3 pt-2 pb-1"
|
|
role="group"
|
|
aria-label="Today's stats"
|
|
>
|
|
<div className="flex-1 rounded-md bg-[rgba(255,255,255,0.02)] px-1 py-1.5 text-center">
|
|
<div
|
|
className="font-label text-sm font-semibold leading-none"
|
|
style={{ color: '#34d399' }}
|
|
aria-label={`${resolved} resolved today`}
|
|
>
|
|
{resolved}
|
|
</div>
|
|
<div className="mt-1 font-label text-[7px] uppercase tracking-[0.1em] text-[#3d4350]">
|
|
Resolved
|
|
</div>
|
|
</div>
|
|
<div className="flex-1 rounded-md bg-[rgba(255,255,255,0.02)] px-1 py-1.5 text-center">
|
|
<div
|
|
className="font-label text-sm font-semibold leading-none"
|
|
style={{ color: '#22d3ee' }}
|
|
aria-label={`${active} active sessions`}
|
|
>
|
|
{active}
|
|
</div>
|
|
<div className="mt-1 font-label text-[7px] uppercase tracking-[0.1em] text-[#3d4350]">
|
|
Active
|
|
</div>
|
|
</div>
|
|
<div className="flex-1 rounded-md bg-[rgba(255,255,255,0.02)] px-1 py-1.5 text-center">
|
|
<div
|
|
className="font-label text-sm font-semibold leading-none text-muted-foreground"
|
|
aria-label={`${formatDuration(sessionMinutes)} in session today`}
|
|
>
|
|
{formatDuration(sessionMinutes)}
|
|
</div>
|
|
<div className="mt-1 font-label text-[7px] uppercase tracking-[0.1em] text-[#3d4350]">
|
|
In Session
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 16: Commit**
|
|
|
|
```bash
|
|
git add frontend/src/components/sidebar/SidebarStatsBar.tsx
|
|
git commit -m "feat: add SidebarStatsBar component"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 7: SidebarActivityFeed Component
|
|
|
|
**Files:**
|
|
- Create: `frontend/src/components/sidebar/SidebarActivityFeed.tsx`
|
|
|
|
- [ ] **Step 17: Create the activity feed component**
|
|
|
|
```typescript
|
|
import { Clock } from 'lucide-react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { ActivityItem } from './ActivityItem'
|
|
import type { SidebarActiveSession, SidebarRecentSession } from '@/api/sidebar'
|
|
|
|
interface SidebarActivityFeedProps {
|
|
activeSessions: SidebarActiveSession[]
|
|
recentCompletions: SidebarRecentSession[]
|
|
totalActive: number
|
|
}
|
|
|
|
export function SidebarActivityFeed({
|
|
activeSessions,
|
|
recentCompletions,
|
|
totalActive,
|
|
}: SidebarActivityFeedProps) {
|
|
const navigate = useNavigate()
|
|
const hasActivity = activeSessions.length > 0 || recentCompletions.length > 0
|
|
|
|
return (
|
|
<div className="px-3 pb-1">
|
|
{/* Header */}
|
|
<div className="flex items-center gap-1.5 px-2.5 py-1 mb-0.5">
|
|
<Clock size={10} style={{ color: '#34d399' }} />
|
|
<span className="font-label text-[0.5625rem] uppercase tracking-[0.12em] text-[#5a6170]">
|
|
Activity
|
|
</span>
|
|
</div>
|
|
|
|
{!hasActivity ? (
|
|
<p className="px-2.5 py-2 text-xs text-muted-foreground">
|
|
No activity today
|
|
</p>
|
|
) : (
|
|
<div className="space-y-0.5">
|
|
{/* Active sessions */}
|
|
{activeSessions.map((session) => (
|
|
<ActivityItem
|
|
key={session.session_id}
|
|
sessionId={session.session_id}
|
|
treeName={session.tree_name}
|
|
treeId={session.tree_id}
|
|
treeType={session.tree_type}
|
|
status="active"
|
|
ticketNumber={session.ticket_number || session.psa_ticket_id}
|
|
/>
|
|
))}
|
|
|
|
{/* Overflow link */}
|
|
{totalActive > 5 && (
|
|
<button
|
|
onClick={() => navigate('/sessions')}
|
|
className="w-full px-2.5 py-1 text-left text-[0.6875rem] text-muted-foreground hover:text-foreground transition-colors"
|
|
>
|
|
View all in Sessions →
|
|
</button>
|
|
)}
|
|
|
|
{/* Divider between active and recent */}
|
|
{activeSessions.length > 0 && recentCompletions.length > 0 && (
|
|
<div className="mx-2.5 my-1" style={{ height: '1px', background: 'rgba(255,255,255,0.03)' }} />
|
|
)}
|
|
|
|
{/* Recent completions */}
|
|
{recentCompletions.map((session) => (
|
|
<ActivityItem
|
|
key={session.session_id}
|
|
sessionId={session.session_id}
|
|
treeName={session.tree_name}
|
|
treeId={session.tree_id}
|
|
treeType={session.tree_type}
|
|
status="recent"
|
|
timestamp={session.completed_at}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 18: Commit**
|
|
|
|
```bash
|
|
git add frontend/src/components/sidebar/SidebarActivityFeed.tsx
|
|
git commit -m "feat: add SidebarActivityFeed component"
|
|
```
|
|
|
|
---
|
|
|
|
## Chunk 3: Frontend — Sidebar Restructure
|
|
|
|
### Task 8: Add Pulse Dot CSS Animation
|
|
|
|
**Files:**
|
|
- Modify: `frontend/src/index.css`
|
|
|
|
- [ ] **Step 19: Add the pulse-dot keyframe animation**
|
|
|
|
Add to `frontend/src/index.css` (in the global styles section, after existing keyframes):
|
|
|
|
```css
|
|
@keyframes pulse-dot {
|
|
0%, 100% { box-shadow: 0 0 4px rgba(52,211,153,0.4); }
|
|
50% { box-shadow: 0 0 8px rgba(52,211,153,0.7); }
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 20: Commit**
|
|
|
|
```bash
|
|
git add frontend/src/index.css
|
|
git commit -m "feat: add pulse-dot animation for active session indicator"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 9: Restructure Sidebar.tsx
|
|
|
|
**Files:**
|
|
- Modify: `frontend/src/components/layout/Sidebar.tsx`
|
|
|
|
This is the largest change. The sidebar is rewritten to use the new layout.
|
|
|
|
- [ ] **Step 21: Rewrite Sidebar.tsx**
|
|
|
|
Replace the entire file content. Key changes:
|
|
1. Remove `PinnedFlowsSection` import and `usePinnedFlowsStore` usage
|
|
2. Remove `sessionsApi` and `treesApi` data fetches (replaced by `sidebarApi.getStats`)
|
|
3. Add `SidebarStatsBar` and `SidebarActivityFeed` in expanded mode
|
|
4. Add `Brain` and `WandSparkles` icon imports
|
|
5. Reorganize nav items into Dashboard → Resolve → Build → Insights groups
|
|
6. Update collapsed view with all 13 items
|
|
7. Add `flowPilot` and `flowAssist` to `NAV_COLORS`
|
|
|
|
The new `Sidebar.tsx` should:
|
|
|
|
**Imports:**
|
|
```typescript
|
|
import { useEffect, useState } from 'react'
|
|
import { useLocation } from 'react-router-dom'
|
|
import {
|
|
LayoutGrid, Network, Wrench, Clock, FileOutput, BarChart3,
|
|
Settings, PanelLeftClose, PanelLeftOpen, MessageSquareText,
|
|
BookOpen, Lightbulb, Code2, Library, Brain, WandSparkles,
|
|
} 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'
|
|
```
|
|
|
|
**NAV_COLORS** — add two new entries, remove `ai`:
|
|
```typescript
|
|
const NAV_COLORS = {
|
|
dashboard: '#22d3ee',
|
|
flows: '#a78bfa',
|
|
editor: '#f59e0b',
|
|
sessions: '#34d399',
|
|
exports: '#60a5fa',
|
|
flowPilot: '#e879f9',
|
|
flowAssist:'#f472b6',
|
|
stepLib: '#fb923c',
|
|
scripts: '#2dd4bf',
|
|
kb: '#fb7185',
|
|
analytics: '#38bdf8',
|
|
guides: '#a3e635',
|
|
feedback: '#818cf8',
|
|
} as const
|
|
```
|
|
|
|
**Data fetch** — replace the two separate fetches with one `sidebarApi.getStats()` call. Use `location.pathname` as a dependency so stats refresh on navigation (the sidebar persists across pages and does NOT re-mount):
|
|
```typescript
|
|
const location = useLocation()
|
|
const [stats, setStats] = useState<SidebarStatsResponse | null>(null)
|
|
|
|
useEffect(() => {
|
|
sidebarApi.getStats().then(setStats).catch(() => {})
|
|
}, [location.pathname])
|
|
```
|
|
|
|
**Expanded layout** (inside the `else` branch):
|
|
```tsx
|
|
{/* Stats Bar */}
|
|
<SidebarStatsBar
|
|
resolved={stats?.resolved_today ?? 0}
|
|
active={stats?.active_count ?? 0}
|
|
sessionMinutes={stats?.total_session_minutes_today ?? 0}
|
|
/>
|
|
|
|
{/* Activity Feed */}
|
|
<SidebarActivityFeed
|
|
activeSessions={stats?.active_sessions ?? []}
|
|
recentCompletions={stats?.recent_completions ?? []}
|
|
totalActive={stats?.active_count ?? 0}
|
|
/>
|
|
|
|
<div style={{ borderBottom: '1px solid var(--glass-border)' }} />
|
|
|
|
{/* Dashboard (standalone) */}
|
|
<div className="px-3 py-2 space-y-0.5">
|
|
<NavItem href="/" icon={LayoutGrid} label="Dashboard" iconColor={NAV_COLORS.dashboard} />
|
|
|
|
{/* Resolve group */}
|
|
<div className="font-label text-[0.5625rem] uppercase tracking-[0.12em] text-[#5a6170] px-3 pt-3 pb-1">
|
|
Resolve
|
|
</div>
|
|
<NavItem href="/sessions" icon={Clock} label="Sessions" badge={stats?.active_count || undefined} iconColor={NAV_COLORS.sessions} />
|
|
<NavItem
|
|
href="/trees"
|
|
icon={Network}
|
|
label="All Flows"
|
|
badge={stats?.tree_counts.total || undefined}
|
|
iconColor={NAV_COLORS.flows}
|
|
matchPaths={['/trees', '/flows']}
|
|
children={[
|
|
{ href: '/trees?type=troubleshooting', label: 'Troubleshooting', count: stats?.tree_counts.troubleshooting || undefined },
|
|
{ href: '/trees?type=procedural', label: 'Projects', count: stats?.tree_counts.procedural || undefined },
|
|
{ href: '/trees?type=maintenance', label: 'Maintenance', count: stats?.tree_counts.maintenance || undefined },
|
|
]}
|
|
/>
|
|
<NavItem href="/assistant" icon={Brain} label="FlowPilot" iconColor={NAV_COLORS.flowPilot} />
|
|
<NavItem href="/scripts" icon={Code2} label="Script Library" iconColor={NAV_COLORS.scripts} />
|
|
|
|
{/* Build group */}
|
|
<div className="font-label text-[0.5625rem] uppercase tracking-[0.12em] text-[#5a6170] px-3 pt-3 pb-1">
|
|
Build
|
|
</div>
|
|
<NavItem href="/my-trees" icon={Wrench} label="Flow Editor" iconColor={NAV_COLORS.editor} />
|
|
<NavItem href="/flow-assist" icon={WandSparkles} label="Flow Assist" iconColor={NAV_COLORS.flowAssist} />
|
|
<NavItem href="/step-library" icon={Library} label="Step Library" iconColor={NAV_COLORS.stepLib} />
|
|
<NavItem href="/kb-accelerator" icon={Lightbulb} label="KB Accelerator" iconColor={NAV_COLORS.kb} />
|
|
|
|
{/* Insights group */}
|
|
<div className="font-label text-[0.5625rem] uppercase tracking-[0.12em] text-[#5a6170] px-3 pt-3 pb-1">
|
|
Insights
|
|
</div>
|
|
<NavItem href="/shares" icon={FileOutput} label="Exports" iconColor={NAV_COLORS.exports} />
|
|
<NavItem href="/analytics" icon={BarChart3} label="Analytics" iconColor={NAV_COLORS.analytics} />
|
|
</div>
|
|
```
|
|
|
|
**Collapsed layout** — update to include all 13 items (add Brain/FlowPilot and WandSparkles/Flow Assist, remove BotMessageSquare/AI Assistant):
|
|
```tsx
|
|
<div className="flex flex-col items-center px-1.5 py-3 space-y-1">
|
|
<NavItem href="/" icon={LayoutGrid} label="Dashboard" iconColor={NAV_COLORS.dashboard} collapsed />
|
|
<NavItem href="/sessions" icon={Clock} label="Sessions" badge={stats?.active_count || undefined} iconColor={NAV_COLORS.sessions} collapsed />
|
|
<NavItem href="/trees" icon={Network} label="All Flows" matchPaths={['/trees', '/flows']} iconColor={NAV_COLORS.flows} collapsed />
|
|
<NavItem href="/assistant" icon={Brain} label="FlowPilot" iconColor={NAV_COLORS.flowPilot} collapsed />
|
|
<NavItem href="/scripts" icon={Code2} label="Script Library" iconColor={NAV_COLORS.scripts} collapsed />
|
|
<NavItem href="/my-trees" icon={Wrench} label="Flow Editor" iconColor={NAV_COLORS.editor} collapsed />
|
|
<NavItem href="/flow-assist" icon={WandSparkles} label="Flow Assist" iconColor={NAV_COLORS.flowAssist} collapsed />
|
|
<NavItem href="/step-library" icon={Library} label="Step Library" iconColor={NAV_COLORS.stepLib} collapsed />
|
|
<NavItem href="/kb-accelerator" icon={Lightbulb} label="KB Accelerator" iconColor={NAV_COLORS.kb} collapsed />
|
|
<NavItem href="/shares" icon={FileOutput} label="Exports" iconColor={NAV_COLORS.exports} collapsed />
|
|
<NavItem href="/analytics" icon={BarChart3} label="Analytics" iconColor={NAV_COLORS.analytics} collapsed />
|
|
<NavItem href="/guides" icon={BookOpen} label="User Guides" iconColor={NAV_COLORS.guides} collapsed />
|
|
<NavItem href="/feedback" icon={MessageSquareText} label="Feedback" iconColor={NAV_COLORS.feedback} collapsed />
|
|
</div>
|
|
```
|
|
|
|
- [ ] **Step 22: Verify the app builds**
|
|
|
|
```bash
|
|
cd frontend && npm run build
|
|
```
|
|
|
|
Expected: Build succeeds. Fix any TypeScript or import errors.
|
|
|
|
- [ ] **Step 23: Commit**
|
|
|
|
```bash
|
|
git add frontend/src/components/layout/Sidebar.tsx
|
|
git commit -m "feat: restructure sidebar with stats bar, activity feed, and grouped nav"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 10: Remove Pinned Flows Frontend Code
|
|
|
|
**Files:**
|
|
- Delete: `frontend/src/components/sidebar/PinnedFlowsSection.tsx`
|
|
- Delete: `frontend/src/store/pinnedFlowsStore.ts`
|
|
- Delete: `frontend/src/api/pinnedFlows.ts`
|
|
- Modify: `frontend/src/pages/TreeLibraryPage.tsx` — remove pin integration
|
|
- Modify: `frontend/src/components/library/TreeGridView.tsx` — remove pin button
|
|
- Modify: `frontend/src/components/library/TreeListView.tsx` — remove pin button
|
|
|
|
- [ ] **Step 24: Delete pinned flows files**
|
|
|
|
```bash
|
|
rm frontend/src/components/sidebar/PinnedFlowsSection.tsx
|
|
rm frontend/src/store/pinnedFlowsStore.ts
|
|
rm frontend/src/api/pinnedFlows.ts
|
|
```
|
|
|
|
- [ ] **Step 25: Remove pin imports and props from TreeLibraryPage.tsx**
|
|
|
|
Remove:
|
|
- `usePinnedFlowsStore` import and all related hooks (`pinnedItems`, `isMutatingByTreeId`, `pinnedTreeIds`, `pinLoadingTreeIds`, `togglePin`, `loadPinned`)
|
|
- The `useEffect` that calls `loadPinned()`
|
|
- The `pinnedTreeIds`, `onTogglePin`, `pinLoadingTreeIds` props passed to `TreeGridView`, `TreeListView`, and `TreeTableView`
|
|
|
|
- [ ] **Step 26: Remove pin button from TreeGridView.tsx**
|
|
|
|
Remove:
|
|
- `Star` icon import from lucide-react
|
|
- `pinnedTreeIds`, `onTogglePin`, `pinLoadingTreeIds` from component props interface
|
|
- The star/pin button JSX (lines ~67-86)
|
|
|
|
- [ ] **Step 27: Remove pin button from TreeListView.tsx**
|
|
|
|
Same pattern as TreeGridView — remove pin-related props and button.
|
|
|
|
- [ ] **Step 27b: Remove pin button from TreeTableView.tsx**
|
|
|
|
Same pattern as TreeGridView — remove pin-related props and button.
|
|
|
|
- [ ] **Step 28: Remove pinnedFlows export from api/index.ts**
|
|
|
|
Remove the `export { pinnedFlowsApi } from './pinnedFlows'` line (or similar).
|
|
|
|
- [ ] **Step 29: Verify the app builds**
|
|
|
|
```bash
|
|
cd frontend && npm run build
|
|
```
|
|
|
|
Expected: Build succeeds with no references to removed files.
|
|
|
|
- [ ] **Step 30: Commit**
|
|
|
|
```bash
|
|
git add frontend/src/components/sidebar/PinnedFlowsSection.tsx frontend/src/store/pinnedFlowsStore.ts frontend/src/api/pinnedFlows.ts frontend/src/pages/TreeLibraryPage.tsx frontend/src/components/library/TreeGridView.tsx frontend/src/components/library/TreeListView.tsx frontend/src/components/library/TreeTableView.tsx frontend/src/api/index.ts
|
|
git commit -m "refactor: remove pinned flows frontend (PinnedFlowsSection, store, API, pin buttons)"
|
|
```
|
|
|
|
Note: The deleted files will show as "deleted" in `git add` — that's expected. Use `git add -u` as a fallback if the explicit paths don't stage the deletions correctly.
|
|
|
|
---
|
|
|
|
## Chunk 4: Frontend — Flow Assist Page & Route
|
|
|
|
### Task 11: FlowAssistPage
|
|
|
|
**Files:**
|
|
- Create: `frontend/src/pages/FlowAssistPage.tsx`
|
|
- Modify: `frontend/src/router.tsx`
|
|
|
|
- [ ] **Step 31: Create FlowAssistPage**
|
|
|
|
This is a standalone page for the conversational flow builder. For now, create a placeholder page that will be expanded when the AI split is fully implemented. The page should use the same glass-card layout pattern as other pages.
|
|
|
|
```typescript
|
|
import { WandSparkles } from 'lucide-react'
|
|
|
|
export default function FlowAssistPage() {
|
|
return (
|
|
<div className="p-6">
|
|
<div className="mb-6">
|
|
<h1 className="font-heading text-2xl font-bold tracking-tight text-foreground">
|
|
<span className="inline-flex items-center gap-2">
|
|
<WandSparkles size={24} style={{ color: '#f472b6' }} />
|
|
Flow Assist
|
|
</span>
|
|
</h1>
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
Build flows from natural language — describe what you need and Flow Assist will generate the decision tree or procedural steps.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="glass-card-static p-8 text-center">
|
|
<WandSparkles size={40} className="mx-auto mb-4 text-muted-foreground" />
|
|
<h2 className="font-heading text-lg font-semibold text-foreground mb-2">
|
|
Coming Soon
|
|
</h2>
|
|
<p className="text-sm text-muted-foreground max-w-md mx-auto">
|
|
Flow Assist will be available here as a dedicated conversational flow builder.
|
|
In the meantime, use the AI panel in the Flow Editor to generate flows.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 32: Add the route in router.tsx**
|
|
|
|
Add inside the protected route children (alongside other page routes like `/assistant`, `/scripts`, etc.):
|
|
|
|
```typescript
|
|
{
|
|
path: 'flow-assist',
|
|
lazy: () => import('./pages/FlowAssistPage').then(m => ({ Component: m.default })),
|
|
},
|
|
```
|
|
|
|
If the router doesn't use lazy loading, use the direct import pattern matching the existing routes.
|
|
|
|
- [ ] **Step 33: Verify the app builds**
|
|
|
|
```bash
|
|
cd frontend && npm run build
|
|
```
|
|
|
|
- [ ] **Step 34: Commit**
|
|
|
|
```bash
|
|
git add frontend/src/pages/FlowAssistPage.tsx frontend/src/router.tsx
|
|
git commit -m "feat: add FlowAssistPage placeholder and /flow-assist route"
|
|
```
|
|
|
|
---
|
|
|
|
## Chunk 5: Final Verification & Cleanup
|
|
|
|
### Task 12: Full Test Suite & Build Verification
|
|
|
|
- [ ] **Step 35: Run backend tests**
|
|
|
|
```bash
|
|
cd backend && python -m pytest tests/test_sidebar_stats.py -v --override-ini="addopts="
|
|
```
|
|
|
|
Expected: All sidebar tests pass.
|
|
|
|
- [ ] **Step 36: Run full backend test suite**
|
|
|
|
```bash
|
|
cd backend && python -m pytest --override-ini="addopts=" -x -q
|
|
```
|
|
|
|
Expected: No regressions. If any pinned-flows tests exist and fail (because we didn't touch backend), that's expected — they should still pass since backend is unchanged.
|
|
|
|
- [ ] **Step 37: Run frontend build**
|
|
|
|
```bash
|
|
cd frontend && npm run build
|
|
```
|
|
|
|
Expected: Clean build, no errors.
|
|
|
|
- [ ] **Step 38: Manual smoke test**
|
|
|
|
Start the dev servers and verify:
|
|
1. Sidebar loads with stats bar showing 0/0/0m on fresh state
|
|
2. Activity feed shows "No activity today" when empty
|
|
3. Nav is grouped: Dashboard → Resolve (Sessions, All Flows, FlowPilot, Script Library) → Build (Flow Editor, Flow Assist, Step Library, KB Accelerator) → Insights (Exports, Analytics)
|
|
4. FlowPilot link goes to `/assistant`
|
|
5. Flow Assist link goes to `/flow-assist` (placeholder page)
|
|
6. Collapsed sidebar shows all 13 icon-only items
|
|
7. Pin buttons are gone from tree library grid/list views
|
|
8. No console errors
|
|
|
|
- [ ] **Step 39: Final commit with any cleanup**
|
|
|
|
If there are any remaining changes from smoke test fixes, stage the specific files and commit:
|
|
|
|
```bash
|
|
git add <changed-files>
|
|
git commit -m "chore: sidebar redesign cleanup and verification"
|
|
```
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
| Chunk | Tasks | What it delivers |
|
|
|-------|-------|-----------------|
|
|
| 1 | Tasks 1-3 | Backend endpoint with tests — `GET /sessions/sidebar-stats` |
|
|
| 2 | Tasks 4-7 | Frontend API client + ActivityItem, StatsBar, ActivityFeed components |
|
|
| 3 | Tasks 8-10 | Sidebar restructure + pinned flows removal |
|
|
| 4 | Task 11 | FlowAssistPage + `/flow-assist` route |
|
|
| 5 | Task 12 | Full verification and cleanup |
|