sitesmith: null-safe esc() across all toHtml + WorkingIndicator
Real-world AI output frequently sends mismatched prop names (e.g. items vs features, cta object vs buttonText/Href). The toHtml functions of section/form/sections-folder components each defined a local esc = (s: string) => s.replace(...) that crashed when called with undefined, taking the auto-save export with it. Patched every local esc() to coerce non-strings: const esc = (s: any) => String(s ?? "").replace(...) 17 files touched; behavior unchanged for valid string inputs. Also adds a WorkingIndicator (Claude Code-style spinner + rotating phrase + elapsed seconds) shown in the modal footer while a generation is in flight, replacing the disabled "Thinking..." placeholder.
This commit is contained in:
@@ -8,6 +8,7 @@ import { UpgradeBanner } from './UpgradeBanner';
|
||||
import { ScopeConfirmDialog } from './ScopeConfirmDialog';
|
||||
import { MessageList } from './MessageList';
|
||||
import { ChatInput } from './ChatInput';
|
||||
import { WorkingIndicator } from './WorkingIndicator';
|
||||
import { SitesmithResponse } from '../../types/sitesmith';
|
||||
|
||||
interface Props { onClose: () => void; }
|
||||
@@ -76,11 +77,15 @@ export const SitesmithModal: React.FC<Props> = ({ onClose }) => {
|
||||
: <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}
|
||||
/>
|
||||
{busy ? (
|
||||
<WorkingIndicator />
|
||||
) : (
|
||||
<ChatInput
|
||||
disabled={!canChat}
|
||||
placeholder={!canChat ? 'Upgrade your plan to use Sitesmith' : 'Describe what you want…'}
|
||||
onSend={handleSend}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<ScopeConfirmDialog
|
||||
open={!!pendingReplace}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
||||
|
||||
const PHRASES = [
|
||||
'Thinking',
|
||||
'Sketching layout',
|
||||
'Choosing colors',
|
||||
'Writing copy',
|
||||
'Picking components',
|
||||
'Wiring up the hero',
|
||||
'Polishing typography',
|
||||
'Arranging sections',
|
||||
'Composing the layout',
|
||||
'Adding finishing touches',
|
||||
];
|
||||
|
||||
/**
|
||||
* Animated "AI is working" indicator. Modeled after Claude Code's bottom-bar
|
||||
* status: a Braille-cycle spinner, a phrase that rotates every few seconds,
|
||||
* and an elapsed-seconds counter. Mounts only while a request is in flight.
|
||||
*/
|
||||
export const WorkingIndicator: React.FC = () => {
|
||||
const [frame, setFrame] = useState(0);
|
||||
const [phrase, setPhrase] = useState('Thinking');
|
||||
const [elapsed, setElapsed] = useState(0);
|
||||
const [startTime] = useState(() => Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
const spinnerTimer = window.setInterval(() => {
|
||||
setFrame((f) => (f + 1) % SPINNER.length);
|
||||
}, 80);
|
||||
const phraseTimer = window.setInterval(() => {
|
||||
setPhrase(PHRASES[Math.floor(Math.random() * PHRASES.length)]);
|
||||
}, 2500);
|
||||
const elapsedTimer = window.setInterval(() => {
|
||||
setElapsed(Math.floor((Date.now() - startTime) / 1000));
|
||||
}, 1000);
|
||||
return () => {
|
||||
window.clearInterval(spinnerTimer);
|
||||
window.clearInterval(phraseTimer);
|
||||
window.clearInterval(elapsedTimer);
|
||||
};
|
||||
}, [startTime]);
|
||||
|
||||
return (
|
||||
<div style={containerStyle} role="status" aria-live="polite">
|
||||
<span style={spinnerStyle} aria-hidden>{SPINNER[frame]}</span>
|
||||
<span style={phraseStyle}>{phrase}…</span>
|
||||
<span style={metaStyle}>({elapsed}s)</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const containerStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
padding: '14px 4px',
|
||||
fontSize: 14,
|
||||
};
|
||||
const spinnerStyle: React.CSSProperties = {
|
||||
color: '#8b5cf6',
|
||||
fontSize: 18,
|
||||
width: 18,
|
||||
display: 'inline-block',
|
||||
textAlign: 'center',
|
||||
fontFamily: 'monospace',
|
||||
};
|
||||
const phraseStyle: React.CSSProperties = {
|
||||
color: '#e4e4e7',
|
||||
fontStyle: 'italic',
|
||||
fontWeight: 500,
|
||||
};
|
||||
const metaStyle: React.CSSProperties = {
|
||||
color: '#71717a',
|
||||
fontSize: 12,
|
||||
marginLeft: 'auto',
|
||||
};
|
||||
Reference in New Issue
Block a user