sitesmith: null-safe esc() in Navbar/Menu/Logo + clear chat button

The prior null-safe esc patch only matched 'const esc =' declarations;
Menu/Navbar/Logo use 'function esc(str: string)' syntax and slipped
through. Patched those three to coerce non-strings the same way.

Added "Clear chat" button in the modal header that appears when there's
any message history. Confirms with the user before posting to the new
clear_history endpoint, which deletes all messages + the thread row
for the current site (usage rows are preserved for billing).
This commit is contained in:
2026-05-24 16:03:02 -07:00
parent 069ea1235a
commit 906695379b
5 changed files with 42 additions and 7 deletions

View File

@@ -38,7 +38,8 @@ async function uploadToWhp(file: File): Promise<string | null> {
} }
/* ---------- Helper: escape HTML ---------- */ /* ---------- Helper: escape HTML ---------- */
function esc(str: string): string { function esc(str: any): string {
str = String(str ?? "");
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
} }

View File

@@ -35,7 +35,8 @@ const defaultLinks: MenuLink[] = [
]; ];
/* ---------- Helper: escape HTML ---------- */ /* ---------- Helper: escape HTML ---------- */
function esc(str: string): string { function esc(str: any): string {
str = String(str ?? "");
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
} }

View File

@@ -71,7 +71,8 @@ async function uploadToWhp(file: File): Promise<string | null> {
} }
/* ---------- Helper: escape HTML ---------- */ /* ---------- Helper: escape HTML ---------- */
function esc(str: string): string { function esc(str: any): string {
str = String(str ?? "");
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
} }

View File

@@ -49,5 +49,22 @@ export function useSitesmith(siteId: number) {
return j; return j;
}, [whpConfig, siteId, fetchHistory, refreshEntitlement]); }, [whpConfig, siteId, fetchHistory, refreshEntitlement]);
return { summary, messages, loading, error, send, refreshEntitlement }; const clearHistory = useCallback(async (): Promise<{ ok: boolean; cleared?: number; error?: string }> => {
if (!whpConfig) return { ok: false, error: 'No WHP config' };
try {
const r = await fetch(`${apiBase(whpConfig.apiUrl)}?action=clear_history`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': whpConfig.csrfToken },
body: JSON.stringify({ site_id: siteId }),
});
const j = await r.json();
if (j.ok) setMessages([]);
return j;
} catch (e: any) {
return { ok: false, error: String(e?.message ?? e) };
}
}, [whpConfig, siteId]);
return { summary, messages, loading, error, send, refreshEntitlement, clearHistory };
} }

View File

@@ -17,7 +17,7 @@ export const SitesmithModal: React.FC<Props> = ({ onClose }) => {
const cfg = useEditorConfig(); const cfg = useEditorConfig();
const siteId = cfg.whpConfig?.siteId ?? 0; const siteId = cfg.whpConfig?.siteId ?? 0;
const { query } = useEditor(); const { query } = useEditor();
const { summary, messages, send, loading } = useSitesmith(siteId); const { summary, messages, send, loading, clearHistory } = useSitesmith(siteId);
const apply = useApplyAiResponse(); const apply = useApplyAiResponse();
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [pendingReplace, setPendingReplace] = useState<SitesmithResponse | null>(null); const [pendingReplace, setPendingReplace] = useState<SitesmithResponse | null>(null);
@@ -27,10 +27,11 @@ export const SitesmithModal: React.FC<Props> = ({ onClose }) => {
const overlay: React.CSSProperties = { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.8)', zIndex: 9000, display: 'flex', alignItems: 'center', justifyContent: 'center' }; 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 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 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 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 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 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 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) => { const handleSend = async (text: string) => {
@@ -62,11 +63,25 @@ export const SitesmithModal: React.FC<Props> = ({ onClose }) => {
<div style={header}> <div style={header}>
<div style={{ fontWeight: 600, color: '#fff' }}> Sitesmith</div> <div style={{ fontWeight: 600, color: '#fff' }}> Sitesmith</div>
{summary && summary.enabled && ( {summary && summary.enabled && (
<div style={{ fontSize: 12, color: '#a1a1aa' }}> <div style={{ fontSize: 12, color: '#a1a1aa', marginLeft: 16 }}>
{summary.monthly_used} / {summary.monthly_cap} this month {summary.monthly_used} / {summary.monthly_cap} this month
{summary.bonus_credits > 0 && ` • +${summary.bonus_credits} bonus`} {summary.bonus_credits > 0 && ` • +${summary.bonus_credits} bonus`}
</div> </div>
)} )}
<div style={{ flex: 1 }} />
{messages.length > 0 && (
<button
onClick={async () => {
if (!window.confirm('Clear all Sitesmith chat history for this site? The canvas is unaffected.')) return;
const r = await clearHistory();
if (!r.ok) setError(r.error || 'Failed to clear history');
}}
style={clearBtn}
title="Clear chat history"
>
Clear chat
</button>
)}
<button onClick={onClose} aria-label="Close" style={closeBtn}></button> <button onClick={onClose} aria-label="Close" style={closeBtn}></button>
</div> </div>
<div style={body}> <div style={body}>