@tpitre/story-ui 4.9.1 → 4.10.0
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/README.md +15 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +3 -1
- package/dist/templates/StoryUI/StoryUIPanel.css +8 -4
- package/dist/templates/StoryUI/StoryUIPanel.d.ts.map +1 -1
- package/dist/templates/StoryUI/StoryUIPanel.js +25 -1
- package/package.json +1 -1
- package/templates/StoryUI/StoryUIPanel.css +8 -4
- package/templates/StoryUI/StoryUIPanel.tsx +30 -3
package/README.md
CHANGED
|
@@ -481,6 +481,21 @@ claude mcp add --transport http story-ui https://your-app-name.up.railway.app/mc
|
|
|
481
481
|
|
|
482
482
|
Or add it to Claude Desktop via **Settings** → **Connectors** → **Add custom connector**.
|
|
483
483
|
|
|
484
|
+
**Important for Storybook Live Mode Deployments:**
|
|
485
|
+
|
|
486
|
+
If deploying Storybook with Story UI integrated (where users can generate stories in the deployed app), ensure the StoryUI panel files are committed to git:
|
|
487
|
+
|
|
488
|
+
```bash
|
|
489
|
+
# Check if StoryUI is incorrectly gitignored
|
|
490
|
+
grep "StoryUI" .gitignore
|
|
491
|
+
|
|
492
|
+
# If found, remove from .gitignore and commit the panel
|
|
493
|
+
git add src/stories/StoryUI/
|
|
494
|
+
git commit -m "Add StoryUI panel for production"
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
> **Note**: Story UI versions prior to 4.10.0 incorrectly added `src/stories/StoryUI/` to `.gitignore`. See [DEPLOYMENT.md](DEPLOYMENT.md#storybook-live-mode-deployment) for full instructions.
|
|
498
|
+
|
|
484
499
|
See [DEPLOYMENT.md](DEPLOYMENT.md) for detailed deployment instructions and troubleshooting.
|
|
485
500
|
|
|
486
501
|
---
|
package/dist/cli/setup.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../cli/setup.ts"],"names":[],"mappings":"AAmDA;;GAEG;AACH,wBAAgB,iCAAiC,SA8ChD;AAiWD,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC7C,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,wBAAsB,YAAY,CAAC,OAAO,GAAE,YAAiB,
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../cli/setup.ts"],"names":[],"mappings":"AAmDA;;GAEG;AACH,wBAAgB,iCAAiC,SA8ChD;AAiWD,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC7C,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,wBAAsB,YAAY,CAAC,OAAO,GAAE,YAAiB,iBA85B5D"}
|
package/dist/cli/setup.js
CHANGED
|
@@ -1153,10 +1153,12 @@ VITE_STORY_UI_PORT=${answers.mcpPort || '4001'}
|
|
|
1153
1153
|
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
1154
1154
|
if (fs.existsSync(gitignorePath)) {
|
|
1155
1155
|
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
|
|
1156
|
+
// NOTE: Do NOT add StoryUI/ to gitignore - it must be committed for production deployments
|
|
1157
|
+
// The StoryUI panel component needs to be deployed to Railway/production environments
|
|
1156
1158
|
const patterns = [
|
|
1157
1159
|
'.env',
|
|
1158
1160
|
path.relative(process.cwd(), config.generatedStoriesPath),
|
|
1159
|
-
|
|
1161
|
+
'.story-ui-history/'
|
|
1160
1162
|
];
|
|
1161
1163
|
let gitignoreUpdated = false;
|
|
1162
1164
|
for (const pattern of patterns) {
|
|
@@ -1047,7 +1047,7 @@
|
|
|
1047
1047
|
============================================ */
|
|
1048
1048
|
.sui-input-form {
|
|
1049
1049
|
display: flex;
|
|
1050
|
-
align-items:
|
|
1050
|
+
align-items: flex-end; /* Align buttons to bottom when textarea expands */
|
|
1051
1051
|
gap: var(--space-2);
|
|
1052
1052
|
background: hsl(var(--card));
|
|
1053
1053
|
border: 1px solid hsl(var(--border));
|
|
@@ -1093,12 +1093,16 @@
|
|
|
1093
1093
|
background: transparent;
|
|
1094
1094
|
color: hsl(var(--foreground));
|
|
1095
1095
|
font-size: 0.9375rem;
|
|
1096
|
-
|
|
1097
|
-
|
|
1096
|
+
font-family: inherit;
|
|
1097
|
+
padding: 10px var(--space-3);
|
|
1098
|
+
min-height: 40px;
|
|
1099
|
+
max-height: 200px;
|
|
1098
1100
|
min-width: 0;
|
|
1099
1101
|
resize: none;
|
|
1100
1102
|
outline: none;
|
|
1101
|
-
|
|
1103
|
+
line-height: 1.5;
|
|
1104
|
+
overflow-y: auto;
|
|
1105
|
+
/* Auto-expands with content via JS, scrolls when max-height reached */
|
|
1102
1106
|
}
|
|
1103
1107
|
|
|
1104
1108
|
.sui-input-form-field::placeholder {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,oBAAoB,CAAC;AAwwB5B,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B;AAED,iBAAS,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE,iBAAiB,
|
|
1
|
+
{"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,oBAAoB,CAAC;AAwwB5B,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B;AAED,iBAAS,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE,iBAAiB,2CA6sCnD;AAED,eAAe,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -449,6 +449,22 @@ function StoryUIPanel({ mcpPort }) {
|
|
|
449
449
|
const [isDeletingOrphans, setIsDeletingOrphans] = useState(false);
|
|
450
450
|
const chatEndRef = useRef(null);
|
|
451
451
|
const inputRef = useRef(null);
|
|
452
|
+
// Auto-resize textarea based on content
|
|
453
|
+
const adjustTextareaHeight = useCallback(() => {
|
|
454
|
+
const textarea = inputRef.current;
|
|
455
|
+
if (textarea) {
|
|
456
|
+
// Reset height to auto to get the correct scrollHeight
|
|
457
|
+
textarea.style.height = 'auto';
|
|
458
|
+
// Set height to scrollHeight, capped at max-height (200px)
|
|
459
|
+
const maxHeight = 200;
|
|
460
|
+
const newHeight = Math.min(textarea.scrollHeight, maxHeight);
|
|
461
|
+
textarea.style.height = `${newHeight}px`;
|
|
462
|
+
}
|
|
463
|
+
}, []);
|
|
464
|
+
// Adjust height when input changes
|
|
465
|
+
useEffect(() => {
|
|
466
|
+
adjustTextareaHeight();
|
|
467
|
+
}, [state.input, adjustTextareaHeight]);
|
|
452
468
|
const fileInputRef = useRef(null);
|
|
453
469
|
const abortControllerRef = useRef(null);
|
|
454
470
|
const hasShownRefreshHint = useRef(false);
|
|
@@ -1374,7 +1390,15 @@ function StoryUIPanel({ mcpPort }) {
|
|
|
1374
1390
|
const provider = state.availableProviders.find(p => p.type === newProvider);
|
|
1375
1391
|
if (provider?.models.length)
|
|
1376
1392
|
dispatch({ type: 'SET_SELECTED_MODEL', payload: provider.models[0] });
|
|
1377
|
-
}, "aria-label": "Select provider", children: state.availableProviders.map(p => _jsx("option", { value: p.type, children: p.name }, p.type)) })] }), _jsxs("div", { className: "sui-select", children: [_jsxs("div", { className: "sui-select-trigger", children: [_jsx("span", { children: getModelDisplayName(state.selectedModel) }), Icons.chevronDown] }), _jsx("select", { className: "sui-select-native", value: state.selectedModel, onChange: e => dispatch({ type: 'SET_SELECTED_MODEL', payload: e.target.value }), "aria-label": "Select model", children: state.availableProviders.find(p => p.type === state.selectedProvider)?.models.map(model => (_jsx("option", { value: model, children: getModelDisplayName(model) }, model))) })] })] })) })] }), _jsxs("section", { className: "sui-chat-area", role: "log", "aria-live": "polite", children: [state.error && _jsx("div", { className: "sui-error", role: "alert", style: { margin: '24px' }, children: state.error }), state.conversation.length === 0 && !state.loading ? (_jsxs("div", { className: "sui-welcome", children: [_jsx("h2", { className: "sui-welcome-greeting", children: "What would you like to create?" }), _jsx("p", { className: "sui-welcome-subtitle", children: "Describe any UI component and I'll generate a Storybook story" }), _jsxs("div", { className: "sui-welcome-chips", children: [_jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a responsive card with image, title, and description' }), children: "Card" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a navigation bar with logo and menu links' }), children: "Navbar" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a form with input fields and validation' }), children: "Form" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a hero section with headline and call-to-action' }), children: "Hero" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a button group with primary and secondary actions' }), children: "Buttons" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a modal dialog with header, content, and footer' }), children: "Modal" })] })] })) : (_jsxs("div", { className: "sui-chat-messages", children: [state.conversation.map((msg, i) => (_jsx("article", { className: `sui-message ${msg.role === 'user' ? 'sui-message-user' : 'sui-message-ai'}`, children: _jsxs("div", { className: "sui-message-bubble", children: [msg.role === 'ai' ? renderMarkdown(msg.content) : msg.content, msg.role === 'user' && msg.attachedImages && msg.attachedImages.length > 0 && (_jsx("div", { className: "sui-message-images", children: msg.attachedImages.map(img => (_jsx("img", { src: img.base64 ? `data:${img.mediaType};base64,${img.base64}` : img.preview, alt: "attached", className: "sui-message-image" }, img.id))) }))] }) }, i))), state.loading && (_jsx("div", { className: "sui-message sui-message-ai", children: state.streamingState ? _jsx(ProgressIndicator, { streamingState: state.streamingState }) : (_jsx("div", { className: "sui-progress", children: _jsxs("span", { className: "sui-progress-label", children: ["Please give us a moment while we generate your story", _jsx("span", { className: "sui-loading" })] }) })) })), _jsx("div", { ref: chatEndRef })] }))] }), _jsx("div", { className: "sui-input-area", children: _jsxs("div", { className: "sui-input-container", children: [_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", multiple: true, style: { display: 'none' }, onChange: handleFileSelect }), state.attachedImages.length > 0 && (_jsxs("div", { className: "sui-image-previews", children: [_jsxs("span", { className: "sui-image-preview-label", children: [Icons.image, " ", state.attachedImages.length, " image", state.attachedImages.length > 1 ? 's' : ''] }), state.attachedImages.map(img => (_jsxs("div", { className: "sui-image-preview-item", children: [_jsx("img", { src: img.preview, alt: "preview", className: "sui-image-preview-thumb" }), _jsx("button", { className: "sui-image-preview-remove", onClick: () => removeAttachedImage(img.id), "aria-label": "Remove", children: Icons.x })] }, img.id)))] })), _jsxs("form", { onSubmit: handleSend, className: "sui-input-form", style: state.attachedImages.length > 0 ? { borderTopLeftRadius: 0, borderTopRightRadius: 0 } : undefined, children: [_jsx("button", { type: "button", className: "sui-input-form-upload", onClick: () => fileInputRef.current?.click(), disabled: state.loading || state.attachedImages.length >= MAX_IMAGES, "aria-label": "Attach images", children: Icons.image }), _jsx("
|
|
1393
|
+
}, "aria-label": "Select provider", children: state.availableProviders.map(p => _jsx("option", { value: p.type, children: p.name }, p.type)) })] }), _jsxs("div", { className: "sui-select", children: [_jsxs("div", { className: "sui-select-trigger", children: [_jsx("span", { children: getModelDisplayName(state.selectedModel) }), Icons.chevronDown] }), _jsx("select", { className: "sui-select-native", value: state.selectedModel, onChange: e => dispatch({ type: 'SET_SELECTED_MODEL', payload: e.target.value }), "aria-label": "Select model", children: state.availableProviders.find(p => p.type === state.selectedProvider)?.models.map(model => (_jsx("option", { value: model, children: getModelDisplayName(model) }, model))) })] })] })) })] }), _jsxs("section", { className: "sui-chat-area", role: "log", "aria-live": "polite", children: [state.error && _jsx("div", { className: "sui-error", role: "alert", style: { margin: '24px' }, children: state.error }), state.conversation.length === 0 && !state.loading ? (_jsxs("div", { className: "sui-welcome", children: [_jsx("h2", { className: "sui-welcome-greeting", children: "What would you like to create?" }), _jsx("p", { className: "sui-welcome-subtitle", children: "Describe any UI component and I'll generate a Storybook story" }), _jsxs("div", { className: "sui-welcome-chips", children: [_jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a responsive card with image, title, and description' }), children: "Card" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a navigation bar with logo and menu links' }), children: "Navbar" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a form with input fields and validation' }), children: "Form" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a hero section with headline and call-to-action' }), children: "Hero" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a button group with primary and secondary actions' }), children: "Buttons" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a modal dialog with header, content, and footer' }), children: "Modal" })] })] })) : (_jsxs("div", { className: "sui-chat-messages", children: [state.conversation.map((msg, i) => (_jsx("article", { className: `sui-message ${msg.role === 'user' ? 'sui-message-user' : 'sui-message-ai'}`, children: _jsxs("div", { className: "sui-message-bubble", children: [msg.role === 'ai' ? renderMarkdown(msg.content) : msg.content, msg.role === 'user' && msg.attachedImages && msg.attachedImages.length > 0 && (_jsx("div", { className: "sui-message-images", children: msg.attachedImages.map(img => (_jsx("img", { src: img.base64 ? `data:${img.mediaType};base64,${img.base64}` : img.preview, alt: "attached", className: "sui-message-image" }, img.id))) }))] }) }, i))), state.loading && (_jsx("div", { className: "sui-message sui-message-ai", children: state.streamingState ? _jsx(ProgressIndicator, { streamingState: state.streamingState }) : (_jsx("div", { className: "sui-progress", children: _jsxs("span", { className: "sui-progress-label", children: ["Please give us a moment while we generate your story", _jsx("span", { className: "sui-loading" })] }) })) })), _jsx("div", { ref: chatEndRef })] }))] }), _jsx("div", { className: "sui-input-area", children: _jsxs("div", { className: "sui-input-container", children: [_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", multiple: true, style: { display: 'none' }, onChange: handleFileSelect }), state.attachedImages.length > 0 && (_jsxs("div", { className: "sui-image-previews", children: [_jsxs("span", { className: "sui-image-preview-label", children: [Icons.image, " ", state.attachedImages.length, " image", state.attachedImages.length > 1 ? 's' : ''] }), state.attachedImages.map(img => (_jsxs("div", { className: "sui-image-preview-item", children: [_jsx("img", { src: img.preview, alt: "preview", className: "sui-image-preview-thumb" }), _jsx("button", { className: "sui-image-preview-remove", onClick: () => removeAttachedImage(img.id), "aria-label": "Remove", children: Icons.x })] }, img.id)))] })), _jsxs("form", { onSubmit: handleSend, className: "sui-input-form", style: state.attachedImages.length > 0 ? { borderTopLeftRadius: 0, borderTopRightRadius: 0 } : undefined, children: [_jsx("button", { type: "button", className: "sui-input-form-upload", onClick: () => fileInputRef.current?.click(), disabled: state.loading || state.attachedImages.length >= MAX_IMAGES, "aria-label": "Attach images", children: Icons.image }), _jsx("textarea", { ref: inputRef, rows: 1, className: "sui-input-form-field", value: state.input, onChange: e => dispatch({ type: 'SET_INPUT', payload: e.target.value }), onKeyDown: e => {
|
|
1394
|
+
// Submit on Enter, newline on Shift+Enter
|
|
1395
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
1396
|
+
e.preventDefault();
|
|
1397
|
+
if (!state.loading && (state.input.trim() || state.attachedImages.length > 0)) {
|
|
1398
|
+
handleSend(e);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}, onPaste: handlePaste, placeholder: state.attachedImages.length > 0 ? 'Describe what to create from these images...' : 'Describe a UI component...' }), _jsx("button", { type: "submit", className: "sui-input-form-send", disabled: state.loading || (!state.input.trim() && state.attachedImages.length === 0), "aria-label": "Send", children: Icons.send })] })] }) })] })] }));
|
|
1378
1402
|
}
|
|
1379
1403
|
export default StoryUIPanel;
|
|
1380
1404
|
export { StoryUIPanel };
|
package/package.json
CHANGED
|
@@ -1047,7 +1047,7 @@
|
|
|
1047
1047
|
============================================ */
|
|
1048
1048
|
.sui-input-form {
|
|
1049
1049
|
display: flex;
|
|
1050
|
-
align-items:
|
|
1050
|
+
align-items: flex-end; /* Align buttons to bottom when textarea expands */
|
|
1051
1051
|
gap: var(--space-2);
|
|
1052
1052
|
background: hsl(var(--card));
|
|
1053
1053
|
border: 1px solid hsl(var(--border));
|
|
@@ -1093,12 +1093,16 @@
|
|
|
1093
1093
|
background: transparent;
|
|
1094
1094
|
color: hsl(var(--foreground));
|
|
1095
1095
|
font-size: 0.9375rem;
|
|
1096
|
-
|
|
1097
|
-
|
|
1096
|
+
font-family: inherit;
|
|
1097
|
+
padding: 10px var(--space-3);
|
|
1098
|
+
min-height: 40px;
|
|
1099
|
+
max-height: 200px;
|
|
1098
1100
|
min-width: 0;
|
|
1099
1101
|
resize: none;
|
|
1100
1102
|
outline: none;
|
|
1101
|
-
|
|
1103
|
+
line-height: 1.5;
|
|
1104
|
+
overflow-y: auto;
|
|
1105
|
+
/* Auto-expands with content via JS, scrolls when max-height reached */
|
|
1102
1106
|
}
|
|
1103
1107
|
|
|
1104
1108
|
.sui-input-form-field::placeholder {
|
|
@@ -795,7 +795,25 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
795
795
|
const [orphanCount, setOrphanCount] = useState<number>(0);
|
|
796
796
|
const [isDeletingOrphans, setIsDeletingOrphans] = useState<boolean>(false);
|
|
797
797
|
const chatEndRef = useRef<HTMLDivElement>(null);
|
|
798
|
-
const inputRef = useRef<
|
|
798
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
799
|
+
|
|
800
|
+
// Auto-resize textarea based on content
|
|
801
|
+
const adjustTextareaHeight = useCallback(() => {
|
|
802
|
+
const textarea = inputRef.current;
|
|
803
|
+
if (textarea) {
|
|
804
|
+
// Reset height to auto to get the correct scrollHeight
|
|
805
|
+
textarea.style.height = 'auto';
|
|
806
|
+
// Set height to scrollHeight, capped at max-height (200px)
|
|
807
|
+
const maxHeight = 200;
|
|
808
|
+
const newHeight = Math.min(textarea.scrollHeight, maxHeight);
|
|
809
|
+
textarea.style.height = `${newHeight}px`;
|
|
810
|
+
}
|
|
811
|
+
}, []);
|
|
812
|
+
|
|
813
|
+
// Adjust height when input changes
|
|
814
|
+
useEffect(() => {
|
|
815
|
+
adjustTextareaHeight();
|
|
816
|
+
}, [state.input, adjustTextareaHeight]);
|
|
799
817
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
800
818
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
801
819
|
const hasShownRefreshHint = useRef(false);
|
|
@@ -1971,12 +1989,21 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
1971
1989
|
<button type="button" className="sui-input-form-upload" onClick={() => fileInputRef.current?.click()} disabled={state.loading || state.attachedImages.length >= MAX_IMAGES} aria-label="Attach images">
|
|
1972
1990
|
{Icons.image}
|
|
1973
1991
|
</button>
|
|
1974
|
-
<
|
|
1992
|
+
<textarea
|
|
1975
1993
|
ref={inputRef}
|
|
1976
|
-
|
|
1994
|
+
rows={1}
|
|
1977
1995
|
className="sui-input-form-field"
|
|
1978
1996
|
value={state.input}
|
|
1979
1997
|
onChange={e => dispatch({ type: 'SET_INPUT', payload: e.target.value })}
|
|
1998
|
+
onKeyDown={e => {
|
|
1999
|
+
// Submit on Enter, newline on Shift+Enter
|
|
2000
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
2001
|
+
e.preventDefault();
|
|
2002
|
+
if (!state.loading && (state.input.trim() || state.attachedImages.length > 0)) {
|
|
2003
|
+
handleSend(e as unknown as React.FormEvent);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
}}
|
|
1980
2007
|
onPaste={handlePaste}
|
|
1981
2008
|
placeholder={state.attachedImages.length > 0 ? 'Describe what to create from these images...' : 'Describe a UI component...'}
|
|
1982
2009
|
/>
|