feat: bold dashboard redesign with inline stats, section labels, and chip icons

Restructure QuickStartPage for a more professional, informative layout:
- Left-aligned hero greeting (text-4xl) with date context and inline stat strip
- GreetingStatStrip shows resolved/active/MTTR at a glance
- Remove collapsible toggle — dashboard stats always visible
- Section labels with trailing border lines for visual hierarchy
- Suggestion chips with category icons, card-style hover, press feedback
- Fix cyan focus ring and icon color to ember orange design system
- Session cards: line-clamp-2 descriptions, font-medium text, problem_domain metadata
- Widen container max-w-3xl → max-w-4xl for breathing room
- Add .impeccable.md and .github/copilot-instructions.md design context
- CLAUDE.md audit: fix stale references, remove duplication, update counts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-27 05:04:20 +00:00
parent 3c0a29115c
commit dbe66a0568
9 changed files with 284 additions and 144 deletions

View File

@@ -15,7 +15,7 @@ function timeAgo(dateStr: string): string {
return `${Math.floor(hours / 24)}d ago`
}
export function ActiveFlowPilotSessions() {
export function ActiveFlowPilotSessions({ hideHeader = false }: { hideHeader?: boolean }) {
const [sessions, setSessions] = useState<AISessionSummary[]>([])
const [loading, setLoading] = useState(true)
const navigate = useNavigate()
@@ -30,9 +30,11 @@ export function ActiveFlowPilotSessions() {
if (loading) {
return (
<div className="card-flat">
<div className="px-5 py-3" style={{ borderBottom: '1px solid var(--color-border-default)' }}>
<h3 className="font-heading text-sm font-bold text-foreground">Active Sessions</h3>
</div>
{!hideHeader && (
<div className="px-5 py-3" style={{ borderBottom: '1px solid var(--color-border-default)' }}>
<h3 className="font-heading text-sm font-bold text-foreground">Active Sessions</h3>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 p-4">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="h-24 rounded-xl bg-card border border-border animate-pulse" />
@@ -44,25 +46,27 @@ export function ActiveFlowPilotSessions() {
return (
<div className="card-flat">
<div
className="flex items-center justify-between px-5 py-3"
style={{ borderBottom: '1px solid var(--color-border-default)' }}
>
<div className="flex items-center gap-2">
<h3 className="font-heading text-sm font-bold text-foreground">Active Sessions</h3>
{sessions.length > 0 && (
<span className="inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-accent-dim px-1.5 text-[0.625rem] font-bold text-primary">
{sessions.length}
</span>
)}
</div>
<Link
to="/sessions?filter=active"
className="flex items-center gap-1 text-[0.6875rem] text-muted-foreground hover:text-foreground transition-colors"
{!hideHeader && (
<div
className="flex items-center justify-between px-5 py-3"
style={{ borderBottom: '1px solid var(--color-border-default)' }}
>
View all <ArrowRight size={10} />
</Link>
</div>
<div className="flex items-center gap-2">
<h3 className="font-heading text-sm font-bold text-foreground">Active Sessions</h3>
{sessions.length > 0 && (
<span className="inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-accent-dim px-1.5 text-[0.625rem] font-bold text-primary">
{sessions.length}
</span>
)}
</div>
<Link
to="/sessions?filter=active"
className="flex items-center gap-1 text-[0.6875rem] text-muted-foreground hover:text-foreground transition-colors"
>
View all <ArrowRight size={10} />
</Link>
</div>
)}
{sessions.length === 0 ? (
<div className="px-5 py-8 text-center">
@@ -95,7 +99,7 @@ export function ActiveFlowPilotSessions() {
{session.confidence_tier || 'starting'}
</span>
</div>
<p className="text-sm font-medium text-foreground truncate">
<p className="text-sm font-medium text-foreground line-clamp-2">
{session.session_type === 'chat'
? (session.title || session.problem_summary || 'Chat in progress')
: (session.problem_summary || 'Session in progress')}

View File

@@ -0,0 +1,54 @@
import { useState, useEffect } from 'react'
import { CheckCircle, Clock, Zap } from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
import { sidebarApi } from '@/api'
interface StatItem {
icon: LucideIcon
value: string | number | null
label: string
color: string
}
export function GreetingStatStrip() {
const [resolved, setResolved] = useState<number | null>(null)
const [active, setActive] = useState<number | null>(null)
const [avgMttr, setAvgMttr] = useState<string | null>(null)
useEffect(() => {
sidebarApi.getStats()
.then((stats) => {
setResolved(stats.resolved_today)
setActive(stats.active_count)
const avg = stats.resolved_today > 0
? Math.round(stats.total_session_minutes_today / stats.resolved_today)
: null
setAvgMttr(avg != null ? `${avg}m` : null)
})
.catch(() => {})
}, [])
const stats: StatItem[] = [
{ icon: CheckCircle, value: resolved, label: 'resolved today', color: '#34d399' },
{ icon: Zap, value: active, label: 'active now', color: '#f97316' },
{ icon: Clock, value: avgMttr, label: 'avg MTTR', color: '#848b9b' },
]
return (
<div className="hidden sm:flex items-center gap-5 pb-1">
{stats.map(({ icon: Icon, value, label, color }) => (
<div key={label} className="flex items-center gap-2">
<Icon size={13} style={{ color }} className="shrink-0" />
<div className="text-right">
<p className="font-heading text-lg font-extrabold leading-none text-[#f0f2f5]">
{value ?? '\u2014'}
</p>
<p className="font-sans text-[0.5625rem] uppercase tracking-[0.1em] text-muted-foreground mt-0.5">
{label}
</p>
</div>
</div>
))}
</div>
)
}

View File

@@ -52,7 +52,7 @@ export function PerformanceCards() {
label: 'Active Now',
value: active,
icon: TrendingUp,
iconColor: '#38bdf8',
iconColor: '#848b9b',
href: '/sessions?filter=active',
},
{

View File

@@ -22,7 +22,7 @@ const STATUS_CONFIG: Record<string, { icon: typeof CheckCircle; color: string }>
abandoned: { icon: XCircle, color: '#8891a0' },
}
export function RecentFlowPilotSessions() {
export function RecentFlowPilotSessions({ hideHeader = false }: { hideHeader?: boolean }) {
const [sessions, setSessions] = useState<AISessionSummary[]>([])
const navigate = useNavigate()
@@ -42,18 +42,20 @@ export function RecentFlowPilotSessions() {
return (
<div className="card-flat">
<div
className="flex items-center justify-between px-5 py-3"
style={{ borderBottom: '1px solid var(--color-border-default)' }}
>
<h3 className="font-heading text-sm font-bold text-foreground">Recent Sessions</h3>
<Link
to="/sessions"
className="flex items-center gap-1 text-[0.6875rem] text-muted-foreground hover:text-foreground transition-colors"
{!hideHeader && (
<div
className="flex items-center justify-between px-5 py-3"
style={{ borderBottom: '1px solid var(--color-border-default)' }}
>
History <ArrowRight size={10} />
</Link>
</div>
<h3 className="font-heading text-sm font-bold text-foreground">Recent Sessions</h3>
<Link
to="/sessions"
className="flex items-center gap-1 text-[0.6875rem] text-muted-foreground hover:text-foreground transition-colors"
>
History <ArrowRight size={10} />
</Link>
</div>
)}
<div>
{sessions.map((session, i) => {
const config = STATUS_CONFIG[session.status] || STATUS_CONFIG.abandoned
@@ -73,11 +75,14 @@ export function RecentFlowPilotSessions() {
<StatusIcon size={14} style={{ color: config.color }} className="shrink-0" />
)}
<div className="flex-1 min-w-0">
<p className="text-sm text-foreground truncate">
<p className="text-sm font-medium text-foreground truncate">
{session.session_type === 'chat'
? (session.title || session.problem_summary || 'Chat')
: (session.problem_summary || 'Session')}
</p>
{session.problem_domain && (
<p className="text-[0.625rem] text-muted-foreground mt-0.5 truncate">{session.problem_domain}</p>
)}
</div>
<span className="shrink-0 font-sans text-xs text-muted-foreground">
{timeAgo(session.resolved_at || session.created_at)}

View File

@@ -1,18 +1,19 @@
import { useState, useRef, useEffect, useCallback } from 'react'
import { useNavigate } from 'react-router-dom'
import { Send, Paperclip, Terminal, Loader2, X, RotateCcw, ImagePlus } from 'lucide-react'
import { Send, Paperclip, Terminal, Loader2, X, RotateCcw, ImagePlus, Globe, Mail, Lock, Printer, Shield } from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
import { uploadsApi } from '@/api/uploads'
import { toast } from '@/lib/toast'
import type { PendingUpload } from '@/types/upload'
const SUGGESTIONS = [
'VPN not connecting',
'Outlook not syncing',
'User locked out',
'Slow internet',
'Printer issues',
'MFA problems',
const SUGGESTIONS: { icon: LucideIcon; label: string }[] = [
{ icon: Globe, label: 'VPN not connecting' },
{ icon: Mail, label: 'Outlook not syncing' },
{ icon: Lock, label: 'User locked out' },
{ icon: Globe, label: 'Slow internet' },
{ icon: Printer, label: 'Printer issues' },
{ icon: Shield, label: 'MFA problems' },
]
const ACCEPTED_FILE_TYPES = 'image/png,image/jpeg,image/gif,image/webp,.txt,.log,.csv,.pdf,.docx'
@@ -199,7 +200,7 @@ export function StartSessionInput() {
<div className={cn(
'relative rounded-2xl border bg-card transition-all',
isDragOver ? 'border-primary/50 bg-primary/5' : 'border-border',
'focus-within:border-[rgba(6,182,212,0.3)] focus-within:ring-1 focus-within:ring-primary/20'
'focus-within:border-[rgba(249,115,22,0.25)] focus-within:ring-1 focus-within:ring-[rgba(249,115,22,0.1)]'
)}>
{/* Drag overlay */}
{isDragOver && (
@@ -337,15 +338,16 @@ export function StartSessionInput() {
</div>
{/* Suggestion chips */}
<div className="flex flex-wrap gap-2 mt-3">
{SUGGESTIONS.map((s) => (
<div className="flex flex-wrap gap-2 mt-4">
{SUGGESTIONS.map(({ icon: Icon, label }) => (
<button
key={s}
key={label}
type="button"
onClick={() => handleSuggestionClick(s)}
className="rounded-full border border-border px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:border-primary/30 hover:bg-primary/5 transition-colors"
onClick={() => handleSuggestionClick(label)}
className="group flex items-center gap-1.5 rounded-md border border-border bg-card px-3 py-1.5 text-xs text-muted-foreground transition-all hover:border-[var(--color-border-hover)] hover:bg-[var(--color-bg-elevated)] hover:text-foreground active:scale-[0.97]"
>
{s}
<Icon size={11} className="text-muted shrink-0 group-hover:text-[#f97316] transition-colors" />
{label}
</button>
))}
</div>

View File

@@ -1,4 +1,3 @@
import { useState } from 'react'
import { PageMeta } from '@/components/common/PageMeta'
import { useAuthStore } from '@/store/authStore'
import { StartSessionInput } from '@/components/dashboard/StartSessionInput'
@@ -8,31 +7,49 @@ import { PerformanceCards } from '@/components/dashboard/PerformanceCards'
import { KnowledgeBaseCards } from '@/components/dashboard/KnowledgeBaseCards'
import { TeamSummary } from '@/components/dashboard/TeamSummary'
import { RecentFlowPilotSessions } from '@/components/dashboard/RecentFlowPilotSessions'
import { ChevronDown } from 'lucide-react'
import { cn } from '@/lib/utils'
import { GreetingStatStrip } from '@/components/dashboard/GreetingStatStrip'
function SectionLabel({ children, action }: { children: React.ReactNode; action?: React.ReactNode }) {
return (
<div className="flex items-center gap-3">
<span className="font-sans text-[0.625rem] uppercase tracking-[0.12em] font-semibold text-muted-foreground">
{children}
</span>
<div className="flex-1 h-px bg-border" />
{action && <div className="shrink-0">{action}</div>}
</div>
)
}
export function QuickStartPage() {
const user = useAuthStore((s) => s.user)
const [dashboardExpanded, setDashboardExpanded] = useState(false)
const greeting = new Date().getHours() < 12
const now = new Date()
const greeting = now.getHours() < 12
? 'morning'
: new Date().getHours() < 18
: now.getHours() < 18
? 'afternoon'
: 'evening'
const dayOfWeek = now.toLocaleDateString('en-US', { weekday: 'long' })
const formattedDate = now.toLocaleDateString('en-US', { month: 'long', day: 'numeric' })
const firstName = user?.name?.split(' ')[0] || 'there'
return (
<div className="overflow-y-auto h-full">
<PageMeta title="ResolutionFlow" />
<div className="max-w-3xl mx-auto px-6 pt-12 pb-8">
{/* Hero: Greeting + Input */}
<div className="text-center mb-6">
<h1 className="font-heading text-2xl font-extrabold tracking-tight text-foreground">
Good {greeting}, {user?.name?.split(' ')[0] || 'there'}
</h1>
<p className="mt-1 text-base text-muted-foreground">
What are you troubleshooting?
</p>
<div className="max-w-4xl mx-auto px-6 pt-12 pb-12">
{/* Hero: Greeting + Stat Strip */}
<div className="flex items-end justify-between mb-8 animate-fade-in-up">
<div>
<p className="font-sans text-xs uppercase tracking-[0.12em] text-muted-foreground mb-1">
{dayOfWeek}, {formattedDate}
</p>
<h1 className="font-heading text-3xl sm:text-4xl font-extrabold tracking-tight text-[#f0f2f5] leading-tight">
Good {greeting},<br className="hidden sm:block" />
{firstName}.
</h1>
</div>
<GreetingStatStrip />
</div>
{/* Chat-style input */}
@@ -44,39 +61,31 @@ export function QuickStartPage() {
</div>
{/* Active Sessions */}
<div className="mt-4">
<ActiveFlowPilotSessions />
<div className="mt-8">
<SectionLabel>Active Sessions</SectionLabel>
<div className="mt-3">
<ActiveFlowPilotSessions hideHeader />
</div>
</div>
{/* Recent Sessions */}
<div className="mt-4">
<RecentFlowPilotSessions />
<div className="mt-8">
<SectionLabel>Recent Sessions</SectionLabel>
<div className="mt-3">
<RecentFlowPilotSessions hideHeader />
</div>
</div>
{/* Collapsible Dashboard section */}
<div className="mt-8">
<button
type="button"
onClick={() => setDashboardExpanded(!dashboardExpanded)}
className="flex items-center gap-2 text-xs font-sans uppercase tracking-wide text-muted-foreground hover:text-foreground transition-colors w-full"
>
<div className="flex-1 h-px bg-border" />
<span className="flex items-center gap-1.5 px-3">
Dashboard
<ChevronDown size={12} className={cn('transition-transform', dashboardExpanded && 'rotate-180')} />
</span>
<div className="flex-1 h-px bg-border" />
</button>
{dashboardExpanded && (
<div className="mt-4 space-y-4 animate-fade-in">
<PerformanceCards />
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<KnowledgeBaseCards />
<TeamSummary />
</div>
{/* Dashboard — always visible */}
<div className="mt-10">
<SectionLabel>Dashboard</SectionLabel>
<div className="mt-3 space-y-4 animate-fade-in">
<PerformanceCards />
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<KnowledgeBaseCards />
<TeamSummary />
</div>
)}
</div>
</div>
</div>
</div>