66 CaretDownIcon ,
77 ChartLineUpIcon ,
88 CopyIcon ,
9+ DotsThreeIcon ,
910 GaugeIcon ,
1011 LightningIcon ,
1112 LinkIcon ,
@@ -19,7 +20,7 @@ import {
1920 XIcon ,
2021} from "@phosphor-icons/react" ;
2122import Link from "next/link" ;
22- import { type ReactNode , useMemo , useState } from "react" ;
23+ import { type ReactNode , useMemo } from "react" ;
2324import { toast } from "sonner" ;
2425import type {
2526 Insight ,
@@ -30,10 +31,15 @@ import type {
3031import {
3132 buildInsightCopyText ,
3233 buildInsightShareUrl ,
33- formatComparisonWindow ,
3434 formatInsightFreshness ,
3535} from "@/app/(main)/insights/lib/insight-meta" ;
3636import type { InsightFeedbackVote } from "@/app/(main)/insights/lib/insight-feedback-vote" ;
37+ import {
38+ DropdownMenu ,
39+ DropdownMenuContent ,
40+ DropdownMenuItem ,
41+ DropdownMenuTrigger ,
42+ } from "@/components/ui/dropdown-menu" ;
3743import { Skeleton } from "@/components/ui/skeleton" ;
3844import { cn } from "@/lib/utils" ;
3945
@@ -108,22 +114,10 @@ const TYPE_STYLES: Record<
108114 } ,
109115} ;
110116
111- const SEVERITY_STYLES : Record <
112- InsightSeverity ,
113- { label : string ; color : string }
114- > = {
115- critical : {
116- label : "Critical" ,
117- color : "text-red-600 dark:text-red-400" ,
118- } ,
119- warning : {
120- label : "Warning" ,
121- color : "text-amber-600 dark:text-amber-400" ,
122- } ,
123- info : {
124- label : "Info" ,
125- color : "text-blue-600 dark:text-blue-400" ,
126- } ,
117+ const SEVERITY_BORDER : Record < InsightSeverity , string > = {
118+ critical : "border-l-red-500" ,
119+ warning : "border-l-amber-500" ,
120+ info : "border-l-blue-400" ,
127121} ;
128122
129123const SENTIMENT_STYLES : Record <
@@ -159,33 +153,32 @@ function buildDiagnosticPrompt(insight: Insight): string {
159153
160154export interface InsightCardProps {
161155 insight : Insight ;
156+ expanded : boolean ;
157+ onToggleAction : ( ) => void ;
162158 onDismissAction ?: ( ) => void ;
163159 feedbackVote ?: InsightFeedbackVote | null ;
164160 onFeedbackAction ?: ( vote : InsightFeedbackVote | null ) => void ;
165161}
166162
167163export function InsightCard ( {
168164 insight,
165+ expanded,
166+ onToggleAction,
169167 onDismissAction,
170168 feedbackVote,
171169 onFeedbackAction,
172170} : InsightCardProps ) {
173- const [ expanded , setExpanded ] = useState ( false ) ;
174171 const typeStyle = TYPE_STYLES [ insight . type ] ;
175- const severityStyle = SEVERITY_STYLES [ insight . severity ] ;
176172 const sentimentStyle = SENTIMENT_STYLES [ insight . sentiment ] ;
173+ const freshnessLine = formatInsightFreshness ( insight ) ;
177174
178175 const agentHref = useMemo ( ( ) => {
179176 const chatId = crypto . randomUUID ( ) ;
180177 const prompt = encodeURIComponent ( buildDiagnosticPrompt ( insight ) ) ;
181178 return `/websites/${ insight . websiteId } /agent/${ chatId } ?prompt=${ prompt } ` ;
182179 } , [ insight ] ) ;
183180
184- const comparisonLine = formatComparisonWindow ( insight ) ;
185- const freshnessLine = formatInsightFreshness ( insight ) ;
186-
187- const copySummaryAction = async ( e : React . MouseEvent ) => {
188- e . stopPropagation ( ) ;
181+ const copySummaryAction = async ( ) => {
189182 try {
190183 await navigator . clipboard . writeText ( buildInsightCopyText ( insight ) ) ;
191184 toast . success ( "Copied insight to clipboard" ) ;
@@ -194,8 +187,7 @@ export function InsightCard({
194187 }
195188 } ;
196189
197- const copyLinkAction = async ( e : React . MouseEvent ) => {
198- e . stopPropagation ( ) ;
190+ const copyLinkAction = async ( ) => {
199191 const url = buildInsightShareUrl ( insight . id ) ;
200192 if ( ! url ) {
201193 return ;
@@ -211,15 +203,23 @@ export function InsightCard({
211203 return (
212204 < div
213205 className = { cn (
214- "scroll-mt-24 transition-colors" ,
206+ "group scroll-mt-24 border-b border-l-2 transition-colors" ,
207+ SEVERITY_BORDER [ insight . severity ] ,
215208 expanded ? "bg-accent/20" : "hover:bg-accent/40"
216209 ) }
217210 id = { `insight-${ insight . id } ` }
218211 >
219- < button
220- className = "flex w-full items-start gap-3 px-4 py-3 text-left sm:px-6"
221- onClick = { ( ) => setExpanded ( ( v ) => ! v ) }
222- type = "button"
212+ < div
213+ className = "flex cursor-pointer items-start gap-3 px-4 py-3 sm:px-6"
214+ onClick = { onToggleAction }
215+ onKeyDown = { ( e ) => {
216+ if ( e . key === "Enter" || e . key === " " ) {
217+ e . preventDefault ( ) ;
218+ onToggleAction ( ) ;
219+ }
220+ } }
221+ role = "button"
222+ tabIndex = { 0 }
223223 >
224224 < div
225225 className = { cn (
@@ -236,9 +236,19 @@ export function InsightCard({
236236 { insight . title }
237237 </ p >
238238 < div className = "flex shrink-0 items-center gap-1.5" >
239- < span className = { cn ( "text-[11px]" , severityStyle . color ) } >
240- { severityStyle . label }
241- </ span >
239+ { onDismissAction && (
240+ < button
241+ aria-label = "Dismiss insight"
242+ className = "flex size-6 items-center justify-center rounded text-muted-foreground opacity-0 transition-all hover:bg-accent hover:text-foreground group-hover:opacity-100"
243+ onClick = { ( e ) => {
244+ e . stopPropagation ( ) ;
245+ onDismissAction ( ) ;
246+ } }
247+ type = "button"
248+ >
249+ < XIcon className = "size-3" weight = "bold" />
250+ </ button >
251+ ) }
242252 < span className = "font-mono text-[11px] text-muted-foreground tabular-nums" >
243253 { insight . priority } /10
244254 </ span >
@@ -283,20 +293,25 @@ export function InsightCard({
283293 </ p >
284294 ) }
285295 </ div >
286- </ button >
296+ </ div >
287297
288298 { expanded && (
289- < div className = "space-y-2.5 px-4 pb-3 pl-14 sm:pl-15" >
290- < p className = "text-pretty text-muted-foreground text-xs leading-relaxed" >
299+ < div
300+ className = "space-y-2.5 px-4 pb-4 pl-14 sm:pl-15"
301+ onClick = { onToggleAction }
302+ onKeyDown = { ( e ) => {
303+ if ( e . key === "Enter" || e . key === " " ) {
304+ e . preventDefault ( ) ;
305+ onToggleAction ( ) ;
306+ }
307+ } }
308+ role = "button"
309+ tabIndex = { - 1 }
310+ >
311+ < p className = "text-pretty text-muted-foreground text-sm leading-relaxed" >
291312 { insight . description }
292313 </ p >
293314
294- { comparisonLine && (
295- < p className = "text-balance text-muted-foreground/70 text-[11px]" >
296- { comparisonLine }
297- </ p >
298- ) }
299-
300315 < div className = "flex items-start gap-2 rounded bg-accent/60 px-2.5 py-2" >
301316 < SparkleIcon
302317 className = "mt-px size-3 shrink-0 text-primary"
@@ -307,67 +322,62 @@ export function InsightCard({
307322 </ p >
308323 </ div >
309324
310- < div className = "flex flex-wrap items-center gap-2" >
325+ < div
326+ className = "flex items-center gap-2 pt-0.5"
327+ onClick = { ( e ) => e . stopPropagation ( ) }
328+ onKeyDown = { ( e ) => e . stopPropagation ( ) }
329+ role = "group"
330+ >
311331 < Link
312332 className = "inline-flex items-center gap-1.5 rounded bg-primary px-3 py-1.5 font-medium text-primary-foreground text-xs transition-opacity hover:opacity-90"
313333 href = { agentHref }
314- onClick = { ( e ) => e . stopPropagation ( ) }
315334 >
316- Investigate with Databunny
335+ Investigate
317336 < ArrowRightIcon className = "size-3" weight = "fill" />
318337 </ Link >
319338 < Link
320- className = "inline-flex items-center gap-1.5 rounded border px-3 py-1.5 font-medium text-foreground text-xs transition-colors hover:bg-accent "
339+ className = "text-muted- foreground text-xs transition-colors hover:text-foreground "
321340 href = { insight . link }
322- onClick = { ( e ) => e . stopPropagation ( ) }
323341 >
324342 View analytics
325343 </ Link >
326- < button
327- className = "inline-flex items-center gap-1 rounded border px-2 py-1.5 text-muted-foreground text-xs transition-colors hover:bg-accent hover:text-foreground"
328- onClick = { copySummaryAction }
329- type = "button"
330- >
331- < CopyIcon aria-hidden className = "size-3.5" weight = "duotone" />
332- </ button >
333- < button
334- className = "inline-flex items-center gap-1 rounded border px-2 py-1.5 text-muted-foreground text-xs transition-colors hover:bg-accent hover:text-foreground"
335- onClick = { copyLinkAction }
336- type = "button"
337- >
338- < LinkIcon aria-hidden className = "size-3.5" weight = "duotone" />
339- </ button >
340344
341- { onDismissAction && (
342- < button
343- aria-label = "Dismiss insight"
344- className = "inline-flex items-center gap-1 rounded border px-2 py-1.5 text-muted-foreground text-xs transition-colors hover:bg-accent hover:text-foreground"
345- onClick = { ( e ) => {
346- e . stopPropagation ( ) ;
347- onDismissAction ( ) ;
348- } }
349- type = "button"
350- >
351- < XIcon aria-hidden className = "size-3.5" weight = "bold" />
352- </ button >
353- ) }
345+ < DropdownMenu >
346+ < DropdownMenuTrigger asChild >
347+ < button
348+ aria-label = "More actions"
349+ className = "flex size-6 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
350+ type = "button"
351+ >
352+ < DotsThreeIcon className = "size-4" weight = "bold" />
353+ </ button >
354+ </ DropdownMenuTrigger >
355+ < DropdownMenuContent align = "start" className = "w-40" >
356+ < DropdownMenuItem onClick = { copySummaryAction } >
357+ < CopyIcon className = "size-4" weight = "duotone" />
358+ Copy insight
359+ </ DropdownMenuItem >
360+ < DropdownMenuItem onClick = { copyLinkAction } >
361+ < LinkIcon className = "size-4" weight = "duotone" />
362+ Copy link
363+ </ DropdownMenuItem >
364+ </ DropdownMenuContent >
365+ </ DropdownMenu >
354366
355367 { onFeedbackAction && (
356- < >
357- < span className = "mx-0.5 h-4 w-px bg-border" />
368+ < div className = "ml-auto flex items-center gap-1" >
358369 < button
359370 aria-label = "Mark as helpful"
360371 aria-pressed = { feedbackVote === "up" }
361372 className = { cn (
362- "inline- flex size-7 items-center justify-center rounded border transition-colors" ,
373+ "flex size-6 items-center justify-center rounded transition-colors" ,
363374 feedbackVote === "up"
364- ? "border-primary bg-primary/10 text-primary"
365- : "text-muted-foreground hover:bg-accent hover:text-foreground"
375+ ? "bg-primary/10 text-primary"
376+ : "text-muted-foreground/50 hover:text-foreground"
366377 ) }
367- onClick = { ( e ) => {
368- e . stopPropagation ( ) ;
369- onFeedbackAction ( feedbackVote === "up" ? null : "up" ) ;
370- } }
378+ onClick = { ( ) =>
379+ onFeedbackAction ( feedbackVote === "up" ? null : "up" )
380+ }
371381 type = "button"
372382 >
373383 < ThumbsUpIcon className = "size-3.5" weight = "duotone" />
@@ -376,20 +386,19 @@ export function InsightCard({
376386 aria-label = "Mark as not helpful"
377387 aria-pressed = { feedbackVote === "down" }
378388 className = { cn (
379- "inline- flex size-7 items-center justify-center rounded border transition-colors" ,
389+ "flex size-6 items-center justify-center rounded transition-colors" ,
380390 feedbackVote === "down"
381- ? "border-destructive bg-destructive/10 text-destructive"
382- : "text-muted-foreground hover:bg-accent hover:text-foreground"
391+ ? "bg-destructive/10 text-destructive"
392+ : "text-muted-foreground/50 hover:text-foreground"
383393 ) }
384- onClick = { ( e ) => {
385- e . stopPropagation ( ) ;
386- onFeedbackAction ( feedbackVote === "down" ? null : "down" ) ;
387- } }
394+ onClick = { ( ) =>
395+ onFeedbackAction ( feedbackVote === "down" ? null : "down" )
396+ }
388397 type = "button"
389398 >
390399 < ThumbsDownIcon className = "size-3.5" weight = "duotone" />
391400 </ button >
392- </ >
401+ </ div >
393402 ) }
394403 </ div >
395404 </ div >
@@ -400,7 +409,7 @@ export function InsightCard({
400409
401410export function InsightCardSkeleton ( ) {
402411 return (
403- < div className = "flex items-start gap-3 px-4 py-3 sm:px-6" >
412+ < div className = "flex items-start gap-3 border-b px-4 py-3 sm:px-6" >
404413 < Skeleton className = "mt-0.5 size-7 shrink-0 rounded" />
405414 < div className = "min-w-0 flex-1 space-y-2" >
406415 < div className = "flex items-start justify-between gap-2" >
0 commit comments