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:
@@ -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')}
|
||||
|
||||
54
frontend/src/components/dashboard/GreetingStatStrip.tsx
Normal file
54
frontend/src/components/dashboard/GreetingStatStrip.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -52,7 +52,7 @@ export function PerformanceCards() {
|
||||
label: 'Active Now',
|
||||
value: active,
|
||||
icon: TrendingUp,
|
||||
iconColor: '#38bdf8',
|
||||
iconColor: '#848b9b',
|
||||
href: '/sessions?filter=active',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user