feat: sidebar redesign — activity feed, grouped nav, AI split #107

Merged
chihlasm merged 20 commits from design/sidebar-icon-concepts into main 2026-03-16 05:35:16 +00:00
29 changed files with 3836 additions and 514 deletions

View File

@@ -0,0 +1,178 @@
"""Sidebar stats and activity feed endpoint."""
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends, Query
from sqlalchemy import func, select, and_
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
# --- Completed session minutes today (active session time computed client-side) ---
duration_expr = func.extract(
"epoch",
Session.completed_at - 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.completed_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,
)

View File

@@ -1,5 +1,5 @@
from fastapi import APIRouter
from app.api.endpoints import auth, trees, sessions, invite, categories, tags, folders, step_categories, steps, admin, accounts, webhooks, shares, shared, tree_markdown
from app.api.endpoints import auth, trees, sessions, sidebar, invite, categories, tags, folders, step_categories, steps, admin, accounts, webhooks, shares, shared, tree_markdown
from app.api.endpoints import admin_dashboard, admin_audit, admin_plan_limits, admin_feature_flags, admin_settings, admin_categories
from app.api.endpoints import ratings, analytics
from app.api.endpoints import target_lists
@@ -23,6 +23,7 @@ api_router = APIRouter()
api_router.include_router(auth.router)
api_router.include_router(trees.router)
api_router.include_router(sidebar.router)
api_router.include_router(sessions.router)
api_router.include_router(invite.router)
api_router.include_router(categories.router)

View File

@@ -0,0 +1,45 @@
"""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]

View File

@@ -0,0 +1,142 @@
"""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"] == []
assert "tree_counts" in data
@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_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_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."""
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
assert len(data["active_sessions"]) == 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."""
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

View File

@@ -0,0 +1,630 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ResolutionFlow — Sidebar Grouping Concepts</title>
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,700&family=IBM+Plex+Sans:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #101114;
--card: #17191d;
--fg: #f8fafc;
--fg-muted: #8891a0;
--fg-dim: #5a6170;
--border: rgba(255,255,255,0.06);
--border-hover: rgba(255,255,255,0.12);
--cyan-500: #06b6d4;
--cyan-400: #22d3ee;
--sidebar-hover: #212329;
--sidebar-active: rgba(6,182,212,0.10);
--glass-bg: rgba(22,24,28,0.55);
--glass-blur: blur(16px);
--amber: #f59e0b;
--emerald: #34d399;
--violet: #a78bfa;
--rose: #fb7185;
--blue: #60a5fa;
--orange: #fb923c;
--teal: #2dd4bf;
--sky: #38bdf8;
--lime: #a3e635;
--indigo: #818cf8;
--fuchsia: #e879f9;
--pink: #f472b6;
--yellow: #facc15;
}
body {
font-family: 'IBM Plex Sans', system-ui, sans-serif;
background: var(--bg);
color: var(--fg);
min-height: 100vh;
padding: 40px;
}
h1 { font-family: 'Bricolage Grotesque', sans-serif; font-weight: 700; font-size: 2rem; letter-spacing: -0.03em; margin-bottom: 6px; }
h1 span { color: var(--cyan-400); }
.subtitle { color: var(--fg-muted); font-size: 0.9375rem; margin-bottom: 48px; max-width: 820px; line-height: 1.6; }
.section-header {
margin-bottom: 24px; margin-top: 56px;
padding-bottom: 16px; border-bottom: 1px solid var(--border);
}
.section-header:first-of-type { margin-top: 0; }
.section-header h2 { font-family: 'Bricolage Grotesque', sans-serif; font-weight: 700; font-size: 1.5rem; letter-spacing: -0.02em; margin-bottom: 4px; }
.section-header p { font-size: 0.8125rem; color: var(--fg-dim); line-height: 1.5; max-width: 700px; }
.variants-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 28px;
max-width: 1400px;
}
.variant {
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border: 1px solid var(--border);
border-radius: 16px;
overflow: hidden;
transition: border-color 300ms, box-shadow 300ms;
}
.variant:hover { border-color: var(--border-hover); box-shadow: 0 12px 40px rgba(0,0,0,0.4); }
.variant-header {
padding: 16px 20px 12px;
border-bottom: 1px solid var(--border);
}
.variant-tag {
font-family: 'JetBrains Mono', monospace;
font-size: 0.5625rem; text-transform: uppercase;
letter-spacing: 0.12em; color: var(--cyan-400); margin-bottom: 4px;
}
.variant-title {
font-family: 'Bricolage Grotesque', sans-serif;
font-weight: 700; font-size: 1.05rem; letter-spacing: -0.02em; margin-bottom: 2px;
}
.variant-desc { font-size: 0.75rem; color: var(--fg-dim); line-height: 1.4; }
/* ── Sidebar simulation ── */
.sidebar-sim { padding: 6px 12px 12px; }
.nav-group-label {
font-family: 'JetBrains Mono', monospace;
font-size: 0.5625rem; text-transform: uppercase;
letter-spacing: 0.12em; color: var(--fg-dim);
padding: 14px 12px 5px;
}
.nav-group-label:first-child { padding-top: 8px; }
.nav-divider {
height: 1px;
background: var(--border);
margin: 6px 12px;
}
.nav-item {
display: flex; align-items: center; gap: 12px;
padding: 8px 12px; border-radius: 10px;
font-size: 0.8125rem; font-weight: 500;
color: var(--fg-muted); cursor: pointer;
transition: all 150ms ease; position: relative;
}
.nav-item:hover { background: var(--sidebar-hover); color: var(--fg); }
.nav-item.active { background: var(--sidebar-active); color: var(--fg); }
.nav-item.active::before {
content: ''; position: absolute; left: 0; top: 50%;
transform: translateY(-50%); width: 3px; height: 24px;
border-radius: 0 4px 4px 0;
background: linear-gradient(135deg, var(--cyan-500), var(--cyan-400));
}
.nav-item .badge {
margin-left: auto;
font-family: 'JetBrains Mono', monospace; font-size: 0.6875rem;
color: var(--fg-dim); background: var(--card);
border: 1px solid var(--border); padding: 1px 8px; border-radius: 9999px;
}
.icon-wrap {
display: flex; align-items: center; justify-content: center;
width: 20px; height: 20px; flex-shrink: 0;
}
.icon-wrap svg {
width: 18px; height: 18px; stroke-width: 1.75;
fill: none; stroke: currentColor;
stroke-linecap: round; stroke-linejoin: round;
}
/* Icon colors */
.ic-dashboard svg { color: var(--cyan-400); }
.ic-flows svg { color: var(--violet); }
.ic-editor svg { color: var(--amber); }
.ic-sessions svg { color: var(--emerald); }
.ic-exports svg { color: var(--blue); }
.ic-ai svg { color: var(--fuchsia); }
.ic-ai-build svg { color: var(--pink); }
.ic-ai-copilot svg { color: var(--fuchsia); }
.ic-steplib svg { color: var(--orange); }
.ic-scripts svg { color: var(--teal); }
.ic-kb svg { color: var(--rose); }
.ic-analytics svg { color: var(--sky); }
.ic-guides svg { color: var(--lime); }
.ic-feedback svg { color: var(--indigo); }
.ic-settings svg { color: var(--fg-dim); }
/* ── Footer section styling ── */
.footer-section {
border-top: 1px solid var(--border);
padding: 8px 12px 4px;
margin-top: 8px;
}
/* ── Callout box ── */
.ai-naming {
margin-top: 56px; max-width: 1000px;
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border: 1px solid var(--border);
border-radius: 16px;
padding: 24px 28px;
}
.ai-naming h2 {
font-family: 'Bricolage Grotesque', sans-serif;
font-weight: 700; font-size: 1.25rem; letter-spacing: -0.02em; margin-bottom: 16px;
}
.naming-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@media (max-width: 700px) { .naming-grid { grid-template-columns: 1fr; } }
.naming-card {
background: rgba(255,255,255,0.02);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px 20px;
}
.naming-card h3 {
font-family: 'Bricolage Grotesque', sans-serif;
font-weight: 700; font-size: 0.9375rem; margin-bottom: 4px;
}
.naming-card .purpose {
font-size: 0.75rem; color: var(--fg-dim); margin-bottom: 12px; line-height: 1.4;
}
.naming-option {
display: flex; align-items: center; gap: 10px;
padding: 6px 0;
font-size: 0.8125rem; color: var(--fg-muted);
}
.naming-option .name { color: var(--fg); font-weight: 600; min-width: 140px; }
.naming-option .desc { font-size: 0.75rem; color: var(--fg-dim); }
.naming-option.recommended .name { color: var(--cyan-400); }
.naming-option .icon-sm {
display: flex; align-items: center; justify-content: center;
width: 18px; height: 18px; flex-shrink: 0;
}
.naming-option .icon-sm svg {
width: 16px; height: 16px; stroke-width: 1.75;
fill: none; stroke: currentColor;
stroke-linecap: round; stroke-linejoin: round;
}
/* SVG icon templates — reused inline */
/* Dashboard: LayoutGrid */
/* Flows: Network */
/* Editor: Wrench */
/* Sessions: Clock */
/* Exports: FileOutput */
/* AI: BotMessageSquare */
/* AI Build: Wand2 / Hammer / MessageSquareCode */
/* AI Copilot: Brain / BotMessageSquare / Sparkles */
/* StepLib: Library */
/* Scripts: Code2 -->
/* KB: Lightbulb */
/* Analytics: BarChart3 */
</style>
</head>
<body>
<h1>Sidebar <span>Grouping Concepts</span></h1>
<p class="subtitle">
Reorganizing the navigation around workflow stages — what engineers do when they're <strong>working a ticket</strong> vs
<strong>building flows</strong> vs <strong>reviewing results</strong>. Also explores splitting AI Assistant into two
distinct tools with clearer purpose.
</p>
<!-- ═══════════════════════════════════════════════════════
CONCEPT A — Three Groups (Resolve / Build / Insights)
═══════════════════════════════════════════════════════ -->
<div class="section-header">
<h2>Concept A — Resolve / Build / Insights</h2>
<p>Three clear workflow stages. "Resolve" is front and center because that's what engineers spend most time doing. AI is split: copilot lives in Resolve, builder lives in Build.</p>
</div>
<div class="variants-grid">
<!-- A1: Clean section labels -->
<div class="variant">
<div class="variant-header">
<div class="variant-tag">Concept A1</div>
<div class="variant-title">With Section Labels</div>
<div class="variant-desc">Explicit uppercase labels separate each group. Clear visual hierarchy.</div>
</div>
<div class="sidebar-sim">
<div class="nav-group-label">Resolve</div>
<div class="nav-item active ic-sessions">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>
<span>Sessions</span>
<span class="badge">3</span>
</div>
<div class="nav-item ic-flows">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/></svg></div>
<span>All Flows</span>
<span class="badge">32</span>
</div>
<div class="nav-item ic-ai-copilot">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M12 18v-5"/><path d="M12 5v1"/></svg></div>
<span>FlowPilot</span>
</div>
<div class="nav-item ic-scripts">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg></div>
<span>Script Library</span>
</div>
<div class="nav-group-label">Build</div>
<div class="nav-item ic-editor">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg></div>
<span>Flow Editor</span>
</div>
<div class="nav-item ic-ai-build">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72Z"/><path d="m14 7 3 3"/><path d="M5 6v4"/><path d="M19 14v4"/><path d="M10 2v2"/><path d="M7 8H3"/><path d="M21 16h-4"/><path d="M11 3H9"/></svg></div>
<span>Flow Assist</span>
</div>
<div class="nav-item ic-steplib">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg></div>
<span>Step Library</span>
</div>
<div class="nav-item ic-kb">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg></div>
<span>KB Accelerator</span>
</div>
<div class="nav-group-label">Insights</div>
<div class="nav-item ic-dashboard">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></div>
<span>Dashboard</span>
</div>
<div class="nav-item ic-exports">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M4 7V4a2 2 0 0 1 2-2 2 2 0 0 0-2 2"/><path d="M4.063 20.999a2 2 0 0 0 2 1L18 22a2 2 0 0 0 2-2V7l-5-5H6"/><path d="m10 18 3-3-3-3"/><path d="M4 15h9"/></svg></div>
<span>Exports</span>
</div>
<div class="nav-item ic-analytics">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><line x1="12" x2="12" y1="20" y2="10"/><line x1="18" x2="18" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="16"/></svg></div>
<span>Analytics</span>
</div>
<div class="footer-section">
<div class="nav-item ic-guides">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg></div>
<span>User Guides</span>
</div>
<div class="nav-item ic-feedback">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg></div>
<span>Feedback</span>
</div>
<div class="nav-item ic-settings">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg></div>
<span>Account</span>
</div>
</div>
</div>
</div>
<!-- A2: Dividers instead of labels -->
<div class="variant">
<div class="variant-header">
<div class="variant-tag">Concept A2</div>
<div class="variant-title">With Dividers Only</div>
<div class="variant-desc">Same grouping but uses subtle divider lines instead of text labels. Cleaner, less visual noise. Groups are implied by proximity.</div>
</div>
<div class="sidebar-sim">
<div class="nav-item active ic-sessions" style="margin-top: 6px">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>
<span>Sessions</span>
<span class="badge">3</span>
</div>
<div class="nav-item ic-flows">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/></svg></div>
<span>All Flows</span>
<span class="badge">32</span>
</div>
<div class="nav-item ic-ai-copilot">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M12 18v-5"/><path d="M12 5v1"/></svg></div>
<span>FlowPilot</span>
</div>
<div class="nav-item ic-scripts">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg></div>
<span>Script Library</span>
</div>
<div class="nav-divider"></div>
<div class="nav-item ic-editor">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg></div>
<span>Flow Editor</span>
</div>
<div class="nav-item ic-ai-build">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72Z"/><path d="m14 7 3 3"/><path d="M5 6v4"/><path d="M19 14v4"/><path d="M10 2v2"/><path d="M7 8H3"/><path d="M21 16h-4"/><path d="M11 3H9"/></svg></div>
<span>Flow Assist</span>
</div>
<div class="nav-item ic-steplib">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg></div>
<span>Step Library</span>
</div>
<div class="nav-item ic-kb">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg></div>
<span>KB Accelerator</span>
</div>
<div class="nav-divider"></div>
<div class="nav-item ic-dashboard">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></div>
<span>Dashboard</span>
</div>
<div class="nav-item ic-exports">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M4 7V4a2 2 0 0 1 2-2 2 2 0 0 0-2 2"/><path d="M4.063 20.999a2 2 0 0 0 2 1L18 22a2 2 0 0 0 2-2V7l-5-5H6"/><path d="m10 18 3-3-3-3"/><path d="M4 15h9"/></svg></div>
<span>Exports</span>
</div>
<div class="nav-item ic-analytics">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><line x1="12" x2="12" y1="20" y2="10"/><line x1="18" x2="18" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="16"/></svg></div>
<span>Analytics</span>
</div>
<div class="footer-section">
<div class="nav-item ic-guides">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg></div>
<span>User Guides</span>
</div>
<div class="nav-item ic-feedback">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg></div>
<span>Feedback</span>
</div>
<div class="nav-item ic-settings">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg></div>
<span>Account</span>
</div>
</div>
</div>
</div>
<!-- A3: Dashboard at top, then workflow groups -->
<div class="variant">
<div class="variant-header">
<div class="variant-tag">Concept A3</div>
<div class="variant-title">Dashboard First</div>
<div class="variant-desc">Dashboard stays at the top as the "home" landing, then workflow groups follow. Some engineers want the overview first before diving into work.</div>
</div>
<div class="sidebar-sim">
<div class="nav-item ic-dashboard" style="margin-top: 6px">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></div>
<span>Dashboard</span>
</div>
<div class="nav-group-label">Resolve</div>
<div class="nav-item active ic-sessions">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>
<span>Sessions</span>
<span class="badge">3</span>
</div>
<div class="nav-item ic-flows">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/></svg></div>
<span>All Flows</span>
<span class="badge">32</span>
</div>
<div class="nav-item ic-ai-copilot">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M12 18v-5"/><path d="M12 5v1"/></svg></div>
<span>FlowPilot</span>
</div>
<div class="nav-item ic-scripts">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg></div>
<span>Script Library</span>
</div>
<div class="nav-group-label">Build</div>
<div class="nav-item ic-editor">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg></div>
<span>Flow Editor</span>
</div>
<div class="nav-item ic-ai-build">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72Z"/><path d="m14 7 3 3"/><path d="M5 6v4"/><path d="M19 14v4"/><path d="M10 2v2"/><path d="M7 8H3"/><path d="M21 16h-4"/><path d="M11 3H9"/></svg></div>
<span>Flow Assist</span>
</div>
<div class="nav-item ic-steplib">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg></div>
<span>Step Library</span>
</div>
<div class="nav-item ic-kb">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg></div>
<span>KB Accelerator</span>
</div>
<div class="nav-group-label">Insights</div>
<div class="nav-item ic-exports">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M4 7V4a2 2 0 0 1 2-2 2 2 0 0 0-2 2"/><path d="M4.063 20.999a2 2 0 0 0 2 1L18 22a2 2 0 0 0 2-2V7l-5-5H6"/><path d="m10 18 3-3-3-3"/><path d="M4 15h9"/></svg></div>
<span>Exports</span>
</div>
<div class="nav-item ic-analytics">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><line x1="12" x2="12" y1="20" y2="10"/><line x1="18" x2="18" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="16"/></svg></div>
<span>Analytics</span>
</div>
<div class="footer-section">
<div class="nav-item ic-guides">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg></div>
<span>User Guides</span>
</div>
<div class="nav-item ic-feedback">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg></div>
<span>Feedback</span>
</div>
<div class="nav-item ic-settings">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg></div>
<span>Account</span>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════
CONCEPT B — Two Groups (Work / Build) + Dashboard
═══════════════════════════════════════════════════════ -->
<div class="section-header">
<h2>Concept B — Work / Build (Simpler Split)</h2>
<p>Only two groups instead of three. Dashboard top, then "Work" (active troubleshooting) and "Build" (authoring + review). Exports and Analytics move into Build since they're about improving process, not resolving tickets.</p>
</div>
<div class="variants-grid">
<div class="variant">
<div class="variant-header">
<div class="variant-tag">Concept B1</div>
<div class="variant-title">Two Groups + Dashboard</div>
<div class="variant-desc">Fewer sections = less cognitive load. "Work" is what you do when tickets come in. "Build" is everything else.</div>
</div>
<div class="sidebar-sim">
<div class="nav-item ic-dashboard" style="margin-top: 6px">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></div>
<span>Dashboard</span>
</div>
<div class="nav-group-label">Work</div>
<div class="nav-item active ic-sessions">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>
<span>Sessions</span>
<span class="badge">3</span>
</div>
<div class="nav-item ic-flows">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/></svg></div>
<span>All Flows</span>
<span class="badge">32</span>
</div>
<div class="nav-item ic-ai-copilot">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M12 18v-5"/><path d="M12 5v1"/></svg></div>
<span>FlowPilot</span>
</div>
<div class="nav-item ic-scripts">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg></div>
<span>Script Library</span>
</div>
<div class="nav-item ic-exports">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M4 7V4a2 2 0 0 1 2-2 2 2 0 0 0-2 2"/><path d="M4.063 20.999a2 2 0 0 0 2 1L18 22a2 2 0 0 0 2-2V7l-5-5H6"/><path d="m10 18 3-3-3-3"/><path d="M4 15h9"/></svg></div>
<span>Exports</span>
</div>
<div class="nav-group-label">Build</div>
<div class="nav-item ic-editor">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg></div>
<span>Flow Editor</span>
</div>
<div class="nav-item ic-ai-build">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72Z"/><path d="m14 7 3 3"/><path d="M5 6v4"/><path d="M19 14v4"/><path d="M10 2v2"/><path d="M7 8H3"/><path d="M21 16h-4"/><path d="M11 3H9"/></svg></div>
<span>Flow Assist</span>
</div>
<div class="nav-item ic-steplib">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg></div>
<span>Step Library</span>
</div>
<div class="nav-item ic-kb">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg></div>
<span>KB Accelerator</span>
</div>
<div class="nav-item ic-analytics">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><line x1="12" x2="12" y1="20" y2="10"/><line x1="18" x2="18" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="16"/></svg></div>
<span>Analytics</span>
</div>
<div class="footer-section">
<div class="nav-item ic-guides">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg></div>
<span>User Guides</span>
</div>
<div class="nav-item ic-feedback">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg></div>
<span>Feedback</span>
</div>
<div class="nav-item ic-settings">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg></div>
<span>Account</span>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════
AI NAMING OPTIONS
═══════════════════════════════════════════════════════ -->
<div class="ai-naming">
<h2>AI Assistant Split — Naming Options</h2>
<div class="naming-grid">
<div class="naming-card">
<h3 style="color: var(--fuchsia)">Troubleshooting AI (Copilot)</h3>
<p class="purpose">Used during active sessions. Suggests next steps, explains errors, provides context from ticket data and knowledge base.</p>
<div class="naming-option recommended">
<div class="icon-sm" style="color: var(--fuchsia)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M12 18v-5"/><path d="M12 5v1"/></svg></div>
<span class="name">FlowPilot</span>
<span class="desc">Brain icon — suggests intelligence guiding you. Already used in the copilot panel.</span>
</div>
<div class="naming-option">
<div class="icon-sm" style="color: var(--fuchsia)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg></div>
<span class="name">AI Copilot</span>
<span class="desc">Current robot icon — generic but clear purpose</span>
</div>
<div class="naming-option">
<div class="icon-sm" style="color: var(--fuchsia)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3Z"/></svg></div>
<span class="name">Resolve AI</span>
<span class="desc">Sparkle icon — ties to the "Resolve" section name</span>
</div>
</div>
<div class="naming-card">
<h3 style="color: var(--pink)">Flow Builder AI (Authoring)</h3>
<p class="purpose">Used when building new flows from scratch. Conversational interface to generate decision trees, procedural steps, and intake forms.</p>
<div class="naming-option recommended">
<div class="icon-sm" style="color: var(--pink)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72Z"/><path d="m14 7 3 3"/><path d="M5 6v4"/><path d="M19 14v4"/><path d="M10 2v2"/><path d="M7 8H3"/><path d="M21 16h-4"/><path d="M11 3H9"/></svg></div>
<span class="name">Flow Assist</span>
<span class="desc">Wand icon — creation/magic. Already the name of the embedded editor AI.</span>
</div>
<div class="naming-option">
<div class="icon-sm" style="color: var(--pink)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg></div>
<span class="name">AI Builder</span>
<span class="desc">Robot icon — explicit "builder" label</span>
</div>
<div class="naming-option">
<div class="icon-sm" style="color: var(--pink)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg></div>
<span class="name">Flow Forge</span>
<span class="desc">Wrench icon — "forge" = crafting/building</span>
</div>
</div>
</div>
</div>
<script>
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', () => item.classList.toggle('active'));
});
</script>
</body>
</html>

View File

@@ -0,0 +1,734 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ResolutionFlow — Sidebar Icon Concepts (Refined)</title>
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,700&family=IBM+Plex+Sans:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #101114;
--card: #17191d;
--fg: #f8fafc;
--fg-muted: #8891a0;
--fg-dim: #5a6170;
--border: rgba(255,255,255,0.06);
--border-hover: rgba(255,255,255,0.12);
--cyan-500: #06b6d4;
--cyan-400: #22d3ee;
--sidebar-hover: #212329;
--sidebar-active: rgba(6,182,212,0.10);
--glass-bg: rgba(22,24,28,0.55);
--glass-blur: blur(16px);
--amber: #f59e0b; --amber-soft: rgba(245,158,11,0.12);
--emerald: #34d399; --emerald-soft: rgba(52,211,153,0.12);
--violet: #a78bfa; --violet-soft: rgba(167,139,250,0.12);
--rose: #fb7185; --rose-soft: rgba(251,113,133,0.12);
--blue: #60a5fa; --blue-soft: rgba(96,165,250,0.12);
--orange: #fb923c; --orange-soft: rgba(251,146,60,0.12);
--teal: #2dd4bf; --teal-soft: rgba(45,212,191,0.12);
--sky: #38bdf8; --sky-soft: rgba(56,189,248,0.12);
--lime: #a3e635; --lime-soft: rgba(163,230,53,0.12);
--indigo: #818cf8; --indigo-soft: rgba(129,140,248,0.12);
--fuchsia: #e879f9; --fuchsia-soft: rgba(232,121,249,0.12);
}
body {
font-family: 'IBM Plex Sans', system-ui, sans-serif;
background: var(--bg);
color: var(--fg);
min-height: 100vh;
padding: 40px;
}
h1 { font-family: 'Bricolage Grotesque', sans-serif; font-weight: 700; font-size: 2rem; letter-spacing: -0.03em; margin-bottom: 6px; }
h1 span { color: var(--cyan-400); }
.subtitle { color: var(--fg-muted); font-size: 0.9375rem; margin-bottom: 48px; max-width: 780px; line-height: 1.6; }
/* ── Section headers ── */
.section-header {
margin-bottom: 24px;
margin-top: 56px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border);
}
.section-header:first-of-type { margin-top: 0; }
.section-header h2 {
font-family: 'Bricolage Grotesque', sans-serif;
font-weight: 700;
font-size: 1.5rem;
letter-spacing: -0.02em;
margin-bottom: 4px;
}
.section-header p { font-size: 0.8125rem; color: var(--fg-dim); line-height: 1.5; }
/* ── Grid of variants ── */
.variants-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
max-width: 1200px;
}
@media (max-width: 1000px) { .variants-grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 680px) { .variants-grid { grid-template-columns: 1fr; } }
.variant {
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
border: 1px solid var(--border);
border-radius: 16px;
overflow: hidden;
transition: border-color 300ms, box-shadow 300ms;
}
.variant:hover { border-color: var(--border-hover); box-shadow: 0 12px 40px rgba(0,0,0,0.4); }
.variant-header {
padding: 16px 20px 12px;
border-bottom: 1px solid var(--border);
}
.variant-tag {
font-family: 'JetBrains Mono', monospace;
font-size: 0.5625rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--cyan-400);
margin-bottom: 4px;
}
.variant-title {
font-family: 'Bricolage Grotesque', sans-serif;
font-weight: 700;
font-size: 1rem;
letter-spacing: -0.02em;
margin-bottom: 2px;
}
.variant-desc { font-size: 0.75rem; color: var(--fg-dim); line-height: 1.4; }
/* ── Sidebar simulation ── */
.sidebar-sim { padding: 8px 12px; }
.nav-section-label {
font-family: 'JetBrains Mono', monospace;
font-size: 0.5625rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--fg-dim);
padding: 10px 12px 4px;
}
.nav-item {
display: flex; align-items: center; gap: 12px;
padding: 8px 12px; border-radius: 10px;
font-size: 0.8125rem; font-weight: 500;
color: var(--fg-muted); cursor: pointer;
transition: all 150ms ease; position: relative;
}
.nav-item:hover { background: var(--sidebar-hover); color: var(--fg); }
.nav-item.active { background: var(--sidebar-active); color: var(--fg); }
.nav-item .badge {
margin-left: auto;
font-family: 'JetBrains Mono', monospace; font-size: 0.6875rem;
color: var(--fg-dim); background: var(--card);
border: 1px solid var(--border); padding: 1px 8px; border-radius: 9999px;
}
.nav-item.active::before {
content: ''; position: absolute; left: 0; top: 50%;
transform: translateY(-50%); width: 3px; height: 24px;
border-radius: 0 4px 4px 0;
background: linear-gradient(135deg, var(--cyan-500), var(--cyan-400));
}
/* ── Icon wrap ── */
.icon-wrap {
display: flex; align-items: center; justify-content: center;
width: 20px; height: 20px; flex-shrink: 0; position: relative;
}
.icon-wrap svg {
width: 18px; height: 18px; stroke-width: 1.75;
fill: none; stroke: currentColor;
stroke-linecap: round; stroke-linejoin: round;
}
/* ── CONCEPT 1: Semantic Colored Icons ── */
.c1 .icon-wrap { transition: transform 150ms ease; }
.c1 .nav-item:hover .icon-wrap { transform: scale(1.1); }
.c1 .ic-dashboard svg { stroke: var(--cyan-400); }
.c1 .ic-flows svg { stroke: var(--violet); }
.c1 .ic-editor svg { stroke: var(--amber); }
.c1 .ic-sessions svg { stroke: var(--emerald); }
.c1 .ic-exports svg { stroke: var(--blue); }
.c1 .ic-ai svg { stroke: var(--fuchsia); }
.c1 .ic-steplib svg { stroke: var(--orange); }
.c1 .ic-scripts svg { stroke: var(--teal); }
.c1 .ic-kb svg { stroke: var(--rose); }
.c1 .ic-analytics svg{ stroke: var(--sky); }
/* ── CONCEPT 2: Tinted Pill Backgrounds ── */
.c2 .icon-wrap {
width: 28px; height: 28px; border-radius: 8px;
transition: transform 150ms ease, box-shadow 200ms ease;
}
.c2 .nav-item:hover .icon-wrap { transform: scale(1.08); }
.c2 .icon-wrap svg { width: 16px; height: 16px; }
.c2 .ic-dashboard .icon-wrap { background: rgba(6,182,212,0.12); }
.c2 .ic-dashboard svg { stroke: var(--cyan-400); }
.c2 .ic-flows .icon-wrap { background: var(--violet-soft); }
.c2 .ic-flows svg { stroke: var(--violet); }
.c2 .ic-editor .icon-wrap { background: var(--amber-soft); }
.c2 .ic-editor svg { stroke: var(--amber); }
.c2 .ic-sessions .icon-wrap { background: var(--emerald-soft); }
.c2 .ic-sessions svg { stroke: var(--emerald); }
.c2 .ic-exports .icon-wrap { background: var(--blue-soft); }
.c2 .ic-exports svg { stroke: var(--blue); }
.c2 .ic-ai .icon-wrap { background: var(--fuchsia-soft); }
.c2 .ic-ai svg { stroke: var(--fuchsia); }
.c2 .ic-steplib .icon-wrap { background: var(--orange-soft); }
.c2 .ic-steplib svg { stroke: var(--orange); }
.c2 .ic-scripts .icon-wrap { background: var(--teal-soft); }
.c2 .ic-scripts svg { stroke: var(--teal); }
.c2 .ic-kb .icon-wrap { background: var(--rose-soft); }
.c2 .ic-kb svg { stroke: var(--rose); }
.c2 .ic-analytics .icon-wrap { background: var(--sky-soft); }
.c2 .ic-analytics svg { stroke: var(--sky); }
.c2 .nav-item.active.ic-dashboard .icon-wrap { box-shadow: 0 0 12px rgba(6,182,212,0.3); }
.c2 .nav-item.active.ic-sessions .icon-wrap { box-shadow: 0 0 12px rgba(52,211,153,0.3); }
.c2 .nav-item.active.ic-flows .icon-wrap { box-shadow: 0 0 12px rgba(167,139,250,0.3); }
/* ── Icon label under each nav section ── */
.icon-label {
font-family: 'JetBrains Mono', monospace;
font-size: 0.5rem;
color: var(--fg-dim);
opacity: 0.5;
margin-left: auto;
white-space: nowrap;
}
.nav-item:hover .icon-label { opacity: 0.8; }
/* ── Comparison table ── */
.comparison {
margin-top: 56px;
max-width: 1200px;
}
.comparison h2 {
font-family: 'Bricolage Grotesque', sans-serif;
font-weight: 700; font-size: 1.25rem;
letter-spacing: -0.02em; margin-bottom: 16px;
}
.comp-table {
width: 100%;
border-collapse: collapse;
font-size: 0.8125rem;
}
.comp-table th {
font-family: 'JetBrains Mono', monospace;
font-size: 0.625rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--fg-dim);
text-align: left;
padding: 8px 12px;
border-bottom: 1px solid var(--border);
}
.comp-table td {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
color: var(--fg-muted);
vertical-align: top;
}
.comp-table td:first-child {
color: var(--fg);
font-weight: 500;
white-space: nowrap;
}
.comp-table .current { color: var(--fg-dim); }
.comp-table .pick { color: var(--cyan-400); font-weight: 500; }
.comp-table .alt { color: var(--fg-muted); }
</style>
</head>
<body>
<h1>Sidebar Icons — <span>Style × Shape Matrix</span></h1>
<p class="subtitle">
Concept 1 (Semantic Colors) and Concept 2 (Tinted Pills) shown with 3 different icon sets each.
<strong>Current</strong> = existing Lucide icons, <strong>Set A</strong> = more descriptive/metaphorical icons,
<strong>Set B</strong> = minimal/geometric alternatives. Click any nav item to toggle active state.
</p>
<!-- ════════════════════════════════════════════════════════
CONCEPT 1 — SEMANTIC COLORED ICONS × 3 icon sets
════════════════════════════════════════════════════════ -->
<div class="section-header">
<h2>Concept 1 — Semantic Colored Icons</h2>
<p>Always-on color per icon. No pill background — just the stroke color creates landmarks.</p>
</div>
<div class="variants-grid">
<!-- C1 × Current Icons -->
<div class="variant">
<div class="variant-header">
<div class="variant-tag">Concept 1 × Current Icons</div>
<div class="variant-title">Original Lucide Set</div>
<div class="variant-desc">LayoutGrid, Box, PenLine, Clock, FileText, BotMessageSquare, Bookmark, Terminal, Sparkles, BarChart3</div>
</div>
<div class="sidebar-sim c1">
<div class="nav-section-label">Navigation</div>
<!-- Dashboard: LayoutGrid -->
<div class="nav-item active ic-dashboard">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></div>
<span>Dashboard</span>
</div>
<!-- All Flows: Box -->
<div class="nav-item ic-flows">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg></div>
<span>All Flows</span>
<span class="badge">12</span>
</div>
<!-- Flow Editor: PenLine -->
<div class="nav-item ic-editor">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg></div>
<span>Flow Editor</span>
</div>
<!-- Sessions: Clock -->
<div class="nav-item ic-sessions">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>
<span>Sessions</span>
<span class="badge">3</span>
</div>
<!-- Exports: FileText -->
<div class="nav-item ic-exports">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg></div>
<span>Exports</span>
</div>
<!-- AI Assistant: BotMessageSquare -->
<div class="nav-item ic-ai">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg></div>
<span>AI Assistant</span>
</div>
<!-- Step Library: Bookmark -->
<div class="nav-item ic-steplib">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/></svg></div>
<span>Step Library</span>
</div>
<!-- Script Library: Terminal -->
<div class="nav-item ic-scripts">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg></div>
<span>Script Library</span>
</div>
<!-- KB Accelerator: Sparkles -->
<div class="nav-item ic-kb">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3Z"/></svg></div>
<span>KB Accelerator</span>
</div>
<!-- Analytics: BarChart3 -->
<div class="nav-item ic-analytics">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><line x1="12" x2="12" y1="20" y2="10"/><line x1="18" x2="18" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="16"/></svg></div>
<span>Analytics</span>
</div>
</div>
</div>
<!-- C1 × Icon Set A — Descriptive/Metaphorical -->
<div class="variant">
<div class="variant-header">
<div class="variant-tag">Concept 1 × Icon Set A</div>
<div class="variant-title">Descriptive / Metaphorical</div>
<div class="variant-desc">Gauge, GitFork, Wrench, Zap, Share2, Brain, Layers, Code2, Rocket, TrendingUp</div>
</div>
<div class="sidebar-sim c1">
<div class="nav-section-label">Navigation</div>
<!-- Dashboard: Gauge (speedometer) -->
<div class="nav-item active ic-dashboard">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/></svg></div>
<span>Dashboard</span>
</div>
<!-- All Flows: GitFork (branching paths) -->
<div class="nav-item ic-flows">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="12" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><path d="M18 9v2c0 .6-.4 1-1 1H7c-.6 0-1-.4-1-1V9"/><path d="M12 12v3"/></svg></div>
<span>All Flows</span>
<span class="badge">12</span>
</div>
<!-- Flow Editor: Wrench (builder) -->
<div class="nav-item ic-editor">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg></div>
<span>Flow Editor</span>
</div>
<!-- Sessions: Zap (active/energy) -->
<div class="nav-item ic-sessions">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg></div>
<span>Sessions</span>
<span class="badge">3</span>
</div>
<!-- Exports: Share2 (share network) -->
<div class="nav-item ic-exports">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" x2="15.42" y1="13.51" y2="17.49"/><line x1="15.41" x2="8.59" y1="6.51" y2="10.49"/></svg></div>
<span>Exports</span>
</div>
<!-- AI Assistant: Brain -->
<div class="nav-item ic-ai">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M12 18v-5"/><path d="M12 5v1"/></svg></div>
<span>AI Assistant</span>
</div>
<!-- Step Library: Layers (stacked) -->
<div class="nav-item ic-steplib">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/></svg></div>
<span>Step Library</span>
</div>
<!-- Script Library: Code2 (code brackets) -->
<div class="nav-item ic-scripts">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg></div>
<span>Script Library</span>
</div>
<!-- KB Accelerator: Rocket -->
<div class="nav-item ic-kb">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg></div>
<span>KB Accelerator</span>
</div>
<!-- Analytics: TrendingUp -->
<div class="nav-item ic-analytics">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/></svg></div>
<span>Analytics</span>
</div>
</div>
</div>
<!-- C1 × Icon Set B — Minimal/Geometric -->
<div class="variant">
<div class="variant-header">
<div class="variant-tag">Concept 1 × Icon Set B</div>
<div class="variant-title">Minimal / Geometric</div>
<div class="variant-desc">Compass, Network, PenTool, Radio, FileOutput, Wand2, Library, ScrollText, Lightbulb, PieChart</div>
</div>
<div class="sidebar-sim c1">
<div class="nav-section-label">Navigation</div>
<!-- Dashboard: Compass (navigation hub) -->
<div class="nav-item active ic-dashboard">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg></div>
<span>Dashboard</span>
</div>
<!-- All Flows: Network (connected nodes) -->
<div class="nav-item ic-flows">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/></svg></div>
<span>All Flows</span>
<span class="badge">12</span>
</div>
<!-- Flow Editor: PenTool (vector pen) -->
<div class="nav-item ic-editor">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m12 19 7-7 3 3-7 7-3-3z"/><path d="m18 13-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="m2 2 7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg></div>
<span>Flow Editor</span>
</div>
<!-- Sessions: Radio (live/active) -->
<div class="nav-item ic-sessions">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.4"/><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.4"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/><circle cx="12" cy="12" r="2"/></svg></div>
<span>Sessions</span>
<span class="badge">3</span>
</div>
<!-- Exports: FileOutput (file with arrow) -->
<div class="nav-item ic-exports">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M4 7V4a2 2 0 0 1 2-2 2 2 0 0 0-2 2"/><path d="M4.063 20.999a2 2 0 0 0 2 1L18 22a2 2 0 0 0 2-2V7l-5-5H6"/><path d="m10 18 3-3-3-3"/><path d="M4 15h9"/></svg></div>
<span>Exports</span>
</div>
<!-- AI Assistant: Wand2 (magic wand) -->
<div class="nav-item ic-ai">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72Z"/><path d="m14 7 3 3"/><path d="M5 6v4"/><path d="M19 14v4"/><path d="M10 2v2"/><path d="M7 8H3"/><path d="M21 16h-4"/><path d="M11 3H9"/></svg></div>
<span>AI Assistant</span>
</div>
<!-- Step Library: Library -->
<div class="nav-item ic-steplib">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg></div>
<span>Step Library</span>
</div>
<!-- Script Library: ScrollText -->
<div class="nav-item ic-scripts">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M8 21h12a2 2 0 0 0 2-2v-2H10v2a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v3h4"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M15 8h-5"/><path d="M15 12h-5"/></svg></div>
<span>Script Library</span>
</div>
<!-- KB Accelerator: Lightbulb -->
<div class="nav-item ic-kb">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg></div>
<span>KB Accelerator</span>
</div>
<!-- Analytics: PieChart -->
<div class="nav-item ic-analytics">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M21.21 15.89A10 10 0 1 1 8 2.83"/><path d="M22 12A10 10 0 0 0 12 2v10z"/></svg></div>
<span>Analytics</span>
</div>
</div>
</div>
</div>
<!-- ════════════════════════════════════════════════════════
CONCEPT 2 — TINTED PILL BACKGROUNDS × 3 icon sets
════════════════════════════════════════════════════════ -->
<div class="section-header">
<h2>Concept 2 — Tinted Pill Backgrounds</h2>
<p>Colored icon inside a soft tinted rounded-square. Creates visual weight — feels like an app dock.</p>
</div>
<div class="variants-grid">
<!-- C2 × Current Icons -->
<div class="variant">
<div class="variant-header">
<div class="variant-tag">Concept 2 × Current Icons</div>
<div class="variant-title">Original Lucide Set</div>
<div class="variant-desc">LayoutGrid, Box, PenLine, Clock, FileText, BotMessageSquare, Bookmark, Terminal, Sparkles, BarChart3</div>
</div>
<div class="sidebar-sim c2">
<div class="nav-section-label">Navigation</div>
<div class="nav-item active ic-dashboard">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></div>
<span>Dashboard</span>
</div>
<div class="nav-item ic-flows">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg></div>
<span>All Flows</span>
<span class="badge">12</span>
</div>
<div class="nav-item ic-editor">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg></div>
<span>Flow Editor</span>
</div>
<div class="nav-item ic-sessions">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>
<span>Sessions</span>
<span class="badge">3</span>
</div>
<div class="nav-item ic-exports">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg></div>
<span>Exports</span>
</div>
<div class="nav-item ic-ai">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg></div>
<span>AI Assistant</span>
</div>
<div class="nav-item ic-steplib">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/></svg></div>
<span>Step Library</span>
</div>
<div class="nav-item ic-scripts">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg></div>
<span>Script Library</span>
</div>
<div class="nav-item ic-kb">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3Z"/></svg></div>
<span>KB Accelerator</span>
</div>
<div class="nav-item ic-analytics">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><line x1="12" x2="12" y1="20" y2="10"/><line x1="18" x2="18" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="16"/></svg></div>
<span>Analytics</span>
</div>
</div>
</div>
<!-- C2 × Icon Set A — Descriptive/Metaphorical -->
<div class="variant">
<div class="variant-header">
<div class="variant-tag">Concept 2 × Icon Set A</div>
<div class="variant-title">Descriptive / Metaphorical</div>
<div class="variant-desc">Gauge, GitFork, Wrench, Zap, Share2, Brain, Layers, Code2, Rocket, TrendingUp</div>
</div>
<div class="sidebar-sim c2">
<div class="nav-section-label">Navigation</div>
<div class="nav-item active ic-dashboard">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/></svg></div>
<span>Dashboard</span>
</div>
<div class="nav-item ic-flows">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="12" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><path d="M18 9v2c0 .6-.4 1-1 1H7c-.6 0-1-.4-1-1V9"/><path d="M12 12v3"/></svg></div>
<span>All Flows</span>
<span class="badge">12</span>
</div>
<div class="nav-item ic-editor">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg></div>
<span>Flow Editor</span>
</div>
<div class="nav-item ic-sessions">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg></div>
<span>Sessions</span>
<span class="badge">3</span>
</div>
<div class="nav-item ic-exports">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" x2="15.42" y1="13.51" y2="17.49"/><line x1="15.41" x2="8.59" y1="6.51" y2="10.49"/></svg></div>
<span>Exports</span>
</div>
<div class="nav-item ic-ai">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M12 18v-5"/><path d="M12 5v1"/></svg></div>
<span>AI Assistant</span>
</div>
<div class="nav-item ic-steplib">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/></svg></div>
<span>Step Library</span>
</div>
<div class="nav-item ic-scripts">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg></div>
<span>Script Library</span>
</div>
<div class="nav-item ic-kb">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg></div>
<span>KB Accelerator</span>
</div>
<div class="nav-item ic-analytics">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/></svg></div>
<span>Analytics</span>
</div>
</div>
</div>
<!-- C2 × Icon Set B — Minimal/Geometric -->
<div class="variant">
<div class="variant-header">
<div class="variant-tag">Concept 2 × Icon Set B</div>
<div class="variant-title">Minimal / Geometric</div>
<div class="variant-desc">Compass, Network, PenTool, Radio, FileOutput, Wand2, Library, ScrollText, Lightbulb, PieChart</div>
</div>
<div class="sidebar-sim c2">
<div class="nav-section-label">Navigation</div>
<div class="nav-item active ic-dashboard">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg></div>
<span>Dashboard</span>
</div>
<div class="nav-item ic-flows">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/></svg></div>
<span>All Flows</span>
<span class="badge">12</span>
</div>
<div class="nav-item ic-editor">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m12 19 7-7 3 3-7 7-3-3z"/><path d="m18 13-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="m2 2 7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg></div>
<span>Flow Editor</span>
</div>
<div class="nav-item ic-sessions">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.4"/><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.4"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/><circle cx="12" cy="12" r="2"/></svg></div>
<span>Sessions</span>
<span class="badge">3</span>
</div>
<div class="nav-item ic-exports">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M4 7V4a2 2 0 0 1 2-2 2 2 0 0 0-2 2"/><path d="M4.063 20.999a2 2 0 0 0 2 1L18 22a2 2 0 0 0 2-2V7l-5-5H6"/><path d="m10 18 3-3-3-3"/><path d="M4 15h9"/></svg></div>
<span>Exports</span>
</div>
<div class="nav-item ic-ai">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72Z"/><path d="m14 7 3 3"/><path d="M5 6v4"/><path d="M19 14v4"/><path d="M10 2v2"/><path d="M7 8H3"/><path d="M21 16h-4"/><path d="M11 3H9"/></svg></div>
<span>AI Assistant</span>
</div>
<div class="nav-item ic-steplib">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg></div>
<span>Step Library</span>
</div>
<div class="nav-item ic-scripts">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M8 21h12a2 2 0 0 0 2-2v-2H10v2a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v3h4"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M15 8h-5"/><path d="M15 12h-5"/></svg></div>
<span>Script Library</span>
</div>
<div class="nav-item ic-kb">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg></div>
<span>KB Accelerator</span>
</div>
<div class="nav-item ic-analytics">
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M21.21 15.89A10 10 0 1 1 8 2.83"/><path d="M22 12A10 10 0 0 0 12 2v10z"/></svg></div>
<span>Analytics</span>
</div>
</div>
</div>
</div>
<!-- ════════════════════════════════════════════════════════
ICON COMPARISON TABLE
════════════════════════════════════════════════════════ -->
<div class="comparison">
<h2>Icon Comparison Reference</h2>
<table class="comp-table">
<thead>
<tr>
<th>Nav Item</th>
<th>Current</th>
<th>Set A (Descriptive)</th>
<th>Set B (Minimal)</th>
<th>Color</th>
</tr>
</thead>
<tbody>
<tr>
<td>Dashboard</td>
<td class="current">LayoutGrid — 4 squares</td>
<td class="pick">Gauge — speedometer, "command center"</td>
<td class="alt">Compass — navigation hub</td>
<td style="color: var(--cyan-400)">Cyan</td>
</tr>
<tr>
<td>All Flows</td>
<td class="current">Box — generic 3D cube</td>
<td class="pick">GitFork — branching paths (literally what flows are)</td>
<td class="alt">Network — connected nodes</td>
<td style="color: var(--violet)">Violet</td>
</tr>
<tr>
<td>Flow Editor</td>
<td class="current">PenLine — pen tip</td>
<td class="alt">Wrench — builder tool</td>
<td class="alt">PenTool — vector/design pen</td>
<td style="color: var(--amber)">Amber</td>
</tr>
<tr>
<td>Sessions</td>
<td class="current">Clock — time-based</td>
<td class="pick">Zap — active energy, live sessions</td>
<td class="alt">Radio — broadcasting/live signal</td>
<td style="color: var(--emerald)">Emerald</td>
</tr>
<tr>
<td>Exports</td>
<td class="current">FileText — generic doc</td>
<td class="alt">Share2 — share network nodes</td>
<td class="alt">FileOutput — file with arrow out</td>
<td style="color: var(--blue)">Blue</td>
</tr>
<tr>
<td>AI Assistant</td>
<td class="current">BotMessageSquare — robot chat</td>
<td class="pick">Brain — intelligence, organic feel</td>
<td class="alt">Wand2 — magic wand with sparkles</td>
<td style="color: var(--fuchsia)">Fuchsia</td>
</tr>
<tr>
<td>Step Library</td>
<td class="current">Bookmark — generic ribbon</td>
<td class="pick">Layers — stacked layers = reusable steps</td>
<td class="alt">Library — book spines</td>
<td style="color: var(--orange)">Orange</td>
</tr>
<tr>
<td>Script Library</td>
<td class="current">Terminal — prompt cursor</td>
<td class="alt">Code2 — angle brackets with slash</td>
<td class="alt">ScrollText — script/scroll with text</td>
<td style="color: var(--teal)">Teal</td>
</tr>
<tr>
<td>KB Accelerator</td>
<td class="current">Sparkles — magic star</td>
<td class="pick">Rocket — acceleration/speed</td>
<td class="alt">Lightbulb — ideas/insight</td>
<td style="color: var(--rose)">Rose</td>
</tr>
<tr>
<td>Analytics</td>
<td class="current">BarChart3 — vertical bars</td>
<td class="alt">TrendingUp — growth line with arrow</td>
<td class="alt">PieChart — pie segments</td>
<td style="color: var(--sky)">Sky</td>
</tr>
</tbody>
</table>
</div>
<script>
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', () => item.classList.toggle('active'));
});
</script>
</body>
</html>

View File

@@ -0,0 +1,83 @@
# Sidebar Redesign — Context & Decisions
> Branch: `design/sidebar-icon-concepts`
> Date: 2026-03-15
## What's Already Implemented
### Icon Changes (committed)
Swapped generic icons for more descriptive ones:
- Dashboard: `LayoutGrid` (kept)
- All Flows: `Box``Network`
- Flow Editor: `PenLine``Wrench`
- Sessions: `Clock` (kept)
- Exports: `FileText``FileOutput`
- AI Assistant: `BotMessageSquare` (kept)
- Step Library: `Bookmark``Library`
- Script Library: `Terminal``Code2`
- KB Accelerator: `Sparkles``Lightbulb`
- Analytics: `BarChart3` (kept)
### Semantic Icon Colors (committed)
Each nav icon now has a permanent color (Concept 1 style):
| Item | Color | Hex |
|------|-------|-----|
| Dashboard | Cyan | `#22d3ee` |
| All Flows | Violet | `#a78bfa` |
| Flow Editor | Amber | `#f59e0b` |
| Sessions | Emerald | `#34d399` |
| Exports | Blue | `#60a5fa` |
| AI Assistant | Fuchsia | `#e879f9` |
| Step Library | Orange | `#fb923c` |
| Script Library | Teal | `#2dd4bf` |
| KB Accelerator | Rose | `#fb7185` |
| Analytics | Sky | `#38bdf8` |
| User Guides | Lime | `#a3e635` |
| Feedback | Indigo | `#818cf8` |
| Account/Collapse | No color (stays muted) |
Colors defined in `NAV_COLORS` constant in `Sidebar.tsx`, applied via `iconColor` prop on `NavItem`.
### Session History Tab Reorder (committed)
- Default tab: `Active` (was `All`)
- Tab order: Active → Prepared → Completed → All
## Still Deciding
### Nav Grouping
Mockups in `sidebar-grouping-concepts.html`. Four options explored:
**Concept A1 — Three labeled groups:**
- Resolve: Sessions, All Flows, FlowPilot, Script Library
- Build: Flow Editor, Flow Assist, Step Library, KB Accelerator
- Insights: Dashboard, Exports, Analytics
**Concept A2 — Same groups, divider lines only (no labels)**
**Concept A3 — Dashboard first, then Resolve / Build / Insights**
**Concept B1 — Two groups only (Work / Build) with Dashboard on top**
### AI Assistant Split
Currently one "AI Assistant" nav item does two jobs. Considering splitting into:
**In-session copilot (Resolve group):**
- Recommended name: **FlowPilot** (already used in copilot panel)
- Icon: Brain (fuchsia)
- Purpose: helps during active troubleshooting sessions
**Flow builder (Build group):**
- Recommended name: **Flow Assist** (already used in embedded editor AI)
- Icon: Wand2 (pink)
- Purpose: conversational flow creation/authoring
Other naming options explored: AI Copilot, Resolve AI, AI Builder, Flow Forge
## Files Changed
- `frontend/src/components/layout/Sidebar.tsx` — new icons, colors, NAV_COLORS constant
- `frontend/src/components/layout/NavItem.tsx` — added `iconColor` prop
- `frontend/src/pages/SessionHistoryPage.tsx` — default tab + tab order
## Mockup Files (open in browser)
- `docs/plans/Frontend/sidebar-icon-concepts.html` — icon + color comparison (6 panels)
- `docs/plans/Frontend/sidebar-grouping-concepts.html` — grouping + AI naming options

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,271 @@
# Sidebar Redesign — Design Spec
> **Date:** 2026-03-15
> **Branch:** `design/sidebar-icon-concepts`
> **Status:** Approved for implementation
---
## Overview
Redesign the ResolutionFlow sidebar from a flat nav list with a pinned flows section into a structured, activity-aware navigation organized around engineer workflow stages. The sidebar becomes a "shift dashboard" — showing what you're working on, how your day is going, and organizing tools by what you do with them.
## Goals
1. **Surface active work** — engineers should see their in-progress sessions and CW ticket context without navigating away
2. **Organize by workflow** — group nav items into Resolve (working tickets) / Build (authoring flows) / Insights (reviewing results)
3. **Provide daily pulse** — lightweight stats (resolved today, active, time in session) visible at a glance
4. **Clarify AI tools** — split the single "AI Assistant" into two purpose-specific tools (FlowPilot for troubleshooting, Flow Assist for authoring)
5. **Remove low-value features** — pinned/quick access flows are too static for the dynamic MSP workflow
## Full Sidebar Layout (top to bottom)
```
┌─────────────────────────┐
│ Stats Bar │ ← 3 counters: Resolved / Active / In Session
│ [7 done] [2 open] [2h] │
├─────────────────────────┤
│ ● Activity │ ← "Activity" label with emerald clock icon
│ ● O365 Mail Flow #4829 │ ← green dot = active, blue ticket #
│ ● DHCP Scope #4828 │ ← amber dot = paused/idle
│ · · · · · · · · · · · │ ← subtle sub-divider
│ · DNS Resolution 2h │ ← muted dot + relative time
│ · Printer Spooler 5h │
├─────────────────────────┤ ← glass-border divider
│ ▣ Dashboard │ ← standalone, no group label
│ │
│ RESOLVE │ ← group label (JetBrains Mono, uppercase)
│ ◷ Sessions [3] │
│ ⬡ All Flows [32] │
│ ◉ FlowPilot │
│ </> Script Library │
│ │
│ BUILD │
│ 🔧 Flow Editor │
│ ✦ Flow Assist │
│ ⊞ Step Library │
│ 💡 KB Accelerator │
│ │
│ INSIGHTS │
│ 📄 Exports │
│ 📊 Analytics │
│ │
│ ── flex spacer ── │ ← pushes footer to bottom
├─────────────────────────┤ ← glass-border divider
│ 📖 User Guides │
│ 💬 Feedback │
│ ⚙ Account │
│ ◁ Collapse │
└─────────────────────────┘
```
No brand/logo section at top (unchanged from current). Dashboard sits below the divider with no group label — it's visually separated from the activity zone by the divider and from the "Resolve" label by standard spacing (same as current "first nav item before first group" pattern).
## What Changes
### Removed
- **Pinned Flows section** — the entire `PinnedFlowsSection` component, `pinnedFlowsStore`, and `pinnedFlows` API module. MSP engineers' work is too dynamic for static pinning; the activity feed replaces this.
- **Single "AI Assistant" nav item** — replaced by two distinct items
### Added
#### 1. Daily Stats Bar (top of sidebar)
Three compact counters in a horizontal row (`display: flex; gap: 2px`), each counter is a flex-1 cell.
| Stat | Color | Source |
|------|-------|--------|
| Resolved | `#34d399` (emerald) | Count of sessions completed today with `outcome = 'resolved'` |
| Active | `#22d3ee` (cyan) | Count of sessions where `completed_at IS NULL` |
| In Session | `#8891a0` (muted) | Sum of time spent in active sessions today (new metric — snapshot, acceptable to be stale) |
**Dimensions:** `padding: 8px 12px 4px` on the container. Each stat cell: `padding: 6px 4px`, `border-radius: 6px`, `background: rgba(255,255,255,0.02)`. Value: `JetBrains Mono, 14px, font-weight: 600`. Label: `JetBrains Mono, 7px, uppercase, letter-spacing: 0.1em, color: #3d4350`.
Stats reset daily. "In Session" is a snapshot value computed server-side — it does not live-update as a timer.
#### 2. Activity Feed (below stats bar)
Two sub-sections separated by a subtle divider (`1px solid rgba(255,255,255,0.03)`):
**Active sessions (max 5):**
- Green pulsing dot — currently active session
- Amber dot (`#f59e0b`, design system amber) — paused/idle session (started but not interacted with recently)
- Flow name (truncated, `13px`, `color: #e2e8f0`)
- ConnectWise ticket number in blue (`#60a5fa`, `JetBrains Mono, 9px`) — only shown if the user's team has a PSA connection AND the session is linked to a ticket
- If more than 5 active sessions, show a "View all in Sessions →" link styled as `text-muted-foreground, 11px, hover:text-foreground`
**Green pulsing dot animation:**
```css
.active-dot {
width: 7px; height: 7px; border-radius: 50%;
background: #34d399;
box-shadow: 0 0 6px rgba(52,211,153,0.5);
animation: pulse-dot 2s ease-in-out infinite;
}
@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); }
}
```
**Recent completions:**
- Small muted dot (`4px`, `#3d4350`)
- Flow name in muted text (`11.5px`, `color: #6b7280`)
- Relative timestamp (`JetBrains Mono, 9px`, `color: #5a6170`)
- Show last 3 completed sessions from today
The activity section has an "Activity" header: `JetBrains Mono, 9px, uppercase, letter-spacing: 0.12em, color: #5a6170`, with a small emerald clock SVG icon (10px).
**Activity feed height:** The zone scrolls with the rest of the sidebar — no independent scroll container. The overall sidebar already has `handleSidebarWheel` for scroll forwarding. With max 5 active + 3 recent, the zone is ~280px max which is acceptable.
**Refresh:** Data fetches on mount (same as current). No polling. Navigating back to a page that renders the sidebar triggers a re-mount/re-fetch. A future enhancement could add polling or WebSocket updates but that's out of scope.
#### 3. Nav Grouping (Concept A3)
Dashboard stands alone at the top (no group label, no special treatment — just a regular `NavItem` above the first group label).
**Resolve** — what engineers do when working tickets:
- Sessions (Clock, emerald `#34d399`, badge: active count)
- All Flows (Network, violet `#a78bfa`, badge: total count, with type sub-items)
- FlowPilot (Brain, fuchsia `#e879f9`) — NEW: in-session AI copilot
- Script Library (Code2, teal `#2dd4bf`)
**Build** — authoring and improving flows:
- Flow Editor (Wrench, amber `#f59e0b`)
- Flow Assist (WandSparkles, pink `#f472b6`) — NEW: conversational flow builder AI
- Step Library (Library, orange `#fb923c`)
- KB Accelerator (Lightbulb, rose `#fb7185`)
**Insights** — reviewing results:
- Exports (FileOutput, blue `#60a5fa`)
- Analytics (BarChart3, sky `#38bdf8`)
Group labels use `JetBrains Mono`, `0.5625rem`, uppercase, `letter-spacing: 0.12em`, `color: #5a6170`, `padding: 14px 12px 5px` (first group: `padding-top: 8px`).
#### 4. AI Assistant Split
| Nav Item | Group | Icon | Color | Purpose | Route |
|----------|-------|------|-------|---------|-------|
| FlowPilot | Resolve | Brain | `#e879f9` (fuchsia) | In-session AI copilot — suggests next steps, explains errors, provides ticket context during active troubleshooting | `/assistant` (existing route, rename label in UI only) |
| Flow Assist | Build | WandSparkles | `#f472b6` (pink) | Conversational flow builder — standalone page with a chat interface for generating flows from natural language. Renders a full-page chat UI (similar to `AssistantChatPage`) that creates flows via the AI chat service with `flow_type` selection. NOT a modal wrapper — it's a dedicated page. | `/flow-assist` (new route + new page component `FlowAssistPage`) |
Both names are already in use internally (FlowPilot in the copilot panel, Flow Assist in the editor AI panel). This just promotes them to top-level navigation.
**Note:** `Wand2` is an alias for `WandSparkles` in lucide-react@0.563.0. Use `WandSparkles` as the canonical import.
### Footer (unchanged structure)
- User Guides (BookOpen, lime `#a3e635`)
- Feedback (MessageSquareText, indigo `#818cf8`)
- Account (Settings, no color)
- Collapse (PanelLeftClose, no color)
### Collapsed Sidebar
When collapsed, the stats bar and activity feed are hidden. Icon-only nav items are shown in a flat list (no group labels). The collapsed view includes all 13 nav items including the two new AI items:
- Dashboard (LayoutGrid, cyan)
- Sessions (Clock, emerald)
- All Flows (Network, violet)
- FlowPilot (Brain, fuchsia)
- Script Library (Code2, teal)
- Flow Editor (Wrench, amber)
- Flow Assist (WandSparkles, pink)
- Step Library (Library, orange)
- KB Accelerator (Lightbulb, rose)
- Exports (FileOutput, blue)
- Analytics (BarChart3, sky)
- User Guides (BookOpen, lime)
- Feedback (MessageSquareText, indigo)
Account and Collapse remain in the footer section of the collapsed view (same as current).
## Data Requirements
### New Backend Endpoint
**`GET /api/v1/sessions/sidebar-stats`** — lightweight endpoint for sidebar data, returns:
```json
{
"resolved_today": 7,
"active_count": 2,
"total_session_minutes_today": 134,
"active_sessions": [
{
"session_id": "uuid",
"tree_name": "O365 Mail Flow",
"tree_id": "uuid",
"tree_type": "troubleshooting",
"started_at": "2026-03-15T14:30:00Z",
"ticket_number": "#48291"
}
],
"recent_completions": [
{
"session_id": "uuid",
"tree_name": "DNS Resolution",
"tree_id": "uuid",
"tree_type": "troubleshooting",
"completed_at": "2026-03-15T12:15:00Z"
}
]
}
```
**Query parameters:**
- `tz_offset` (integer, required): Client's UTC offset in minutes (e.g., `-300` for EST). Used to compute "today" boundaries. Frontend sends `new Date().getTimezoneOffset()`.
**Backend logic:**
- `resolved_today`: `SELECT COUNT(*) FROM sessions WHERE user_id = :uid AND completed_at >= :today_start AND outcome = 'resolved'`
- `active_count`: `SELECT COUNT(*) FROM sessions WHERE user_id = :uid AND completed_at IS NULL`
- `total_session_minutes_today`: Sum of `EXTRACT(EPOCH FROM (COALESCE(completed_at, now()) - started_at)) / 60` for all sessions where `started_at >= :today_start`
- `active_sessions`: Up to 5, ordered by `started_at DESC`, with joined tree name/type
- `recent_completions`: Up to 3 completed today, ordered by `completed_at DESC`
- `ticket_number`: Populated from PSA connection context if available (depends on CW integration state — nullable)
- `today_start`: Computed as midnight in the client's timezone, converted to UTC using `tz_offset`
**Sessions spanning midnight:** A session started yesterday that is still active today counts toward `total_session_minutes_today` for its full duration (from `started_at`, not from midnight). It also appears in `active_sessions`. This matches user intuition — "I've been on this for 3 hours" regardless of when midnight fell.
### Frontend Components
**New components:**
- `SidebarStatsBar` — three-stat row component
- `SidebarActivityFeed` — active sessions + recents feed with "Activity" header
- `ActivityItem` — single activity row (dot + name + ticket/timestamp)
- `FlowAssistPage` — standalone page for conversational flow building
**Modified components:**
- `Sidebar.tsx` — major restructure: remove PinnedFlowsSection, add stats bar + activity feed, reorganize nav into groups with labels, add FlowPilot + Flow Assist items, update collapsed view icon set
- `router.tsx` — add `/flow-assist` route
- `NavItem.tsx` — no changes needed (already supports `iconColor` prop)
**Removed:**
- `PinnedFlowsSection.tsx`
- `pinnedFlowsStore.ts`
- `api/pinnedFlows.ts` (if it exists)
- Related pinning UI in flow cards/pages (pin buttons, pin actions)
## Visual Reference
Mockups created during brainstorming are in `.superpowers/brainstorm/` (HTML files, open in browser). The approved composite is `a3-full-sidebar.html` showing the "With AI Split" variant.
## Out of Scope
- ConnectWise ticket deep-linking (future — just display the number for now)
- Team activity feed (showing what other engineers are doing — potential future enhancement)
- Removing the pinned flows backend API/database tables (just remove frontend usage; backend cleanup is separate)
- Changes to the analytics pages themselves
- Mobile/responsive sidebar behavior (current behavior unchanged)
- Polling/WebSocket for live activity updates (fetch on mount only)
## Edge Cases
- **No active sessions:** Stats bar shows "0" for Active and "0m" for In Session. Activity section shows only recents (or "No activity today" placeholder if no recents either).
- **No CW integration:** Ticket numbers simply don't appear. Activity items show flow name only.
- **Many active sessions:** Activity feed caps at 5 active sessions with a "View all in Sessions →" link below.
- **Midnight rollover:** Stats reset based on client timezone offset passed to the API. Frontend sends `tz_offset` on each sidebar fetch.
- **Sessions spanning midnight:** Count full duration from `started_at`, not partial duration from midnight.
- **New user / no sessions ever:** Stats bar shows all zeros. Activity section shows an empty state: "No activity today".
## Accessibility
- Pulsing green dot: add `aria-label="Active session"` to the dot element
- Amber dot: `aria-label="Paused session"`
- Stats bar values: wrap in elements with `aria-label` combining value + label (e.g., "7 resolved today")
- Activity items: render as `<button>` elements (clickable to navigate to session) with descriptive `title` attributes

View File

@@ -11,7 +11,6 @@ export { default as stepCategoriesApi } from './stepCategories'
export { default as accountsApi } from './accounts'
export { default as adminApi } from './admin'
export { treeMarkdownApi } from './treeMarkdown'
export { default as pinnedFlowsApi } from './pinnedFlows'
export { default as analyticsApi } from './analytics'
export { targetListsApi } from './targetLists'
export { maintenanceSchedulesApi, batchLaunchApi } from './maintenanceSchedules'
@@ -23,3 +22,4 @@ export { flowTransferApi } from './flowTransfer'
export { kbAcceleratorApi } from './kbAccelerator'
export { scriptsApi } from './scripts'
export { integrationsApi, sessionPsaApi } from './integrations'
export { sidebarApi } from './sidebar'

View File

@@ -1,34 +0,0 @@
import { apiClient } from './client'
export interface PinnedFlow {
id: string
tree_id: string
tree_name: string
tree_type: 'troubleshooting' | 'procedural' | 'maintenance'
category_emoji?: string
category_name?: string
pinned_at: string
display_order: number
}
export interface PinnedFlowsResponse {
items: PinnedFlow[]
count: number
}
export const pinnedFlowsApi = {
list: async (): Promise<PinnedFlowsResponse> => {
const { data } = await apiClient.get('/trees/pinned')
return data
},
unpin: async (treeId: string): Promise<void> => {
await apiClient.delete(`/trees/${treeId}/pin`)
},
pin: async (treeId: string): Promise<void> => {
await apiClient.post(`/trees/${treeId}/pin`)
},
}
export default pinnedFlowsApi

View File

@@ -31,6 +31,7 @@ export const sessionsApi = {
async create(data: SessionCreate): Promise<Session> {
const response = await apiClient.post<Session>('/sessions', data)
window.dispatchEvent(new Event('session-changed'))
return response.data
},
@@ -41,6 +42,7 @@ export const sessionsApi = {
async complete(id: string, data: SessionComplete): Promise<Session> {
const response = await apiClient.post<Session>(`/sessions/${id}/complete`, data)
window.dispatchEvent(new Event('session-changed'))
return response.data
},

View File

@@ -0,0 +1,45 @@
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
},
}

View File

@@ -15,12 +15,13 @@ interface NavItemProps {
icon: LucideIcon
label: string
badge?: number | 'dot'
iconColor?: string
matchPaths?: string[]
collapsed?: boolean
children?: NavSubItem[]
}
export function NavItem({ href, icon: Icon, label, badge, matchPaths, collapsed, children }: NavItemProps) {
export function NavItem({ href, icon: Icon, label, badge, iconColor, matchPaths, collapsed, children }: NavItemProps) {
const location = useLocation()
const fullPath = location.pathname + location.search
const isActive = matchPaths
@@ -49,7 +50,7 @@ export function NavItem({ href, icon: Icon, label, badge, matchPaths, collapsed,
{isActive && (
<div className="absolute left-0 top-1/2 h-6 w-[3px] -translate-y-1/2 rounded-r-full bg-gradient-brand" />
)}
<Icon size={18} className={cn('shrink-0', isActive ? 'opacity-100' : 'opacity-70')} />
<Icon size={18} className={cn('shrink-0', isActive ? 'opacity-100' : 'opacity-70')} style={iconColor ? { color: iconColor } : undefined} />
{badge !== undefined && badge !== 0 && badge !== 'dot' && (
<span className="absolute -right-0.5 -top-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-primary text-[0.5rem] font-bold text-primary-foreground">
{badge}
@@ -78,7 +79,7 @@ export function NavItem({ href, icon: Icon, label, badge, matchPaths, collapsed,
<div className="absolute left-0 top-1/2 h-6 w-[3px] -translate-y-1/2 rounded-r-full bg-gradient-brand" />
)}
<Icon size={18} className={cn('shrink-0', isActive ? 'opacity-100' : 'opacity-70')} />
<Icon size={18} className={cn('shrink-0', isActive ? 'opacity-100' : 'opacity-70')} style={iconColor ? { color: iconColor } : undefined} />
<span className="truncate">{label}</span>
{/* Badge */}

View File

@@ -1,50 +1,57 @@
import { useEffect, useState } from 'react'
import { LayoutGrid, Box, PenLine, Clock, FileText, Bookmark, BarChart3, Settings, PanelLeftClose, PanelLeftOpen, MessageSquareText, BotMessageSquare, BookOpen, Sparkles, Terminal } from 'lucide-react'
import { useCallback, 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 { usePinnedFlowsStore } from '@/store/pinnedFlowsStore'
import { PinnedFlowsSection } from '@/components/sidebar/PinnedFlowsSection'
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'
import { sessionsApi, treesApi } from '@/api'
// 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
} as const
export function Sidebar() {
const sidebarCollapsed = useUserPreferencesStore(s => s.sidebarCollapsed)
const toggleSidebar = useUserPreferencesStore(s => s.toggleSidebar)
const location = useLocation()
const pinnedItems = usePinnedFlowsStore((s) => s.items)
const loadPinned = usePinnedFlowsStore((s) => s.load)
const unpinFlow = usePinnedFlowsStore((s) => s.unpin)
const [stats, setStats] = useState<SidebarStatsResponse | null>(null)
const [activeSessionCount, setActiveSessionCount] = useState(0)
const [treeCounts, setTreeCounts] = useState({ total: 0, troubleshooting: 0, procedural: 0, maintenance: 0 })
// Load pinned flows on mount
useEffect(() => {
loadPinned()
}, [loadPinned])
// Fetch sidebar data on mount
useEffect(() => {
const fetchData = async () => {
try {
const [activeSessions, allTrees] = await Promise.all([
sessionsApi.list({ completed: false, size: 50 }).catch(() => []),
treesApi.list({ sort_by: 'name' }).catch(() => []),
])
setActiveSessionCount(activeSessions.length)
const total = allTrees.length
const troubleshooting = allTrees.filter(t => t.tree_type === 'troubleshooting').length
const procedural = allTrees.filter(t => t.tree_type === 'procedural').length
const maintenance = allTrees.filter(t => t.tree_type === 'maintenance').length
setTreeCounts({ total, troubleshooting, procedural, maintenance })
} catch {
// Silently handle errors
}
}
fetchData()
const refreshStats = useCallback(() => {
sidebarApi.getStats().then(setStats).catch(() => {})
}, [])
// Fetch sidebar stats — refreshes on navigation
useEffect(() => {
refreshStats()
}, [location.pathname, refreshStats])
// Refresh when sessions are created or completed
useEffect(() => {
window.addEventListener('session-changed', refreshStats)
return () => window.removeEventListener('session-changed', refreshStats)
}, [refreshStats])
const handleSidebarWheel = (e: React.WheelEvent<HTMLElement>) => {
const sidebar = e.currentTarget
const canSidebarScroll = sidebar.scrollHeight > sidebar.clientHeight
@@ -76,50 +83,81 @@ export function Sidebar() {
<>
{/* Collapsed: icon-only nav */}
<div className="flex flex-col items-center px-1.5 py-3 space-y-1">
<NavItem href="/" icon={LayoutGrid} label="Dashboard" collapsed />
<NavItem href="/trees" icon={Box} label="All Flows" matchPaths={['/trees', '/flows']} collapsed />
<NavItem href="/my-trees" icon={PenLine} label="Flow Editor" collapsed />
<NavItem href="/sessions" icon={Clock} label="Sessions" badge={activeSessionCount || undefined} collapsed />
<NavItem href="/shares" icon={FileText} label="Exports" collapsed />
<NavItem href="/assistant" icon={BotMessageSquare} label="AI Assistant" collapsed />
<NavItem href="/step-library" icon={Bookmark} label="Step Library" collapsed />
<NavItem href="/scripts" icon={Terminal} label="Script Library" collapsed />
<NavItem href="/kb-accelerator" icon={Sparkles} label="KB Accelerator" collapsed />
<NavItem href="/analytics" icon={BarChart3} label="Analytics" collapsed />
<NavItem href="/guides" icon={BookOpen} label="User Guides" collapsed />
<NavItem href="/feedback" icon={MessageSquareText} label="Feedback" collapsed />
<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>
</>
) : (
<>
{/* Pinned Flows */}
<PinnedFlowsSection flows={pinnedItems} onUnpin={unpinFlow} />
{/* Stats Bar */}
<SidebarStatsBar
resolved={stats?.resolved_today ?? 0}
active={stats?.active_count ?? 0}
completedMinutes={stats?.total_session_minutes_today ?? 0}
activeSessionStartTimes={stats?.active_sessions.map(s => s.started_at) ?? []}
/>
{/* Activity Feed */}
<SidebarActivityFeed
activeSessions={stats?.active_sessions ?? []}
recentCompletions={stats?.recent_completions ?? []}
totalActive={stats?.active_count ?? 0}
/>
<div style={{ borderBottom: '1px solid var(--glass-border)' }} />
{/* Primary Navigation */}
{/* Navigation */}
<div className="px-3 py-2 space-y-0.5">
<NavItem href="/" icon={LayoutGrid} label="Dashboard" />
{/* Dashboard (standalone) */}
<NavItem href="/" icon={LayoutGrid} label="Dashboard" iconColor={NAV_COLORS.dashboard} />
{/* Resolve */}
<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={Box}
icon={Network}
label="All Flows"
badge={treeCounts.total || undefined}
badge={stats?.tree_counts.total || undefined}
iconColor={NAV_COLORS.flows}
matchPaths={['/trees', '/flows']}
children={[
{ href: '/trees?type=troubleshooting', label: 'Troubleshooting', count: treeCounts.troubleshooting || undefined },
{ href: '/trees?type=procedural', label: 'Projects', count: treeCounts.procedural || undefined },
{ href: '/trees?type=maintenance', label: 'Maintenance', count: treeCounts.maintenance || undefined },
{ 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="/my-trees" icon={PenLine} label="Flow Editor" />
<NavItem href="/sessions" icon={Clock} label="Sessions" badge={activeSessionCount || undefined} />
<NavItem href="/shares" icon={FileText} label="Exports" />
<NavItem href="/assistant" icon={BotMessageSquare} label="AI Assistant" />
<NavItem href="/step-library" icon={Bookmark} label="Step Library" />
<NavItem href="/scripts" icon={Terminal} label="Script Library" />
<NavItem href="/kb-accelerator" icon={Sparkles} label="KB Accelerator" />
<NavItem href="/analytics" icon={BarChart3} label="Analytics" />
<NavItem href="/assistant" icon={Brain} label="FlowPilot" iconColor={NAV_COLORS.flowPilot} />
<NavItem href="/scripts" icon={Code2} label="Script Library" iconColor={NAV_COLORS.scripts} />
{/* Build */}
<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 */}
<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>
</>
)}
@@ -137,8 +175,8 @@ export function Sidebar() {
>
{!sidebarCollapsed && (
<>
<NavItem href="/guides" icon={BookOpen} label="User Guides" />
<NavItem href="/feedback" icon={MessageSquareText} label="Feedback" />
<NavItem href="/guides" icon={BookOpen} label="User Guides" iconColor={NAV_COLORS.guides} />
<NavItem href="/feedback" icon={MessageSquareText} label="Feedback" iconColor={NAV_COLORS.feedback} />
<NavItem href="/account" icon={Settings} label="Account" />
</>
)}

View File

@@ -1,5 +1,5 @@
import { Link } from 'react-router-dom'
import { Pencil, Globe, Lock, Trash2, GitBranch, FileText, Wrench, Star, Download, ClipboardList } from 'lucide-react'
import { Pencil, Globe, Lock, Trash2, GitBranch, FileText, Wrench, Download, ClipboardList } from 'lucide-react'
import type { TreeListItem } from '@/types'
import { TagBadges } from '@/components/common/TagBadges'
import { StaggerList } from '@/components/common/StaggerList'
@@ -16,9 +16,6 @@ interface TreeGridViewProps {
onDeleteTree: (tree: TreeListItem) => void
onForkTree?: (treeId: string) => void
onExportTree?: (treeId: string) => void
pinnedTreeIds?: Set<string>
onTogglePin?: (treeId: string) => void
pinLoadingTreeIds?: Set<string>
}
export function TreeGridView({
@@ -29,9 +26,6 @@ export function TreeGridView({
onDeleteTree,
onForkTree,
onExportTree,
pinnedTreeIds,
onTogglePin,
pinLoadingTreeIds,
}: TreeGridViewProps) {
const { canEditTree, canDeleteTree } = usePermissions()
@@ -64,26 +58,6 @@ export function TreeGridView({
)}
</div>
<div className="flex items-center gap-2">
{onTogglePin && (
<button
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
onTogglePin(tree.id)
}}
disabled={pinLoadingTreeIds?.has(tree.id)}
aria-label={pinnedTreeIds?.has(tree.id) ? 'Remove from favorites' : 'Add to favorites'}
className={cn(
'rounded-md p-1 transition-colors',
pinnedTreeIds?.has(tree.id)
? 'text-amber-400 hover:text-amber-300'
: 'text-muted-foreground/40 hover:text-amber-400',
pinLoadingTreeIds?.has(tree.id) && 'opacity-50 pointer-events-none'
)}
>
<Star size={14} fill={pinnedTreeIds?.has(tree.id) ? 'currentColor' : 'none'} />
</button>
)}
{tree.is_public ? (
<span title="Public tree">
<Globe className="h-4 w-4 text-muted-foreground" />

View File

@@ -1,5 +1,5 @@
import { Link } from 'react-router-dom'
import { Pencil, Globe, Lock, GitBranch, FileText, Trash2, Wrench, Star, Download, ClipboardList } from 'lucide-react'
import { Pencil, Globe, Lock, GitBranch, FileText, Trash2, Wrench, Download, ClipboardList } from 'lucide-react'
import type { TreeListItem } from '@/types'
import { TagBadges } from '@/components/common/TagBadges'
import { cn } from '@/lib/utils'
@@ -15,9 +15,6 @@ interface TreeListViewProps {
onDeleteTree: (tree: TreeListItem) => void
onForkTree?: (treeId: string) => void
onExportTree?: (treeId: string) => void
pinnedTreeIds?: Set<string>
onTogglePin?: (treeId: string) => void
pinLoadingTreeIds?: Set<string>
}
export function TreeListView({
@@ -28,9 +25,6 @@ export function TreeListView({
onDeleteTree,
onForkTree,
onExportTree,
pinnedTreeIds,
onTogglePin,
pinLoadingTreeIds,
}: TreeListViewProps) {
const { canEditTree } = usePermissions()
@@ -99,26 +93,6 @@ export function TreeListView({
</div>
<div className="flex items-center gap-2">
{onTogglePin && (
<button
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
onTogglePin(tree.id)
}}
disabled={pinLoadingTreeIds?.has(tree.id)}
aria-label={pinnedTreeIds?.has(tree.id) ? 'Remove from favorites' : 'Add to favorites'}
className={cn(
'shrink-0 rounded-md p-1 transition-colors',
pinnedTreeIds?.has(tree.id)
? 'text-amber-400 hover:text-amber-300'
: 'text-muted-foreground/40 hover:text-amber-400',
pinLoadingTreeIds?.has(tree.id) && 'opacity-50 pointer-events-none'
)}
>
<Star size={16} fill={pinnedTreeIds?.has(tree.id) ? 'currentColor' : 'none'} />
</button>
)}
{onExportTree && (
<button
type="button"

View File

@@ -1,6 +1,6 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { Pencil, Globe, Lock, ChevronUp, ChevronDown, GitBranch, FileText, Trash2, Wrench, Star, Download, ClipboardList } from 'lucide-react'
import { Pencil, Globe, Lock, ChevronUp, ChevronDown, GitBranch, FileText, Trash2, Wrench, Download, ClipboardList } from 'lucide-react'
import type { TreeListItem } from '@/types'
import { TagBadges } from '@/components/common/TagBadges'
import { cn } from '@/lib/utils'
@@ -17,9 +17,6 @@ interface TreeTableViewProps {
onSortChange?: (sortBy: string) => void
onForkTree?: (treeId: string) => void
onExportTree?: (treeId: string) => void
pinnedTreeIds?: Set<string>
onTogglePin?: (treeId: string) => void
pinLoadingTreeIds?: Set<string>
}
type SortColumn = 'name' | 'category' | 'version' | 'usage' | 'updated'
@@ -33,9 +30,6 @@ export function TreeTableView({
onSortChange,
onForkTree,
onExportTree,
pinnedTreeIds,
onTogglePin,
pinLoadingTreeIds,
}: TreeTableViewProps) {
const { canEditTree } = usePermissions()
const [sortColumn, setSortColumn] = useState<SortColumn | null>(null)
@@ -83,11 +77,6 @@ export function TreeTableView({
<table className="w-full">
<thead className="bg-accent/50 sticky top-0 z-10">
<tr className="border-b border-border">
{onTogglePin && (
<th className="w-10 px-2 py-3 text-center">
<Star size={14} className="inline text-muted-foreground" />
</th>
)}
<th
className="px-4 py-3 text-left text-sm font-medium text-muted-foreground cursor-pointer hover:text-foreground"
onClick={() => handleSort('name')}
@@ -147,28 +136,6 @@ export function TreeTableView({
<tbody className="bg-transparent">
{trees.map((tree) => (
<tr key={tree.id} className="border-b border-border last:border-0 hover:bg-accent/50">
{onTogglePin && (
<td className="w-10 px-2 py-3 text-center">
<button
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
onTogglePin(tree.id)
}}
disabled={pinLoadingTreeIds?.has(tree.id)}
aria-label={pinnedTreeIds?.has(tree.id) ? 'Remove from favorites' : 'Add to favorites'}
className={cn(
'rounded-md p-1 transition-colors',
pinnedTreeIds?.has(tree.id)
? 'text-amber-400 hover:text-amber-300'
: 'text-muted-foreground/40 hover:text-amber-400',
pinLoadingTreeIds?.has(tree.id) && 'opacity-50 pointer-events-none'
)}
>
<Star size={14} fill={pinnedTreeIds?.has(tree.id) ? 'currentColor' : 'none'} />
</button>
</td>
)}
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<span className="font-medium text-foreground truncate max-w-[200px]">

View File

@@ -0,0 +1,105 @@
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>
)
}

View File

@@ -1,98 +0,0 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { ChevronDown, ChevronRight, Pin } from 'lucide-react'
import { getTreeNavigatePath } from '@/lib/routing'
import { cn } from '@/lib/utils'
import type { PinnedFlow } from '@/api/pinnedFlows'
interface PinnedFlowsSectionProps {
flows: PinnedFlow[]
onUnpin: (treeId: string) => void
}
const TRUNCATE_COUNT = 5
export function PinnedFlowsSection({ flows, onUnpin }: PinnedFlowsSectionProps) {
const navigate = useNavigate()
const [collapsed, setCollapsed] = useState(false)
const [showAll, setShowAll] = useState(false)
const handleToggleCollapse = () => {
if (collapsed) {
setShowAll(false) // Reset to truncated on re-expand
}
setCollapsed(!collapsed)
}
const visibleFlows = showAll ? flows : flows.slice(0, TRUNCATE_COUNT)
const hasMore = flows.length > TRUNCATE_COUNT
const handleFlowClick = (flow: PinnedFlow) => {
setShowAll(false) // Collapse back to 5 on navigation
navigate(getTreeNavigatePath(flow.tree_id, flow.tree_type))
}
return (
<div className="px-3 py-2">
<button
onClick={handleToggleCollapse}
className="flex w-full items-center gap-1 px-3 mb-1 font-heading text-[0.6875rem] font-bold uppercase tracking-[0.04em] text-muted-foreground hover:text-foreground transition-colors"
>
{collapsed ? <ChevronRight size={12} /> : <ChevronDown size={12} />}
Pinned
{flows.length > 0 && (
<span className="ml-auto text-[0.625rem] font-normal">{flows.length}</span>
)}
</button>
<div
className="overflow-hidden transition-[max-height] duration-250 ease-out"
style={{
maxHeight: collapsed ? 0 : showAll
? `${flows.length * 36 + 40}px`
: `${Math.min(flows.length, TRUNCATE_COUNT) * 36 + 40}px`,
}}
>
<div className="space-y-0.5">
{flows.length === 0 ? (
<p className="px-3 py-2 text-xs text-muted-foreground">
Pin your most-used flows here
</p>
) : (
<>
{visibleFlows.map(flow => (
<button
key={flow.tree_id}
onClick={() => handleFlowClick(flow)}
onContextMenu={(e) => {
e.preventDefault()
onUnpin(flow.tree_id)
}}
className={cn(
'group flex w-full items-center gap-2.5 rounded-lg px-3 py-1.5 text-[0.8125rem] font-medium transition-colors',
'text-muted-foreground hover:bg-[var(--sidebar-hover)] hover:text-foreground'
)}
title={`${flow.tree_name} (right-click to unpin)`}
>
<span className="text-sm shrink-0">
{flow.tree_type === 'procedural' ? '📋' : flow.tree_type === 'maintenance' ? '🛠️' : '🔧'}
</span>
<span className="truncate flex-1 text-left">{flow.tree_name}</span>
<Pin size={12} className="shrink-0 opacity-0 group-hover:opacity-40 transition-opacity" />
</button>
))}
{hasMore && (
<button
onClick={() => setShowAll(!showAll)}
className="w-full px-3 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors text-left"
>
{showAll ? 'Show less' : `Show more (${flows.length})`}
</button>
)}
</>
)}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,80 @@
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>
)
}

View File

@@ -0,0 +1,93 @@
import { useEffect, useState } from 'react'
interface SidebarStatsBarProps {
resolved: number
active: number
/** Minutes from completed sessions today (server-computed) */
completedMinutes: number
/** Start times of currently active sessions (ISO strings) */
activeSessionStartTimes: string[]
}
function formatDuration(totalSeconds: number): string {
if (totalSeconds < 60) return `${totalSeconds}s`
const totalMinutes = Math.floor(totalSeconds / 60)
if (totalMinutes < 60) return `${totalMinutes}m`
const h = Math.floor(totalMinutes / 60)
const m = totalMinutes % 60
return m > 0 ? `${h}h ${m}m` : `${h}h`
}
function parseUTCTimestamp(st: string): number {
// Backend returns naive UTC timestamps without 'Z' suffix.
// JS Date() treats bare strings as local time, so append Z to force UTC.
return new Date(st.endsWith('Z') ? st : st + 'Z').getTime()
}
function calcActiveSeconds(startTimes: string[]): number {
const now = Date.now()
return startTimes.reduce((sum, st) => {
const elapsed = Math.floor((now - parseUTCTimestamp(st)) / 1000)
return sum + Math.max(0, elapsed)
}, 0)
}
export function SidebarStatsBar({ resolved, active, completedMinutes, activeSessionStartTimes }: SidebarStatsBarProps) {
const [liveSeconds, setLiveSeconds] = useState(() => calcActiveSeconds(activeSessionStartTimes))
// Tick every second to keep the timer in sync with the session timer
useEffect(() => {
setLiveSeconds(calcActiveSeconds(activeSessionStartTimes))
if (activeSessionStartTimes.length === 0) return
const interval = setInterval(() => {
setLiveSeconds(calcActiveSeconds(activeSessionStartTimes))
}, 1000)
return () => clearInterval(interval)
}, [activeSessionStartTimes])
const totalSeconds = (completedMinutes * 60) + liveSeconds
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(totalSeconds)} total session time today`}
>
{formatDuration(totalSeconds)}
</div>
<div className="mt-1 font-label text-[7px] uppercase tracking-[0.1em] text-[#3d4350]">
Total Time
</div>
</div>
</div>
)
}

View File

@@ -153,6 +153,11 @@
100% { transform: rotate(0deg); }
}
@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); }
}
@keyframes stagger-fade-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }

View File

@@ -0,0 +1,30 @@
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>
)
}

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useRef, useMemo, useCallback } from 'react'
import { useState, useEffect, useRef, useCallback } from 'react'
import { useNavigate } from 'react-router-dom'
import { Search, Loader2, Star, ChevronLeft, ChevronRight, GitBranch } from 'lucide-react'
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'
@@ -9,7 +9,6 @@ import type { Session } from '@/types/session'
import { getTreeNavigatePath } from '@/lib/routing'
import { usePermissions } from '@/hooks/usePermissions'
import { useAuthStore } from '@/store/authStore'
import { usePinnedFlowsStore } from '@/store/pinnedFlowsStore'
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
import { usePaginationParams } from '@/hooks/usePaginationParams'
import { useCachedQuota } from '@/hooks/useCachedQuota'
@@ -66,7 +65,7 @@ export function QuickStartPage() {
const [allFlowsCeiling, setAllFlowsCeiling] = useState(false)
// Favorites state
const [showAllFavorites, setShowAllFavorites] = useState(false)
// AI Builder
const { aiEnabled } = useCachedQuota()
@@ -81,19 +80,6 @@ export function QuickStartPage() {
const [forkReason, setForkReason] = useState('')
const [isForking, setIsForking] = useState(false)
// Pin store
const pinnedItems = usePinnedFlowsStore((s) => s.items)
const pinnedIsLoading = usePinnedFlowsStore((s) => s.isLoading)
const loadPinned = usePinnedFlowsStore((s) => s.load)
const isMutatingByTreeId = usePinnedFlowsStore((s) => s.isMutatingByTreeId)
const togglePin = usePinnedFlowsStore((s) => s.toggle)
const pinnedTreeIds = useMemo(() => new Set(pinnedItems.map((f) => f.tree_id)), [pinnedItems])
const pinLoadingTreeIds = useMemo(
() => new Set(Object.entries(isMutatingByTreeId).filter(([, v]) => v).map(([k]) => k)),
[isMutatingByTreeId]
)
// Preferences
const { dashboardMyFlowsView, setDashboardMyFlowsView } = useUserPreferencesStore()
@@ -103,8 +89,7 @@ export function QuickStartPage() {
allowedPageSizes: [10, 25, 50, 'all'],
})
// Load pinned flows
useEffect(() => { loadPinned() }, [loadPinned])
// Load sessions on mount
useEffect(() => {
@@ -241,11 +226,6 @@ export function QuickStartPage() {
// recentSessionItems removed — replaced by RecentActivity component
// Favorites display
const MAX_VISIBLE_FAVORITES = 8
const visibleFavorites = showAllFavorites ? pinnedItems : pinnedItems.slice(0, MAX_VISIBLE_FAVORITES)
const hasMoreFavorites = pinnedItems.length > MAX_VISIBLE_FAVORITES
// Handlers
const handleStartSession = (treeId: string, treeType?: string) => {
navigate(getTreeNavigatePath(treeId, treeType))
@@ -319,7 +299,6 @@ export function QuickStartPage() {
{ label: 'Active Flows', value: myFlows.length, gradient: true, glow: true },
{ label: 'This Week', value: todaySessions },
{ label: 'Open Sessions', value: openSessions },
{ label: 'Favorites', value: pinnedItems.length },
].map((stat, i) => (
<div
key={stat.label}
@@ -387,63 +366,6 @@ export function QuickStartPage() {
)}
</div>
{/* Favorites Section */}
<div>
<div className="mb-3 flex items-center justify-between">
<h2 className="font-heading text-lg font-semibold text-foreground">
Favorites
{pinnedItems.length > 0 && (
<span className="ml-2 text-sm font-normal text-muted-foreground">({pinnedItems.length})</span>
)}
</h2>
{hasMoreFavorites && (
<button
onClick={() => setShowAllFavorites(!showAllFavorites)}
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
{showAllFavorites ? 'Show less' : 'View all favorites'}
</button>
)}
</div>
{pinnedIsLoading ? (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="h-20 rounded-xl bg-card border border-border animate-pulse" />
))}
</div>
) : pinnedItems.length === 0 ? (
<p className="text-sm text-muted-foreground py-4">
Star a flow to pin it here for quick access.
</p>
) : (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
{visibleFavorites.map((flow) => (
<button
key={flow.tree_id}
onClick={() => navigate(getTreeNavigatePath(flow.tree_id, flow.tree_type))}
className="group relative flex items-center gap-3 rounded-xl bg-card border border-border p-4 text-left transition-colors hover:border-border/80 hover:bg-accent/50"
>
<span className="text-lg shrink-0">
{flow.tree_type === 'procedural' ? '📋' : flow.tree_type === 'maintenance' ? '🛠️' : '🔧'}
</span>
<span className="truncate text-sm font-medium text-foreground">{flow.tree_name}</span>
<button
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
togglePin(flow.tree_id)
}}
aria-label="Remove from favorites"
className="absolute top-2 right-2 rounded-md p-1 text-amber-400 opacity-0 group-hover:opacity-100 hover:text-amber-300 transition-all"
>
<Star size={14} fill="currentColor" />
</button>
</button>
))}
</div>
)}
</div>
{/* My Flows Section — tabbed */}
<div>
<div className="mb-3 flex items-center gap-1 border-b border-border">
@@ -513,9 +435,6 @@ export function QuickStartPage() {
onTagClick={handleTagClick}
onFolderCreated={handleFolderCreated}
onDeleteTree={handleDeleteTree}
pinnedTreeIds={pinnedTreeIds}
onTogglePin={togglePin}
pinLoadingTreeIds={pinLoadingTreeIds}
/>
)}
{dashboardMyFlowsView === 'list' && (
@@ -525,9 +444,6 @@ export function QuickStartPage() {
onTagClick={handleTagClick}
onFolderCreated={handleFolderCreated}
onDeleteTree={handleDeleteTree}
pinnedTreeIds={pinnedTreeIds}
onTogglePin={togglePin}
pinLoadingTreeIds={pinLoadingTreeIds}
/>
)}
{dashboardMyFlowsView === 'table' && (
@@ -537,9 +453,6 @@ export function QuickStartPage() {
onTagClick={handleTagClick}
onFolderCreated={handleFolderCreated}
onDeleteTree={handleDeleteTree}
pinnedTreeIds={pinnedTreeIds}
onTogglePin={togglePin}
pinLoadingTreeIds={pinLoadingTreeIds}
/>
)}

View File

@@ -21,7 +21,7 @@ export function SessionHistoryPage() {
const [hasMore, setHasMore] = useState(false)
const [trees, setTrees] = useState<TreeListItem[]>([])
const [isLoading, setIsLoading] = useState(true)
const [filter, setFilter] = useState<'all' | 'completed' | 'active' | 'prepared'>('all')
const [filter, setFilter] = useState<'all' | 'completed' | 'active' | 'prepared'>('active')
// Close session popover state
const [closingSessionId, setClosingSessionId] = useState<string | null>(null)
@@ -227,7 +227,7 @@ export function SessionHistoryPage() {
{/* Filter Tabs */}
<div className="mb-6 flex gap-2 border-b border-border">
{(['all', 'active', 'completed', 'prepared'] as const).map((tab) => (
{(['active', 'prepared', 'completed', 'all'] as const).map((tab) => (
<button
key={tab}
onClick={() => setFilter(tab)}

View File

@@ -1,4 +1,4 @@
import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
import { useEffect, useState, useCallback, useRef } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { X, RotateCcw, Play, FileUp } from 'lucide-react'
import { PageMeta } from '@/components/common/PageMeta'
@@ -24,7 +24,6 @@ import { cn, safeGetItem } from '@/lib/utils'
import { getSessionResumePath, getTreeNavigatePath } from '@/lib/routing'
import { usePermissions } from '@/hooks/usePermissions'
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
import { usePinnedFlowsStore } from '@/store/pinnedFlowsStore'
import { useCachedQuota } from '@/hooks/useCachedQuota'
import { CreateFlowDropdown } from '@/components/common/CreateFlowDropdown'
import { Spinner } from '@/components/common/Spinner'
@@ -96,17 +95,6 @@ export function TreeLibraryPage() {
const { aiEnabled } = useCachedQuota()
// Pin store
const pinnedItems = usePinnedFlowsStore((s) => s.items)
const isMutatingByTreeId = usePinnedFlowsStore((s) => s.isMutatingByTreeId)
const pinnedTreeIds = useMemo(() => new Set(pinnedItems.map((f) => f.tree_id)), [pinnedItems])
const pinLoadingTreeIds = useMemo(
() => new Set(Object.entries(isMutatingByTreeId).filter(([, v]) => v).map(([k]) => k)),
[isMutatingByTreeId]
)
const togglePin = usePinnedFlowsStore((s) => s.toggle)
const loadPinned = usePinnedFlowsStore((s) => s.load)
// Repeat Last Session
const lastSessionData = (() => {
const raw = safeGetItem('last-session')
@@ -140,9 +128,6 @@ export function TreeLibraryPage() {
.catch((err) => console.error('Failed to load incomplete sessions:', err))
}, [])
// Load pinned flows
useEffect(() => { loadPinned() }, [loadPinned])
const dismissSession = (sessionId: string) => {
const next = new Set(dismissedSessionIds)
next.add(sessionId)
@@ -534,9 +519,6 @@ export function TreeLibraryPage() {
}}
onForkTree={handleForkTree}
onExportTree={handleExportTree}
pinnedTreeIds={pinnedTreeIds}
onTogglePin={togglePin}
pinLoadingTreeIds={pinLoadingTreeIds}
/>
)}
{treeLibraryView === 'list' && (
@@ -552,9 +534,6 @@ export function TreeLibraryPage() {
}}
onForkTree={handleForkTree}
onExportTree={handleExportTree}
pinnedTreeIds={pinnedTreeIds}
onTogglePin={togglePin}
pinLoadingTreeIds={pinLoadingTreeIds}
/>
)}
{treeLibraryView === 'table' && (
@@ -575,9 +554,6 @@ export function TreeLibraryPage() {
}}
onForkTree={handleForkTree}
onExportTree={handleExportTree}
pinnedTreeIds={pinnedTreeIds}
onTogglePin={togglePin}
pinLoadingTreeIds={pinLoadingTreeIds}
/>
)}
</>

View File

@@ -44,6 +44,7 @@ const StepLibraryPage = lazy(() => import('@/pages/StepLibraryPage'))
const ScriptLibraryPage = lazy(() => import('@/pages/ScriptLibraryPage'))
const ScriptManagePage = lazy(() => import('@/pages/ScriptManagePage'))
const AssistantChatPage = lazy(() => import('@/pages/AssistantChatPage'))
const FlowAssistPage = lazy(() => import('@/pages/FlowAssistPage'))
const KBAcceleratorPage = lazy(() => import('@/pages/KBAcceleratorPage'))
const GuidesHubPage = lazy(() => import('@/pages/GuidesHubPage'))
const GuideDetailPage = lazy(() => import('@/pages/GuideDetailPage'))
@@ -167,6 +168,7 @@ export const router = sentryCreateBrowserRouter([
{ path: 'scripts/manage', element: page(ScriptManagePage) },
{ path: 'kb-accelerator', element: page(KBAcceleratorPage) },
{ path: 'assistant', element: page(AssistantChatPage) },
{ path: 'flow-assist', element: page(FlowAssistPage) },
{ path: 'guides', element: page(GuidesHubPage) },
{ path: 'guides/:slug', element: page(GuideDetailPage) },
// Admin routes

View File

@@ -1,104 +0,0 @@
import { create } from 'zustand'
import { pinnedFlowsApi } from '@/api/pinnedFlows'
import type { PinnedFlow } from '@/api/pinnedFlows'
import { toast } from '@/lib/toast'
interface PinnedFlowsState {
items: PinnedFlow[]
isLoaded: boolean
isLoading: boolean
isMutatingByTreeId: Record<string, boolean>
error: string | null
load: (force?: boolean) => Promise<void>
pin: (treeId: string) => Promise<void>
unpin: (treeId: string) => Promise<void>
toggle: (treeId: string) => void
isPinned: (treeId: string) => boolean
}
export const usePinnedFlowsStore = create<PinnedFlowsState>()((set, get) => ({
items: [],
isLoaded: false,
isLoading: false,
isMutatingByTreeId: {},
error: null,
load: async (force = false) => {
const state = get()
if (state.isLoaded && !force) return
if (state.isLoading) return
set({ isLoading: true, error: null })
try {
const data = await pinnedFlowsApi.list()
set({ items: data.items, isLoaded: true, isLoading: false })
} catch {
set({ error: 'Failed to load pinned flows', isLoading: false })
}
},
pin: async (treeId: string) => {
const state = get()
if (state.isMutatingByTreeId[treeId]) return
set({ isMutatingByTreeId: { ...state.isMutatingByTreeId, [treeId]: true } })
try {
await pinnedFlowsApi.pin(treeId)
const data = await pinnedFlowsApi.list()
set((s) => ({
items: data.items,
isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false },
}))
} catch (err: unknown) {
const status = (err as { response?: { status?: number } })?.response?.status
if (status === 409) {
toast.error('Maximum of 15 favorites reached. Unpin a flow to add a new one.')
} else {
toast.error('Failed to pin flow')
}
set((s) => ({
isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false },
}))
}
},
unpin: async (treeId: string) => {
const state = get()
if (state.isMutatingByTreeId[treeId]) return
const prevItems = state.items
set({
items: state.items.filter((f) => f.tree_id !== treeId),
isMutatingByTreeId: { ...state.isMutatingByTreeId, [treeId]: true },
})
try {
await pinnedFlowsApi.unpin(treeId)
set((s) => ({
isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false },
}))
} catch {
toast.error('Failed to unpin flow')
set((s) => ({
items: prevItems,
isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false },
}))
}
},
toggle: (treeId: string) => {
const state = get()
if (state.isPinned(treeId)) {
state.unpin(treeId)
} else {
state.pin(treeId)
}
},
isPinned: (treeId: string) => {
return get().items.some((f) => f.tree_id === treeId)
},
}))