@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.
- package/dist/mcp-server/routes/canvasGenerate.d.ts.map +1 -1
- package/dist/mcp-server/routes/canvasGenerate.js +7 -6
- package/dist/templates/StoryUI/voice/VoiceCanvas.d.ts.map +1 -1
- package/dist/templates/StoryUI/voice/VoiceCanvas.js +17 -16
- package/dist/templates/StoryUI/voice/VoiceCanvas.tsx +20 -11
- package/package.json +1 -1
- package/templates/StoryUI/voice/VoiceCanvas.tsx +20 -11
|
@@ -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;
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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,
|
|
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
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* and the story reads it on mount.
|
|
74
|
+
* Push code to the story preview iframe via postMessage.
|
|
75
|
+
* No localStorage persistence — each 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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
},
|
|
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
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* and the story reads it on mount.
|
|
110
|
+
* Push code to the story preview iframe via postMessage.
|
|
111
|
+
* No localStorage persistence — each 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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
},
|
|
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
|
@@ -107,12 +107,10 @@ function VoiceCanvas({
|
|
|
107
107
|
// ── Code → iframe bridge ─────────────────────────────────────
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* and the story reads it on mount.
|
|
110
|
+
* Push code to the story preview iframe via postMessage.
|
|
111
|
+
* No localStorage persistence — each 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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
},
|
|
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
|
|