@tpitre/story-ui 4.16.7 → 4.16.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"canvasGenerate.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/canvasGenerate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAkB5C,eAAO,MAAM,qBAAqB,oCAAoC,CAAC;AAuIvE,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAmCrD;AAID;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAU/D;AAID;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOrD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAc1D;AAgED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAsBvD;AAID,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,+CA0GtE"}
1
+ {"version":3,"file":"canvasGenerate.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/canvasGenerate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAkB5C,eAAO,MAAM,qBAAqB,oCAAoC,CAAC;AAwIvE,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAmCrD;AAID;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAU/D;AAID;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOrD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAc1D;AAgED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAsBvD;AAID,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,+CA0GtE"}
@@ -120,18 +120,19 @@ render(<Canvas />);\`;
120
120
 
121
121
  export const Default: StoryObj = {
122
122
  render: () => {
123
- const [code, setCode] = useState(() => {
124
- try { return localStorage.getItem('${LS_KEY}') || PLACEHOLDER; }
125
- catch { return PLACEHOLDER; }
126
- });
123
+ // Always start with the placeholder — no localStorage restore.
124
+ // Code updates arrive exclusively via postMessage from the parent panel.
125
+ // This prevents stale code from a previous session causing errors.
126
+ const [code, setCode] = useState(PLACEHOLDER);
127
127
 
128
128
  useEffect(() => {
129
+ // Clear any stale code left in localStorage from older versions
130
+ try { localStorage.removeItem('${LS_KEY}'); } catch {}
131
+
129
132
  const handler = (e: MessageEvent) => {
130
- // Only accept messages from same origin to prevent cross-origin code injection
131
133
  if (e.origin !== window.location.origin) return;
132
134
  if (e.data?.type === 'VOICE_CANVAS_UPDATE' && typeof e.data.code === 'string') {
133
135
  setCode(e.data.code);
134
- try { localStorage.setItem('${LS_KEY}', e.data.code); } catch {}
135
136
  }
136
137
  };
137
138
  window.addEventListener('message', handler);
@@ -1 +1 @@
1
- {"version":3,"file":"VoiceCanvas.d.ts","sourceRoot":"","sources":["../../../../templates/StoryUI/voice/VoiceCanvas.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAwE,MAAM,OAAO,CAAC;AAY7F,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC7E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,8EAA8E;AAC9E,MAAM,WAAW,iBAAiB;IAChC,4EAA4E;IAC5E,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAID,eAAO,MAAM,WAAW,4FAowBtB,CAAC"}
1
+ {"version":3,"file":"VoiceCanvas.d.ts","sourceRoot":"","sources":["../../../../templates/StoryUI/voice/VoiceCanvas.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAwE,MAAM,OAAO,CAAC;AAY7F,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC7E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,8EAA8E;AAC9E,MAAM,WAAW,iBAAiB;IAChC,4EAA4E;IAC5E,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAID,eAAO,MAAM,WAAW,4FA6wBtB,CAAC"}
@@ -71,21 +71,23 @@ export const VoiceCanvas = React.forwardRef(function VoiceCanvas({ apiBase, prov
71
71
  const iframeLoadedRef = useRef(false);
72
72
  // ── Code → iframe bridge ─────────────────────────────────────
73
73
  /**
74
- * Persist code and push it to the story preview iframe.
75
- * Safe to call before the iframe is loaded the code is stored in localStorage
76
- * and the story reads it on mount.
74
+ * Push code to the story preview iframe via postMessage.
75
+ * No localStorage persistenceeach session starts clean.
77
76
  */
78
77
  const sendCodeToIframe = useCallback((code) => {
79
- try {
80
- localStorage.setItem(LS_KEY, code);
81
- }
82
- catch { }
83
78
  if (iframeRef.current?.contentWindow && iframeLoadedRef.current) {
84
79
  iframeRef.current.contentWindow.postMessage({ type: 'VOICE_CANVAS_UPDATE', code }, IFRAME_ORIGIN);
85
80
  }
86
81
  }, []);
87
82
  // ── Generate / Edit ───────────────────────────────────────────
88
83
  const sendCanvasRequest = useCallback(async (transcript) => {
84
+ // Reject prompts that are too short to produce useful output.
85
+ // Fragments like "create a" or "please" result in broken code.
86
+ const wordCount = transcript.trim().split(/\s+/).length;
87
+ if (wordCount < 3) {
88
+ setErrorMessage('Say a bit more — describe what you want to build.');
89
+ return;
90
+ }
89
91
  if (abortRef.current)
90
92
  abortRef.current.abort();
91
93
  // Stamp this generation so stale finally blocks from aborted requests
@@ -156,10 +158,6 @@ export const VoiceCanvas = React.forwardRef(function VoiceCanvas({ apiBase, prov
156
158
  }
157
159
  lastPromptRef.current = transcript;
158
160
  setLastPrompt(transcript);
159
- try {
160
- localStorage.setItem(LS_PROMPT_KEY, firstPromptRef.current);
161
- }
162
- catch { }
163
161
  conversationRef.current.push({ role: 'user', content: transcript }, { role: 'assistant', content: '[Generated canvas component]' });
164
162
  if (conversationRef.current.length > 40) {
165
163
  conversationRef.current = conversationRef.current.slice(-40);
@@ -319,15 +317,18 @@ export const VoiceCanvas = React.forwardRef(function VoiceCanvas({ apiBase, prov
319
317
  clearTimeout(autoSubmitRef.current);
320
318
  autoSubmitRef.current = setTimeout(() => {
321
319
  const prompt = transcript.trim();
322
- if (prompt) {
323
- // Clear the pending transcript BEFORE sending so that stopListening
324
- // (if pressed moments later) doesn't fire a duplicate request.
320
+ // Require at least 3 words to avoid sending fragments like "create a"
321
+ // or "please" that produce bad LLM output. Short pauses mid-thought
322
+ // are common in natural speech the longer delay (3s) gives the user
323
+ // time to continue before auto-submitting.
324
+ const wordCount = prompt.split(/\s+/).length;
325
+ if (prompt && wordCount >= 3) {
325
326
  pendingTranscriptRef.current = '';
326
327
  setPendingTranscript('');
327
328
  sendCanvasRequest(prompt);
328
329
  }
329
330
  autoSubmitRef.current = null;
330
- }, 1200);
331
+ }, 3000);
331
332
  }, [sendCanvasRequest]);
332
333
  // ── Voice: start ───────────────────────────────────────────────
333
334
  const startListening = useCallback(() => {
@@ -582,5 +583,5 @@ export const VoiceCanvas = React.forwardRef(function VoiceCanvas({ apiBase, prov
582
583
  // ── Render ─────────────────────────────────────────────────────
583
584
  return (_jsxs("div", { className: "sui-canvas-container", children: [_jsxs("div", { className: "sui-canvas-preview", children: [!storyReady && !isGenerating && (_jsxs("div", { className: "sui-canvas-empty", children: [_jsx("div", { className: "sui-canvas-empty-icon", children: _jsxs("svg", { width: "48", height: "48", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }), _jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }), _jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" })] }) }), _jsx("h2", { className: "sui-canvas-empty-title", children: "Voice Canvas" }), _jsx("p", { className: "sui-canvas-empty-desc", children: speechSupported
584
585
  ? 'Speak or type to build interfaces live with your design system components.'
585
- : 'Type a prompt to build interfaces live with your design system components.' }), _jsx("p", { className: "sui-canvas-empty-hint", children: "Try: \"Create a product card with an image, title, price, and buy button\"" })] })), !storyReady && isGenerating && (_jsxs("div", { className: "sui-canvas-progress", children: [_jsx("div", { className: "sui-canvas-progress-spinner" }), _jsx("span", { className: "sui-canvas-progress-text", children: statusText || 'Building...' })] })), storyReady && (_jsxs("div", { className: "sui-canvas-live-wrapper", children: [isGenerating && (_jsxs("div", { className: "sui-canvas-regen-overlay", children: [_jsx("div", { className: "sui-canvas-progress-spinner sui-canvas-progress-spinner--sm" }), _jsx("span", { children: statusText || 'Regenerating...' })] })), _jsx("iframe", { ref: iframeRef, src: iframeSrc, title: "Voice Canvas Preview", className: "sui-canvas-iframe", onLoad: handleIframeLoad }, iframeKey)] })), !isGenerating && errorMessage && (_jsxs("div", { className: "sui-canvas-error", children: [_jsx("span", { children: errorMessage }), _jsx("button", { type: "button", className: "sui-canvas-error-dismiss", onClick: () => setErrorMessage(''), "aria-label": "Dismiss error", children: "\u00D7" })] })), savedMessage && (_jsxs("div", { className: "sui-canvas-saved-toast", children: [_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("polyline", { points: "20 6 9 17 4 12" }) }), _jsxs("span", { children: ["Saved: ", savedMessage] })] }))] }), statusText && !isGenerating && (_jsx("div", { className: "sui-canvas-status-bar", children: _jsx("span", { className: "sui-canvas-explanation", children: statusText }) })), _jsxs("div", { className: `sui-canvas-bar ${isListening ? 'sui-canvas-bar--active' : ''}`, children: [_jsxs("div", { className: "sui-canvas-bar-left", children: [speechSupported && (_jsxs("button", { type: "button", className: `sui-canvas-mic ${isListening ? 'sui-canvas-mic--active' : ''}`, onClick: toggleListening, "aria-label": isListening ? 'Stop voice input' : 'Start voice input', children: [_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }), _jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }), _jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" })] }), isListening && _jsx("span", { className: "sui-canvas-mic-pulse" })] })), (isListening || isGenerating) && (_jsx("div", { className: "sui-canvas-transcript", children: isGenerating ? (_jsx("span", { className: "sui-canvas-status-rendering", children: statusText || 'Building interface...' })) : interimText ? (_jsxs("span", { className: "sui-canvas-status-interim", children: [pendingTranscript ? pendingTranscript + ' ' : '', interimText] })) : pendingTranscript ? (_jsx("span", { className: "sui-canvas-status-final", children: pendingTranscript })) : (_jsx("span", { className: "sui-canvas-status-listening", children: "Listening... describe what you want to build" })) })), !isListening && !isGenerating && (_jsx("input", { ref: textInputRef, type: "text", className: "sui-canvas-text-input", placeholder: "Type what to build...", value: textInput, onChange: (e) => setTextInput(e.target.value), onKeyDown: handleTextSubmit, disabled: isGenerating }))] }), _jsxs("div", { className: "sui-canvas-bar-right", children: [canUndo && (_jsx("button", { type: "button", className: "sui-canvas-action", onClick: undo, title: "Undo (Cmd+Z)", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("polyline", { points: "1 4 1 10 7 10" }), _jsx("path", { d: "M3.51 15a9 9 0 1 0 2.13-9.36L1 10" })] }) })), canRedo && (_jsx("button", { type: "button", className: "sui-canvas-action", onClick: redo, title: "Redo (Cmd+Shift+Z)", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("polyline", { points: "23 4 23 10 17 10" }), _jsx("path", { d: "M20.49 15a9 9 0 1 1-2.13-9.36L23 10" })] }) })), hasContent && (_jsx("button", { type: "button", className: "sui-canvas-action", onClick: saveStory, title: "Save as story", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("path", { d: "M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" }), _jsx("polyline", { points: "17 21 17 13 7 13 7 21" }), _jsx("polyline", { points: "7 3 7 8 15 8" })] }) })), hasContent && (_jsx("button", { type: "button", className: "sui-canvas-action", onClick: clear, title: "Clear canvas", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("path", { d: "M3 6h18" }), _jsx("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }), _jsx("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" })] }) }))] })] })] }));
586
+ : 'Type a prompt to build interfaces live with your design system components.' }), _jsx("p", { className: "sui-canvas-empty-hint", children: "Try: \"Create a product card with an image, title, price, and buy button\"" })] })), !storyReady && isGenerating && (_jsxs("div", { className: "sui-canvas-progress", children: [_jsx("div", { className: "sui-canvas-progress-spinner" }), _jsx("span", { className: "sui-canvas-progress-text", children: statusText || 'Building...' })] })), storyReady && (_jsxs("div", { className: "sui-canvas-live-wrapper", children: [isGenerating && (_jsxs("div", { className: "sui-canvas-regen-overlay", children: [_jsx("div", { className: "sui-canvas-progress-spinner sui-canvas-progress-spinner--sm" }), _jsx("span", { children: statusText || 'Regenerating...' })] })), _jsx("iframe", { ref: iframeRef, src: iframeSrc, title: "Voice Canvas Preview", className: "sui-canvas-iframe", onLoad: handleIframeLoad }, iframeKey)] })), !isGenerating && errorMessage && (_jsxs("div", { className: "sui-canvas-error", children: [_jsx("span", { children: errorMessage }), _jsx("button", { type: "button", className: "sui-canvas-error-dismiss", onClick: () => setErrorMessage(''), "aria-label": "Dismiss error", children: "\u00D7" })] })), savedMessage && (_jsxs("div", { className: "sui-canvas-saved-toast", children: [_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("polyline", { points: "20 6 9 17 4 12" }) }), _jsxs("span", { children: ["Saved: ", savedMessage] })] }))] }), statusText && !isGenerating && (_jsx("div", { className: "sui-canvas-status-bar", children: _jsx("span", { className: "sui-canvas-explanation", children: statusText }) })), _jsxs("div", { className: `sui-canvas-bar ${isListening ? 'sui-canvas-bar--active' : ''}`, children: [_jsxs("div", { className: "sui-canvas-bar-left", children: [speechSupported && (_jsxs("button", { type: "button", className: `sui-canvas-mic ${isListening && !isGenerating ? 'sui-canvas-mic--active' : ''}`, onClick: toggleListening, disabled: isGenerating, "aria-label": isListening ? 'Stop voice input' : 'Start voice input', children: [_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }), _jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }), _jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" })] }), isListening && !isGenerating && _jsx("span", { className: "sui-canvas-mic-pulse" })] })), (isListening || isGenerating) && (_jsx("div", { className: "sui-canvas-transcript", children: isGenerating ? (_jsx("span", { className: "sui-canvas-status-rendering", children: statusText || 'Building interface...' })) : interimText ? (_jsxs("span", { className: "sui-canvas-status-interim", children: [pendingTranscript ? pendingTranscript + ' ' : '', interimText] })) : pendingTranscript ? (_jsx("span", { className: "sui-canvas-status-final", children: pendingTranscript })) : (_jsx("span", { className: "sui-canvas-status-listening", children: "Listening... describe what you want to build" })) })), !isListening && !isGenerating && (_jsx("input", { ref: textInputRef, type: "text", className: "sui-canvas-text-input", placeholder: "Type what to build...", value: textInput, onChange: (e) => setTextInput(e.target.value), onKeyDown: handleTextSubmit, disabled: isGenerating }))] }), _jsxs("div", { className: "sui-canvas-bar-right", children: [canUndo && (_jsx("button", { type: "button", className: "sui-canvas-action", onClick: undo, title: "Undo (Cmd+Z)", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("polyline", { points: "1 4 1 10 7 10" }), _jsx("path", { d: "M3.51 15a9 9 0 1 0 2.13-9.36L1 10" })] }) })), canRedo && (_jsx("button", { type: "button", className: "sui-canvas-action", onClick: redo, title: "Redo (Cmd+Shift+Z)", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("polyline", { points: "23 4 23 10 17 10" }), _jsx("path", { d: "M20.49 15a9 9 0 1 1-2.13-9.36L23 10" })] }) })), hasContent && (_jsx("button", { type: "button", className: "sui-canvas-action", onClick: saveStory, title: "Save as story", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("path", { d: "M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" }), _jsx("polyline", { points: "17 21 17 13 7 13 7 21" }), _jsx("polyline", { points: "7 3 7 8 15 8" })] }) })), hasContent && (_jsx("button", { type: "button", className: "sui-canvas-action", onClick: clear, title: "Clear canvas", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("path", { d: "M3 6h18" }), _jsx("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }), _jsx("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" })] }) }))] })] })] }));
586
587
  });
@@ -107,12 +107,10 @@ function VoiceCanvas({
107
107
  // ── Code → iframe bridge ─────────────────────────────────────
108
108
 
109
109
  /**
110
- * Persist code and push it to the story preview iframe.
111
- * Safe to call before the iframe is loaded the code is stored in localStorage
112
- * and the story reads it on mount.
110
+ * Push code to the story preview iframe via postMessage.
111
+ * No localStorage persistenceeach session starts clean.
113
112
  */
114
113
  const sendCodeToIframe = useCallback((code: string) => {
115
- try { localStorage.setItem(LS_KEY, code); } catch {}
116
114
  if (iframeRef.current?.contentWindow && iframeLoadedRef.current) {
117
115
  iframeRef.current.contentWindow.postMessage(
118
116
  { type: 'VOICE_CANVAS_UPDATE', code },
@@ -124,6 +122,14 @@ function VoiceCanvas({
124
122
  // ── Generate / Edit ───────────────────────────────────────────
125
123
 
126
124
  const sendCanvasRequest = useCallback(async (transcript: string) => {
125
+ // Reject prompts that are too short to produce useful output.
126
+ // Fragments like "create a" or "please" result in broken code.
127
+ const wordCount = transcript.trim().split(/\s+/).length;
128
+ if (wordCount < 3) {
129
+ setErrorMessage('Say a bit more — describe what you want to build.');
130
+ return;
131
+ }
132
+
127
133
  if (abortRef.current) abortRef.current.abort();
128
134
 
129
135
  // Stamp this generation so stale finally blocks from aborted requests
@@ -204,7 +210,6 @@ function VoiceCanvas({
204
210
  }
205
211
  lastPromptRef.current = transcript;
206
212
  setLastPrompt(transcript);
207
- try { localStorage.setItem(LS_PROMPT_KEY, firstPromptRef.current); } catch {}
208
213
  conversationRef.current.push(
209
214
  { role: 'user', content: transcript },
210
215
  { role: 'assistant', content: '[Generated canvas component]' },
@@ -371,15 +376,18 @@ function VoiceCanvas({
371
376
  if (autoSubmitRef.current) clearTimeout(autoSubmitRef.current);
372
377
  autoSubmitRef.current = setTimeout(() => {
373
378
  const prompt = transcript.trim();
374
- if (prompt) {
375
- // Clear the pending transcript BEFORE sending so that stopListening
376
- // (if pressed moments later) doesn't fire a duplicate request.
379
+ // Require at least 3 words to avoid sending fragments like "create a"
380
+ // or "please" that produce bad LLM output. Short pauses mid-thought
381
+ // are common in natural speech the longer delay (3s) gives the user
382
+ // time to continue before auto-submitting.
383
+ const wordCount = prompt.split(/\s+/).length;
384
+ if (prompt && wordCount >= 3) {
377
385
  pendingTranscriptRef.current = '';
378
386
  setPendingTranscript('');
379
387
  sendCanvasRequest(prompt);
380
388
  }
381
389
  autoSubmitRef.current = null;
382
- }, 1200);
390
+ }, 3000);
383
391
  }, [sendCanvasRequest]);
384
392
 
385
393
  // ── Voice: start ───────────────────────────────────────────────
@@ -720,8 +728,9 @@ function VoiceCanvas({
720
728
  {speechSupported && (
721
729
  <button
722
730
  type="button"
723
- className={`sui-canvas-mic ${isListening ? 'sui-canvas-mic--active' : ''}`}
731
+ className={`sui-canvas-mic ${isListening && !isGenerating ? 'sui-canvas-mic--active' : ''}`}
724
732
  onClick={toggleListening}
733
+ disabled={isGenerating}
725
734
  aria-label={isListening ? 'Stop voice input' : 'Start voice input'}
726
735
  >
727
736
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
@@ -729,7 +738,7 @@ function VoiceCanvas({
729
738
  <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
730
739
  <line x1="12" x2="12" y1="19" y2="22" />
731
740
  </svg>
732
- {isListening && <span className="sui-canvas-mic-pulse" />}
741
+ {isListening && !isGenerating && <span className="sui-canvas-mic-pulse" />}
733
742
  </button>
734
743
  )}
735
744
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpitre/story-ui",
3
- "version": "4.16.7",
3
+ "version": "4.16.9",
4
4
  "description": "AI-powered Storybook story generator with dynamic component discovery",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -107,12 +107,10 @@ function VoiceCanvas({
107
107
  // ── Code → iframe bridge ─────────────────────────────────────
108
108
 
109
109
  /**
110
- * Persist code and push it to the story preview iframe.
111
- * Safe to call before the iframe is loaded the code is stored in localStorage
112
- * and the story reads it on mount.
110
+ * Push code to the story preview iframe via postMessage.
111
+ * No localStorage persistenceeach session starts clean.
113
112
  */
114
113
  const sendCodeToIframe = useCallback((code: string) => {
115
- try { localStorage.setItem(LS_KEY, code); } catch {}
116
114
  if (iframeRef.current?.contentWindow && iframeLoadedRef.current) {
117
115
  iframeRef.current.contentWindow.postMessage(
118
116
  { type: 'VOICE_CANVAS_UPDATE', code },
@@ -124,6 +122,14 @@ function VoiceCanvas({
124
122
  // ── Generate / Edit ───────────────────────────────────────────
125
123
 
126
124
  const sendCanvasRequest = useCallback(async (transcript: string) => {
125
+ // Reject prompts that are too short to produce useful output.
126
+ // Fragments like "create a" or "please" result in broken code.
127
+ const wordCount = transcript.trim().split(/\s+/).length;
128
+ if (wordCount < 3) {
129
+ setErrorMessage('Say a bit more — describe what you want to build.');
130
+ return;
131
+ }
132
+
127
133
  if (abortRef.current) abortRef.current.abort();
128
134
 
129
135
  // Stamp this generation so stale finally blocks from aborted requests
@@ -204,7 +210,6 @@ function VoiceCanvas({
204
210
  }
205
211
  lastPromptRef.current = transcript;
206
212
  setLastPrompt(transcript);
207
- try { localStorage.setItem(LS_PROMPT_KEY, firstPromptRef.current); } catch {}
208
213
  conversationRef.current.push(
209
214
  { role: 'user', content: transcript },
210
215
  { role: 'assistant', content: '[Generated canvas component]' },
@@ -371,15 +376,18 @@ function VoiceCanvas({
371
376
  if (autoSubmitRef.current) clearTimeout(autoSubmitRef.current);
372
377
  autoSubmitRef.current = setTimeout(() => {
373
378
  const prompt = transcript.trim();
374
- if (prompt) {
375
- // Clear the pending transcript BEFORE sending so that stopListening
376
- // (if pressed moments later) doesn't fire a duplicate request.
379
+ // Require at least 3 words to avoid sending fragments like "create a"
380
+ // or "please" that produce bad LLM output. Short pauses mid-thought
381
+ // are common in natural speech the longer delay (3s) gives the user
382
+ // time to continue before auto-submitting.
383
+ const wordCount = prompt.split(/\s+/).length;
384
+ if (prompt && wordCount >= 3) {
377
385
  pendingTranscriptRef.current = '';
378
386
  setPendingTranscript('');
379
387
  sendCanvasRequest(prompt);
380
388
  }
381
389
  autoSubmitRef.current = null;
382
- }, 1200);
390
+ }, 3000);
383
391
  }, [sendCanvasRequest]);
384
392
 
385
393
  // ── Voice: start ───────────────────────────────────────────────
@@ -720,8 +728,9 @@ function VoiceCanvas({
720
728
  {speechSupported && (
721
729
  <button
722
730
  type="button"
723
- className={`sui-canvas-mic ${isListening ? 'sui-canvas-mic--active' : ''}`}
731
+ className={`sui-canvas-mic ${isListening && !isGenerating ? 'sui-canvas-mic--active' : ''}`}
724
732
  onClick={toggleListening}
733
+ disabled={isGenerating}
725
734
  aria-label={isListening ? 'Stop voice input' : 'Start voice input'}
726
735
  >
727
736
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
@@ -729,7 +738,7 @@ function VoiceCanvas({
729
738
  <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
730
739
  <line x1="12" x2="12" y1="19" y2="22" />
731
740
  </svg>
732
- {isListening && <span className="sui-canvas-mic-pulse" />}
741
+ {isListening && !isGenerating && <span className="sui-canvas-mic-pulse" />}
733
742
  </button>
734
743
  )}
735
744