import React, { useCallback, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { useLocation, useNavigate } from 'react-router-dom'; import { LuArrowRight, LuArrowUp, LuCheckCircle, LuHome, LuImagePlus, LuLoader2, LuMaximize2, LuMinimize2, LuRocket, LuSquare, LuTrash2, LuTrendingUp, LuX, } from 'react-icons/lu'; import { recomaze_ai_personalization_env } from '../../env'; import { useCopilot } from '../../context/copilot.context'; import { fetchCopilotArticleStatus, fetchCopilotContentState, fetchCopilotHistory, fetchCopilotQuests, publishCopilotArticleToBlog, refreshCopilotQuests, saveCopilotConversation, streamCopilot, } from '../../service/copilot/copilot.service'; import type { ICopilotAction, ICopilotMessage, ICopilotQuestOverview, } from '../../service/copilot/copilot.interface'; import ArticleModal from '../visibility/ArticleModal'; import { cn } from './cn'; import { CopilotMarkdown } from './CopilotMarkdown'; import { CopilotSteps } from './CopilotSteps'; import { CopilotHome } from './CopilotHome'; /** Tool ids whose result is an async generation worth showing as a status card. */ const GENERATION_TOOL_IDS: ReadonlySet = new Set([ 'generate_article', 'start_visibility_scan', 'generate_product_content', 'bulk_generate_product_content', ]); /** How often an in-chat generation card polls its job status. */ const GENERATION_POLL_INTERVAL_MS = 6_000; /** Article statuses that mean the in-chat generation card can show "ready". */ const READY_ARTICLE_STATUSES: ReadonlySet = new Set([ 'ready', 'published', ]); /** Content-job states that mean the emailed result is on its way / done. */ const DONE_CONTENT_STATES: ReadonlySet = new Set([ 'completed', 'failed', ]); /** Tool ids that change server data - after these, affected pages re-fetch. */ const WRITE_TOOL_IDS: ReadonlySet = new Set([ 'call_api', 'bulk_delete', 'generate_article', 'generate_product_content', 'start_visibility_scan', 'update_brand_keywords', 'update_visibility_settings', 'generate_kb_document', 'push_kb_document', 'add_knowledge_text', ]); /** Logo shown in the launcher / panel header (WordPress-localized plugin icon). */ const COPILOT_LOGO: string = recomaze_ai_personalization_env?.plugin_icon ?? ''; /** * Effective width of the open (non-wide) panel, in px. The panel is * ``sm:w-[380px] lg:w-[400px]``; the wider 400px is used so the WP admin * content is pushed clear of the panel on desktop, where the overlap matters. */ const PANEL_OPEN_WIDTH_PX = 400; /** Body class toggled while the panel is open so WP admin content shifts left. */ const COPILOT_OPEN_BODY_CLASS = 'recomaze-copilot-open'; /** * CSS that (a) keeps the fixed panel + launcher below the WP admin bar, and * (b) pushes ONLY the plugin's own React root (#recomaze) left while the panel * is open, so the panel never overlaps the page. We deliberately do NOT touch * #wpcontent/#wpfooter: shrinking those disturbs the WordPress admin layout * (left menu) and can trigger horizontal overflow. Injected once, app-wide. */ const COPILOT_LAYOUT_CSS: string = ` .recomaze-copilot-panel { top: 32px; height: calc(100% - 32px); } .recomaze-copilot-launcher { bottom: 1.25rem; } body.${COPILOT_OPEN_BODY_CLASS} #recomaze { margin-right: ${PANEL_OPEN_WIDTH_PX}px; transition: margin-right .2s; } @media screen and (max-width: 782px) { .recomaze-copilot-panel { top: 46px; height: calc(100% - 46px); } body.${COPILOT_OPEN_BODY_CLASS} #recomaze { margin-right: 0; } } /* WordPress admin styles bare inputs/textareas with a grey field, border and a dark focus box-shadow. Neutralize that inside the panel so the chat input stays transparent (no grey box / black focus outline when typing). */ .recomaze-copilot-panel input, .recomaze-copilot-panel textarea { background: transparent !important; border: 0 !important; border-radius: 0 !important; box-shadow: none !important; outline: none !important; min-height: 0 !important; color: inherit !important; } `; /** Whether the app-wide copilot layout CSS has been injected this session. */ let copilotLayoutCssInjected = false; /** * Inject the panel-layout stylesheet once. Self-contained: appends a single *
{COPILOT_LOGO ? ( Recomaze ) : ( R )} recomaze
{messages.length > 0 && ( )}
{view === 'home' || messages.length === 0 ? ( void sendMessage(text)} /> ) : ( messages.map((message: ICopilotMessage, index: number) => message.role === 'user' ? (
{message.content}
) : (
{COPILOT_LOGO ? ( Recomaze Co-pilot ) : ( R )}
{message.content ? ( ) : ( (message.steps?.length ?? 0) === 0 && ( Thinking... ) )} {(message.steps ?? []).some(step => GENERATION_TOOL_IDS.has(step.id) ) && (message.generationKind === 'content' ? ( message.generationStatus === 'ready' ? (

Sent to your email

Your product content is on its way - check your inbox.

) : (

Generating product content

We'll email you the result when it's ready.

) ) : message.generationStatus === 'ready' ? (

Ready

Your content is ready to review.

{publishedArticleIds.has( message.generationArticleId ?? '' ) ? ( Published to blog ) : ( )}
) : (

Generating

In progress - it will appear in the list shortly.

))} {(message.actions?.length ?? 0) > 0 && (
{message.actions?.map( (action: ICopilotAction, actionIndex: number) => ( ) )}
)}
) ) )}
{pendingLogo && (
{pendingLogo.name} {pendingLogo.name}
)} { const file: File | undefined = event.target.files?.[0]; if (file) void handleAttachLogo(file); event.target.value = ''; }} />
setInput(event.target.value)} placeholder="Ask anything..." className="flex-1 bg-transparent text-sm text-gray-800 placeholder:text-gray-400 focus:outline-none" /> {isStreaming ? ( ) : ( )}

Recomaze Co-pilot can make mistakes. Check important info.

{/* Reader for a generated article opened from a ready generation card. */} setOpenArticleId(null)} clientId={clientId} token={token} /> , document.body ); }