import { useEffect, useMemo, useState } from 'react'; import { Callout } from '../../components/ui/callout'; import { NO_AI_ACCESS } from '../../lib/seo/ai-access'; import { classNames } from '../../lib/utils'; import { PARAGRAPH, wordDiff } from '../lib/diff'; /** What the fix dialogs share: the error of a call and the word diff of a proposal. */ interface FetchError { code?: string; message?: string; data?: { status?: number }; } export interface FixError { message: string; status: number; } /** * What apiFetch says when the site's answer never came as JSON, in words * that say what happened: a host that cut the request answers with an * error page of its own. */ const FETCH_FAILURES: Record = { invalid_json: "The site's server answered with an error page instead of Recomaze's answer, usually because it cut a request that ran too long. Nothing was applied; try again, and if it keeps happening, write to Recomaze support.", fetch_error: 'The connection to the site dropped before the answer came. Check the connection and try again.', }; /** The message and status of a failed apiFetch, readable as the server sent them. */ export const errorOf = (error: unknown): FixError => { const failed = (error ?? {}) as FetchError; return { message: FETCH_FAILURES[failed.code ?? ''] ?? (typeof failed.message === 'string' && failed.message !== '' ? failed.message : 'Recomaze could not be reached. Try again in a moment.'), status: Number(failed.data?.status ?? 0), }; }; /** The error of a call, with the way to more credits when that is what it is about. */ export const FixErrorCallout = ({ error, pricingUrl, }: { error: FixError; pricingUrl: string; }): JSX.Element => ( {error.message} {error.status === 402 && ( <> {' '} Top up credits )} ); /** The words that changed between two texts, marked up. */ export const Diff = ({ before, after, }: { before: string; after: string; }): JSX.Element => { const parts = useMemo(() => wordDiff(before, after), [before, after]); return (

{parts.map((part, index) => ( {part.text.replace(new RegExp(` ?${PARAGRAPH} ?`, 'g'), '\n\n')} {index < parts.length - 1 ? ' ' : ''} ))}

); }; /** Why the AI buttons are greyed out for this user. */ export const NoAiAccess = (): JSX.Element => ( {NO_AI_ACCESS} ); /** * A run that already went through on this page today asks first: the * first press shows the question, the second runs it. * * @param question - The question to ask (lib/ai-runs), or null when nothing ran today; a new question starts over. * @return Whether the question is showing, and the press handler. */ export const useRepeatGuard = ( question: string | null ): { asking: boolean; press: (run: () => void) => void } => { const [asking, setAsking] = useState(false); useEffect(() => { setAsking(false); }, [question]); const press = (run: () => void): void => { if (question !== null && !asking) { setAsking(true); return; } setAsking(false); run(); }; return { asking: asking && question !== null, press }; };