// The standing content disclaimer, edited where the content lives: a merchant // looks for it next to the contents it rides along with, not in a settings // drawer three clicks away. import React, { useEffect, useState } from 'react'; import { LuScrollText } from 'react-icons/lu'; import { cn } from '../copilot/cn'; import { ARTICLE_STATUS } from '../../service/visibility/visibility.constants'; import type { ArticleSummary, BrandResponse, DisclaimerPosition, } from '../../service/visibility/visibility.interface'; import { getArticle, patchBrand, } from '../../service/visibility/visibility.service'; import Modal from './Modal'; import PagePreview from './PagePreview'; // Marker the generator wraps the disclaimer in, mirrored on the backend. const DISCLAIMER_CLASS = 'recomaze-disclaimer'; function withDisclaimer( markdown: string, text: string, position: DisclaimerPosition ): string { const line = text.trim(); if (!line) return markdown; const span = `${line}`; return position === 'top' ? `${span}\n\n${markdown}` : `${markdown}\n\n${span}`; } interface DisclaimerDialogProps { clientId: string; token: string; brand: BrandResponse | null; articles?: ArticleSummary[]; onSaved?: (brand: BrandResponse) => void; onToast?: (message: string) => void; } export function DisclaimerDialog({ clientId, token, brand, articles = [], onSaved, onToast, }: DisclaimerDialogProps): JSX.Element { const [open, setOpen] = useState(false); const [sample, setSample] = useState<{ title: string; slug: string | null; markdown: string; } | null>(null); const [text, setText] = useState(''); const [position, setPosition] = useState('bottom'); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); // Re-seed whenever the dialog opens, so a cancelled edit never lingers. useEffect(() => { if (!open) return; setText(brand?.article_disclaimer || ''); setPosition( brand?.article_disclaimer_position === 'top' ? 'top' : 'bottom' ); }, [open, brand]); // Preview against one of the merchant's own articles rather than grey // placeholder bars: seeing the line land under their actual prose is the // whole question they are trying to answer. useEffect(() => { if (!open || sample) return; const ready = articles.filter( article => article.status === ARTICLE_STATUS.READY ); const pick = ready[Math.floor(Math.random() * ready.length)]; if (!pick) return; let cancelled = false; getArticle(clientId, token, pick.article_id) .then(detail => { if (cancelled) return; setSample({ title: detail.h1 || detail.summary.title || pick.title, slug: detail.summary.slug ?? null, markdown: detail.markdown || '', }); }) .catch(() => { // Fall back to the skeleton preview below. }); return () => { cancelled = true; }; }, [open, sample, articles, clientId, token]); const save = async (): Promise => { if (!brand) return; setSaving(true); try { const updated = await patchBrand(clientId, token, brand.brand_id, { article_disclaimer: text.trim(), article_disclaimer_position: position, }); onSaved?.(updated); onToast?.( text.trim() ? 'Disclaimer saved. It rides with every content generated from now on.' : 'Disclaimer removed from future contents.' ); setOpen(false); } catch (saveError) { setError( saveError instanceof Error ? saveError.message : 'Could not save the disclaimer.' ); } finally { setSaving(false); } }; const active = Boolean(brand?.article_disclaimer?.trim()); return ( <> setOpen(false)} title="Content Disclaimer" size="lg" footer={ <> } >

A standing line - prices change, not medical advice, an affiliate notice - added to every content generated from now on. Contents already written keep what they have.