Files
site-builder/craft/src/panels/sitesmith/WorkingIndicator.tsx
T
shadowdao 069ea1235a 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.
2026-05-24 15:54:48 -07:00

80 lines
2.2 KiB
TypeScript

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',
};