Files
site-builder/craft/src/hooks/useSitesmith.ts
T
shadowdaoandClaude Opus 4.7 d0925d9e2d site-builder: dynamic CTAs, section anchors, edit-with-Sitesmith
Three related features:

1. Dynamic CTA buttons on HeroSimple, CTASection, CallToAction.
   New shared ctas[] array (text + href + variant + target) replaces the
   primary/secondary pair. Settings panel gets add/remove/reorder controls.
   Legacy fields stay readable for backwards compat — first user edit
   migrates the section onto the new array.

2. Anchor IDs on all layout/section components (Container, Section,
   BackgroundSection, ColumnLayout, plus 6 section blocks done by parallel
   subagent, plus Hero/CTA/CallToAction). Anchor input lives in the
   settings panel with an "auto from heading" button that walks the
   subtree for the first Heading.text. Renders as id="..." on the
   outermost element so #anchor URLs resolve.

3. Edit-with-Sitesmith targeted invocation. Right-click → "Ask Sitesmith"
   and a button at the top of the right-side settings panel both open the
   modal pre-targeted at the selected node. The node's serialized subtree
   is sent to the server; system prompt is augmented to require a patch
   with replace_node. Editor lifts modal state into a new SitesmithContext.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:43:28 -07:00

77 lines
3.2 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react';
import { useEditorConfig } from '../state/EditorConfigContext';
import { SitesmithSummary, SitesmithMessage, SendResult } from '../types/sitesmith';
function apiBase(apiUrl: string): string {
return apiUrl.replace(/site-builder\.php$/, 'sitesmith.php');
}
export function useSitesmith(siteId: number) {
const { whpConfig } = useEditorConfig();
const [summary, setSummary] = useState<SitesmithSummary | null>(null);
const [messages, setMessages] = useState<SitesmithMessage[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const refreshEntitlement = useCallback(async () => {
if (!whpConfig) return;
try {
const r = await fetch(`${apiBase(whpConfig.apiUrl)}?action=entitlement`, { credentials: 'include' });
const j = await r.json();
if (j.ok) setSummary(j.summary);
} catch (e: any) { setError(String(e?.message ?? e)); }
}, [whpConfig]);
const fetchHistory = useCallback(async () => {
if (!whpConfig) { setLoading(false); return; }
try {
const r = await fetch(`${apiBase(whpConfig.apiUrl)}?action=history&site_id=${siteId}`, { credentials: 'include' });
const j = await r.json();
if (j.ok) setMessages(j.messages);
} catch (e: any) { setError(String(e?.message ?? e)); }
finally { setLoading(false); }
}, [whpConfig, siteId]);
useEffect(() => { void refreshEntitlement(); void fetchHistory(); }, [refreshEntitlement, fetchHistory]);
const send = useCallback(async (
userText: string,
canvasSummary: string,
target?: { node_id: string; display_name: string; tree_json: string },
): Promise<SendResult> => {
if (!whpConfig) return { ok: false, status: 'BLOCKED', message: 'No WHP config' };
setMessages((m) => [...m, { role: 'user', content: userText, response_type: null, created_at: new Date().toISOString() }]);
const body: Record<string, unknown> = { site_id: siteId, message: userText, canvas_summary: canvasSummary };
if (target) body.target = target;
const r = await fetch(`${apiBase(whpConfig.apiUrl)}?action=send`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': whpConfig.csrfToken },
body: JSON.stringify(body),
});
const j: SendResult = await r.json();
void fetchHistory();
void refreshEntitlement();
return j;
}, [whpConfig, siteId, fetchHistory, 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 };
}