import React, { useState, useRef, useEffect } from 'react'; import { useEditor } from '@craftjs/core'; import { useEditorConfig } from '../../state/EditorConfigContext'; import { useSitesmith } from '../../hooks/useSitesmith'; import { useApplyAiResponse } from '../../utils/apply-ai-response'; import { summarizeCanvas } from '../../utils/canvas-summary'; import { SitesmithTarget } from '../../state/SitesmithContext'; import { UpgradeBanner } from './UpgradeBanner'; import { ScopeConfirmDialog } from './ScopeConfirmDialog'; import { MessageList } from './MessageList'; import { ChatInput } from './ChatInput'; import { WorkingIndicator } from './WorkingIndicator'; import { SitesmithResponse } from '../../types/sitesmith'; import { Modal } from '../../ui/Modal'; interface Props { onClose: () => void; /** When set, the chat is biased toward editing this specific node and the AI is * instructed to return a `patch` op. The node's serialized tree is sent along. */ target?: SitesmithTarget | null; } export const SitesmithModal: React.FC = ({ onClose, target }) => { const cfg = useEditorConfig(); const siteId = cfg.whpConfig?.siteId ?? 0; const { query } = useEditor(); const { summary, messages, send, loading, clearHistory } = useSitesmith(siteId); const apply = useApplyAiResponse(); const [busy, setBusy] = useState(false); const [pendingReplace, setPendingReplace] = useState(null); const [error, setError] = useState(null); const [confirmClearChat, setConfirmClearChat] = useState(false); const clearChatTimeoutRef = useRef | null>(null); useEffect(() => { return () => { if (clearChatTimeoutRef.current) clearTimeout(clearChatTimeoutRef.current); }; }, []); const canChat = summary && (summary.status === 'OK_BONUS' || summary.status === 'OK_MONTHLY'); const panel: React.CSSProperties = { background: '#0f0f17', border: '1px solid #27272a', borderRadius: 12, width: 'min(720px, 90vw)', maxHeight: '90vh', display: 'flex', flexDirection: 'column' }; const header: React.CSSProperties = { display: 'flex', alignItems: 'center', padding: '14px 18px', borderBottom: '1px solid #27272a', gap: 8 }; const body: React.CSSProperties = { flex: 1, padding: '14px 18px', overflowY: 'auto', display: 'flex', flexDirection: 'column' }; const footer: React.CSSProperties = { padding: '12px 18px', borderTop: '1px solid #27272a' }; const closeBtn:React.CSSProperties = { background: 'transparent', color: '#a1a1aa', border: 'none', fontSize: 18, cursor: 'pointer' }; const clearBtn:React.CSSProperties = { background: 'transparent', color: '#a1a1aa', border: '1px solid #3f3f46', borderRadius: 4, padding: '4px 10px', fontSize: 12, cursor: 'pointer', marginRight: 8 }; const errBox: React.CSSProperties = { background: '#3b1d1d', border: '1px solid #7f1d1d', color: '#fecaca', padding: '8px 12px', borderRadius: 6, marginBottom: 10, fontSize: 13 }; const handleSend = async (text: string) => { setBusy(true); setError(null); try { const canvas = summarizeCanvas(query.getSerializedNodes()); const result = await send(text, canvas, target ? { node_id: target.nodeId, display_name: target.displayName, tree_json: target.treeJson, } : undefined); if (!result.ok) { setError(result.message || 'Failed'); return; } if (result.response.type === 'replace' && result.response.scope === 'site') { setPendingReplace(result.response); return; } const applied = await apply(result.response, target?.nodeId); if (!applied.ok) setError(applied.message || 'Apply failed'); } catch (e: any) { setError(String(e?.message ?? e)); } finally { setBusy(false); } }; const confirmReplace = async () => { if (!pendingReplace) return; const r = await apply(pendingReplace); setPendingReplace(null); if (!r.ok) setError(r.message || 'Apply failed'); }; const cancelClearChat = () => { if (clearChatTimeoutRef.current) clearTimeout(clearChatTimeoutRef.current); setConfirmClearChat(false); }; const handleClearChatClick = async () => { if (!confirmClearChat) { setConfirmClearChat(true); if (clearChatTimeoutRef.current) clearTimeout(clearChatTimeoutRef.current); clearChatTimeoutRef.current = setTimeout(() => setConfirmClearChat(false), 4000); return; } cancelClearChat(); const r = await clearHistory(); if (!r.ok) setError(r.error || 'Failed to clear history'); }; return (
Sitesmith
{summary && summary.enabled && (
{summary.monthly_used} / {summary.monthly_cap} this month {summary.bonus_credits > 0 && ` • +${summary.bonus_credits} bonus`}
)}
{messages.length > 0 && ( confirmClearChat ? ( <> ) : ( ) )}
{target && (
Editing {target.displayName} — describe the change you want and Sitesmith will modify just this block.
)} {error &&
{error}
} {loading ?
Loading…
: }
{busy ? ( ) : ( )}
setPendingReplace(null)} />
); };