import React from 'react'; import { ArrowDownRightIcon, ArrowUpRightIcon, EyeIcon } from 'lucide-react'; import { bandForScore, type ScoreBand, } from '../../../lib/dashboard/score-bands'; import { useCopilot } from '../../../context/copilot.context'; import { TURN_CREDITS } from '../../../service/copilot/pricing'; import type { ScanHistoryPoint, WeeklyReportResponse, } from '../../../service/visibility/visibility.interface'; /** One number on the strip, with everything needed to draw it. */ interface Metric { value: string; label: string; /** The same number last week, or empty when there is no last week. */ previous: string; /** Week-over-week change, or null when there is nothing to compare. */ delta: number | null; /** Unit appended to the delta. */ deltaSuffix: string; /** Colour for the number itself. */ valueColor: string; /** What this number means at its current level, when it has a reading. */ reading: string; } /** One week-over-week change beside its number. */ function Delta({ delta, suffix, }: { delta: number | null; suffix: string; }): React.ReactElement | null { if (delta === null) return null; const rounded: number = Math.round(delta * 10) / 10; if (rounded === 0) return null; const isUp: boolean = rounded > 0; const Icon = isUp ? ArrowUpRightIcon : ArrowDownRightIcon; return ( {isUp ? '+' : ''} {rounded} {suffix} ); } /** * Where visibility stands this week, where it stood last week and — for the * score itself — what that level means, coloured on the same bands the * dashboard's ring uses. A number below the band where AI engines start * citing you should not be the same colour as one above it. */ export function WeekPanel({ report, scans, onAsk, }: { report: WeeklyReportResponse | null; scans: ScanHistoryPoint[]; onAsk: (prompt: string) => void; }): React.ReactElement { const copilot = useCopilot(); const thisWeek: ScanHistoryPoint | undefined = scans[0]; const lastWeek: ScanHistoryPoint | undefined = scans[1]; if (!report && !thisWeek) { return (

AI Visibility

); } const score: number = report?.visibility_score ?? thisWeek?.visibility_score ?? 0; const shareOfVoice: number = report?.share_of_voice ?? thisWeek?.share_of_voice ?? 0; const mentions: number = report?.brand_mentions ?? thisWeek?.brand_mentions ?? 0; const band: ScoreBand = bandForScore(score); // The report's own deltas first; the scan history stands in when this week // has no report row yet. const scoreDelta: number | null = report?.delta_score ?? (lastWeek ? score - (lastWeek.visibility_score ?? 0) : null); const sovDelta: number | null = report?.delta_sov ?? (lastWeek ? shareOfVoice - (lastWeek.share_of_voice ?? 0) : null); const mentionsDelta: number | null = lastWeek ? mentions - (lastWeek.brand_mentions ?? 0) : null; const metrics: readonly Metric[] = [ { value: String(Math.round(score)), label: 'Visibility score', previous: lastWeek ? String(Math.round(lastWeek.visibility_score ?? 0)) : '', delta: scoreDelta, deltaSuffix: '', valueColor: band.text, reading: band.label, }, { value: `${Math.round(shareOfVoice)}%`, label: 'Share of voice', previous: lastWeek ? `${Math.round(lastWeek.share_of_voice ?? 0)}%` : '', delta: sovDelta, deltaSuffix: '%', valueColor: 'text-foreground', reading: '', }, { value: String(mentions), label: 'Brand mentions', previous: lastWeek ? String(lastWeek.brand_mentions ?? 0) : '', delta: mentionsDelta, deltaSuffix: '', valueColor: 'text-foreground', reading: '', }, ]; return (

AI Visibility {report?.week_iso ?? thisWeek?.week_iso ?? ''}

{metrics.map(metric => (
{metric.value}

{metric.label}

{metric.reading && (

{metric.reading}

)} {metric.previous && (

last week {metric.previous}

)}
))}
); }