From 906695379bfcd7dab551bb36a7d404e242cf6437 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 24 May 2026 16:03:02 -0700 Subject: [PATCH] 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). --- craft/src/components/basic/Logo.tsx | 3 ++- craft/src/components/basic/Menu.tsx | 3 ++- craft/src/components/basic/Navbar.tsx | 3 ++- craft/src/hooks/useSitesmith.ts | 19 ++++++++++++++++- craft/src/panels/sitesmith/SitesmithModal.tsx | 21 ++++++++++++++++--- 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/craft/src/components/basic/Logo.tsx b/craft/src/components/basic/Logo.tsx index af68498..984a01a 100644 --- a/craft/src/components/basic/Logo.tsx +++ b/craft/src/components/basic/Logo.tsx @@ -38,7 +38,8 @@ async function uploadToWhp(file: File): Promise { } /* ---------- Helper: escape HTML ---------- */ -function esc(str: string): string { +function esc(str: any): string { + str = String(str ?? ""); return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } diff --git a/craft/src/components/basic/Menu.tsx b/craft/src/components/basic/Menu.tsx index 5c9b16e..5963187 100644 --- a/craft/src/components/basic/Menu.tsx +++ b/craft/src/components/basic/Menu.tsx @@ -35,7 +35,8 @@ const defaultLinks: MenuLink[] = [ ]; /* ---------- Helper: escape HTML ---------- */ -function esc(str: string): string { +function esc(str: any): string { + str = String(str ?? ""); return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } diff --git a/craft/src/components/basic/Navbar.tsx b/craft/src/components/basic/Navbar.tsx index 7ef548a..ac59b96 100644 --- a/craft/src/components/basic/Navbar.tsx +++ b/craft/src/components/basic/Navbar.tsx @@ -71,7 +71,8 @@ async function uploadToWhp(file: File): Promise { } /* ---------- Helper: escape HTML ---------- */ -function esc(str: string): string { +function esc(str: any): string { + str = String(str ?? ""); return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } diff --git a/craft/src/hooks/useSitesmith.ts b/craft/src/hooks/useSitesmith.ts index 87dae41..95ab7d4 100644 --- a/craft/src/hooks/useSitesmith.ts +++ b/craft/src/hooks/useSitesmith.ts @@ -49,5 +49,22 @@ export function useSitesmith(siteId: number) { return j; }, [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 }; } diff --git a/craft/src/panels/sitesmith/SitesmithModal.tsx b/craft/src/panels/sitesmith/SitesmithModal.tsx index 8d55ce9..055aabb 100644 --- a/craft/src/panels/sitesmith/SitesmithModal.tsx +++ b/craft/src/panels/sitesmith/SitesmithModal.tsx @@ -17,7 +17,7 @@ export const SitesmithModal: React.FC = ({ onClose }) => { const cfg = useEditorConfig(); const siteId = cfg.whpConfig?.siteId ?? 0; const { query } = useEditor(); - const { summary, messages, send, loading } = useSitesmith(siteId); + const { summary, messages, send, loading, clearHistory } = useSitesmith(siteId); const apply = useApplyAiResponse(); const [busy, setBusy] = useState(false); const [pendingReplace, setPendingReplace] = useState(null); @@ -27,10 +27,11 @@ export const SitesmithModal: React.FC = ({ 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 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 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) => { @@ -62,11 +63,25 @@ export const SitesmithModal: React.FC = ({ onClose }) => {
✨ Sitesmith
{summary && summary.enabled && ( -
+
{summary.monthly_used} / {summary.monthly_cap} this month {summary.bonus_credits > 0 && ` • +${summary.bonus_credits} bonus`}
)} +
+ {messages.length > 0 && ( + + )}