@tpitre/story-ui 4.9.2 → 4.11.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.
|
@@ -639,6 +639,92 @@
|
|
|
639
639
|
flex-shrink: 0;
|
|
640
640
|
}
|
|
641
641
|
|
|
642
|
+
/* ============================================
|
|
643
|
+
MCP Toggle Switch Component
|
|
644
|
+
============================================ */
|
|
645
|
+
.sui-mcp-toggle {
|
|
646
|
+
display: flex;
|
|
647
|
+
align-items: center;
|
|
648
|
+
margin-left: var(--space-3);
|
|
649
|
+
padding-left: var(--space-3);
|
|
650
|
+
border-left: 1px solid hsl(var(--border));
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
.sui-toggle-label {
|
|
654
|
+
display: flex;
|
|
655
|
+
align-items: center;
|
|
656
|
+
gap: var(--space-2);
|
|
657
|
+
cursor: pointer;
|
|
658
|
+
user-select: none;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.sui-toggle-text {
|
|
662
|
+
font-size: 0.75rem;
|
|
663
|
+
font-weight: 500;
|
|
664
|
+
color: hsl(var(--muted-foreground));
|
|
665
|
+
white-space: nowrap;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.sui-toggle-switch {
|
|
669
|
+
position: relative;
|
|
670
|
+
width: 36px;
|
|
671
|
+
height: 20px;
|
|
672
|
+
flex-shrink: 0;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.sui-toggle-switch input {
|
|
676
|
+
opacity: 0;
|
|
677
|
+
width: 0;
|
|
678
|
+
height: 0;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.sui-toggle-slider {
|
|
682
|
+
position: absolute;
|
|
683
|
+
cursor: pointer;
|
|
684
|
+
top: 0;
|
|
685
|
+
left: 0;
|
|
686
|
+
right: 0;
|
|
687
|
+
bottom: 0;
|
|
688
|
+
background-color: hsl(var(--muted));
|
|
689
|
+
border: 1px solid hsl(var(--border));
|
|
690
|
+
border-radius: var(--radius-full);
|
|
691
|
+
transition: all var(--transition-fast);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
.sui-toggle-slider::before {
|
|
695
|
+
position: absolute;
|
|
696
|
+
content: "";
|
|
697
|
+
height: 14px;
|
|
698
|
+
width: 14px;
|
|
699
|
+
left: 2px;
|
|
700
|
+
bottom: 2px;
|
|
701
|
+
background-color: hsl(var(--background));
|
|
702
|
+
border-radius: var(--radius-full);
|
|
703
|
+
box-shadow: var(--shadow-sm);
|
|
704
|
+
transition: all var(--transition-fast);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.sui-toggle-switch input:checked + .sui-toggle-slider {
|
|
708
|
+
background-color: hsl(var(--primary));
|
|
709
|
+
border-color: hsl(var(--primary));
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
.sui-toggle-switch input:checked + .sui-toggle-slider::before {
|
|
713
|
+
transform: translateX(16px);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
.sui-toggle-switch input:focus + .sui-toggle-slider {
|
|
717
|
+
box-shadow: 0 0 0 2px hsl(var(--ring) / 0.2);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
.sui-toggle-label:hover .sui-toggle-slider {
|
|
721
|
+
background-color: hsl(var(--accent));
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.sui-toggle-label:hover .sui-toggle-switch input:checked + .sui-toggle-slider {
|
|
725
|
+
background-color: hsl(var(--primary) / 0.9);
|
|
726
|
+
}
|
|
727
|
+
|
|
642
728
|
/* ============================================
|
|
643
729
|
ShadCN-Style Badge Component
|
|
644
730
|
============================================ */
|
|
@@ -1047,7 +1133,7 @@
|
|
|
1047
1133
|
============================================ */
|
|
1048
1134
|
.sui-input-form {
|
|
1049
1135
|
display: flex;
|
|
1050
|
-
align-items:
|
|
1136
|
+
align-items: flex-end; /* Align buttons to bottom when textarea expands */
|
|
1051
1137
|
gap: var(--space-2);
|
|
1052
1138
|
background: hsl(var(--card));
|
|
1053
1139
|
border: 1px solid hsl(var(--border));
|
|
@@ -1093,12 +1179,16 @@
|
|
|
1093
1179
|
background: transparent;
|
|
1094
1180
|
color: hsl(var(--foreground));
|
|
1095
1181
|
font-size: 0.9375rem;
|
|
1096
|
-
|
|
1097
|
-
|
|
1182
|
+
font-family: inherit;
|
|
1183
|
+
padding: 10px var(--space-3);
|
|
1184
|
+
min-height: 40px;
|
|
1185
|
+
max-height: 200px;
|
|
1098
1186
|
min-width: 0;
|
|
1099
1187
|
resize: none;
|
|
1100
1188
|
outline: none;
|
|
1101
|
-
|
|
1189
|
+
line-height: 1.5;
|
|
1190
|
+
overflow-y: auto;
|
|
1191
|
+
/* Auto-expands with content via JS, scrolls when max-height reached */
|
|
1102
1192
|
}
|
|
1103
1193
|
|
|
1104
1194
|
.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;
|
|
1
|
+
{"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,oBAAoB,CAAC;AAy2B5B,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B;AAED,iBAAS,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE,iBAAiB,2CAovCnD;AAED,eAAe,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -30,6 +30,8 @@ const initialState = {
|
|
|
30
30
|
error: null,
|
|
31
31
|
considerations: '',
|
|
32
32
|
isDarkMode: false,
|
|
33
|
+
storybookMcpAvailable: false,
|
|
34
|
+
useStorybookMcp: true, // Default to enabled when available
|
|
33
35
|
};
|
|
34
36
|
function panelReducer(state, action) {
|
|
35
37
|
switch (action.type) {
|
|
@@ -98,6 +100,10 @@ function panelReducer(state, action) {
|
|
|
98
100
|
return { ...state, considerations: action.payload };
|
|
99
101
|
case 'SET_DARK_MODE':
|
|
100
102
|
return { ...state, isDarkMode: action.payload };
|
|
103
|
+
case 'SET_STORYBOOK_MCP_AVAILABLE':
|
|
104
|
+
return { ...state, storybookMcpAvailable: action.payload };
|
|
105
|
+
case 'SET_USE_STORYBOOK_MCP':
|
|
106
|
+
return { ...state, useStorybookMcp: action.payload };
|
|
101
107
|
case 'NEW_CHAT':
|
|
102
108
|
return { ...state, conversation: [], activeChatId: null, activeTitle: '' };
|
|
103
109
|
default:
|
|
@@ -216,6 +222,80 @@ function saveProviderPrefs(provider, model) {
|
|
|
216
222
|
console.error('Failed to save provider preferences:', e);
|
|
217
223
|
}
|
|
218
224
|
}
|
|
225
|
+
// Storage key for Storybook MCP preference
|
|
226
|
+
const STORYBOOK_MCP_PREF_KEY = 'story-ui-use-storybook-mcp';
|
|
227
|
+
/**
|
|
228
|
+
* Detect if Storybook MCP addon is available.
|
|
229
|
+
* Checks for the MCP endpoint that @storybook/addon-mcp exposes.
|
|
230
|
+
* The addon returns SSE (Server-Sent Events) responses, not JSON.
|
|
231
|
+
*/
|
|
232
|
+
async function detectStorybookMcp() {
|
|
233
|
+
try {
|
|
234
|
+
// Try to detect Storybook MCP on the same origin (works when running in Storybook)
|
|
235
|
+
const storybookOrigin = typeof window !== 'undefined' ? window.location.origin : '';
|
|
236
|
+
const mcpEndpoint = `${storybookOrigin}/mcp`;
|
|
237
|
+
const response = await fetch(mcpEndpoint, {
|
|
238
|
+
method: 'POST',
|
|
239
|
+
headers: { 'Content-Type': 'application/json' },
|
|
240
|
+
body: JSON.stringify({
|
|
241
|
+
jsonrpc: '2.0',
|
|
242
|
+
id: 1,
|
|
243
|
+
method: 'tools/list',
|
|
244
|
+
params: {}
|
|
245
|
+
})
|
|
246
|
+
});
|
|
247
|
+
if (!response.ok)
|
|
248
|
+
return false;
|
|
249
|
+
// Storybook MCP addon returns SSE (Server-Sent Events) responses
|
|
250
|
+
// Check content-type or read a small portion to verify it's SSE format
|
|
251
|
+
const contentType = response.headers.get('content-type') || '';
|
|
252
|
+
if (contentType.includes('text/event-stream')) {
|
|
253
|
+
console.log('[StoryUI] Storybook MCP addon detected (SSE endpoint)');
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
// Also check by reading a portion of the response
|
|
257
|
+
const text = await response.text();
|
|
258
|
+
if (text.startsWith('event:') || text.startsWith('data:')) {
|
|
259
|
+
console.log('[StoryUI] Storybook MCP addon detected (SSE response)');
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
// Try parsing as JSON as fallback (some implementations may return JSON)
|
|
263
|
+
try {
|
|
264
|
+
const data = JSON.parse(text);
|
|
265
|
+
if (data && data.result && Array.isArray(data.result.tools)) {
|
|
266
|
+
console.log('[StoryUI] Storybook MCP addon detected (JSON response)');
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// Not JSON, but might still be valid SSE that we missed
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
// Not available - this is normal if addon-mcp isn't installed
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function loadStorybookMcpPref() {
|
|
281
|
+
try {
|
|
282
|
+
const stored = localStorage.getItem(STORYBOOK_MCP_PREF_KEY);
|
|
283
|
+
if (stored !== null)
|
|
284
|
+
return JSON.parse(stored);
|
|
285
|
+
}
|
|
286
|
+
catch (e) {
|
|
287
|
+
console.error('Failed to load Storybook MCP preference:', e);
|
|
288
|
+
}
|
|
289
|
+
return true; // Default to enabled
|
|
290
|
+
}
|
|
291
|
+
function saveStorybookMcpPref(enabled) {
|
|
292
|
+
try {
|
|
293
|
+
localStorage.setItem(STORYBOOK_MCP_PREF_KEY, JSON.stringify(enabled));
|
|
294
|
+
}
|
|
295
|
+
catch (e) {
|
|
296
|
+
console.error('Failed to save Storybook MCP preference:', e);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
219
299
|
async function testMCPConnection() {
|
|
220
300
|
try {
|
|
221
301
|
const response = await fetch(PROVIDERS_API, { method: 'GET' });
|
|
@@ -288,12 +368,21 @@ function formatTime(timestamp) {
|
|
|
288
368
|
}
|
|
289
369
|
function getModelDisplayName(model) {
|
|
290
370
|
const displayNames = {
|
|
371
|
+
// Claude models
|
|
291
372
|
'claude-opus-4-5-20251101': 'Claude Opus 4.5',
|
|
292
373
|
'claude-sonnet-4-5-20250929': 'Claude Sonnet 4.5',
|
|
293
374
|
'claude-haiku-4-5-20251001': 'Claude Haiku 4.5',
|
|
375
|
+
'claude-sonnet-4-20250514': 'Claude Sonnet 4',
|
|
376
|
+
// OpenAI models
|
|
377
|
+
'gpt-5.2': 'GPT-5.2',
|
|
378
|
+
'gpt-5.1': 'GPT-5.1',
|
|
294
379
|
'gpt-4o': 'GPT-4o',
|
|
295
380
|
'gpt-4o-mini': 'GPT-4o Mini',
|
|
296
381
|
'o1': 'o1',
|
|
382
|
+
// Gemini models
|
|
383
|
+
'gemini-3-pro-preview': 'Gemini 3 Pro Preview',
|
|
384
|
+
'gemini-2.5-pro': 'Gemini 2.5 Pro',
|
|
385
|
+
'gemini-2.5-flash': 'Gemini 2.5 Flash',
|
|
297
386
|
'gemini-2.0-flash': 'Gemini 2.0 Flash',
|
|
298
387
|
'gemini-1.5-pro': 'Gemini 1.5 Pro',
|
|
299
388
|
};
|
|
@@ -449,6 +538,22 @@ function StoryUIPanel({ mcpPort }) {
|
|
|
449
538
|
const [isDeletingOrphans, setIsDeletingOrphans] = useState(false);
|
|
450
539
|
const chatEndRef = useRef(null);
|
|
451
540
|
const inputRef = useRef(null);
|
|
541
|
+
// Auto-resize textarea based on content
|
|
542
|
+
const adjustTextareaHeight = useCallback(() => {
|
|
543
|
+
const textarea = inputRef.current;
|
|
544
|
+
if (textarea) {
|
|
545
|
+
// Reset height to auto to get the correct scrollHeight
|
|
546
|
+
textarea.style.height = 'auto';
|
|
547
|
+
// Set height to scrollHeight, capped at max-height (200px)
|
|
548
|
+
const maxHeight = 200;
|
|
549
|
+
const newHeight = Math.min(textarea.scrollHeight, maxHeight);
|
|
550
|
+
textarea.style.height = `${newHeight}px`;
|
|
551
|
+
}
|
|
552
|
+
}, []);
|
|
553
|
+
// Adjust height when input changes
|
|
554
|
+
useEffect(() => {
|
|
555
|
+
adjustTextareaHeight();
|
|
556
|
+
}, [state.input, adjustTextareaHeight]);
|
|
452
557
|
const fileInputRef = useRef(null);
|
|
453
558
|
const abortControllerRef = useRef(null);
|
|
454
559
|
const hasShownRefreshHint = useRef(false);
|
|
@@ -525,6 +630,19 @@ function StoryUIPanel({ mcpPort }) {
|
|
|
525
630
|
pollForExternalStories();
|
|
526
631
|
return () => clearInterval(intervalId);
|
|
527
632
|
}, []);
|
|
633
|
+
// Detect Storybook MCP addon availability
|
|
634
|
+
useEffect(() => {
|
|
635
|
+
const checkStorybookMcp = async () => {
|
|
636
|
+
const available = await detectStorybookMcp();
|
|
637
|
+
dispatch({ type: 'SET_STORYBOOK_MCP_AVAILABLE', payload: available });
|
|
638
|
+
// Load saved preference if MCP is available
|
|
639
|
+
if (available) {
|
|
640
|
+
const savedPref = loadStorybookMcpPref();
|
|
641
|
+
dispatch({ type: 'SET_USE_STORYBOOK_MCP', payload: savedPref });
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
checkStorybookMcp();
|
|
645
|
+
}, []);
|
|
528
646
|
// Detect Storybook MANAGER theme (not preview background)
|
|
529
647
|
// This ensures Story UI follows Storybook's overall theme, not the story preview background toggle
|
|
530
648
|
useEffect(() => {
|
|
@@ -1054,6 +1172,7 @@ function StoryUIPanel({ mcpPort }) {
|
|
|
1054
1172
|
provider: state.selectedProvider || undefined,
|
|
1055
1173
|
model: state.selectedModel || undefined,
|
|
1056
1174
|
considerations: state.considerations || undefined,
|
|
1175
|
+
useStorybookMcp: state.storybookMcpAvailable && state.useStorybookMcp,
|
|
1057
1176
|
};
|
|
1058
1177
|
console.log('[StoryUI DEBUG] Request body being sent:', {
|
|
1059
1178
|
fileName: requestBody.fileName,
|
|
@@ -1144,6 +1263,7 @@ function StoryUIPanel({ mcpPort }) {
|
|
|
1144
1263
|
provider: state.selectedProvider || undefined,
|
|
1145
1264
|
model: state.selectedModel || undefined,
|
|
1146
1265
|
considerations: state.considerations || undefined,
|
|
1266
|
+
useStorybookMcp: state.storybookMcpAvailable && state.useStorybookMcp,
|
|
1147
1267
|
}),
|
|
1148
1268
|
});
|
|
1149
1269
|
const data = await res.json();
|
|
@@ -1368,13 +1488,25 @@ function StoryUIPanel({ mcpPort }) {
|
|
|
1368
1488
|
handleConfirmRename(chat.id);
|
|
1369
1489
|
if (e.key === 'Escape')
|
|
1370
1490
|
handleCancelRename();
|
|
1371
|
-
}, onClick: e => e.stopPropagation(), autoFocus: true }), _jsx("button", { className: "sui-button sui-button-icon sui-button-sm", onClick: e => { e.stopPropagation(); handleConfirmRename(chat.id); }, "aria-label": "Save", children: Icons.check }), _jsx("button", { className: "sui-button sui-button-icon sui-button-sm", onClick: e => { e.stopPropagation(); handleCancelRename(); }, "aria-label": "Cancel", children: Icons.x })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "sui-chat-item-title", children: chat.title }), _jsxs("div", { className: "sui-chat-item-actions", children: [_jsx("button", { className: "sui-chat-item-menu sui-button sui-button-icon sui-button-sm", onClick: e => { e.stopPropagation(); setContextMenuId(contextMenuId === chat.id ? null : chat.id); }, "aria-label": "More options", children: Icons.moreVertical }), contextMenuId === chat.id && (_jsxs("div", { className: "sui-context-menu", children: [_jsxs("button", { className: "sui-context-menu-item", onClick: e => handleStartRename(chat.id, chat.title, e), children: [Icons.pencil, _jsx("span", { children: "Rename" })] }), _jsxs("button", { className: "sui-context-menu-item sui-context-menu-item-danger", onClick: e => handleDeleteChat(chat.id, e), children: [Icons.trash, _jsx("span", { children: "Delete" })] })] }))] })] })) }, chat.id))) }), orphanCount > 0 && (_jsx("div", { className: "sui-orphan-footer", children: _jsx("button", { className: "sui-orphan-delete-btn", onClick: handleDeleteOrphans, disabled: isDeletingOrphans, title: `${orphanCount} story ${orphanCount === 1 ? 'file has' : 'files have'} no associated chat`, children: isDeletingOrphans ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "sui-orphan-spinner" }), _jsx("span", { children: "Deleting..." })] })) : (_jsxs(_Fragment, { children: [Icons.trash, _jsxs("span", { children: [orphanCount, " orphan ", orphanCount === 1 ? 'story' : 'stories'] })] })) }) }))] })), !state.sidebarOpen && (_jsx("div", { style: { padding: '12px', display: 'flex', justifyContent: 'center' }, children: _jsx("button", { className: "sui-button sui-button-ghost sui-button-icon", onClick: () => dispatch({ type: 'SET_SIDEBAR', payload: true }), "aria-label": "Show sidebar", children: Icons.panelLeft }) }))] }), _jsxs("main", { className: "sui-main", onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, children: [state.isDragging && (_jsx("div", { className: "sui-drop-overlay", children: _jsxs("div", { className: "sui-drop-overlay-text", children: [Icons.image, _jsx("span", { children: "Drop images here" })] }) })), _jsxs("header", { className: "sui-header", children: [_jsxs("div", { className: "sui-header-left", children: [_jsx("span", { className: "sui-header-title", children: "Story UI" }), _jsxs(Badge, { variant: state.connectionStatus.connected ? 'success' : 'destructive', children: [_jsx("span", { className: "sui-badge-dot" }), state.connectionStatus.connected ? getConnectionDisplayText() : 'Disconnected'] })] }),
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1491
|
+
}, onClick: e => e.stopPropagation(), autoFocus: true }), _jsx("button", { className: "sui-button sui-button-icon sui-button-sm", onClick: e => { e.stopPropagation(); handleConfirmRename(chat.id); }, "aria-label": "Save", children: Icons.check }), _jsx("button", { className: "sui-button sui-button-icon sui-button-sm", onClick: e => { e.stopPropagation(); handleCancelRename(); }, "aria-label": "Cancel", children: Icons.x })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "sui-chat-item-title", children: chat.title }), _jsxs("div", { className: "sui-chat-item-actions", children: [_jsx("button", { className: "sui-chat-item-menu sui-button sui-button-icon sui-button-sm", onClick: e => { e.stopPropagation(); setContextMenuId(contextMenuId === chat.id ? null : chat.id); }, "aria-label": "More options", children: Icons.moreVertical }), contextMenuId === chat.id && (_jsxs("div", { className: "sui-context-menu", children: [_jsxs("button", { className: "sui-context-menu-item", onClick: e => handleStartRename(chat.id, chat.title, e), children: [Icons.pencil, _jsx("span", { children: "Rename" })] }), _jsxs("button", { className: "sui-context-menu-item sui-context-menu-item-danger", onClick: e => handleDeleteChat(chat.id, e), children: [Icons.trash, _jsx("span", { children: "Delete" })] })] }))] })] })) }, chat.id))) }), orphanCount > 0 && (_jsx("div", { className: "sui-orphan-footer", children: _jsx("button", { className: "sui-orphan-delete-btn", onClick: handleDeleteOrphans, disabled: isDeletingOrphans, title: `${orphanCount} story ${orphanCount === 1 ? 'file has' : 'files have'} no associated chat`, children: isDeletingOrphans ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "sui-orphan-spinner" }), _jsx("span", { children: "Deleting..." })] })) : (_jsxs(_Fragment, { children: [Icons.trash, _jsxs("span", { children: [orphanCount, " orphan ", orphanCount === 1 ? 'story' : 'stories'] })] })) }) }))] })), !state.sidebarOpen && (_jsx("div", { style: { padding: '12px', display: 'flex', justifyContent: 'center' }, children: _jsx("button", { className: "sui-button sui-button-ghost sui-button-icon", onClick: () => dispatch({ type: 'SET_SIDEBAR', payload: true }), "aria-label": "Show sidebar", children: Icons.panelLeft }) }))] }), _jsxs("main", { className: "sui-main", onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, children: [state.isDragging && (_jsx("div", { className: "sui-drop-overlay", children: _jsxs("div", { className: "sui-drop-overlay-text", children: [Icons.image, _jsx("span", { children: "Drop images here" })] }) })), _jsxs("header", { className: "sui-header", children: [_jsxs("div", { className: "sui-header-left", children: [_jsx("span", { className: "sui-header-title", children: "Story UI" }), _jsxs(Badge, { variant: state.connectionStatus.connected ? 'success' : 'destructive', children: [_jsx("span", { className: "sui-badge-dot" }), state.connectionStatus.connected ? getConnectionDisplayText() : 'Disconnected'] })] }), _jsxs("div", { className: "sui-header-right", children: [state.connectionStatus.connected && state.availableProviders.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sui-select", children: [_jsxs("div", { className: "sui-select-trigger", children: [_jsx("span", { children: state.availableProviders.find(p => p.type === state.selectedProvider)?.name || 'Provider' }), Icons.chevronDown] }), _jsx("select", { className: "sui-select-native", value: state.selectedProvider, onChange: e => {
|
|
1492
|
+
const newProvider = e.target.value;
|
|
1493
|
+
dispatch({ type: 'SET_SELECTED_PROVIDER', payload: newProvider });
|
|
1494
|
+
const provider = state.availableProviders.find(p => p.type === newProvider);
|
|
1495
|
+
if (provider?.models.length)
|
|
1496
|
+
dispatch({ type: 'SET_SELECTED_MODEL', payload: provider.models[0] });
|
|
1497
|
+
}, "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))) })] })] })), state.storybookMcpAvailable && (_jsx("div", { className: "sui-mcp-toggle", title: "Use Storybook MCP context for enhanced component generation", children: _jsxs("label", { className: "sui-toggle-label", children: [_jsx("span", { className: "sui-toggle-text", children: "MCP Context" }), _jsxs("div", { className: "sui-toggle-switch", children: [_jsx("input", { type: "checkbox", checked: state.useStorybookMcp, onChange: e => {
|
|
1498
|
+
const enabled = e.target.checked;
|
|
1499
|
+
dispatch({ type: 'SET_USE_STORYBOOK_MCP', payload: enabled });
|
|
1500
|
+
saveStorybookMcpPref(enabled);
|
|
1501
|
+
}, "aria-label": "Use Storybook MCP context" }), _jsx("span", { className: "sui-toggle-slider" })] })] }) }))] })] }), _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 => {
|
|
1502
|
+
// Submit on Enter, newline on Shift+Enter
|
|
1503
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
1504
|
+
e.preventDefault();
|
|
1505
|
+
if (!state.loading && (state.input.trim() || state.attachedImages.length > 0)) {
|
|
1506
|
+
handleSend(e);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}, 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
1510
|
}
|
|
1379
1511
|
export default StoryUIPanel;
|
|
1380
1512
|
export { StoryUIPanel };
|
package/package.json
CHANGED
|
@@ -639,6 +639,92 @@
|
|
|
639
639
|
flex-shrink: 0;
|
|
640
640
|
}
|
|
641
641
|
|
|
642
|
+
/* ============================================
|
|
643
|
+
MCP Toggle Switch Component
|
|
644
|
+
============================================ */
|
|
645
|
+
.sui-mcp-toggle {
|
|
646
|
+
display: flex;
|
|
647
|
+
align-items: center;
|
|
648
|
+
margin-left: var(--space-3);
|
|
649
|
+
padding-left: var(--space-3);
|
|
650
|
+
border-left: 1px solid hsl(var(--border));
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
.sui-toggle-label {
|
|
654
|
+
display: flex;
|
|
655
|
+
align-items: center;
|
|
656
|
+
gap: var(--space-2);
|
|
657
|
+
cursor: pointer;
|
|
658
|
+
user-select: none;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.sui-toggle-text {
|
|
662
|
+
font-size: 0.75rem;
|
|
663
|
+
font-weight: 500;
|
|
664
|
+
color: hsl(var(--muted-foreground));
|
|
665
|
+
white-space: nowrap;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.sui-toggle-switch {
|
|
669
|
+
position: relative;
|
|
670
|
+
width: 36px;
|
|
671
|
+
height: 20px;
|
|
672
|
+
flex-shrink: 0;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.sui-toggle-switch input {
|
|
676
|
+
opacity: 0;
|
|
677
|
+
width: 0;
|
|
678
|
+
height: 0;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.sui-toggle-slider {
|
|
682
|
+
position: absolute;
|
|
683
|
+
cursor: pointer;
|
|
684
|
+
top: 0;
|
|
685
|
+
left: 0;
|
|
686
|
+
right: 0;
|
|
687
|
+
bottom: 0;
|
|
688
|
+
background-color: hsl(var(--muted));
|
|
689
|
+
border: 1px solid hsl(var(--border));
|
|
690
|
+
border-radius: var(--radius-full);
|
|
691
|
+
transition: all var(--transition-fast);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
.sui-toggle-slider::before {
|
|
695
|
+
position: absolute;
|
|
696
|
+
content: "";
|
|
697
|
+
height: 14px;
|
|
698
|
+
width: 14px;
|
|
699
|
+
left: 2px;
|
|
700
|
+
bottom: 2px;
|
|
701
|
+
background-color: hsl(var(--background));
|
|
702
|
+
border-radius: var(--radius-full);
|
|
703
|
+
box-shadow: var(--shadow-sm);
|
|
704
|
+
transition: all var(--transition-fast);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.sui-toggle-switch input:checked + .sui-toggle-slider {
|
|
708
|
+
background-color: hsl(var(--primary));
|
|
709
|
+
border-color: hsl(var(--primary));
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
.sui-toggle-switch input:checked + .sui-toggle-slider::before {
|
|
713
|
+
transform: translateX(16px);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
.sui-toggle-switch input:focus + .sui-toggle-slider {
|
|
717
|
+
box-shadow: 0 0 0 2px hsl(var(--ring) / 0.2);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
.sui-toggle-label:hover .sui-toggle-slider {
|
|
721
|
+
background-color: hsl(var(--accent));
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.sui-toggle-label:hover .sui-toggle-switch input:checked + .sui-toggle-slider {
|
|
725
|
+
background-color: hsl(var(--primary) / 0.9);
|
|
726
|
+
}
|
|
727
|
+
|
|
642
728
|
/* ============================================
|
|
643
729
|
ShadCN-Style Badge Component
|
|
644
730
|
============================================ */
|
|
@@ -1047,7 +1133,7 @@
|
|
|
1047
1133
|
============================================ */
|
|
1048
1134
|
.sui-input-form {
|
|
1049
1135
|
display: flex;
|
|
1050
|
-
align-items:
|
|
1136
|
+
align-items: flex-end; /* Align buttons to bottom when textarea expands */
|
|
1051
1137
|
gap: var(--space-2);
|
|
1052
1138
|
background: hsl(var(--card));
|
|
1053
1139
|
border: 1px solid hsl(var(--border));
|
|
@@ -1093,12 +1179,16 @@
|
|
|
1093
1179
|
background: transparent;
|
|
1094
1180
|
color: hsl(var(--foreground));
|
|
1095
1181
|
font-size: 0.9375rem;
|
|
1096
|
-
|
|
1097
|
-
|
|
1182
|
+
font-family: inherit;
|
|
1183
|
+
padding: 10px var(--space-3);
|
|
1184
|
+
min-height: 40px;
|
|
1185
|
+
max-height: 200px;
|
|
1098
1186
|
min-width: 0;
|
|
1099
1187
|
resize: none;
|
|
1100
1188
|
outline: none;
|
|
1101
|
-
|
|
1189
|
+
line-height: 1.5;
|
|
1190
|
+
overflow-y: auto;
|
|
1191
|
+
/* Auto-expands with content via JS, scrolls when max-height reached */
|
|
1102
1192
|
}
|
|
1103
1193
|
|
|
1104
1194
|
.sui-input-form-field::placeholder {
|
|
@@ -158,6 +158,8 @@ interface PanelState {
|
|
|
158
158
|
error: string | null;
|
|
159
159
|
considerations: string;
|
|
160
160
|
isDarkMode: boolean;
|
|
161
|
+
storybookMcpAvailable: boolean;
|
|
162
|
+
useStorybookMcp: boolean;
|
|
161
163
|
}
|
|
162
164
|
|
|
163
165
|
type PanelAction =
|
|
@@ -188,6 +190,8 @@ type PanelAction =
|
|
|
188
190
|
| { type: 'SET_ERROR'; payload: string | null }
|
|
189
191
|
| { type: 'SET_CONSIDERATIONS'; payload: string }
|
|
190
192
|
| { type: 'SET_DARK_MODE'; payload: boolean }
|
|
193
|
+
| { type: 'SET_STORYBOOK_MCP_AVAILABLE'; payload: boolean }
|
|
194
|
+
| { type: 'SET_USE_STORYBOOK_MCP'; payload: boolean }
|
|
191
195
|
| { type: 'NEW_CHAT' };
|
|
192
196
|
|
|
193
197
|
const initialState: PanelState = {
|
|
@@ -212,6 +216,8 @@ const initialState: PanelState = {
|
|
|
212
216
|
error: null,
|
|
213
217
|
considerations: '',
|
|
214
218
|
isDarkMode: false,
|
|
219
|
+
storybookMcpAvailable: false,
|
|
220
|
+
useStorybookMcp: true, // Default to enabled when available
|
|
215
221
|
};
|
|
216
222
|
|
|
217
223
|
function panelReducer(state: PanelState, action: PanelAction): PanelState {
|
|
@@ -280,6 +286,10 @@ function panelReducer(state: PanelState, action: PanelAction): PanelState {
|
|
|
280
286
|
return { ...state, considerations: action.payload };
|
|
281
287
|
case 'SET_DARK_MODE':
|
|
282
288
|
return { ...state, isDarkMode: action.payload };
|
|
289
|
+
case 'SET_STORYBOOK_MCP_AVAILABLE':
|
|
290
|
+
return { ...state, storybookMcpAvailable: action.payload };
|
|
291
|
+
case 'SET_USE_STORYBOOK_MCP':
|
|
292
|
+
return { ...state, useStorybookMcp: action.payload };
|
|
283
293
|
case 'NEW_CHAT':
|
|
284
294
|
return { ...state, conversation: [], activeChatId: null, activeTitle: '' };
|
|
285
295
|
default:
|
|
@@ -403,6 +413,84 @@ function saveProviderPrefs(provider: string, model: string): void {
|
|
|
403
413
|
}
|
|
404
414
|
}
|
|
405
415
|
|
|
416
|
+
// Storage key for Storybook MCP preference
|
|
417
|
+
const STORYBOOK_MCP_PREF_KEY = 'story-ui-use-storybook-mcp';
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Detect if Storybook MCP addon is available.
|
|
421
|
+
* Checks for the MCP endpoint that @storybook/addon-mcp exposes.
|
|
422
|
+
* The addon returns SSE (Server-Sent Events) responses, not JSON.
|
|
423
|
+
*/
|
|
424
|
+
async function detectStorybookMcp(): Promise<boolean> {
|
|
425
|
+
try {
|
|
426
|
+
// Try to detect Storybook MCP on the same origin (works when running in Storybook)
|
|
427
|
+
const storybookOrigin = typeof window !== 'undefined' ? window.location.origin : '';
|
|
428
|
+
const mcpEndpoint = `${storybookOrigin}/mcp`;
|
|
429
|
+
|
|
430
|
+
const response = await fetch(mcpEndpoint, {
|
|
431
|
+
method: 'POST',
|
|
432
|
+
headers: { 'Content-Type': 'application/json' },
|
|
433
|
+
body: JSON.stringify({
|
|
434
|
+
jsonrpc: '2.0',
|
|
435
|
+
id: 1,
|
|
436
|
+
method: 'tools/list',
|
|
437
|
+
params: {}
|
|
438
|
+
})
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
if (!response.ok) return false;
|
|
442
|
+
|
|
443
|
+
// Storybook MCP addon returns SSE (Server-Sent Events) responses
|
|
444
|
+
// Check content-type or read a small portion to verify it's SSE format
|
|
445
|
+
const contentType = response.headers.get('content-type') || '';
|
|
446
|
+
if (contentType.includes('text/event-stream')) {
|
|
447
|
+
console.log('[StoryUI] Storybook MCP addon detected (SSE endpoint)');
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Also check by reading a portion of the response
|
|
452
|
+
const text = await response.text();
|
|
453
|
+
if (text.startsWith('event:') || text.startsWith('data:')) {
|
|
454
|
+
console.log('[StoryUI] Storybook MCP addon detected (SSE response)');
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Try parsing as JSON as fallback (some implementations may return JSON)
|
|
459
|
+
try {
|
|
460
|
+
const data = JSON.parse(text);
|
|
461
|
+
if (data && data.result && Array.isArray(data.result.tools)) {
|
|
462
|
+
console.log('[StoryUI] Storybook MCP addon detected (JSON response)');
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
} catch {
|
|
466
|
+
// Not JSON, but might still be valid SSE that we missed
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return false;
|
|
470
|
+
} catch (e) {
|
|
471
|
+
// Not available - this is normal if addon-mcp isn't installed
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function loadStorybookMcpPref(): boolean {
|
|
477
|
+
try {
|
|
478
|
+
const stored = localStorage.getItem(STORYBOOK_MCP_PREF_KEY);
|
|
479
|
+
if (stored !== null) return JSON.parse(stored);
|
|
480
|
+
} catch (e) {
|
|
481
|
+
console.error('Failed to load Storybook MCP preference:', e);
|
|
482
|
+
}
|
|
483
|
+
return true; // Default to enabled
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function saveStorybookMcpPref(enabled: boolean): void {
|
|
487
|
+
try {
|
|
488
|
+
localStorage.setItem(STORYBOOK_MCP_PREF_KEY, JSON.stringify(enabled));
|
|
489
|
+
} catch (e) {
|
|
490
|
+
console.error('Failed to save Storybook MCP preference:', e);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
406
494
|
async function testMCPConnection(): Promise<{ connected: boolean; error?: string }> {
|
|
407
495
|
try {
|
|
408
496
|
const response = await fetch(PROVIDERS_API, { method: 'GET' });
|
|
@@ -471,12 +559,21 @@ function formatTime(timestamp: number): string {
|
|
|
471
559
|
|
|
472
560
|
function getModelDisplayName(model: string): string {
|
|
473
561
|
const displayNames: Record<string, string> = {
|
|
562
|
+
// Claude models
|
|
474
563
|
'claude-opus-4-5-20251101': 'Claude Opus 4.5',
|
|
475
564
|
'claude-sonnet-4-5-20250929': 'Claude Sonnet 4.5',
|
|
476
565
|
'claude-haiku-4-5-20251001': 'Claude Haiku 4.5',
|
|
566
|
+
'claude-sonnet-4-20250514': 'Claude Sonnet 4',
|
|
567
|
+
// OpenAI models
|
|
568
|
+
'gpt-5.2': 'GPT-5.2',
|
|
569
|
+
'gpt-5.1': 'GPT-5.1',
|
|
477
570
|
'gpt-4o': 'GPT-4o',
|
|
478
571
|
'gpt-4o-mini': 'GPT-4o Mini',
|
|
479
572
|
'o1': 'o1',
|
|
573
|
+
// Gemini models
|
|
574
|
+
'gemini-3-pro-preview': 'Gemini 3 Pro Preview',
|
|
575
|
+
'gemini-2.5-pro': 'Gemini 2.5 Pro',
|
|
576
|
+
'gemini-2.5-flash': 'Gemini 2.5 Flash',
|
|
480
577
|
'gemini-2.0-flash': 'Gemini 2.0 Flash',
|
|
481
578
|
'gemini-1.5-pro': 'Gemini 1.5 Pro',
|
|
482
579
|
};
|
|
@@ -795,7 +892,25 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
795
892
|
const [orphanCount, setOrphanCount] = useState<number>(0);
|
|
796
893
|
const [isDeletingOrphans, setIsDeletingOrphans] = useState<boolean>(false);
|
|
797
894
|
const chatEndRef = useRef<HTMLDivElement>(null);
|
|
798
|
-
const inputRef = useRef<
|
|
895
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
896
|
+
|
|
897
|
+
// Auto-resize textarea based on content
|
|
898
|
+
const adjustTextareaHeight = useCallback(() => {
|
|
899
|
+
const textarea = inputRef.current;
|
|
900
|
+
if (textarea) {
|
|
901
|
+
// Reset height to auto to get the correct scrollHeight
|
|
902
|
+
textarea.style.height = 'auto';
|
|
903
|
+
// Set height to scrollHeight, capped at max-height (200px)
|
|
904
|
+
const maxHeight = 200;
|
|
905
|
+
const newHeight = Math.min(textarea.scrollHeight, maxHeight);
|
|
906
|
+
textarea.style.height = `${newHeight}px`;
|
|
907
|
+
}
|
|
908
|
+
}, []);
|
|
909
|
+
|
|
910
|
+
// Adjust height when input changes
|
|
911
|
+
useEffect(() => {
|
|
912
|
+
adjustTextareaHeight();
|
|
913
|
+
}, [state.input, adjustTextareaHeight]);
|
|
799
914
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
800
915
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
801
916
|
const hasShownRefreshHint = useRef(false);
|
|
@@ -881,6 +996,22 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
881
996
|
return () => clearInterval(intervalId);
|
|
882
997
|
}, []);
|
|
883
998
|
|
|
999
|
+
// Detect Storybook MCP addon availability
|
|
1000
|
+
useEffect(() => {
|
|
1001
|
+
const checkStorybookMcp = async () => {
|
|
1002
|
+
const available = await detectStorybookMcp();
|
|
1003
|
+
dispatch({ type: 'SET_STORYBOOK_MCP_AVAILABLE', payload: available });
|
|
1004
|
+
|
|
1005
|
+
// Load saved preference if MCP is available
|
|
1006
|
+
if (available) {
|
|
1007
|
+
const savedPref = loadStorybookMcpPref();
|
|
1008
|
+
dispatch({ type: 'SET_USE_STORYBOOK_MCP', payload: savedPref });
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
checkStorybookMcp();
|
|
1013
|
+
}, []);
|
|
1014
|
+
|
|
884
1015
|
// Detect Storybook MANAGER theme (not preview background)
|
|
885
1016
|
// This ensures Story UI follows Storybook's overall theme, not the story preview background toggle
|
|
886
1017
|
useEffect(() => {
|
|
@@ -1418,6 +1549,7 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
1418
1549
|
provider: state.selectedProvider || undefined,
|
|
1419
1550
|
model: state.selectedModel || undefined,
|
|
1420
1551
|
considerations: state.considerations || undefined,
|
|
1552
|
+
useStorybookMcp: state.storybookMcpAvailable && state.useStorybookMcp,
|
|
1421
1553
|
};
|
|
1422
1554
|
console.log('[StoryUI DEBUG] Request body being sent:', {
|
|
1423
1555
|
fileName: requestBody.fileName,
|
|
@@ -1501,6 +1633,7 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
1501
1633
|
provider: state.selectedProvider || undefined,
|
|
1502
1634
|
model: state.selectedModel || undefined,
|
|
1503
1635
|
considerations: state.considerations || undefined,
|
|
1636
|
+
useStorybookMcp: state.storybookMcpAvailable && state.useStorybookMcp,
|
|
1504
1637
|
}),
|
|
1505
1638
|
});
|
|
1506
1639
|
const data = await res.json();
|
|
@@ -1890,6 +2023,27 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
1890
2023
|
</div>
|
|
1891
2024
|
</>
|
|
1892
2025
|
)}
|
|
2026
|
+
{/* Storybook MCP Toggle - only shown when MCP addon is detected */}
|
|
2027
|
+
{state.storybookMcpAvailable && (
|
|
2028
|
+
<div className="sui-mcp-toggle" title="Use Storybook MCP context for enhanced component generation">
|
|
2029
|
+
<label className="sui-toggle-label">
|
|
2030
|
+
<span className="sui-toggle-text">MCP Context</span>
|
|
2031
|
+
<div className="sui-toggle-switch">
|
|
2032
|
+
<input
|
|
2033
|
+
type="checkbox"
|
|
2034
|
+
checked={state.useStorybookMcp}
|
|
2035
|
+
onChange={e => {
|
|
2036
|
+
const enabled = e.target.checked;
|
|
2037
|
+
dispatch({ type: 'SET_USE_STORYBOOK_MCP', payload: enabled });
|
|
2038
|
+
saveStorybookMcpPref(enabled);
|
|
2039
|
+
}}
|
|
2040
|
+
aria-label="Use Storybook MCP context"
|
|
2041
|
+
/>
|
|
2042
|
+
<span className="sui-toggle-slider" />
|
|
2043
|
+
</div>
|
|
2044
|
+
</label>
|
|
2045
|
+
</div>
|
|
2046
|
+
)}
|
|
1893
2047
|
</div>
|
|
1894
2048
|
</header>
|
|
1895
2049
|
|
|
@@ -1971,12 +2125,21 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
1971
2125
|
<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
2126
|
{Icons.image}
|
|
1973
2127
|
</button>
|
|
1974
|
-
<
|
|
2128
|
+
<textarea
|
|
1975
2129
|
ref={inputRef}
|
|
1976
|
-
|
|
2130
|
+
rows={1}
|
|
1977
2131
|
className="sui-input-form-field"
|
|
1978
2132
|
value={state.input}
|
|
1979
2133
|
onChange={e => dispatch({ type: 'SET_INPUT', payload: e.target.value })}
|
|
2134
|
+
onKeyDown={e => {
|
|
2135
|
+
// Submit on Enter, newline on Shift+Enter
|
|
2136
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
2137
|
+
e.preventDefault();
|
|
2138
|
+
if (!state.loading && (state.input.trim() || state.attachedImages.length > 0)) {
|
|
2139
|
+
handleSend(e as unknown as React.FormEvent);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
}}
|
|
1980
2143
|
onPaste={handlePaste}
|
|
1981
2144
|
placeholder={state.attachedImages.length > 0 ? 'Describe what to create from these images...' : 'Describe a UI component...'}
|
|
1982
2145
|
/>
|