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 && (
)}
{source}
);
})}
)}
);
};
/**
* 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 && (
)}
{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;