import React, { useState } from 'react'; import { ArrowRightIcon, CornerDownRightIcon, RotateCcwIcon, ShieldCheckIcon, } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { Button } from '../../ui/button'; import { isActionDisabled, readCatalog, startedScan, } from '../../../lib/copilot/thread-turns'; import type { ICopilotAction, ICopilotMessage, } from '../../../service/copilot/copilot.interface'; import { cn } from '../cn'; import { CopilotMarkdown } from '../CopilotMarkdown'; import { CopilotSteps } from '../CopilotSteps'; import { GenerationCard, type ThreadArticleCard } from './GenerationCard'; import { KbDocumentCard } from './KbDocumentCard'; import { ScanCard } from './ScanCard'; export type { ThreadArticleCard }; /** * A turn a page sent for the merchant (Ask Copilot, a quest, the guided * setup): what was asked, quietly, so the reply under it does not answer * nothing. Long ones fold to three lines. * * @param props - Component props. * @param props.content - The words the page sent. * @return The line. */ function PageTurn({ content }: { content: string }): React.ReactElement { const [isOpen, setIsOpen] = useState(false); const isLong: boolean = content.length > 160 || content.includes('\n'); return (
Sent from the page

{content}

{isLong && ( )}
); } /** * The conversation itself: the merchant's bubbles, Reco's replies with their * steps and buttons, and the cards of whatever a turn set running. The docked * panel and the HQ page both render this, so a thread reads the same wherever * it is open. * * @param props - Component props. * @param props.messages - The turns, oldest first. * @param props.logo - The mark beside a reply; the plugin icon, when * WordPress gave one. * @param props.isStreaming - A turn is still arriving, so a chip that would * ask a question cannot be pressed yet. * @param props.onAction - A quick-action chip was pressed. * @param props.onLink - A link in a reply was followed, by its href and the * words it was written as. * @param props.onOpenScan - Show a finished scan; the page decides whether * that means opening a canvas or navigating. * @param props.onOpenCatalog - Show the catalog a turn just read. * @param props.onRetry - Ask the last question again, after its answer failed. * @param props.article - What can be done with a finished article. * @return The turns. */ export function CopilotThread({ messages, logo, isStreaming = false, onAction, onLink, onOpenScan, onOpenCatalog, onRetry, article, }: { messages: ICopilotMessage[]; logo: string; isStreaming?: boolean; onAction: (action: ICopilotAction, messageIndex: number) => void; onLink: (href: string, label: string) => void; onRetry?: () => void; onOpenScan?: (brandId: string) => void; onOpenCatalog?: () => void; article: ThreadArticleCard; }): React.ReactElement { const navigate = useNavigate(); const openScan = onOpenScan ?? (() => navigate('/ai-visibility')); const openCatalog = onOpenCatalog ?? (() => navigate('/catalog-quality')); return ( <> {messages.map((message: ICopilotMessage, index: number) => message.role === 'user' && message.hidden ? ( ) : message.role === 'user' ? (
{message.content}
) : (
{logo ? ( Recomaze Co-pilot ) : ( R )}
{message.failed ? (

{message.content}

) : message.content ? ( ) : ( (message.steps?.length ?? 0) === 0 && (isStreaming && index === messages.length - 1 ? ( Thinking... ) : ( No answer came back. )) )} {onRetry && !isStreaming && index === messages.length - 1 && (message.failed || (!message.content && (message.steps?.length ?? 0) === 0)) && ( )} {startedScan(message) && } {readCatalog(message) && ( )} {message.kbDocumentFileName && ( )} navigate(path)} /> {(message.actions?.length ?? 0) > 0 && (
{message.actions?.map( (action: ICopilotAction, actionIndex: number) => ( ) )}
)}
) )} ); }