* docs: add 5 sidebar icon color concepts for UX review Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(ui): add semantic icon colors and updated icons to sidebar nav Swap generic icons for more descriptive alternatives (Network, Wrench, FileOutput, Library, Code2, Lightbulb) and assign each nav item a unique semantic color for instant visual landmarks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ui): default Sessions page to Active tab, reorder tabs Active sessions are what engineers care about most. Tab order is now Active, Prepared, Completed, All. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add sidebar grouping and AI naming concept mockups Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add sidebar redesign context and decision summary Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add sidebar redesign spec and implementation plan 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> * feat: add sidebar stats Pydantic schemas Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add failing tests for sidebar stats endpoint Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add sidebar stats endpoint with daily stats and activity feed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add sidebar API client, stats bar, activity feed components New components: SidebarStatsBar, SidebarActivityFeed, ActivityItem. New API client for sidebar stats endpoint. Pulse-dot CSS animation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: restructure sidebar with stats bar, activity feed, and grouped nav Dashboard-first layout with Resolve/Build/Insights groups. AI split: FlowPilot (Resolve) + Flow Assist (Build). Stats bar: Resolved/Active/In Session daily counters. Activity feed: active sessions with CW ticket #, recent completions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: remove pinned flows frontend (PinnedFlowsSection, store, API, pin buttons) Removed: PinnedFlowsSection component, pinnedFlowsStore, pinnedFlows API client. Cleaned: pin buttons from TreeGridView, TreeListView, TreeTableView. Cleaned: favorites section from QuickStartPage, pin props from TreeLibraryPage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add FlowAssistPage placeholder and /flow-assist route Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: real-time sidebar stats via session-changed events Sidebar now refreshes stats when sessions are created or completed, not just on page navigation. Uses window event bus pattern (same as folder-changed events in codebase). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: live-ticking In Session timer using active session start times SidebarStatsBar now computes active session elapsed time client-side from started_at timestamps, ticking every 60s. Backend only returns completed session minutes to avoid double-counting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: sidebar In Session timer ticks every second and shows seconds Timer now uses 1s interval (not 60s) and displays seconds when under a minute so it matches the session timer in the flow UI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: trigger PR environment redeploy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * debug: add console.log to SidebarStatsBar for timer investigation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: parse sidebar timestamps as UTC (append Z suffix) Backend returns naive UTC timestamps without timezone indicator. JS Date() treats bare ISO strings as local time, causing the timer to compute negative elapsed time (future timestamps). Appending 'Z' forces UTC parsing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: rename 'In Session' to 'Total Time' for clarity Makes it clear the timer is an aggregate of all sessions today, not just the current one. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- 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 |
|