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

* docs: add 5 sidebar icon color concepts for UX review

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(ui): add semantic icon colors and updated icons to sidebar nav

Swap generic icons for more descriptive alternatives (Network, Wrench,
FileOutput, Library, Code2, Lightbulb) and assign each nav item a unique
semantic color for instant visual landmarks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ui): default Sessions page to Active tab, reorder tabs

Active sessions are what engineers care about most. Tab order is now
Active, Prepared, Completed, All.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add sidebar grouping and AI naming concept mockups

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add sidebar redesign context and decision summary

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add sidebar redesign spec and implementation plan

Design spec covers: activity zone with daily stats + session feed,
nav grouping (Resolve/Build/Insights), AI split (FlowPilot + Flow Assist),
pinned flows removal. Implementation plan has 5 chunks, 12 tasks, 39 steps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add sidebar stats Pydantic schemas

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add failing tests for sidebar stats endpoint

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add sidebar stats endpoint with daily stats and activity feed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add sidebar API client, stats bar, activity feed components

New components: SidebarStatsBar, SidebarActivityFeed, ActivityItem.
New API client for sidebar stats endpoint. Pulse-dot CSS animation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: restructure sidebar with stats bar, activity feed, and grouped nav

Dashboard-first layout with Resolve/Build/Insights groups.
AI split: FlowPilot (Resolve) + Flow Assist (Build).
Stats bar: Resolved/Active/In Session daily counters.
Activity feed: active sessions with CW ticket #, recent completions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: remove pinned flows frontend (PinnedFlowsSection, store, API, pin buttons)

Removed: PinnedFlowsSection component, pinnedFlowsStore, pinnedFlows API client.
Cleaned: pin buttons from TreeGridView, TreeListView, TreeTableView.
Cleaned: favorites section from QuickStartPage, pin props from TreeLibraryPage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add FlowAssistPage placeholder and /flow-assist route

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: real-time sidebar stats via session-changed events

Sidebar now refreshes stats when sessions are created or completed,
not just on page navigation. Uses window event bus pattern (same as
folder-changed events in codebase).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: live-ticking In Session timer using active session start times

SidebarStatsBar now computes active session elapsed time client-side
from started_at timestamps, ticking every 60s. Backend only returns
completed session minutes to avoid double-counting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: sidebar In Session timer ticks every second and shows seconds

Timer now uses 1s interval (not 60s) and displays seconds when under
a minute so it matches the session timer in the flow UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: trigger PR environment redeploy

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* debug: add console.log to SidebarStatsBar for timer investigation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: parse sidebar timestamps as UTC (append Z suffix)

Backend returns naive UTC timestamps without timezone indicator.
JS Date() treats bare ISO strings as local time, causing the timer
to compute negative elapsed time (future timestamps). Appending 'Z'
forces UTC parsing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: rename 'In Session' to 'Total Time' for clarity

Makes it clear the timer is an aggregate of all sessions today,
not just the current one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #107.
This commit is contained in:
chihlasm
2026-03-16 01:35:16 -04:00
committed by GitHub
parent 46865882c6
commit 357f8e2d08
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)
},
}))