import React, { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import { Link } from 'react-router-dom'; import { HiSparkles as Sparkles } from 'react-icons/hi'; import { FaPaperPlane as Send } from 'react-icons/fa'; import { prepareGapKbDraft } from '../../service/agent-analytics/agent-analytics.service'; import { createTextEntry } from '../../service/knowledge-base/knowledge-base.service'; import { TEXT_ENTRY_BODY_MAX_LENGTH, TEXT_ENTRY_TITLE_MAX_LENGTH, } from '../../service/knowledge-base/knowledge-base.routes'; import type { IAgentGap, IAgentGapKbDraft, } from '../../service/agent-analytics/agent-analytics.interface'; /** Server-enforced max length of the "suggest more" hint - matches the * ``extra_prompt`` cap on ``AgentGapKbDraftRequest`` in recomaze-agent. */ const SUGGEST_MORE_MAX_LENGTH: number = 600; interface GapKbDraftModalProps { /** Gap row the merchant clicked "Prepare fix" on; used for the title * fallback when the LLM returns an empty title. */ gap: IAgentGap; /** Editable draft the agent generated (already language-detected). */ draft: IAgentGapKbDraft; /** Called when the merchant cancels (also fired after a successful publish). */ onClose: () => void; /** Called once the publish round-trip succeeds so the trigger button can * flip into a "Published" state. */ onPublished?: () => void; } /** * Modal that lets the merchant edit the LLM-drafted KB entry before * pushing it through ``createTextEntry``. The "Suggest more" zone * re-fires ``prepareGapKbDraft`` with the merchant's hint so the draft * can be regenerated in place without closing the modal. * * @param {GapKbDraftModalProps} props - Component props. * @return {JSX.Element} Modal overlay. */ function GapKbDraftModal({ gap, draft, onClose, onPublished, }: GapKbDraftModalProps): JSX.Element { const [title, setTitle] = useState(draft.title || gap.target); const [body, setBody] = useState(draft.body); const [language, setLanguage] = useState(draft.language || 'en'); const [extraPrompt, setExtraPrompt] = useState(''); const [submitting, setSubmitting] = useState(false); const [regenerating, setRegenerating] = useState(false); const [error, setError] = useState(null); const busy: boolean = submitting || regenerating; useEffect(() => { setTitle(draft.title || gap.target); setBody(draft.body); setLanguage(draft.language || 'en'); setExtraPrompt(''); setError(null); }, [draft.title, draft.body, draft.language, gap.target]); useEffect(() => { const handleEscape = (event: KeyboardEvent) => { if (event.key === 'Escape' && !busy) onClose(); }; document.addEventListener('keydown', handleEscape); return () => { document.removeEventListener('keydown', handleEscape); }; }, [onClose, busy]); const handleRegenerate = async (): Promise => { if (busy) return; setError(null); setRegenerating(true); try { const trimmedHint: string = extraPrompt.trim(); const firstSample = gap.samples[0]; const result: IAgentGapKbDraft | null = await prepareGapKbDraft({ target: gap.target, category: gap.category, sample_fingerprint: firstSample?.fingerprint ?? null, sample_message_index: firstSample?.message_index ?? null, extra_prompt: trimmedHint || null, }); if (!result) { setError('Failed to regenerate the draft.'); return; } setTitle(result.title || gap.target); setBody(result.body); setLanguage(result.language || 'en'); setExtraPrompt(''); } catch (regenerateError) { setError( regenerateError instanceof Error ? regenerateError.message : 'Failed to regenerate the draft.' ); } finally { setRegenerating(false); } }; const handlePublish = async (): Promise => { const trimmedTitle: string = title.trim(); const trimmedBody: string = body.trim(); if (!trimmedTitle || !trimmedBody) { setError('Title and body are both required.'); return; } if (trimmedTitle.length > TEXT_ENTRY_TITLE_MAX_LENGTH) { setError( `Title must be ${TEXT_ENTRY_TITLE_MAX_LENGTH} characters or fewer.` ); return; } if (trimmedBody.length > TEXT_ENTRY_BODY_MAX_LENGTH) { setError( `Body must be ${TEXT_ENTRY_BODY_MAX_LENGTH} characters or fewer. For longer documents, upload a file from the Knowledge Base page.` ); return; } setError(null); setSubmitting(true); try { await createTextEntry({ title: trimmedTitle, description: trimmedBody }); onPublished?.(); onClose(); } catch (publishError) { setError( publishError instanceof Error ? publishError.message : 'Failed to publish the KB entry.' ); } finally { setSubmitting(false); } }; if (typeof document === 'undefined') return <>; return createPortal(
!busy && onClose()} />

Publish KB entry from gap

Edit the draft below, then publish it to the knowledge base. The agent will use it the next time a customer asks about{' '} {gap.target}. Detected language: {language}.

setTitle(event.target.value)} maxLength={TEXT_ENTRY_TITLE_MAX_LENGTH} disabled={busy} placeholder="Short customer-facing question" className="block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-[#B7007C] focus:outline-none focus:ring-1 focus:ring-[#B7007C]" />

{title.length}/{TEXT_ENTRY_TITLE_MAX_LENGTH}