import React, { useCallback, useEffect, useMemo, useState } from 'react'; import type { PromptMetricTrend, PromptResponse, PromptResponseEntry, PromptResponsesResponse, } from '../../service/visibility/visibility.interface'; import { getPromptResponses } from '../../service/visibility/visibility.service'; import { isOwnDomain, sentimentToTone, toneToBadgeClass } from './helpers'; import Modal from './Modal'; import { PROMPT_TREND_STATUS_META, PromptSparkline, TrendStatusIcon, VISIBILITY_SCORE_HELP, trendChange, } from './PromptTrend'; interface PromptResponseModalProps { open: boolean; prompt: PromptResponse | null; /** This prompt's composite-score trend (Beta), or ``null`` when unavailable. */ trend: PromptMetricTrend | null; onClose: () => void; clientId: string; token: string; brandId: string; brandDomain: string | null; } /** * Detailed composite-score trend block for the modal header (Beta). Renders * the latest score, delta vs the oldest week in the window, and a larger * sparkline. Returns a "collecting data" hint when no score has landed yet. * * @param {{ trend: PromptMetricTrend }} props - The prompt's trend. * @returns {JSX.Element} The trend block. */ const PromptTrendBlock = ({ trend, }: { trend: PromptMetricTrend; }): JSX.Element => { if (trend.current_score === null) { return (

Visibility trend (Beta): collecting data - check back after the next weekly scan.

); } const meta = PROMPT_TREND_STATUS_META[trend.status]; const baselineScore = trend.series.length ? trend.series[0].composite_score : null; const change = trendChange( baselineScore, trend.current_score, trend.delta_score ); const tone = change.kind === 'delta' ? meta.className : 'text-gray-500'; return (
Visibility trend (Beta)

{VISIBILITY_SCORE_HELP}

{trend.current_score} {change.kind === 'new' && ( New )} {change.kind === 'delta' && ( {`${change.value > 0 ? '+' : ''}${change.value}`} )} {change.kind === 'new' ? 'now surfacing you' : meta.label}
); }; const SNIPPET_PREVIEW_CHARS = 500; const EntryCard = ({ entry, brandDomain, }: { entry: PromptResponseEntry; brandDomain: string | null; }): JSX.Element => { const [expanded, setExpanded] = useState(false); const snippet = entry.response_snippet ?? ''; const shouldFold = snippet.length > SNIPPET_PREVIEW_CHARS; const visibleSnippet = shouldFold && !expanded ? `${snippet.slice(0, SNIPPET_PREVIEW_CHARS).trimEnd()}…` : snippet; const ownCitation = useMemo( () => entry.sources.some(source => isOwnDomain(source, brandDomain)), [entry.sources, brandDomain] ); return (
{entry.engine.toUpperCase()} {entry.scan_date && ( {`Scan date: ${entry.scan_date.slice(0, 10)}`} )} {entry.brand_mentioned ? ( {entry.brand_position ? `Mentioned · position #${entry.brand_position}` : 'Mentioned'} ) : ( Not mentioned )} {entry.sentiment && ( {entry.sentiment} )} {ownCitation && ( Cited from your domain )}
{snippet && (
{visibleSnippet} {shouldFold && (
)}
)} {entry.sources.length > 0 && (

Sources cited

    {entry.sources.map((source, idx) => { const own = isOwnDomain(source, brandDomain); return (
  • {own && (
  • ); })}
)}
); }; /** * Modal showing every historical LLM response recorded for a tracked * prompt. Used as the "proof layer" behind visibility scores. */ const PromptResponseModal = ({ open, prompt, trend, onClose, clientId, token, brandId, brandDomain, }: PromptResponseModalProps): JSX.Element => { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const load = useCallback(async () => { if (!prompt) return; setLoading(true); setError(null); try { const response = await getPromptResponses( clientId, token, brandId, prompt.prompt_id ); setData(response); } catch (err) { setError( err instanceof Error ? err.message : 'Failed to load responses.' ); } finally { setLoading(false); } }, [clientId, token, brandId, prompt]); useEffect(() => { if (open && prompt) { load(); } if (!open) { setData(null); setError(null); } }, [open, prompt, load]); return ( Close } >
{prompt && (
{prompt.category} {prompt.language.toUpperCase()}

{prompt.prompt_text}

{trend && }
)} {error && (

{error}

)} {loading ? (
) : data && data.entries.length > 0 ? (
{data.entries.map((entry, idx) => ( ))}
) : (

No scan responses recorded yet for this prompt. Run a weekly scan to populate this history.

)}
); }; export default PromptResponseModal;