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:
@@ -38,7 +38,8 @@ async function uploadToWhp(file: File): Promise<string | null> {
|
||||
}
|
||||
|
||||
/* ---------- Helper: escape HTML ---------- */
|
||||
function esc(str: string): string {
|
||||
function esc(str: any): string {
|
||||
str = String(str ?? "");
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
|
||||
@@ -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, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,8 @@ async function uploadToWhp(file: File): Promise<string | null> {
|
||||
}
|
||||
|
||||
/* ---------- Helper: escape HTML ---------- */
|
||||
function esc(str: string): string {
|
||||
function esc(str: any): string {
|
||||
str = String(str ?? "");
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ 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 { summary, messages, send, loading, clearHistory } = useSitesmith(siteId);
|
||||
const apply = useApplyAiResponse();
|
||||
const [busy, setBusy] = useState(false);
|
||||
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 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<Props> = ({ onClose }) => {
|
||||
<div style={header}>
|
||||
<div style={{ fontWeight: 600, color: '#fff' }}>✨ Sitesmith</div>
|
||||
{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.bonus_credits > 0 && ` • +${summary.bonus_credits} bonus`}
|
||||
</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>
|
||||
</div>
|
||||
<div style={body}>
|
||||
|
||||
Reference in New Issue
Block a user