import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { CardSkeleton } from '../agent-analytics/CardSkeleton'; import EmptyState from '../agent-analytics/EmptyState'; import InfoTooltip from '../agent-analytics/InfoTooltip'; import ConfirmDialog from '../ui/ConfirmDialog'; import type { ChatPromptVolumeEntry } from '../../service/visibility/visibility.interface'; import { adoptFromChat, getChatPromptVolumes, stopTrackingFromChat, } from '../../service/visibility/visibility.service'; import { stopTrackingChatThemeTexts } from '../../utils/confirmTexts'; import { toneToBadgeClass } from './helpers'; interface TrendingInYourChatProps { clientId: string; token: string; brandId: string; onAdopted?: () => void; } const DEFAULT_VISIBLE = 8; /** * Surfaces trending themes mined from the merchant's own chatbot, with * "Track in AI search" to promote a theme into the tracked prompt set. */ const TrendingInYourChat = ({ clientId, token, brandId, onAdopted, }: TrendingInYourChatProps): JSX.Element => { const [themes, setThemes] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [busyByTheme, setBusyByTheme] = useState>({}); const [adoptedByTheme, setAdoptedByTheme] = useState>( {} ); const [expanded, setExpanded] = useState(false); const [pendingStopTheme, setPendingStopTheme] = useState(null); const load = useCallback(async () => { setLoading(true); setError(null); try { const response = await getChatPromptVolumes(clientId, token, brandId); setThemes(response.themes || []); } catch (err) { setError( err instanceof Error ? err.message : 'Failed to load chat themes.' ); } finally { setLoading(false); } }, [clientId, token, brandId]); useEffect(() => { load(); }, [load]); const sortedThemes = useMemo(() => { return [...themes].sort((a, b) => { if (b.trending_score !== a.trending_score) { return (b.trending_score || 0) - (a.trending_score || 0); } return (b.volume_30d || 0) - (a.volume_30d || 0); }); }, [themes]); const visibleThemes = expanded ? sortedThemes : sortedThemes.slice(0, DEFAULT_VISIBLE); const hiddenCount = sortedThemes.length - visibleThemes.length; const handleAdopt = async (theme: ChatPromptVolumeEntry) => { setBusyByTheme(prev => ({ ...prev, [theme.prompt_theme_id]: true })); try { await adoptFromChat(clientId, token, brandId, { theme_ids: [theme.prompt_theme_id], }); setAdoptedByTheme(prev => ({ ...prev, [theme.prompt_theme_id]: true, })); onAdopted?.(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to adopt theme.'); } finally { setBusyByTheme(prev => ({ ...prev, [theme.prompt_theme_id]: false })); } }; /** * Backdrop/Esc/cancel handler for the stop-tracking confirm modal. * * @param next {boolean} Requested open state from the modal. * @returns {void} */ const handleStopThemeOpenChange = (next: boolean): void => { if (!next) setPendingStopTheme(null); }; /** * Confirm-side handler that forwards the staged theme to the actual * untrack runner. Kept separate so the JSX stays declarative. * * @returns {void} */ const handleStopThemeConfirm = (): void => { if (pendingStopTheme) handleStopTracking(pendingStopTheme); }; const handleStopTracking = async (theme: ChatPromptVolumeEntry) => { setPendingStopTheme(null); setBusyByTheme(prev => ({ ...prev, [theme.prompt_theme_id]: true })); try { await stopTrackingFromChat(clientId, token, brandId, { theme_ids: [theme.prompt_theme_id], }); // Backend hard-deletes the theme row so it should disappear from the // trending list entirely (per product decision). The miner may // resurface it on the next scan if user questions keep clustering // around it, which is fine. setThemes(prev => prev.filter(row => row.prompt_theme_id !== theme.prompt_theme_id) ); setAdoptedByTheme(prev => { const next = { ...prev }; delete next[theme.prompt_theme_id]; return next; }); onAdopted?.(); } catch (err) { setError( err instanceof Error ? err.message : 'Failed to stop tracking theme.' ); } finally { setBusyByTheme(prev => { const next = { ...prev }; delete next[theme.prompt_theme_id]; return next; }); } }; return (

Trending in your chat

{error && (

{error}

)} {loading && themes.length === 0 ? (
) : sortedThemes.length === 0 ? ( ) : ( <>
{visibleThemes.map(theme => { const alreadyTracked = adoptedByTheme[theme.prompt_theme_id] || Boolean(theme.mapped_visibility_prompt_id); const trendingStrong = theme.trending_score > 50; const samples = (theme.sample_questions || []) .map(q => (q || '').trim()) .filter(q => q !== '') .slice(0, 2); return (

{theme.theme_label}

{samples.length > 0 && (

{samples.map((q, idx) => ( {idx > 0 ? ' · ' : ''}e.g. "{q}" ))}

)}
{`Trending ${theme.trending_score}`} {`7d ${theme.volume_7d} · 30d ${theme.volume_30d}`} {alreadyTracked ? ( <> Tracked ) : ( )}
); })}
{hiddenCount > 0 && !expanded && (
)} {expanded && sortedThemes.length > DEFAULT_VISIBLE && (
)} )}
); }; export default TrendingInYourChat;