95 lines
4.6 KiB
TypeScript
95 lines
4.6 KiB
TypeScript
import React, { useState } 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 { UpgradeBanner } from './UpgradeBanner';
|
|
import { ScopeConfirmDialog } from './ScopeConfirmDialog';
|
|
import { MessageList } from './MessageList';
|
|
import { ChatInput } from './ChatInput';
|
|
import { SitesmithResponse } from '../../types/sitesmith';
|
|
|
|
interface Props { onClose: () => void; }
|
|
|
|
export const SitesmithModal: React.FC<Props> = ({ onClose }) => {
|
|
const cfg = useEditorConfig();
|
|
const siteId = cfg.whpConfig?.siteId ?? 0;
|
|
const { query } = useEditor();
|
|
const { summary, messages, send, loading } = useSitesmith(siteId);
|
|
const apply = useApplyAiResponse();
|
|
const [busy, setBusy] = useState(false);
|
|
const [pendingReplace, setPendingReplace] = useState<SitesmithResponse | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const canChat = summary && (summary.status === 'OK_BONUS' || summary.status === 'OK_MONTHLY');
|
|
|
|
const overlay: React.CSSProperties = { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.8)', zIndex: 9000, display: 'flex', alignItems: 'center', justifyContent: 'center' };
|
|
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', justifyContent: 'space-between', padding: '14px 18px', borderBottom: '1px solid #27272a' };
|
|
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 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);
|
|
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);
|
|
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');
|
|
};
|
|
|
|
return (
|
|
<div role="dialog" aria-modal="true" style={overlay}>
|
|
<div style={panel}>
|
|
<div style={header}>
|
|
<div style={{ fontWeight: 600, color: '#fff' }}>✨ Sitesmith</div>
|
|
{summary && summary.enabled && (
|
|
<div style={{ fontSize: 12, color: '#a1a1aa' }}>
|
|
{summary.monthly_used} / {summary.monthly_cap} this month
|
|
{summary.bonus_credits > 0 && ` • +${summary.bonus_credits} bonus`}
|
|
</div>
|
|
)}
|
|
<button onClick={onClose} aria-label="Close" style={closeBtn}>✕</button>
|
|
</div>
|
|
<div style={body}>
|
|
<UpgradeBanner summary={summary} />
|
|
{error && <div role="alert" style={errBox}>{error}</div>}
|
|
{loading
|
|
? <div style={{ color: '#71717a', textAlign: 'center', padding: 30 }}>Loading…</div>
|
|
: <MessageList messages={messages} />}
|
|
</div>
|
|
<div style={footer}>
|
|
<ChatInput
|
|
disabled={!canChat || busy}
|
|
placeholder={!canChat ? 'Upgrade your plan to use Sitesmith' : busy ? 'Thinking…' : 'Describe what you want…'}
|
|
onSend={handleSend}
|
|
/>
|
|
</div>
|
|
<ScopeConfirmDialog
|
|
open={!!pendingReplace}
|
|
pendingMessage={pendingReplace && 'message' in pendingReplace ? (pendingReplace as any).message : undefined}
|
|
onConfirm={confirmReplace}
|
|
onCancel={() => setPendingReplace(null)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|