Skip to content

Commit 82dd6a0

Browse files
IMRAN MOHAMMEDIMRAN MOHAMMED
authored andcommitted
Audit UX overhaul + consolidated handoff card
- Sample-screenshots row (Claude/ChatGPT/Copilot) in upload empty state - Right-rail redesign: 2-col picker grid, sub-panel with example patterns, "What you'll get" anticipation card, tightened copy - Empty-state reworded from blame ("doesn't look like AI") to explanation ("We scanned for 36 AI UX patterns") - "What we audited" moved to right-side aside, height-matched to screenshot - Quick Wins merged into the right aside; bottom section removed - Facts table reframe: Surface / Device / Screenshots / Applicable patterns / Gaps found — communicates the funnel honestly (36 -> 6 -> 3) - Accessibility pass: bumped text-xs->text-sm and text-sm->text-base across right rail, GapCard, and pin side panel - Blow-up guardrails: per-item line-clamp-3 on quick wins, +N more toggle capped at 4, mobile max-h-[640px] - min-h-screen gap fix on results view - Consolidated handoff card: composeHandoffPrompt + new audit_handoff_copied event; copy-to-clipboard + Inspect disclosure; marketing tag inside the card
1 parent a46edf3 commit 82dd6a0

8 files changed

Lines changed: 426 additions & 104 deletions

File tree

CLAUDE.md

Lines changed: 7 additions & 8 deletions
Large diffs are not rendered by default.

src/components/audit/AuditClient.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ export default function AuditClient({
227227
const showIntakeBanner = oneRemaining && isIntakeFlow;
228228

229229
return (
230-
<div className={isIntakeFlow ? '' : 'min-h-screen'}>
230+
<div className={step === 'demo' ? 'min-h-screen' : ''}>
231231
{/* Rate Limit Modal */}
232232
{rateLimitError && (
233233
<UsageLimitModal

src/components/audit/FullPageResults.tsx

Lines changed: 160 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import {
1212
XMarkIcon,
1313
ArrowPathIcon,
1414
SparklesIcon,
15-
DocumentMagnifyingGlassIcon,
15+
CommandLineIcon,
1616
} from '@heroicons/react/24/outline';
17+
import { composeHandoffPrompt } from '@/lib/audit/handoff';
1718
import type { AnalysisResults, TopGap, ProductContext } from '@/types/audit';
1819
import { GapCard } from './GapCard';
1920
import { EmailReportModal } from './EmailReportModal';
@@ -121,7 +122,7 @@ function GapSidePanel({ gap, pinNumber, onClose }: { gap: TopGap; pinNumber: num
121122
<span className="flex items-center justify-center w-8 h-8 rounded-full bg-accent-primary text-white dark:text-gray-900 text-sm font-bold">
122123
{pinNumber}
123124
</span>
124-
<p className="text-xs font-medium uppercase tracking-wider text-text-tertiary">Pattern detected</p>
125+
<p className="text-sm font-semibold uppercase tracking-wider text-text-tertiary">Pattern detected</p>
125126
</div>
126127
<button
127128
onClick={onClose}
@@ -301,16 +302,11 @@ function EmptyAuditState({
301302
<div className="bg-background-primary border border-border-primary rounded-2xl p-8 sm:p-12 lg:p-14">
302303
<div className="grid lg:grid-cols-5 gap-10 lg:gap-12 items-center min-h-[360px]">
303304
<div className="lg:col-span-3">
304-
<div className="flex items-center gap-4 mb-6">
305-
<div className="inline-flex items-center justify-center w-14 h-14 rounded-full bg-accent-subtle flex-shrink-0">
306-
<DocumentMagnifyingGlassIcon className="w-7 h-7 text-accent-primary" />
307-
</div>
308-
<h2 className="text-3xl sm:text-4xl font-bold text-text-primary leading-tight">
309-
This doesn&apos;t look like an AI product surface
310-
</h2>
311-
</div>
305+
<h2 className="text-3xl sm:text-4xl font-bold text-text-primary leading-tight mb-6">
306+
We scanned for 36 AI UX patterns
307+
</h2>
312308
<p className="text-lg text-text-secondary leading-relaxed mb-8">
313-
The audit evaluates designs against 36 patterns built for AI products. We didn&apos;t find anything to flag on this screenshot, usually because the surface isn&apos;t showing AI output.
309+
These patterns cover behaviors like confidence cues, error recovery, and explainability. None showed up in your screenshot, which usually means the surface isn&apos;t displaying AI output yet.
314310
</p>
315311
<div className="flex items-center gap-4 flex-wrap">
316312
<button
@@ -480,6 +476,9 @@ export function FullPageResults({ results, onNewAudit, isAnalyzing, isDemoMode,
480476

481477
// Tab + Chat state
482478
const [activeTab, setActiveTab] = useState<'issues' | 'chat'>('issues');
479+
const [showAllQuickWins, setShowAllQuickWins] = useState(false);
480+
const [handoffCopied, setHandoffCopied] = useState(false);
481+
const [showHandoffSource, setShowHandoffSource] = useState(false);
483482
const [hasSentOpener, setHasSentOpener] = useState(false);
484483
const [messages, setMessages] = useState<ChatMessage[]>([]);
485484
const [inputValue, setInputValue] = useState('');
@@ -874,16 +873,8 @@ export function FullPageResults({ results, onNewAudit, isAnalyzing, isDemoMode,
874873
}
875874

876875
return (
877-
<div className="pb-8 sm:pb-12">
878-
<div className="max-w-7xl mx-auto px-4 sm:px-6 pt-8 sm:pt-12">
879-
880-
{/* What we saw — grounds the audit in the actual surface(s) */}
881-
{surfaceDescription && (
882-
<div className="mb-4 max-w-3xl mx-auto px-4 py-3 rounded-xl bg-background-primary border border-border-primary text-sm text-text-secondary">
883-
<p className="text-xs font-semibold uppercase tracking-wider text-text-tertiary mb-1">What we audited</p>
884-
<p>{surfaceDescription}</p>
885-
</div>
886-
)}
876+
<div>
877+
<div className="max-w-7xl mx-auto px-4 sm:px-6 pt-8 sm:pt-12 pb-8">
887878

888879
{/* Screenshot canvas + optional chat side panel.
889880
Both columns have explicit fixed heights on desktop so the row size never changes
@@ -1009,6 +1000,77 @@ export function FullPageResults({ results, onNewAudit, isAnalyzing, isDemoMode,
10091000
)}
10101001
</div>
10111002

1003+
{/* What we audited + Quick Wins — sits beside the screenshot when chat isn't active */}
1004+
{activeTab !== 'chat' && (surfaceDescription || quickWins.length > 0 || results.applicablePatterns?.length) && (
1005+
<aside className={`w-full lg:w-[360px] lg:flex-shrink-0 rounded-2xl border border-border-primary bg-background-primary px-5 py-4 overflow-y-auto max-h-[640px] lg:max-h-none ${
1006+
heroDeviceType === 'mobile' ? 'lg:h-[711px]' : 'lg:h-[660px]'
1007+
}`}>
1008+
{(() => {
1009+
const productLabel: Record<string, string> = {
1010+
'chat-interface': 'Chat interface',
1011+
'ai-agent': 'AI agent',
1012+
'recommendation-system': 'Recommendations',
1013+
'content-generation': 'Content generation',
1014+
other: 'AI product',
1015+
};
1016+
const productType = results.productContext?.productType;
1017+
const facts: Array<{ label: string; value: string }> = [];
1018+
if (productType) facts.push({ label: 'Surface', value: productLabel[productType] || productType });
1019+
facts.push({ label: 'Device', value: heroDeviceType === 'mobile' ? 'Mobile' : 'Desktop' });
1020+
if (allScreenshots.length > 0) facts.push({ label: 'Screenshots', value: String(allScreenshots.length) });
1021+
if (results.applicablePatterns?.length) facts.push({ label: 'Applicable patterns', value: `${results.applicablePatterns.length} of 36` });
1022+
if (issues.length > 0) facts.push({ label: 'Gaps found', value: String(issues.length) });
1023+
return (
1024+
<div className={(surfaceDescription || quickWins.length > 0) ? 'pb-4 mb-4 border-b border-border-primary' : ''}>
1025+
<p className="text-sm font-semibold uppercase tracking-wider text-text-tertiary mb-3">What we audited</p>
1026+
<dl className="rounded-lg border border-border-primary bg-background-secondary divide-y divide-border-primary mb-3">
1027+
{facts.map((f) => (
1028+
<div key={f.label} className="flex items-center justify-between gap-3 px-3.5 py-2.5">
1029+
<dt className="text-sm text-text-secondary">{f.label}</dt>
1030+
<dd className="text-base font-semibold text-text-primary text-right truncate">{f.value}</dd>
1031+
</div>
1032+
))}
1033+
</dl>
1034+
{surfaceDescription && (
1035+
<p className="text-sm text-text-secondary leading-relaxed line-clamp-4">{surfaceDescription}</p>
1036+
)}
1037+
</div>
1038+
);
1039+
})()}
1040+
{quickWins.length > 0 && (() => {
1041+
const QUICK_WINS_CAP = 4;
1042+
const visibleWins = showAllQuickWins ? quickWins : quickWins.slice(0, QUICK_WINS_CAP);
1043+
const hiddenCount = quickWins.length - QUICK_WINS_CAP;
1044+
return (
1045+
<div>
1046+
<p className="text-sm font-semibold uppercase tracking-wider text-text-tertiary mb-3 flex items-center gap-1.5">
1047+
<LightBulbIcon className="w-4 h-4 text-amber-500" />
1048+
Quick Wins
1049+
</p>
1050+
<ul className="space-y-3">
1051+
{visibleWins.map((win, i) => (
1052+
<li key={i} className="flex gap-2.5 text-sm">
1053+
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-amber-100 dark:bg-amber-950/40 text-amber-700 dark:text-amber-400 flex items-center justify-center text-xs font-semibold">
1054+
{i + 1}
1055+
</span>
1056+
<span className="text-text-secondary leading-relaxed line-clamp-3">{win}</span>
1057+
</li>
1058+
))}
1059+
</ul>
1060+
{hiddenCount > 0 && (
1061+
<button
1062+
onClick={() => setShowAllQuickWins((v) => !v)}
1063+
className="mt-3 text-sm font-medium text-accent-primary hover:text-accent-hover cursor-pointer"
1064+
>
1065+
{showAllQuickWins ? 'Show fewer' : `+ ${hiddenCount} more`}
1066+
</button>
1067+
)}
1068+
</div>
1069+
);
1070+
})()}
1071+
</aside>
1072+
)}
1073+
10121074
{/* In-flow chat side panel — sits next to the screenshot */}
10131075
{activeTab === 'chat' && (
10141076
<aside className={`w-full lg:w-[360px] lg:flex-shrink-0 rounded-2xl border border-border-primary bg-background-primary shadow-sm flex flex-col overflow-hidden min-h-0 ${
@@ -1094,8 +1156,84 @@ export function FullPageResults({ results, onNewAudit, isAnalyzing, isDemoMode,
10941156
</div>
10951157
{/* /flex-row screenshot+chat wrapper */}
10961158

1159+
{/* Consolidated handoff — one prompt the user pastes into Claude Code / Cursor */}
1160+
{issues.length > 0 && (() => {
1161+
const handoffPrompt = composeHandoffPrompt({
1162+
surfaceDescription,
1163+
productType: results.productContext?.productType,
1164+
gaps: issues,
1165+
});
1166+
const handleCopyHandoff = async () => {
1167+
try {
1168+
if (navigator.clipboard?.writeText) {
1169+
await navigator.clipboard.writeText(handoffPrompt);
1170+
} else {
1171+
const ta = document.createElement('textarea');
1172+
ta.value = handoffPrompt;
1173+
document.body.appendChild(ta);
1174+
ta.select();
1175+
document.execCommand('copy');
1176+
document.body.removeChild(ta);
1177+
}
1178+
trackAuditEvent('audit_handoff_copied', { gapCount: issues.length });
1179+
setHandoffCopied(true);
1180+
setTimeout(() => setHandoffCopied(false), 2000);
1181+
} catch {
1182+
/* swallow — clipboard can fail in private browsing; user can still copy from disclosure */
1183+
}
1184+
};
1185+
return (
1186+
<section className="mt-8">
1187+
<div className="rounded-2xl border border-border-primary bg-background-primary p-6 sm:p-10">
1188+
<p className="text-sm font-semibold uppercase tracking-wider text-accent-primary mb-2">Apply with Claude Code</p>
1189+
<h2 className="text-xl sm:text-2xl font-bold tracking-tight text-text-primary mb-2">Take this to your IDE</h2>
1190+
<p className="text-base text-text-secondary leading-relaxed mb-5">
1191+
Paste this into Claude Code or Cursor. It will find the affected surfaces in your repo, apply each of the {issues.length} pattern{issues.length === 1 ? '' : 's'} in the right files, and report back what changed.
1192+
</p>
1193+
<button
1194+
onClick={handleCopyHandoff}
1195+
className="inline-flex items-center gap-2 px-6 py-3 rounded-full bg-accent-primary text-white dark:text-gray-900 font-semibold text-base hover:bg-accent-hover transition-colors active:scale-95 cursor-pointer"
1196+
>
1197+
{handoffCopied ? (
1198+
<>
1199+
<CheckCircleIcon className="w-5 h-5" />
1200+
Copied
1201+
</>
1202+
) : (
1203+
<>
1204+
<CommandLineIcon className="w-5 h-5" />
1205+
Copy handoff prompt
1206+
</>
1207+
)}
1208+
</button>
1209+
<button
1210+
onClick={() => setShowHandoffSource((v) => !v)}
1211+
aria-expanded={showHandoffSource}
1212+
className="ml-3 sm:ml-4 inline-flex items-center gap-1.5 text-base font-medium text-text-secondary hover:text-text-primary cursor-pointer"
1213+
>
1214+
<ChevronDownIcon className={`w-4 h-4 transition-transform ${showHandoffSource ? 'rotate-180' : ''}`} />
1215+
{showHandoffSource ? 'Hide' : 'Inspect'}
1216+
</button>
1217+
{showHandoffSource && (
1218+
<div className="mt-3 rounded-lg border border-border-primary bg-background-secondary p-4 max-h-96 overflow-y-auto">
1219+
<pre className="text-sm font-mono whitespace-pre-wrap text-text-secondary leading-relaxed">{handoffPrompt}</pre>
1220+
</div>
1221+
)}
1222+
<div className="mt-6 pt-5 border-t border-border-primary">
1223+
<p className="text-base text-text-primary font-semibold mb-1">
1224+
Audit every time you ship.
1225+
</p>
1226+
<p className="text-sm text-text-secondary leading-relaxed">
1227+
Catch AI slop before your users do. Re-run after each change to keep your interface honest.
1228+
</p>
1229+
</div>
1230+
</div>
1231+
</section>
1232+
);
1233+
})()}
1234+
10971235
{/* CTAs */}
1098-
<div className="mt-10 flex flex-col sm:flex-row gap-3 justify-center max-w-2xl mx-auto">
1236+
<div className="mt-6 flex flex-col sm:flex-row gap-3 justify-center max-w-2xl mx-auto">
10991237
<button
11001238
onClick={() => setShowEmailModal(true)}
11011239
className="flex-1 inline-flex items-center justify-center px-5 py-3 rounded-full border border-border-primary bg-background-primary text-text-primary text-sm font-medium hover:bg-background-secondary transition-colors cursor-pointer"
@@ -1126,27 +1264,6 @@ export function FullPageResults({ results, onNewAudit, isAnalyzing, isDemoMode,
11261264
</button>
11271265
</div>
11281266

1129-
{/* Quick Wins */}
1130-
{quickWins.length > 0 && (
1131-
<section className="mt-10 sm:mt-12 max-w-3xl mx-auto">
1132-
<h2 className="text-lg font-semibold text-text-primary mb-4 flex items-center gap-2">
1133-
<LightBulbIcon className="w-5 h-5 text-amber-500" />
1134-
Quick Wins
1135-
</h2>
1136-
<div className="rounded-xl border border-border-primary bg-background-primary p-5">
1137-
<ul className="space-y-3">
1138-
{quickWins.map((win, i) => (
1139-
<li key={i} className="flex gap-3 text-sm">
1140-
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-amber-100 dark:bg-amber-950/40 text-amber-700 dark:text-amber-400 flex items-center justify-center text-xs font-semibold">
1141-
{i + 1}
1142-
</span>
1143-
<span className="text-text-secondary leading-relaxed">{win}</span>
1144-
</li>
1145-
))}
1146-
</ul>
1147-
</div>
1148-
</section>
1149-
)}
11501267
</div>
11511268

11521269
{showEmailModal && (

src/components/audit/GapCard.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,23 +51,23 @@ export function GapCard({ gap, index, isHighlighted, onMouseEnter, onMouseLeave
5151
</span>
5252
)}
5353
<div className="flex-1 min-w-0">
54-
<div className="flex items-center gap-2 flex-wrap mb-2">
55-
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${config.badge}`}>
56-
<Icon className="w-3.5 h-3.5" />
54+
<div className="flex items-center gap-2 flex-wrap mb-3">
55+
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-sm font-medium ${config.badge}`}>
56+
<Icon className="w-4 h-4" />
5757
{config.label}
5858
</span>
59-
<h3 className="font-semibold text-text-primary">{gap.pattern}</h3>
59+
<h3 className="text-lg font-semibold text-text-primary">{gap.pattern}</h3>
6060
</div>
61-
<p className="text-sm text-text-secondary leading-relaxed mb-2">{gap.finding}</p>
61+
<p className="text-base text-text-secondary leading-relaxed mb-3">{gap.finding}</p>
6262
{gap.evidence && (
63-
<p className="text-xs text-text-tertiary leading-relaxed mb-2 pl-3 border-l-2 border-border-primary italic">
64-
<span className="font-semibold not-italic text-text-secondary">What we saw: </span>
63+
<p className="text-sm text-text-secondary leading-relaxed mb-3 pl-3 border-l-2 border-border-primary italic">
64+
<span className="font-semibold not-italic text-text-primary">What we saw: </span>
6565
{gap.evidence}
6666
</p>
6767
)}
6868
{gap.recommendation && (
69-
<p className="text-sm text-text-secondary leading-relaxed">
70-
<span className="font-medium text-text-primary">Fix: </span>
69+
<p className="text-base text-text-secondary leading-relaxed">
70+
<span className="font-semibold text-text-primary">Fix: </span>
7171
{gap.recommendation}
7272
</p>
7373
)}
@@ -85,7 +85,7 @@ export function GapCard({ gap, index, isHighlighted, onMouseEnter, onMouseLeave
8585
href={`/patterns/${slug}`}
8686
target="_blank"
8787
rel="noopener noreferrer"
88-
className="inline-flex items-center gap-1 mt-3 text-sm font-medium text-accent-primary hover:underline"
88+
className="inline-flex items-center gap-1 mt-4 text-base font-medium text-accent-primary hover:underline"
8989
>
9090
See how {gap.pattern} solves this &rarr;
9191
</Link>
@@ -97,7 +97,7 @@ export function GapCard({ gap, index, isHighlighted, onMouseEnter, onMouseLeave
9797
href={gap.resource}
9898
target="_blank"
9999
rel="noopener noreferrer"
100-
className="inline-flex items-center gap-1 mt-3 text-sm font-medium text-accent-primary hover:underline"
100+
className="inline-flex items-center gap-1 mt-4 text-base font-medium text-accent-primary hover:underline"
101101
>
102102
Learn more about this pattern &rarr;
103103
</a>

0 commit comments

Comments
 (0)