@@ -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' ;
1718import type { AnalysisResults , TopGap , ProductContext } from '@/types/audit' ;
1819import { GapCard } from './GapCard' ;
1920import { 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'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't find anything to flag on this screenshot, usually because the surface isn'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'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 && (
0 commit comments