@tpitre/story-ui 2.2.0 → 2.3.1
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/.env.sample +82 -11
- package/README.md +89 -0
- package/dist/cli/deploy.d.ts +17 -0
- package/dist/cli/deploy.d.ts.map +1 -0
- package/dist/cli/deploy.js +696 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +26 -2
- package/dist/cli/setup.d.ts +11 -0
- package/dist/cli/setup.d.ts.map +1 -0
- package/dist/cli/setup.js +437 -110
- package/dist/mcp-server/index.d.ts +2 -0
- package/dist/mcp-server/index.d.ts.map +1 -0
- package/dist/mcp-server/index.js +120 -2
- package/dist/mcp-server/mcp-stdio-server.d.ts +3 -0
- package/dist/mcp-server/mcp-stdio-server.d.ts.map +1 -0
- package/dist/mcp-server/mcp-stdio-server.js +8 -1
- package/dist/mcp-server/routes/claude.d.ts +3 -0
- package/dist/mcp-server/routes/claude.d.ts.map +1 -0
- package/dist/mcp-server/routes/claude.js +60 -23
- package/dist/mcp-server/routes/components.d.ts +4 -0
- package/dist/mcp-server/routes/components.d.ts.map +1 -0
- package/dist/mcp-server/routes/frameworks.d.ts +38 -0
- package/dist/mcp-server/routes/frameworks.d.ts.map +1 -0
- package/dist/mcp-server/routes/frameworks.js +183 -0
- package/dist/mcp-server/routes/generateStory.d.ts +3 -0
- package/dist/mcp-server/routes/generateStory.d.ts.map +1 -0
- package/dist/mcp-server/routes/generateStory.js +160 -76
- package/dist/mcp-server/routes/generateStoryStream.d.ts +12 -0
- package/dist/mcp-server/routes/generateStoryStream.d.ts.map +1 -0
- package/dist/mcp-server/routes/generateStoryStream.js +947 -0
- package/dist/mcp-server/routes/hybridStories.d.ts +18 -0
- package/dist/mcp-server/routes/hybridStories.d.ts.map +1 -0
- package/dist/mcp-server/routes/mcpRemote.d.ts +14 -0
- package/dist/mcp-server/routes/mcpRemote.d.ts.map +1 -0
- package/dist/mcp-server/routes/mcpRemote.js +489 -0
- package/dist/mcp-server/routes/memoryStories.d.ts +26 -0
- package/dist/mcp-server/routes/memoryStories.d.ts.map +1 -0
- package/dist/mcp-server/routes/providers.d.ts +89 -0
- package/dist/mcp-server/routes/providers.d.ts.map +1 -0
- package/dist/mcp-server/routes/providers.js +369 -0
- package/dist/mcp-server/routes/storySync.d.ts +26 -0
- package/dist/mcp-server/routes/storySync.d.ts.map +1 -0
- package/dist/mcp-server/routes/streamTypes.d.ts +110 -0
- package/dist/mcp-server/routes/streamTypes.d.ts.map +1 -0
- package/dist/mcp-server/routes/streamTypes.js +18 -0
- package/dist/mcp-server/sessionManager.d.ts +50 -0
- package/dist/mcp-server/sessionManager.d.ts.map +1 -0
- package/dist/story-generator/componentBlacklist.d.ts +21 -0
- package/dist/story-generator/componentBlacklist.d.ts.map +1 -0
- package/dist/story-generator/componentDiscovery.d.ts +28 -0
- package/dist/story-generator/componentDiscovery.d.ts.map +1 -0
- package/dist/story-generator/componentRegistryGenerator.d.ts +49 -0
- package/dist/story-generator/componentRegistryGenerator.d.ts.map +1 -0
- package/dist/story-generator/componentRegistryGenerator.js +205 -0
- package/dist/story-generator/configLoader.d.ts +33 -0
- package/dist/story-generator/configLoader.d.ts.map +1 -0
- package/dist/story-generator/considerationsLoader.d.ts +32 -0
- package/dist/story-generator/considerationsLoader.d.ts.map +1 -0
- package/dist/story-generator/documentation-sources.d.ts +28 -0
- package/dist/story-generator/documentation-sources.d.ts.map +1 -0
- package/dist/story-generator/documentationLoader.d.ts +64 -0
- package/dist/story-generator/documentationLoader.d.ts.map +1 -0
- package/dist/story-generator/dynamicPackageDiscovery.d.ts +97 -0
- package/dist/story-generator/dynamicPackageDiscovery.d.ts.map +1 -0
- package/dist/story-generator/enhancedComponentDiscovery.d.ts +125 -0
- package/dist/story-generator/enhancedComponentDiscovery.d.ts.map +1 -0
- package/dist/story-generator/enhancedComponentDiscovery.js +111 -11
- package/dist/story-generator/framework-adapters/angular-adapter.d.ts +40 -0
- package/dist/story-generator/framework-adapters/angular-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/angular-adapter.js +427 -0
- package/dist/story-generator/framework-adapters/base-adapter.d.ts +75 -0
- package/dist/story-generator/framework-adapters/base-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/base-adapter.js +147 -0
- package/dist/story-generator/framework-adapters/framework-detector.d.ts +55 -0
- package/dist/story-generator/framework-adapters/framework-detector.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/framework-detector.js +323 -0
- package/dist/story-generator/framework-adapters/index.d.ts +97 -0
- package/dist/story-generator/framework-adapters/index.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/index.js +198 -0
- package/dist/story-generator/framework-adapters/react-adapter.d.ts +40 -0
- package/dist/story-generator/framework-adapters/react-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/react-adapter.js +316 -0
- package/dist/story-generator/framework-adapters/svelte-adapter.d.ts +40 -0
- package/dist/story-generator/framework-adapters/svelte-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/svelte-adapter.js +372 -0
- package/dist/story-generator/framework-adapters/types.d.ts +182 -0
- package/dist/story-generator/framework-adapters/types.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/types.js +8 -0
- package/dist/story-generator/framework-adapters/vue-adapter.d.ts +36 -0
- package/dist/story-generator/framework-adapters/vue-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/vue-adapter.js +336 -0
- package/dist/story-generator/framework-adapters/web-components-adapter.d.ts +54 -0
- package/dist/story-generator/framework-adapters/web-components-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/web-components-adapter.js +387 -0
- package/dist/story-generator/generateStory.d.ts +7 -0
- package/dist/story-generator/generateStory.d.ts.map +1 -0
- package/dist/story-generator/gitignoreManager.d.ts +50 -0
- package/dist/story-generator/gitignoreManager.d.ts.map +1 -0
- package/dist/story-generator/imageProcessor.d.ts +80 -0
- package/dist/story-generator/imageProcessor.d.ts.map +1 -0
- package/dist/story-generator/imageProcessor.js +391 -0
- package/dist/story-generator/inMemoryStoryService.d.ts +89 -0
- package/dist/story-generator/inMemoryStoryService.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/base-provider.d.ts +36 -0
- package/dist/story-generator/llm-providers/base-provider.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/base-provider.js +135 -0
- package/dist/story-generator/llm-providers/claude-provider.d.ts +23 -0
- package/dist/story-generator/llm-providers/claude-provider.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/claude-provider.js +414 -0
- package/dist/story-generator/llm-providers/gemini-provider.d.ts +24 -0
- package/dist/story-generator/llm-providers/gemini-provider.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/gemini-provider.js +406 -0
- package/dist/story-generator/llm-providers/index.d.ts +63 -0
- package/dist/story-generator/llm-providers/index.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/index.js +169 -0
- package/dist/story-generator/llm-providers/openai-provider.d.ts +24 -0
- package/dist/story-generator/llm-providers/openai-provider.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/openai-provider.js +458 -0
- package/dist/story-generator/llm-providers/settings-manager.d.ts +75 -0
- package/dist/story-generator/llm-providers/settings-manager.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/settings-manager.js +173 -0
- package/dist/story-generator/llm-providers/story-llm-service.d.ts +79 -0
- package/dist/story-generator/llm-providers/story-llm-service.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/story-llm-service.js +240 -0
- package/dist/story-generator/llm-providers/types.d.ts +153 -0
- package/dist/story-generator/llm-providers/types.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/types.js +8 -0
- package/dist/story-generator/logger.d.ts +14 -0
- package/dist/story-generator/logger.d.ts.map +1 -0
- package/dist/story-generator/logger.js +96 -29
- package/dist/story-generator/postProcessStory.d.ts +6 -0
- package/dist/story-generator/postProcessStory.d.ts.map +1 -0
- package/dist/story-generator/productionGitignoreManager.d.ts +91 -0
- package/dist/story-generator/productionGitignoreManager.d.ts.map +1 -0
- package/dist/story-generator/promptGenerator.d.ts +48 -0
- package/dist/story-generator/promptGenerator.d.ts.map +1 -0
- package/dist/story-generator/promptGenerator.js +186 -1
- package/dist/story-generator/storyHistory.d.ts +44 -0
- package/dist/story-generator/storyHistory.d.ts.map +1 -0
- package/dist/story-generator/storySync.d.ts +68 -0
- package/dist/story-generator/storySync.d.ts.map +1 -0
- package/dist/story-generator/storyTracker.d.ts +48 -0
- package/dist/story-generator/storyTracker.d.ts.map +1 -0
- package/dist/story-generator/storyValidator.d.ts +6 -0
- package/dist/story-generator/storyValidator.d.ts.map +1 -0
- package/dist/story-generator/universalDesignSystemAdapter.d.ts +68 -0
- package/dist/story-generator/universalDesignSystemAdapter.d.ts.map +1 -0
- package/dist/story-generator/universalDesignSystemAdapter.js +138 -1
- package/dist/story-generator/urlRedirectService.d.ts +21 -0
- package/dist/story-generator/urlRedirectService.d.ts.map +1 -0
- package/dist/story-generator/validateStory.d.ts +19 -0
- package/dist/story-generator/validateStory.d.ts.map +1 -0
- package/dist/story-generator/validateStory.js +6 -2
- package/dist/story-generator/visionPrompts.d.ts +88 -0
- package/dist/story-generator/visionPrompts.d.ts.map +1 -0
- package/dist/story-generator/visionPrompts.js +462 -0
- package/dist/story-ui.config.d.ts +78 -0
- package/dist/story-ui.config.d.ts.map +1 -0
- package/dist/templates/StoryUI/StoryUIPanel.d.ts +4 -0
- package/dist/templates/StoryUI/StoryUIPanel.d.ts.map +1 -0
- package/dist/templates/StoryUI/StoryUIPanel.js +1874 -0
- package/dist/templates/StoryUI/StoryUIPanel.stories.d.ts +18 -0
- package/dist/templates/StoryUI/StoryUIPanel.stories.d.ts.map +1 -0
- package/dist/templates/StoryUI/StoryUIPanel.stories.js +37 -0
- package/dist/templates/StoryUI/index.d.ts +3 -0
- package/dist/templates/StoryUI/index.d.ts.map +1 -0
- package/dist/templates/StoryUI/index.js +2 -0
- package/package.json +17 -3
- package/templates/StoryUI/StoryUIPanel.tsx +1960 -384
- package/templates/StoryUI/index.tsx +1 -1
- package/templates/StoryUI/manager.tsx +264 -0
- package/templates/production-app/.env.example +11 -0
- package/templates/production-app/index.html +66 -0
- package/templates/production-app/package.json +30 -0
- package/templates/production-app/public/favicon.svg +5 -0
- package/templates/production-app/src/App.tsx +1560 -0
- package/templates/production-app/src/LivePreviewRenderer.tsx +420 -0
- package/templates/production-app/src/componentRegistry.ts +315 -0
- package/templates/production-app/src/considerations.ts +16 -0
- package/templates/production-app/src/index.css +284 -0
- package/templates/production-app/src/main.tsx +25 -0
- package/templates/production-app/tsconfig.json +32 -0
- package/templates/production-app/tsconfig.node.json +11 -0
- package/templates/production-app/vite.config.ts +83 -0
- package/templates/react-import-rule.json +2 -2
- package/dist/index.js +0 -12
- package/dist/story-ui.config.loader.js +0 -205
|
@@ -0,0 +1,1874 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
3
|
+
// Simple markdown renderer for AI messages with icon marker support
|
|
4
|
+
const renderMarkdown = (text) => {
|
|
5
|
+
// Split by double newlines to get paragraphs
|
|
6
|
+
const paragraphs = text.split(/\n\n+/);
|
|
7
|
+
// Parse inline formatting within text
|
|
8
|
+
const parseInline = (str, paragraphIndex) => {
|
|
9
|
+
const parts = [];
|
|
10
|
+
let remaining = str;
|
|
11
|
+
let keyIndex = 0;
|
|
12
|
+
while (remaining.length > 0) {
|
|
13
|
+
// Icon markers: [SUCCESS], [ERROR], [TIP], [WRENCH]
|
|
14
|
+
const iconMatch = remaining.match(/^\[(SUCCESS|ERROR|TIP|WRENCH)\]/);
|
|
15
|
+
if (iconMatch) {
|
|
16
|
+
const iconType = iconMatch[1].toLowerCase();
|
|
17
|
+
parts.push(_jsx("span", { children: StatusIcons[iconType] }, `icon-${paragraphIndex}-${keyIndex++}`));
|
|
18
|
+
remaining = remaining.slice(iconMatch[0].length);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
// Bold: **text**
|
|
22
|
+
const boldMatch = remaining.match(/^\*\*(.+?)\*\*/);
|
|
23
|
+
if (boldMatch) {
|
|
24
|
+
parts.push(_jsx("strong", { children: boldMatch[1] }, `b-${paragraphIndex}-${keyIndex++}`));
|
|
25
|
+
remaining = remaining.slice(boldMatch[0].length);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
// Italic: _text_
|
|
29
|
+
const italicMatch = remaining.match(/^_(.+?)_/);
|
|
30
|
+
if (italicMatch) {
|
|
31
|
+
parts.push(_jsx("em", { style: { opacity: 0.7, fontSize: '0.9em' }, children: italicMatch[1] }, `i-${paragraphIndex}-${keyIndex++}`));
|
|
32
|
+
remaining = remaining.slice(italicMatch[0].length);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
// Code: `text`
|
|
36
|
+
const codeMatch = remaining.match(/^`([^`]+)`/);
|
|
37
|
+
if (codeMatch) {
|
|
38
|
+
parts.push(_jsx("code", { style: {
|
|
39
|
+
background: 'rgba(0,0,0,0.08)',
|
|
40
|
+
padding: '1px 5px',
|
|
41
|
+
borderRadius: '3px',
|
|
42
|
+
fontFamily: 'ui-monospace, monospace',
|
|
43
|
+
fontSize: '0.88em'
|
|
44
|
+
}, children: codeMatch[1] }, `c-${paragraphIndex}-${keyIndex++}`));
|
|
45
|
+
remaining = remaining.slice(codeMatch[0].length);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
// Single newline within paragraph - convert to space or line break
|
|
49
|
+
if (remaining.startsWith('\n')) {
|
|
50
|
+
parts.push(' ');
|
|
51
|
+
remaining = remaining.slice(1);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// Regular text - consume until next special character or bracket
|
|
55
|
+
const nextSpecial = remaining.search(/[*_`\[\n]/);
|
|
56
|
+
if (nextSpecial === -1) {
|
|
57
|
+
parts.push(remaining);
|
|
58
|
+
remaining = '';
|
|
59
|
+
}
|
|
60
|
+
else if (nextSpecial === 0) {
|
|
61
|
+
// Special char that didn't match a pattern, treat as regular text
|
|
62
|
+
parts.push(remaining[0]);
|
|
63
|
+
remaining = remaining.slice(1);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
parts.push(remaining.slice(0, nextSpecial));
|
|
67
|
+
remaining = remaining.slice(nextSpecial);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return parts;
|
|
71
|
+
};
|
|
72
|
+
return (_jsx(_Fragment, { children: paragraphs.map((paragraph, index) => (_jsx("div", { style: { marginBottom: index < paragraphs.length - 1 ? '12px' : 0 }, children: parseInline(paragraph.trim(), index) }, `p-${index}`))) }));
|
|
73
|
+
};
|
|
74
|
+
// Inline SVG icons for status indicators (avoiding emojis)
|
|
75
|
+
const StatusIcons = {
|
|
76
|
+
success: (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { color: '#22c55e', verticalAlign: 'middle', marginRight: '6px' }, children: [_jsx("path", { d: "M22 11.08V12a10 10 0 1 1-5.93-9.14" }), _jsx("polyline", { points: "22,4 12,14.01 9,11.01" })] })),
|
|
77
|
+
error: (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { color: '#ef4444', verticalAlign: 'middle', marginRight: '6px' }, children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("line", { x1: "15", y1: "9", x2: "9", y2: "15" }), _jsx("line", { x1: "9", y1: "9", x2: "15", y2: "15" })] })),
|
|
78
|
+
tip: (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { color: '#f59e0b', verticalAlign: 'middle', marginRight: '4px' }, children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("line", { x1: "12", y1: "16", x2: "12", y2: "12" }), _jsx("line", { x1: "12", y1: "8", x2: "12.01", y2: "8" })] })),
|
|
79
|
+
wrench: (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { color: '#6366f1', verticalAlign: 'middle', marginRight: '4px' }, children: _jsx("path", { d: "M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" }) }))
|
|
80
|
+
};
|
|
81
|
+
// Determine the MCP API base URL.
|
|
82
|
+
// Priority order:
|
|
83
|
+
// 1. VITE_STORY_UI_EDGE_URL - Edge Worker URL for cloud deployments
|
|
84
|
+
// 2. window.__STORY_UI_EDGE_URL__ - Runtime override for edge URL
|
|
85
|
+
// 3. VITE_STORY_UI_PORT - Custom port for localhost
|
|
86
|
+
// 4. window.__STORY_UI_PORT__ - Legacy port override
|
|
87
|
+
// 5. window.STORY_UI_MCP_PORT - MCP port override
|
|
88
|
+
// 6. Default to localhost:4001
|
|
89
|
+
const getApiBaseUrl = () => {
|
|
90
|
+
// Check for Edge Worker URL (cloud deployment)
|
|
91
|
+
const edgeUrl = import.meta.env?.VITE_STORY_UI_EDGE_URL;
|
|
92
|
+
if (edgeUrl)
|
|
93
|
+
return edgeUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
94
|
+
// Check for window override for edge URL (support both naming conventions)
|
|
95
|
+
const windowEdgeUrl = window.__STORY_UI_EDGE_URL__ || window.STORY_UI_EDGE_URL;
|
|
96
|
+
if (windowEdgeUrl)
|
|
97
|
+
return windowEdgeUrl.replace(/\/$/, '');
|
|
98
|
+
// Check for Vite port environment variable
|
|
99
|
+
const vitePort = import.meta.env?.VITE_STORY_UI_PORT;
|
|
100
|
+
if (vitePort)
|
|
101
|
+
return `http://localhost:${vitePort}`;
|
|
102
|
+
// Check for window override (legacy support)
|
|
103
|
+
const windowOverride = window.__STORY_UI_PORT__;
|
|
104
|
+
if (windowOverride)
|
|
105
|
+
return `http://localhost:${windowOverride}`;
|
|
106
|
+
// Check for MCP port override set by stories file
|
|
107
|
+
const mcpOverride = window.STORY_UI_MCP_PORT;
|
|
108
|
+
if (mcpOverride)
|
|
109
|
+
return `http://localhost:${mcpOverride}`;
|
|
110
|
+
return 'http://localhost:4001';
|
|
111
|
+
};
|
|
112
|
+
// Helper to check if we're using Edge mode (cloud deployment)
|
|
113
|
+
const isEdgeMode = () => {
|
|
114
|
+
const baseUrl = getApiBaseUrl();
|
|
115
|
+
return baseUrl.includes('workers.dev') || baseUrl.includes('pages.dev') ||
|
|
116
|
+
baseUrl.startsWith('https://') && !baseUrl.includes('localhost');
|
|
117
|
+
};
|
|
118
|
+
// Legacy helper for backwards compatibility
|
|
119
|
+
const getApiPort = () => {
|
|
120
|
+
const baseUrl = getApiBaseUrl();
|
|
121
|
+
const match = baseUrl.match(/:(\d+)$/);
|
|
122
|
+
return match ? match[1] : '4001';
|
|
123
|
+
};
|
|
124
|
+
// Get connection display text
|
|
125
|
+
const getConnectionDisplayText = () => {
|
|
126
|
+
const baseUrl = getApiBaseUrl();
|
|
127
|
+
if (isEdgeMode()) {
|
|
128
|
+
// Extract domain for Edge URL
|
|
129
|
+
try {
|
|
130
|
+
const url = new URL(baseUrl);
|
|
131
|
+
return `Edge Worker (${url.hostname})`;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return 'Edge Worker';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return `MCP server (port ${getApiPort()})`;
|
|
138
|
+
};
|
|
139
|
+
const API_BASE = getApiBaseUrl();
|
|
140
|
+
const MCP_API = `${API_BASE}/story-ui/generate`;
|
|
141
|
+
const MCP_STREAM_API = `${API_BASE}/story-ui/generate-stream`;
|
|
142
|
+
const STORIES_API = `${API_BASE}/story-ui/stories`;
|
|
143
|
+
const DELETE_API_BASE = `${API_BASE}/story-ui/stories`;
|
|
144
|
+
const PROVIDERS_API = `${API_BASE}/story-ui/providers`;
|
|
145
|
+
// Considerations API URL - includes storybookOrigin param for Edge mode
|
|
146
|
+
const getConsiderationsApiUrl = () => {
|
|
147
|
+
const baseUrl = `${API_BASE}/story-ui/considerations`;
|
|
148
|
+
if (isEdgeMode()) {
|
|
149
|
+
// In Edge mode, tell the Edge Worker where to fetch considerations from
|
|
150
|
+
// The Storybook origin is where the panel is running (window.location.origin)
|
|
151
|
+
const storybookOrigin = window.location.origin;
|
|
152
|
+
return `${baseUrl}?storybookOrigin=${encodeURIComponent(storybookOrigin)}`;
|
|
153
|
+
}
|
|
154
|
+
return baseUrl;
|
|
155
|
+
};
|
|
156
|
+
const CONSIDERATIONS_API = getConsiderationsApiUrl();
|
|
157
|
+
const STORAGE_KEY = `story-ui-chats-${window.location.port}`;
|
|
158
|
+
const MAX_RECENT_CHATS = 20;
|
|
159
|
+
// Feature flag: Enable streaming mode (can be toggled for testing)
|
|
160
|
+
const USE_STREAMING = true;
|
|
161
|
+
// Load from localStorage
|
|
162
|
+
const loadChats = () => {
|
|
163
|
+
try {
|
|
164
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
165
|
+
if (!stored)
|
|
166
|
+
return [];
|
|
167
|
+
const chats = JSON.parse(stored);
|
|
168
|
+
// Sort by lastUpdated and limit
|
|
169
|
+
return chats
|
|
170
|
+
.sort((a, b) => b.lastUpdated - a.lastUpdated)
|
|
171
|
+
.slice(0, MAX_RECENT_CHATS);
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
console.error('Failed to load chats:', e);
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
// Save to localStorage
|
|
179
|
+
const saveChats = (chats) => {
|
|
180
|
+
try {
|
|
181
|
+
// Keep only the most recent chats
|
|
182
|
+
const toSave = chats
|
|
183
|
+
.sort((a, b) => b.lastUpdated - a.lastUpdated)
|
|
184
|
+
.slice(0, MAX_RECENT_CHATS);
|
|
185
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(toSave));
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
console.error('Failed to save chats:', e);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
// Sync with memory stories from backend
|
|
192
|
+
const syncWithActualStories = async () => {
|
|
193
|
+
try {
|
|
194
|
+
const response = await fetch(STORIES_API);
|
|
195
|
+
if (!response.ok) {
|
|
196
|
+
console.error('Failed to fetch stories from backend');
|
|
197
|
+
return loadChats();
|
|
198
|
+
}
|
|
199
|
+
// Check if response is JSON
|
|
200
|
+
const contentType = response.headers.get('content-type');
|
|
201
|
+
if (!contentType || !contentType.includes('application/json')) {
|
|
202
|
+
console.error('Server returned non-JSON response, likely server not running or wrong port');
|
|
203
|
+
return loadChats();
|
|
204
|
+
}
|
|
205
|
+
const data = await response.json();
|
|
206
|
+
const memoryStories = data.stories || [];
|
|
207
|
+
// Load existing chats
|
|
208
|
+
const existingChats = loadChats();
|
|
209
|
+
// Create a map for quick lookup - using chat.id as the primary key
|
|
210
|
+
const chatMap = new Map();
|
|
211
|
+
existingChats.forEach(chat => {
|
|
212
|
+
chatMap.set(chat.id, chat);
|
|
213
|
+
});
|
|
214
|
+
// Update or add memory stories
|
|
215
|
+
memoryStories.forEach((story) => {
|
|
216
|
+
const storyId = story.storyId || story.fileName;
|
|
217
|
+
// Look for existing chat by ID or by matching fileName
|
|
218
|
+
let existingChat = chatMap.get(storyId);
|
|
219
|
+
// If not found by ID, search by fileName
|
|
220
|
+
if (!existingChat && story.fileName) {
|
|
221
|
+
for (const [id, chat] of chatMap.entries()) {
|
|
222
|
+
if (chat.fileName === story.fileName) {
|
|
223
|
+
existingChat = chat;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (existingChat) {
|
|
229
|
+
// Update existing chat with latest info
|
|
230
|
+
existingChat.title = story.title || existingChat.title;
|
|
231
|
+
existingChat.fileName = story.fileName || existingChat.fileName;
|
|
232
|
+
existingChat.lastUpdated = new Date(story.updatedAt || story.createdAt).getTime();
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
// Create new chat from memory story
|
|
236
|
+
const newChat = {
|
|
237
|
+
id: storyId,
|
|
238
|
+
title: story.title || story.fileName,
|
|
239
|
+
fileName: story.fileName,
|
|
240
|
+
conversation: [{
|
|
241
|
+
role: 'user',
|
|
242
|
+
content: story.prompt || `Generate ${story.title}`
|
|
243
|
+
}, {
|
|
244
|
+
role: 'ai',
|
|
245
|
+
content: `✅ Created story: "${story.title}"\n\nThis story was recovered from memory. You can continue updating it or view it in Storybook.`
|
|
246
|
+
}],
|
|
247
|
+
lastUpdated: new Date(story.updatedAt || story.createdAt).getTime()
|
|
248
|
+
};
|
|
249
|
+
chatMap.set(storyId, newChat);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
// Convert back to array and save
|
|
253
|
+
const syncedChats = Array.from(chatMap.values());
|
|
254
|
+
saveChats(syncedChats);
|
|
255
|
+
return syncedChats;
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
console.error('Error syncing with backend:', error);
|
|
259
|
+
return loadChats();
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
// Delete story and chat
|
|
263
|
+
const deleteStoryAndChat = async (chatId) => {
|
|
264
|
+
try {
|
|
265
|
+
// Remove .stories.tsx extension if present to get the actual story ID
|
|
266
|
+
const storyId = chatId.replace(/\.stories\.tsx$/, '');
|
|
267
|
+
console.log(`Attempting to delete story: chatId="${chatId}", storyId="${storyId}"`);
|
|
268
|
+
let serverDeleteSucceeded = false;
|
|
269
|
+
// First try to delete from backend
|
|
270
|
+
try {
|
|
271
|
+
const response = await fetch(`${DELETE_API_BASE}/${storyId}`, {
|
|
272
|
+
method: 'DELETE',
|
|
273
|
+
headers: { 'Content-Type': 'application/json' }
|
|
274
|
+
});
|
|
275
|
+
// 404 means story doesn't exist on server - that's OK, we can still clean up localStorage
|
|
276
|
+
if (response.ok || response.status === 404) {
|
|
277
|
+
serverDeleteSucceeded = true;
|
|
278
|
+
if (response.status === 404) {
|
|
279
|
+
console.log('Story not found on server (may have been a failed generation), cleaning up localStorage');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
console.warn(`Backend delete returned ${response.status}, trying legacy endpoint`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch (fetchError) {
|
|
287
|
+
console.warn('Backend delete request failed, trying legacy endpoint:', fetchError);
|
|
288
|
+
}
|
|
289
|
+
// Try legacy endpoint as fallback only if primary didn't succeed
|
|
290
|
+
if (!serverDeleteSucceeded) {
|
|
291
|
+
try {
|
|
292
|
+
const legacyResponse = await fetch(`${API_BASE}/story-ui/delete`, {
|
|
293
|
+
method: 'POST',
|
|
294
|
+
headers: { 'Content-Type': 'application/json' },
|
|
295
|
+
body: JSON.stringify({
|
|
296
|
+
chatId: storyId,
|
|
297
|
+
storyId: storyId
|
|
298
|
+
})
|
|
299
|
+
});
|
|
300
|
+
// 404 is also OK for legacy endpoint
|
|
301
|
+
if (legacyResponse.ok || legacyResponse.status === 404) {
|
|
302
|
+
serverDeleteSucceeded = true;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
console.warn('Legacy delete endpoint also returned non-success status');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch (legacyError) {
|
|
309
|
+
console.warn('Legacy delete request failed:', legacyError);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Always clean up localStorage - the chat/story data is primarily client-side
|
|
313
|
+
// Even if server delete failed, we should allow users to clean up their chat history
|
|
314
|
+
const chats = loadChats().filter(chat => chat.id !== chatId);
|
|
315
|
+
saveChats(chats);
|
|
316
|
+
console.log('Cleaned up localStorage chat entry');
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
console.error('Error deleting story:', error);
|
|
321
|
+
// Still try to clean up localStorage even on error
|
|
322
|
+
try {
|
|
323
|
+
const chats = loadChats().filter(chat => chat.id !== chatId);
|
|
324
|
+
saveChats(chats);
|
|
325
|
+
console.log('Cleaned up localStorage despite error');
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
catch (localError) {
|
|
329
|
+
console.error('Failed to clean up localStorage:', localError);
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
// Test connection to MCP server
|
|
335
|
+
const testMCPConnection = async () => {
|
|
336
|
+
try {
|
|
337
|
+
const response = await fetch(STORIES_API, {
|
|
338
|
+
method: 'GET',
|
|
339
|
+
headers: { 'Content-Type': 'application/json' },
|
|
340
|
+
});
|
|
341
|
+
if (!response.ok) {
|
|
342
|
+
return { connected: false, error: `HTTP ${response.status}: ${response.statusText}` };
|
|
343
|
+
}
|
|
344
|
+
const contentType = response.headers.get('content-type');
|
|
345
|
+
if (!contentType || !contentType.includes('application/json')) {
|
|
346
|
+
return { connected: false, error: 'Server returned non-JSON response (likely wrong port or server not running)' };
|
|
347
|
+
}
|
|
348
|
+
return { connected: true };
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
return { connected: false, error: error instanceof Error ? error.message : 'Unknown error' };
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
// Component styles
|
|
355
|
+
const STYLES = {
|
|
356
|
+
container: {
|
|
357
|
+
display: 'flex',
|
|
358
|
+
flexDirection: 'row',
|
|
359
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
|
360
|
+
height: '100vh',
|
|
361
|
+
overflow: 'hidden',
|
|
362
|
+
background: 'linear-gradient(135deg, #1e293b 0%, #334155 100%)',
|
|
363
|
+
color: '#e2e8f0',
|
|
364
|
+
},
|
|
365
|
+
// Sidebar
|
|
366
|
+
sidebar: {
|
|
367
|
+
width: '240px',
|
|
368
|
+
background: 'rgba(255, 255, 255, 0.03)',
|
|
369
|
+
borderRight: '1px solid rgba(255, 255, 255, 0.08)',
|
|
370
|
+
display: 'flex',
|
|
371
|
+
flexDirection: 'column',
|
|
372
|
+
backdropFilter: 'blur(10px)',
|
|
373
|
+
transition: 'width 0.3s ease',
|
|
374
|
+
position: 'relative',
|
|
375
|
+
},
|
|
376
|
+
sidebarCollapsed: {
|
|
377
|
+
width: '56px',
|
|
378
|
+
},
|
|
379
|
+
sidebarToggle: {
|
|
380
|
+
width: '100%',
|
|
381
|
+
padding: '8px 12px',
|
|
382
|
+
background: '#3b82f6',
|
|
383
|
+
color: 'white',
|
|
384
|
+
border: 'none',
|
|
385
|
+
borderRadius: '6px',
|
|
386
|
+
fontSize: '13px',
|
|
387
|
+
fontWeight: '500',
|
|
388
|
+
cursor: 'pointer',
|
|
389
|
+
marginBottom: '6px',
|
|
390
|
+
transition: 'all 0.2s ease',
|
|
391
|
+
boxShadow: 'none',
|
|
392
|
+
display: 'flex',
|
|
393
|
+
alignItems: 'center',
|
|
394
|
+
justifyContent: 'center',
|
|
395
|
+
gap: '6px',
|
|
396
|
+
lineHeight: '1',
|
|
397
|
+
},
|
|
398
|
+
newChatButton: {
|
|
399
|
+
width: '100%',
|
|
400
|
+
padding: '8px 12px',
|
|
401
|
+
background: '#3b82f6',
|
|
402
|
+
color: 'white',
|
|
403
|
+
border: 'none',
|
|
404
|
+
borderRadius: '6px',
|
|
405
|
+
fontSize: '13px',
|
|
406
|
+
fontWeight: '500',
|
|
407
|
+
cursor: 'pointer',
|
|
408
|
+
marginBottom: '12px',
|
|
409
|
+
transition: 'all 0.2s ease',
|
|
410
|
+
boxShadow: 'none',
|
|
411
|
+
display: 'flex',
|
|
412
|
+
alignItems: 'center',
|
|
413
|
+
justifyContent: 'center',
|
|
414
|
+
gap: '6px',
|
|
415
|
+
lineHeight: '1',
|
|
416
|
+
},
|
|
417
|
+
chatItem: {
|
|
418
|
+
padding: '8px 10px',
|
|
419
|
+
marginBottom: '4px',
|
|
420
|
+
background: 'rgba(255, 255, 255, 0.05)',
|
|
421
|
+
borderRadius: '6px',
|
|
422
|
+
cursor: 'pointer',
|
|
423
|
+
transition: 'all 0.15s ease',
|
|
424
|
+
position: 'relative',
|
|
425
|
+
paddingRight: '32px',
|
|
426
|
+
},
|
|
427
|
+
chatItemActive: {
|
|
428
|
+
background: 'rgba(59, 130, 246, 0.15)',
|
|
429
|
+
borderLeft: '2px solid #3b82f6',
|
|
430
|
+
},
|
|
431
|
+
chatItemTitle: {
|
|
432
|
+
fontSize: '13px',
|
|
433
|
+
fontWeight: '500',
|
|
434
|
+
marginBottom: '2px',
|
|
435
|
+
whiteSpace: 'nowrap',
|
|
436
|
+
overflow: 'hidden',
|
|
437
|
+
textOverflow: 'ellipsis',
|
|
438
|
+
},
|
|
439
|
+
chatItemTime: {
|
|
440
|
+
fontSize: '11px',
|
|
441
|
+
color: '#94a3b8',
|
|
442
|
+
},
|
|
443
|
+
deleteButton: {
|
|
444
|
+
position: 'absolute',
|
|
445
|
+
right: '8px',
|
|
446
|
+
top: '50%',
|
|
447
|
+
transform: 'translateY(-50%)',
|
|
448
|
+
background: 'rgba(239, 68, 68, 0.8)',
|
|
449
|
+
color: 'white',
|
|
450
|
+
border: 'none',
|
|
451
|
+
borderRadius: '4px',
|
|
452
|
+
padding: '4px 8px',
|
|
453
|
+
fontSize: '12px',
|
|
454
|
+
cursor: 'pointer',
|
|
455
|
+
opacity: 0,
|
|
456
|
+
transition: 'opacity 0.2s ease',
|
|
457
|
+
},
|
|
458
|
+
// Main content
|
|
459
|
+
mainContent: {
|
|
460
|
+
flex: 1,
|
|
461
|
+
display: 'flex',
|
|
462
|
+
flexDirection: 'column',
|
|
463
|
+
overflow: 'hidden',
|
|
464
|
+
},
|
|
465
|
+
chatHeader: {
|
|
466
|
+
padding: '12px 16px',
|
|
467
|
+
borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
|
|
468
|
+
background: 'rgba(255, 255, 255, 0.03)',
|
|
469
|
+
backdropFilter: 'blur(10px)',
|
|
470
|
+
},
|
|
471
|
+
chatContainer: {
|
|
472
|
+
flex: 1,
|
|
473
|
+
padding: '16px',
|
|
474
|
+
overflowY: 'auto',
|
|
475
|
+
scrollBehavior: 'smooth',
|
|
476
|
+
},
|
|
477
|
+
emptyState: {
|
|
478
|
+
color: '#94a3b8',
|
|
479
|
+
textAlign: 'center',
|
|
480
|
+
marginTop: '60px',
|
|
481
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
|
482
|
+
},
|
|
483
|
+
emptyStateTitle: {
|
|
484
|
+
fontSize: '16px',
|
|
485
|
+
fontWeight: '500',
|
|
486
|
+
marginBottom: '8px',
|
|
487
|
+
color: '#cbd5e1',
|
|
488
|
+
},
|
|
489
|
+
emptyStateSubtitle: {
|
|
490
|
+
fontSize: '13px',
|
|
491
|
+
color: '#64748b',
|
|
492
|
+
},
|
|
493
|
+
// Message bubbles
|
|
494
|
+
messageContainer: {
|
|
495
|
+
display: 'flex',
|
|
496
|
+
marginBottom: '10px',
|
|
497
|
+
},
|
|
498
|
+
userMessage: {
|
|
499
|
+
background: '#3b82f6',
|
|
500
|
+
color: '#ffffff',
|
|
501
|
+
borderRadius: '16px 16px 4px 16px',
|
|
502
|
+
padding: '10px 14px',
|
|
503
|
+
maxWidth: '85%',
|
|
504
|
+
marginLeft: 'auto',
|
|
505
|
+
fontSize: '14px',
|
|
506
|
+
lineHeight: '1.5',
|
|
507
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
|
508
|
+
boxShadow: 'none',
|
|
509
|
+
wordWrap: 'break-word',
|
|
510
|
+
},
|
|
511
|
+
aiMessage: {
|
|
512
|
+
background: 'rgba(255, 255, 255, 0.95)',
|
|
513
|
+
color: '#1f2937',
|
|
514
|
+
borderRadius: '16px 16px 16px 4px',
|
|
515
|
+
padding: '10px 14px',
|
|
516
|
+
maxWidth: '85%',
|
|
517
|
+
fontSize: '14px',
|
|
518
|
+
lineHeight: '1.5',
|
|
519
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
|
520
|
+
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.08)',
|
|
521
|
+
wordWrap: 'break-word',
|
|
522
|
+
whiteSpace: 'pre-wrap',
|
|
523
|
+
},
|
|
524
|
+
loadingMessage: {
|
|
525
|
+
background: 'rgba(255, 255, 255, 0.9)',
|
|
526
|
+
color: '#6b7280',
|
|
527
|
+
borderRadius: '16px 16px 16px 4px',
|
|
528
|
+
padding: '10px 14px',
|
|
529
|
+
fontSize: '14px',
|
|
530
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
|
531
|
+
display: 'flex',
|
|
532
|
+
alignItems: 'center',
|
|
533
|
+
gap: '6px',
|
|
534
|
+
},
|
|
535
|
+
// Input form
|
|
536
|
+
inputForm: {
|
|
537
|
+
display: 'flex',
|
|
538
|
+
alignItems: 'center',
|
|
539
|
+
gap: '10px',
|
|
540
|
+
margin: '0 16px 16px 16px',
|
|
541
|
+
padding: '10px',
|
|
542
|
+
background: 'rgba(255, 255, 255, 0.03)',
|
|
543
|
+
borderRadius: '10px',
|
|
544
|
+
border: '1px solid rgba(255, 255, 255, 0.08)',
|
|
545
|
+
backdropFilter: 'blur(10px)',
|
|
546
|
+
},
|
|
547
|
+
textInput: {
|
|
548
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
|
549
|
+
flex: 1,
|
|
550
|
+
padding: '10px 14px',
|
|
551
|
+
borderRadius: '6px',
|
|
552
|
+
border: '1px solid rgba(255, 255, 255, 0.15)',
|
|
553
|
+
fontSize: '13px',
|
|
554
|
+
color: '#1f2937',
|
|
555
|
+
background: '#ffffff',
|
|
556
|
+
outline: 'none',
|
|
557
|
+
transition: 'all 0.15s ease',
|
|
558
|
+
boxSizing: 'border-box',
|
|
559
|
+
},
|
|
560
|
+
sendButton: {
|
|
561
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
|
562
|
+
padding: '10px 16px',
|
|
563
|
+
borderRadius: '6px',
|
|
564
|
+
border: 'none',
|
|
565
|
+
background: '#10b981',
|
|
566
|
+
color: '#ffffff',
|
|
567
|
+
fontSize: '13px',
|
|
568
|
+
fontWeight: '500',
|
|
569
|
+
cursor: 'pointer',
|
|
570
|
+
display: 'flex',
|
|
571
|
+
alignItems: 'center',
|
|
572
|
+
gap: '5px',
|
|
573
|
+
transition: 'all 0.15s ease',
|
|
574
|
+
boxShadow: 'none',
|
|
575
|
+
},
|
|
576
|
+
errorMessage: {
|
|
577
|
+
background: 'rgba(248, 113, 113, 0.1)',
|
|
578
|
+
color: '#f87171',
|
|
579
|
+
padding: '8px 12px',
|
|
580
|
+
borderRadius: '6px',
|
|
581
|
+
fontSize: '13px',
|
|
582
|
+
marginBottom: '10px',
|
|
583
|
+
border: '1px solid rgba(248, 113, 113, 0.2)',
|
|
584
|
+
},
|
|
585
|
+
loadingDots: {
|
|
586
|
+
display: 'inline-block',
|
|
587
|
+
animation: 'loadingDots 1.4s infinite',
|
|
588
|
+
},
|
|
589
|
+
'@keyframes loadingDots': {
|
|
590
|
+
'0%': { content: '""' },
|
|
591
|
+
'25%': { content: '"."' },
|
|
592
|
+
'50%': { content: '".."' },
|
|
593
|
+
'75%': { content: '"..."' },
|
|
594
|
+
},
|
|
595
|
+
codeBlock: {
|
|
596
|
+
background: '#1e293b',
|
|
597
|
+
padding: '10px 12px',
|
|
598
|
+
borderRadius: '6px',
|
|
599
|
+
fontFamily: 'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace',
|
|
600
|
+
fontSize: '12px',
|
|
601
|
+
lineHeight: '1.5',
|
|
602
|
+
overflowX: 'auto',
|
|
603
|
+
marginTop: '6px',
|
|
604
|
+
border: '1px solid rgba(255, 255, 255, 0.08)',
|
|
605
|
+
},
|
|
606
|
+
// Streaming progress styles
|
|
607
|
+
streamingContainer: {
|
|
608
|
+
background: 'rgba(255, 255, 255, 0.95)',
|
|
609
|
+
borderRadius: '16px 16px 16px 4px',
|
|
610
|
+
padding: '12px',
|
|
611
|
+
maxWidth: '85%',
|
|
612
|
+
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.08)',
|
|
613
|
+
},
|
|
614
|
+
intentPreview: {
|
|
615
|
+
background: 'rgba(59, 130, 246, 0.08)',
|
|
616
|
+
borderRadius: '8px',
|
|
617
|
+
padding: '10px',
|
|
618
|
+
marginBottom: '10px',
|
|
619
|
+
border: '1px solid rgba(59, 130, 246, 0.15)',
|
|
620
|
+
},
|
|
621
|
+
intentTitle: {
|
|
622
|
+
fontSize: '13px',
|
|
623
|
+
fontWeight: '600',
|
|
624
|
+
color: '#1e40af',
|
|
625
|
+
marginBottom: '6px',
|
|
626
|
+
display: 'flex',
|
|
627
|
+
alignItems: 'center',
|
|
628
|
+
gap: '5px',
|
|
629
|
+
},
|
|
630
|
+
intentStrategy: {
|
|
631
|
+
fontSize: '12px',
|
|
632
|
+
color: '#4b5563',
|
|
633
|
+
marginBottom: '4px',
|
|
634
|
+
},
|
|
635
|
+
intentComponents: {
|
|
636
|
+
display: 'flex',
|
|
637
|
+
flexWrap: 'wrap',
|
|
638
|
+
gap: '4px',
|
|
639
|
+
marginTop: '6px',
|
|
640
|
+
},
|
|
641
|
+
componentTag: {
|
|
642
|
+
background: 'rgba(59, 130, 246, 0.12)',
|
|
643
|
+
color: '#1d4ed8',
|
|
644
|
+
fontSize: '10px',
|
|
645
|
+
padding: '2px 6px',
|
|
646
|
+
borderRadius: '10px',
|
|
647
|
+
fontWeight: '500',
|
|
648
|
+
},
|
|
649
|
+
progressBar: {
|
|
650
|
+
background: 'rgba(0, 0, 0, 0.08)',
|
|
651
|
+
borderRadius: '3px',
|
|
652
|
+
height: '4px',
|
|
653
|
+
marginTop: '10px',
|
|
654
|
+
marginBottom: '6px',
|
|
655
|
+
overflow: 'hidden',
|
|
656
|
+
},
|
|
657
|
+
progressFill: {
|
|
658
|
+
background: '#3b82f6',
|
|
659
|
+
height: '100%',
|
|
660
|
+
borderRadius: '3px',
|
|
661
|
+
transition: 'width 0.3s ease',
|
|
662
|
+
},
|
|
663
|
+
progressPhase: {
|
|
664
|
+
fontSize: '11px',
|
|
665
|
+
color: '#6b7280',
|
|
666
|
+
display: 'flex',
|
|
667
|
+
alignItems: 'center',
|
|
668
|
+
gap: '5px',
|
|
669
|
+
},
|
|
670
|
+
phaseIcon: {
|
|
671
|
+
fontSize: '12px',
|
|
672
|
+
},
|
|
673
|
+
validationBox: {
|
|
674
|
+
marginTop: '8px',
|
|
675
|
+
padding: '8px',
|
|
676
|
+
borderRadius: '6px',
|
|
677
|
+
fontSize: '11px',
|
|
678
|
+
},
|
|
679
|
+
validationSuccess: {
|
|
680
|
+
background: 'rgba(16, 185, 129, 0.08)',
|
|
681
|
+
border: '1px solid rgba(16, 185, 129, 0.15)',
|
|
682
|
+
color: '#047857',
|
|
683
|
+
},
|
|
684
|
+
validationWarning: {
|
|
685
|
+
background: 'rgba(245, 158, 11, 0.08)',
|
|
686
|
+
border: '1px solid rgba(245, 158, 11, 0.15)',
|
|
687
|
+
color: '#b45309',
|
|
688
|
+
},
|
|
689
|
+
validationError: {
|
|
690
|
+
background: 'rgba(239, 68, 68, 0.08)',
|
|
691
|
+
border: '1px solid rgba(239, 68, 68, 0.15)',
|
|
692
|
+
color: '#dc2626',
|
|
693
|
+
},
|
|
694
|
+
retryBadge: {
|
|
695
|
+
background: 'rgba(245, 158, 11, 0.12)',
|
|
696
|
+
color: '#b45309',
|
|
697
|
+
fontSize: '10px',
|
|
698
|
+
padding: '2px 8px',
|
|
699
|
+
borderRadius: '10px',
|
|
700
|
+
display: 'inline-flex',
|
|
701
|
+
alignItems: 'center',
|
|
702
|
+
gap: '3px',
|
|
703
|
+
marginTop: '6px',
|
|
704
|
+
},
|
|
705
|
+
completionSummary: {
|
|
706
|
+
marginTop: '10px',
|
|
707
|
+
paddingTop: '10px',
|
|
708
|
+
borderTop: '1px solid rgba(0, 0, 0, 0.08)',
|
|
709
|
+
},
|
|
710
|
+
summaryTitle: {
|
|
711
|
+
fontSize: '14px',
|
|
712
|
+
fontWeight: '600',
|
|
713
|
+
color: '#111827',
|
|
714
|
+
marginBottom: '6px',
|
|
715
|
+
display: 'flex',
|
|
716
|
+
alignItems: 'center',
|
|
717
|
+
gap: '6px',
|
|
718
|
+
},
|
|
719
|
+
summaryDescription: {
|
|
720
|
+
fontSize: '12px',
|
|
721
|
+
color: '#4b5563',
|
|
722
|
+
lineHeight: '1.5',
|
|
723
|
+
},
|
|
724
|
+
metricsRow: {
|
|
725
|
+
display: 'flex',
|
|
726
|
+
gap: '12px',
|
|
727
|
+
marginTop: '8px',
|
|
728
|
+
fontSize: '10px',
|
|
729
|
+
color: '#6b7280',
|
|
730
|
+
},
|
|
731
|
+
metric: {
|
|
732
|
+
display: 'flex',
|
|
733
|
+
alignItems: 'center',
|
|
734
|
+
gap: '3px',
|
|
735
|
+
},
|
|
736
|
+
// Code viewer styles for generated stories
|
|
737
|
+
codeViewerContainer: {
|
|
738
|
+
marginTop: '12px',
|
|
739
|
+
borderTop: '1px solid rgba(0, 0, 0, 0.08)',
|
|
740
|
+
paddingTop: '12px',
|
|
741
|
+
},
|
|
742
|
+
codeViewerToggle: {
|
|
743
|
+
display: 'flex',
|
|
744
|
+
alignItems: 'center',
|
|
745
|
+
justifyContent: 'space-between',
|
|
746
|
+
padding: '8px 12px',
|
|
747
|
+
background: 'rgba(59, 130, 246, 0.08)',
|
|
748
|
+
borderRadius: '6px',
|
|
749
|
+
cursor: 'pointer',
|
|
750
|
+
border: '1px solid rgba(59, 130, 246, 0.15)',
|
|
751
|
+
transition: 'all 0.2s ease',
|
|
752
|
+
fontSize: '13px',
|
|
753
|
+
fontWeight: '500',
|
|
754
|
+
color: '#1e40af',
|
|
755
|
+
},
|
|
756
|
+
codeViewerToggleHover: {
|
|
757
|
+
background: 'rgba(59, 130, 246, 0.15)',
|
|
758
|
+
},
|
|
759
|
+
codeViewerContent: {
|
|
760
|
+
marginTop: '10px',
|
|
761
|
+
background: '#1e293b',
|
|
762
|
+
borderRadius: '8px',
|
|
763
|
+
overflow: 'hidden',
|
|
764
|
+
border: '1px solid rgba(255, 255, 255, 0.08)',
|
|
765
|
+
},
|
|
766
|
+
codeViewerHeader: {
|
|
767
|
+
display: 'flex',
|
|
768
|
+
alignItems: 'center',
|
|
769
|
+
justifyContent: 'space-between',
|
|
770
|
+
padding: '8px 12px',
|
|
771
|
+
background: 'rgba(0, 0, 0, 0.2)',
|
|
772
|
+
borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
|
|
773
|
+
},
|
|
774
|
+
codeViewerFileName: {
|
|
775
|
+
fontSize: '12px',
|
|
776
|
+
color: '#94a3b8',
|
|
777
|
+
fontFamily: 'ui-monospace, monospace',
|
|
778
|
+
},
|
|
779
|
+
copyButton: {
|
|
780
|
+
padding: '4px 10px',
|
|
781
|
+
fontSize: '11px',
|
|
782
|
+
fontWeight: '500',
|
|
783
|
+
color: '#e2e8f0',
|
|
784
|
+
background: 'rgba(59, 130, 246, 0.3)',
|
|
785
|
+
border: '1px solid rgba(59, 130, 246, 0.5)',
|
|
786
|
+
borderRadius: '4px',
|
|
787
|
+
cursor: 'pointer',
|
|
788
|
+
transition: 'all 0.2s ease',
|
|
789
|
+
},
|
|
790
|
+
copyButtonSuccess: {
|
|
791
|
+
background: 'rgba(34, 197, 94, 0.3)',
|
|
792
|
+
borderColor: 'rgba(34, 197, 94, 0.5)',
|
|
793
|
+
color: '#86efac',
|
|
794
|
+
},
|
|
795
|
+
codeViewerPre: {
|
|
796
|
+
margin: 0,
|
|
797
|
+
padding: '12px',
|
|
798
|
+
fontSize: '11px',
|
|
799
|
+
lineHeight: '1.5',
|
|
800
|
+
fontFamily: 'ui-monospace, Consolas, Monaco, monospace',
|
|
801
|
+
color: '#e2e8f0',
|
|
802
|
+
overflowX: 'auto',
|
|
803
|
+
maxHeight: '400px',
|
|
804
|
+
overflowY: 'auto',
|
|
805
|
+
},
|
|
806
|
+
// Image upload styles
|
|
807
|
+
uploadButton: {
|
|
808
|
+
display: 'flex',
|
|
809
|
+
alignItems: 'center',
|
|
810
|
+
justifyContent: 'center',
|
|
811
|
+
width: '36px',
|
|
812
|
+
height: '36px',
|
|
813
|
+
borderRadius: '6px',
|
|
814
|
+
border: '1px solid rgba(255, 255, 255, 0.15)',
|
|
815
|
+
background: 'rgba(255, 255, 255, 0.08)',
|
|
816
|
+
color: '#e2e8f0',
|
|
817
|
+
cursor: 'pointer',
|
|
818
|
+
transition: 'all 0.2s ease',
|
|
819
|
+
flexShrink: 0,
|
|
820
|
+
},
|
|
821
|
+
uploadButtonHover: {
|
|
822
|
+
background: 'rgba(59, 130, 246, 0.2)',
|
|
823
|
+
borderColor: 'rgba(59, 130, 246, 0.5)',
|
|
824
|
+
},
|
|
825
|
+
imagePreviewContainer: {
|
|
826
|
+
display: 'flex',
|
|
827
|
+
flexWrap: 'wrap',
|
|
828
|
+
gap: '6px',
|
|
829
|
+
padding: '8px 12px',
|
|
830
|
+
background: 'rgba(255, 255, 255, 0.03)',
|
|
831
|
+
borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
|
|
832
|
+
margin: '0 16px',
|
|
833
|
+
borderRadius: '6px 6px 0 0',
|
|
834
|
+
},
|
|
835
|
+
imagePreviewItem: {
|
|
836
|
+
position: 'relative',
|
|
837
|
+
width: '56px',
|
|
838
|
+
height: '56px',
|
|
839
|
+
borderRadius: '6px',
|
|
840
|
+
overflow: 'hidden',
|
|
841
|
+
border: '1px solid rgba(255, 255, 255, 0.15)',
|
|
842
|
+
background: '#1e293b',
|
|
843
|
+
},
|
|
844
|
+
imagePreviewImg: {
|
|
845
|
+
width: '100%',
|
|
846
|
+
height: '100%',
|
|
847
|
+
objectFit: 'cover',
|
|
848
|
+
},
|
|
849
|
+
imageRemoveButton: {
|
|
850
|
+
position: 'absolute',
|
|
851
|
+
top: '2px',
|
|
852
|
+
right: '2px',
|
|
853
|
+
width: '18px',
|
|
854
|
+
height: '18px',
|
|
855
|
+
borderRadius: '50%',
|
|
856
|
+
background: 'rgba(239, 68, 68, 0.9)',
|
|
857
|
+
color: 'white',
|
|
858
|
+
border: 'none',
|
|
859
|
+
fontSize: '12px',
|
|
860
|
+
cursor: 'pointer',
|
|
861
|
+
display: 'flex',
|
|
862
|
+
alignItems: 'center',
|
|
863
|
+
justifyContent: 'center',
|
|
864
|
+
lineHeight: 1,
|
|
865
|
+
},
|
|
866
|
+
imagePreviewLabel: {
|
|
867
|
+
display: 'flex',
|
|
868
|
+
alignItems: 'center',
|
|
869
|
+
gap: '6px',
|
|
870
|
+
fontSize: '12px',
|
|
871
|
+
color: '#94a3b8',
|
|
872
|
+
marginRight: 'auto',
|
|
873
|
+
},
|
|
874
|
+
userMessageImages: {
|
|
875
|
+
display: 'flex',
|
|
876
|
+
gap: '6px',
|
|
877
|
+
marginTop: '6px',
|
|
878
|
+
flexWrap: 'wrap',
|
|
879
|
+
},
|
|
880
|
+
userMessageImage: {
|
|
881
|
+
width: '40px',
|
|
882
|
+
height: '40px',
|
|
883
|
+
borderRadius: '6px',
|
|
884
|
+
objectFit: 'cover',
|
|
885
|
+
border: '1px solid rgba(255, 255, 255, 0.25)',
|
|
886
|
+
},
|
|
887
|
+
// Drag and drop overlay
|
|
888
|
+
dropOverlay: {
|
|
889
|
+
position: 'absolute',
|
|
890
|
+
top: 0,
|
|
891
|
+
left: 0,
|
|
892
|
+
right: 0,
|
|
893
|
+
bottom: 0,
|
|
894
|
+
background: 'rgba(59, 130, 246, 0.12)',
|
|
895
|
+
border: '2px dashed rgba(59, 130, 246, 0.4)',
|
|
896
|
+
borderRadius: '10px',
|
|
897
|
+
display: 'flex',
|
|
898
|
+
alignItems: 'center',
|
|
899
|
+
justifyContent: 'center',
|
|
900
|
+
zIndex: 100,
|
|
901
|
+
backdropFilter: 'blur(3px)',
|
|
902
|
+
},
|
|
903
|
+
dropOverlayText: {
|
|
904
|
+
background: 'rgba(59, 130, 246, 0.85)',
|
|
905
|
+
color: 'white',
|
|
906
|
+
padding: '12px 24px',
|
|
907
|
+
borderRadius: '8px',
|
|
908
|
+
fontSize: '14px',
|
|
909
|
+
fontWeight: '500',
|
|
910
|
+
display: 'flex',
|
|
911
|
+
alignItems: 'center',
|
|
912
|
+
gap: '8px',
|
|
913
|
+
boxShadow: '0 2px 8px rgba(59, 130, 246, 0.3)',
|
|
914
|
+
},
|
|
915
|
+
};
|
|
916
|
+
// Add custom style for loading animation
|
|
917
|
+
const styleSheet = document.createElement('style');
|
|
918
|
+
styleSheet.textContent = `
|
|
919
|
+
@keyframes loadingDots {
|
|
920
|
+
0%, 20% { content: "."; }
|
|
921
|
+
40% { content: ".."; }
|
|
922
|
+
60%, 100% { content: "..."; }
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
.loading-dots::after {
|
|
926
|
+
content: ".";
|
|
927
|
+
animation: loadingDots 1.4s infinite;
|
|
928
|
+
}
|
|
929
|
+
`;
|
|
930
|
+
document.head.appendChild(styleSheet);
|
|
931
|
+
// Helper function to format timestamp
|
|
932
|
+
const formatTime = (timestamp) => {
|
|
933
|
+
const date = new Date(timestamp);
|
|
934
|
+
const now = new Date();
|
|
935
|
+
const diffMs = now.getTime() - date.getTime();
|
|
936
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
937
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
938
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
939
|
+
if (diffMins < 1)
|
|
940
|
+
return 'just now';
|
|
941
|
+
if (diffMins < 60)
|
|
942
|
+
return `${diffMins}m ago`;
|
|
943
|
+
if (diffHours < 24)
|
|
944
|
+
return `${diffHours}h ago`;
|
|
945
|
+
if (diffDays < 7)
|
|
946
|
+
return `${diffDays}d ago`;
|
|
947
|
+
return date.toLocaleDateString();
|
|
948
|
+
};
|
|
949
|
+
// Helper to get phase icon and text
|
|
950
|
+
const getPhaseInfo = (phase) => {
|
|
951
|
+
const phases = {
|
|
952
|
+
config_loaded: { icon: '⚙️', text: 'Loading configuration' },
|
|
953
|
+
components_discovered: { icon: '🔍', text: 'Discovering components' },
|
|
954
|
+
prompt_built: { icon: '📝', text: 'Building prompt' },
|
|
955
|
+
llm_thinking: { icon: '🤔', text: 'AI is thinking' },
|
|
956
|
+
code_extracted: { icon: '📦', text: 'Extracting code' },
|
|
957
|
+
validating: { icon: '✅', text: 'Validating output' },
|
|
958
|
+
post_processing: { icon: '🔧', text: 'Processing' },
|
|
959
|
+
saving: { icon: '💾', text: 'Saving story' },
|
|
960
|
+
};
|
|
961
|
+
return phases[phase] || { icon: '⏳', text: 'Working' };
|
|
962
|
+
};
|
|
963
|
+
// Streaming Progress Message Component
|
|
964
|
+
const StreamingProgressMessage = ({ streamingData }) => {
|
|
965
|
+
const { intent, progress, validation, retry, completion, error } = streamingData;
|
|
966
|
+
const [showCode, setShowCode] = useState(true); // Show code by default for better UX
|
|
967
|
+
const [copyStatus, setCopyStatus] = useState('idle');
|
|
968
|
+
// Handle copy to clipboard
|
|
969
|
+
const handleCopyCode = async (code) => {
|
|
970
|
+
try {
|
|
971
|
+
await navigator.clipboard.writeText(code);
|
|
972
|
+
setCopyStatus('copied');
|
|
973
|
+
setTimeout(() => setCopyStatus('idle'), 2000);
|
|
974
|
+
}
|
|
975
|
+
catch (err) {
|
|
976
|
+
console.error('Failed to copy:', err);
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
// If completed, show completion summary
|
|
980
|
+
if (completion) {
|
|
981
|
+
return (_jsx("div", { style: STYLES.streamingContainer, children: _jsxs("div", { style: STYLES.completionSummary, children: [_jsxs("div", { style: STYLES.summaryTitle, children: [completion.success ? '✅' : '❌', " ", completion.title] }), _jsx("div", { style: STYLES.summaryDescription, children: completion.summary.description }), completion.componentsUsed.length > 0 && (_jsxs("div", { style: { marginTop: '10px' }, children: [_jsx("div", { style: { fontSize: '12px', color: '#6b7280', marginBottom: '6px' }, children: "Components used:" }), _jsx("div", { style: STYLES.intentComponents, children: completion.componentsUsed.map((comp, i) => (_jsx("span", { style: STYLES.componentTag, children: comp.name }, i))) })] })), completion.layoutChoices.length > 0 && (_jsxs("div", { style: { marginTop: '10px' }, children: [_jsx("div", { style: { fontSize: '12px', color: '#6b7280', marginBottom: '6px' }, children: "Layout:" }), _jsx("div", { style: { fontSize: '12px', color: '#4b5563' }, children: completion.layoutChoices.map(l => l.pattern).join(', ') })] })), completion.validation && !completion.validation.isValid && (_jsxs("div", { style: { ...STYLES.validationBox, ...STYLES.validationWarning }, children: ["\u26A0\uFE0F ", completion.validation.autoFixApplied ? 'Auto-fixed issues' : 'Minor issues detected'] })), completion.suggestions && completion.suggestions.length > 0 && (_jsxs("div", { style: { marginTop: '10px', fontSize: '12px', color: '#6b7280' }, children: ["\uD83D\uDCA1 ", completion.suggestions[0]] })), completion.metrics && (_jsxs("div", { style: STYLES.metricsRow, children: [_jsxs("span", { style: STYLES.metric, children: ["\u23F1\uFE0F ", (completion.metrics.totalTimeMs / 1000).toFixed(1), "s"] }), _jsxs("span", { style: STYLES.metric, children: ["\uD83D\uDD04 ", completion.metrics.llmCallsCount, " LLM calls"] })] })), completion.code && (_jsxs("div", { style: STYLES.codeViewerContainer, children: [_jsxs("div", { style: STYLES.codeViewerToggle, onClick: () => setShowCode(!showCode), role: "button", tabIndex: 0, onKeyDown: (e) => e.key === 'Enter' && setShowCode(!showCode), children: [_jsxs("span", { children: [showCode ? '▼' : '▶', " View Generated Code"] }), _jsx("span", { style: { fontSize: '11px', color: '#6366f1' }, children: completion.fileName })] }), showCode && (_jsxs("div", { style: STYLES.codeViewerContent, children: [_jsxs("div", { style: STYLES.codeViewerHeader, children: [_jsx("span", { style: STYLES.codeViewerFileName, children: completion.fileName }), _jsx("button", { style: {
|
|
982
|
+
...STYLES.copyButton,
|
|
983
|
+
...(copyStatus === 'copied' ? STYLES.copyButtonSuccess : {})
|
|
984
|
+
}, onClick: () => handleCopyCode(completion.code), children: copyStatus === 'copied' ? '✓ Copied!' : 'Copy Code' })] }), _jsx("pre", { style: STYLES.codeViewerPre, children: _jsx("code", { children: completion.code }) })] }))] }))] }) }));
|
|
985
|
+
}
|
|
986
|
+
// If error, show error
|
|
987
|
+
if (error) {
|
|
988
|
+
return (_jsx("div", { style: STYLES.streamingContainer, children: _jsxs("div", { style: { ...STYLES.validationBox, ...STYLES.validationError }, children: [_jsxs("strong", { children: ["\u274C ", error.message] }), error.details && _jsx("div", { style: { marginTop: '4px' }, children: error.details }), error.suggestion && _jsxs("div", { style: { marginTop: '8px' }, children: ["\uD83D\uDCA1 ", error.suggestion] })] }) }));
|
|
989
|
+
}
|
|
990
|
+
// Show progress
|
|
991
|
+
return (_jsxs("div", { style: STYLES.streamingContainer, children: [intent && (_jsxs("div", { style: STYLES.intentPreview, children: [_jsxs("div", { style: STYLES.intentTitle, children: [intent.requestType === 'modification' ? '✏️' : '✨', intent.requestType === 'modification' ? ' Modifying Story' : ' Creating New Story'] }), _jsx("div", { style: STYLES.intentStrategy, children: intent.strategy }), intent.detectedDesignSystem && (_jsxs("div", { style: { fontSize: '12px', color: '#6b7280' }, children: ["Design system: ", _jsx("strong", { children: intent.detectedDesignSystem })] })), intent.estimatedComponents.length > 0 && (_jsx("div", { style: STYLES.intentComponents, children: intent.estimatedComponents.map((comp, i) => (_jsx("span", { style: STYLES.componentTag, children: comp }, i))) }))] })), progress && (_jsxs(_Fragment, { children: [_jsx("div", { style: STYLES.progressBar, children: _jsx("div", { style: {
|
|
992
|
+
...STYLES.progressFill,
|
|
993
|
+
width: `${(progress.step / progress.totalSteps) * 100}%`
|
|
994
|
+
} }) }), _jsxs("div", { style: STYLES.progressPhase, children: [_jsx("span", { style: STYLES.phaseIcon, children: getPhaseInfo(progress.phase).icon }), _jsx("span", { children: progress.message || getPhaseInfo(progress.phase).text }), _jsxs("span", { style: { marginLeft: 'auto', color: '#9ca3af' }, children: [progress.step, "/", progress.totalSteps] })] })] })), retry && (_jsxs("div", { style: STYLES.retryBadge, children: ["\uD83D\uDD04 Retry ", retry.attempt, "/", retry.maxAttempts, ": ", retry.reason] })), validation && !validation.isValid && (_jsxs("div", { style: { ...STYLES.validationBox, ...STYLES.validationWarning }, children: [validation.autoFixApplied ? '🔧 Auto-fixing issues...' : '⚠️ Validation issues found', validation.errors.slice(0, 2).map((err, i) => (_jsxs("div", { style: { marginTop: '4px', fontSize: '11px' }, children: ["\u2022 ", err] }, i)))] })), !progress && !intent && (_jsx("div", { style: STYLES.progressPhase, children: _jsx("span", { className: "loading-dots", children: "Connecting" }) }))] }));
|
|
995
|
+
};
|
|
996
|
+
// Main component
|
|
997
|
+
function StoryUIPanel() {
|
|
998
|
+
const [input, setInput] = useState('');
|
|
999
|
+
const [conversation, setConversation] = useState([]);
|
|
1000
|
+
const [loading, setLoading] = useState(false);
|
|
1001
|
+
const [error, setError] = useState(null);
|
|
1002
|
+
const [recentChats, setRecentChats] = useState([]);
|
|
1003
|
+
const [activeChatId, setActiveChatId] = useState(null);
|
|
1004
|
+
const [activeTitle, setActiveTitle] = useState('');
|
|
1005
|
+
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
1006
|
+
const [connectionStatus, setConnectionStatus] = useState({ connected: false });
|
|
1007
|
+
const [availableProviders, setAvailableProviders] = useState([]);
|
|
1008
|
+
const [selectedProvider, setSelectedProvider] = useState('');
|
|
1009
|
+
const [selectedModel, setSelectedModel] = useState('');
|
|
1010
|
+
const [streamingState, setStreamingState] = useState(null);
|
|
1011
|
+
const [attachedImages, setAttachedImages] = useState([]);
|
|
1012
|
+
const [considerations, setConsiderations] = useState('');
|
|
1013
|
+
const chatEndRef = useRef(null);
|
|
1014
|
+
const inputRef = useRef(null);
|
|
1015
|
+
const fileInputRef = useRef(null);
|
|
1016
|
+
const abortControllerRef = useRef(null);
|
|
1017
|
+
// Maximum images allowed
|
|
1018
|
+
const MAX_IMAGES = 4;
|
|
1019
|
+
const MAX_IMAGE_SIZE_MB = 20;
|
|
1020
|
+
// Helper to convert file to base64
|
|
1021
|
+
const fileToBase64 = (file) => {
|
|
1022
|
+
return new Promise((resolve, reject) => {
|
|
1023
|
+
const reader = new FileReader();
|
|
1024
|
+
reader.readAsDataURL(file);
|
|
1025
|
+
reader.onload = () => {
|
|
1026
|
+
const result = reader.result;
|
|
1027
|
+
// Extract base64 data (remove data:image/...;base64, prefix)
|
|
1028
|
+
const base64 = result.split(',')[1];
|
|
1029
|
+
resolve(base64);
|
|
1030
|
+
};
|
|
1031
|
+
reader.onerror = error => reject(error);
|
|
1032
|
+
});
|
|
1033
|
+
};
|
|
1034
|
+
// Handle file selection
|
|
1035
|
+
const handleFileSelect = async (e) => {
|
|
1036
|
+
const files = e.target.files;
|
|
1037
|
+
if (!files)
|
|
1038
|
+
return;
|
|
1039
|
+
const newImages = [];
|
|
1040
|
+
const errors = [];
|
|
1041
|
+
for (let i = 0; i < files.length && (attachedImages.length + newImages.length) < MAX_IMAGES; i++) {
|
|
1042
|
+
const file = files[i];
|
|
1043
|
+
// Validate file type
|
|
1044
|
+
if (!file.type.startsWith('image/')) {
|
|
1045
|
+
errors.push(`${file.name}: Not an image file`);
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
// Validate file size
|
|
1049
|
+
if (file.size > MAX_IMAGE_SIZE_MB * 1024 * 1024) {
|
|
1050
|
+
errors.push(`${file.name}: File too large (max ${MAX_IMAGE_SIZE_MB}MB)`);
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
try {
|
|
1054
|
+
const base64 = await fileToBase64(file);
|
|
1055
|
+
const preview = URL.createObjectURL(file);
|
|
1056
|
+
newImages.push({
|
|
1057
|
+
id: `${Date.now()}-${i}`,
|
|
1058
|
+
file,
|
|
1059
|
+
preview,
|
|
1060
|
+
base64,
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
catch (err) {
|
|
1064
|
+
errors.push(`${file.name}: Failed to process`);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (errors.length > 0) {
|
|
1068
|
+
setError(errors.join('\n'));
|
|
1069
|
+
}
|
|
1070
|
+
setAttachedImages(prev => [...prev, ...newImages]);
|
|
1071
|
+
// Reset file input
|
|
1072
|
+
if (fileInputRef.current) {
|
|
1073
|
+
fileInputRef.current.value = '';
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
// Remove attached image
|
|
1077
|
+
const removeAttachedImage = (id) => {
|
|
1078
|
+
setAttachedImages(prev => {
|
|
1079
|
+
const removed = prev.find(img => img.id === id);
|
|
1080
|
+
if (removed) {
|
|
1081
|
+
URL.revokeObjectURL(removed.preview);
|
|
1082
|
+
}
|
|
1083
|
+
return prev.filter(img => img.id !== id);
|
|
1084
|
+
});
|
|
1085
|
+
};
|
|
1086
|
+
// Clear all attached images
|
|
1087
|
+
const clearAttachedImages = () => {
|
|
1088
|
+
attachedImages.forEach(img => URL.revokeObjectURL(img.preview));
|
|
1089
|
+
setAttachedImages([]);
|
|
1090
|
+
};
|
|
1091
|
+
// Drag and drop state
|
|
1092
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
1093
|
+
// Handle drag events
|
|
1094
|
+
const handleDragEnter = useCallback((e) => {
|
|
1095
|
+
e.preventDefault();
|
|
1096
|
+
e.stopPropagation();
|
|
1097
|
+
if (e.dataTransfer.types.includes('Files')) {
|
|
1098
|
+
setIsDragging(true);
|
|
1099
|
+
}
|
|
1100
|
+
}, []);
|
|
1101
|
+
const handleDragLeave = useCallback((e) => {
|
|
1102
|
+
e.preventDefault();
|
|
1103
|
+
e.stopPropagation();
|
|
1104
|
+
// Only set to false if we're leaving the main container
|
|
1105
|
+
if (!e.currentTarget.contains(e.relatedTarget)) {
|
|
1106
|
+
setIsDragging(false);
|
|
1107
|
+
}
|
|
1108
|
+
}, []);
|
|
1109
|
+
const handleDragOver = useCallback((e) => {
|
|
1110
|
+
e.preventDefault();
|
|
1111
|
+
e.stopPropagation();
|
|
1112
|
+
}, []);
|
|
1113
|
+
const handleDrop = useCallback(async (e) => {
|
|
1114
|
+
e.preventDefault();
|
|
1115
|
+
e.stopPropagation();
|
|
1116
|
+
setIsDragging(false);
|
|
1117
|
+
const files = Array.from(e.dataTransfer.files);
|
|
1118
|
+
const imageFiles = files.filter(f => f.type.startsWith('image/'));
|
|
1119
|
+
if (imageFiles.length === 0) {
|
|
1120
|
+
setError('Please drop image files only');
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
const newImages = [];
|
|
1124
|
+
const errors = [];
|
|
1125
|
+
for (let i = 0; i < imageFiles.length && (attachedImages.length + newImages.length) < MAX_IMAGES; i++) {
|
|
1126
|
+
const file = imageFiles[i];
|
|
1127
|
+
if (file.size > MAX_IMAGE_SIZE_MB * 1024 * 1024) {
|
|
1128
|
+
errors.push(`${file.name}: File too large (max ${MAX_IMAGE_SIZE_MB}MB)`);
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
try {
|
|
1132
|
+
const base64 = await fileToBase64(file);
|
|
1133
|
+
const preview = URL.createObjectURL(file);
|
|
1134
|
+
newImages.push({
|
|
1135
|
+
id: `${Date.now()}-${i}`,
|
|
1136
|
+
file,
|
|
1137
|
+
preview,
|
|
1138
|
+
base64,
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
catch (err) {
|
|
1142
|
+
errors.push(`${file.name}: Failed to process`);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
if (errors.length > 0) {
|
|
1146
|
+
setError(errors.join('\n'));
|
|
1147
|
+
}
|
|
1148
|
+
setAttachedImages(prev => [...prev, ...newImages]);
|
|
1149
|
+
}, [attachedImages.length, fileToBase64]);
|
|
1150
|
+
// Handle clipboard paste for images
|
|
1151
|
+
const handlePaste = useCallback(async (e) => {
|
|
1152
|
+
const items = e.clipboardData?.items;
|
|
1153
|
+
if (!items)
|
|
1154
|
+
return;
|
|
1155
|
+
const imageItems = [];
|
|
1156
|
+
for (let i = 0; i < items.length; i++) {
|
|
1157
|
+
if (items[i].type.startsWith('image/')) {
|
|
1158
|
+
imageItems.push(items[i]);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (imageItems.length === 0)
|
|
1162
|
+
return;
|
|
1163
|
+
// Prevent default text paste behavior when pasting images
|
|
1164
|
+
e.preventDefault();
|
|
1165
|
+
if (attachedImages.length >= MAX_IMAGES) {
|
|
1166
|
+
setError(`Maximum ${MAX_IMAGES} images allowed`);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
const newImages = [];
|
|
1170
|
+
const errors = [];
|
|
1171
|
+
for (let i = 0; i < imageItems.length && (attachedImages.length + newImages.length) < MAX_IMAGES; i++) {
|
|
1172
|
+
const item = imageItems[i];
|
|
1173
|
+
const file = item.getAsFile();
|
|
1174
|
+
if (!file) {
|
|
1175
|
+
errors.push('Failed to get image from clipboard');
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
if (file.size > MAX_IMAGE_SIZE_MB * 1024 * 1024) {
|
|
1179
|
+
errors.push(`Pasted image too large (max ${MAX_IMAGE_SIZE_MB}MB)`);
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
try {
|
|
1183
|
+
const base64 = await fileToBase64(file);
|
|
1184
|
+
const preview = URL.createObjectURL(file);
|
|
1185
|
+
// Create a meaningful name for pasted images
|
|
1186
|
+
const timestamp = new Date().toISOString().slice(11, 19).replace(/:/g, '-');
|
|
1187
|
+
const renamedFile = new File([file], `pasted-image-${timestamp}.${file.type.split('/')[1] || 'png'}`, { type: file.type });
|
|
1188
|
+
newImages.push({
|
|
1189
|
+
id: `paste-${Date.now()}-${i}`,
|
|
1190
|
+
file: renamedFile,
|
|
1191
|
+
preview,
|
|
1192
|
+
base64,
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
catch (err) {
|
|
1196
|
+
errors.push('Failed to process pasted image');
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
if (errors.length > 0) {
|
|
1200
|
+
setError(errors.join('\n'));
|
|
1201
|
+
}
|
|
1202
|
+
if (newImages.length > 0) {
|
|
1203
|
+
setAttachedImages(prev => [...prev, ...newImages]);
|
|
1204
|
+
// Clear any existing error on successful paste
|
|
1205
|
+
if (errors.length === 0) {
|
|
1206
|
+
setError(null);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}, [attachedImages.length, fileToBase64]);
|
|
1210
|
+
// Load and sync chats on mount
|
|
1211
|
+
useEffect(() => {
|
|
1212
|
+
const initializeChats = async () => {
|
|
1213
|
+
// Test connection first
|
|
1214
|
+
const connectionTest = await testMCPConnection();
|
|
1215
|
+
setConnectionStatus(connectionTest);
|
|
1216
|
+
if (connectionTest.connected) {
|
|
1217
|
+
// Fetch available providers
|
|
1218
|
+
try {
|
|
1219
|
+
const providersRes = await fetch(PROVIDERS_API);
|
|
1220
|
+
if (providersRes.ok) {
|
|
1221
|
+
const providersData = await providersRes.json();
|
|
1222
|
+
setAvailableProviders(providersData.providers.filter(p => p.configured));
|
|
1223
|
+
// Set initial selection from server defaults
|
|
1224
|
+
if (providersData.current) {
|
|
1225
|
+
setSelectedProvider(providersData.current.provider.toLowerCase());
|
|
1226
|
+
setSelectedModel(providersData.current.model);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
catch (e) {
|
|
1231
|
+
console.error('Failed to fetch providers:', e);
|
|
1232
|
+
}
|
|
1233
|
+
// Fetch design system considerations for environment parity
|
|
1234
|
+
// This ensures production gets the same considerations as local development
|
|
1235
|
+
try {
|
|
1236
|
+
const considerationsRes = await fetch(CONSIDERATIONS_API);
|
|
1237
|
+
if (considerationsRes.ok) {
|
|
1238
|
+
const considerationsData = await considerationsRes.json();
|
|
1239
|
+
if (considerationsData.hasConsiderations && considerationsData.considerations) {
|
|
1240
|
+
setConsiderations(considerationsData.considerations);
|
|
1241
|
+
console.log(`Loaded considerations from ${considerationsData.source}`);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
catch (e) {
|
|
1246
|
+
console.error('Failed to fetch considerations:', e);
|
|
1247
|
+
}
|
|
1248
|
+
const syncedChats = await syncWithActualStories();
|
|
1249
|
+
const sortedChats = syncedChats.sort((a, b) => b.lastUpdated - a.lastUpdated).slice(0, MAX_RECENT_CHATS);
|
|
1250
|
+
setRecentChats(sortedChats);
|
|
1251
|
+
if (sortedChats.length > 0) {
|
|
1252
|
+
setConversation(sortedChats[0].conversation);
|
|
1253
|
+
setActiveChatId(sortedChats[0].id);
|
|
1254
|
+
setActiveTitle(sortedChats[0].title);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
else {
|
|
1258
|
+
// Load from local storage if server is not available
|
|
1259
|
+
const localChats = loadChats();
|
|
1260
|
+
setRecentChats(localChats);
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
initializeChats();
|
|
1264
|
+
}, []);
|
|
1265
|
+
// Scroll to bottom on new message
|
|
1266
|
+
useEffect(() => {
|
|
1267
|
+
if (chatEndRef.current) {
|
|
1268
|
+
chatEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
|
1269
|
+
}
|
|
1270
|
+
}, [conversation, loading]);
|
|
1271
|
+
// Helper function for non-streaming fallback
|
|
1272
|
+
const handleSendNonStreaming = async (userInput, newConversation) => {
|
|
1273
|
+
const res = await fetch(MCP_API, {
|
|
1274
|
+
method: 'POST',
|
|
1275
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1276
|
+
body: JSON.stringify({
|
|
1277
|
+
prompt: userInput,
|
|
1278
|
+
conversation: newConversation,
|
|
1279
|
+
fileName: activeChatId || undefined,
|
|
1280
|
+
provider: selectedProvider || undefined,
|
|
1281
|
+
model: selectedModel || undefined,
|
|
1282
|
+
considerations: considerations || undefined,
|
|
1283
|
+
}),
|
|
1284
|
+
});
|
|
1285
|
+
const contentType = res.headers.get('content-type');
|
|
1286
|
+
if (!contentType || !contentType.includes('application/json')) {
|
|
1287
|
+
const text = await res.text();
|
|
1288
|
+
throw new Error(`Server returned non-JSON response. Response: ${text.substring(0, 200)}...`);
|
|
1289
|
+
}
|
|
1290
|
+
const data = await res.json();
|
|
1291
|
+
if (!res.ok || !data.success)
|
|
1292
|
+
throw new Error(data.error || 'Story generation failed');
|
|
1293
|
+
return data;
|
|
1294
|
+
};
|
|
1295
|
+
// Helper function to build a conversational response from completion data
|
|
1296
|
+
// Uses special markers [SUCCESS], [ERROR], [TIP], [WRENCH] that renderMarkdown converts to icons
|
|
1297
|
+
// Track whether we've shown the refresh hint in this session
|
|
1298
|
+
const hasShownRefreshHint = useRef(false);
|
|
1299
|
+
const buildConversationalResponse = (completion, isUpdate) => {
|
|
1300
|
+
const parts = [];
|
|
1301
|
+
const statusMarker = completion.success ? '[SUCCESS]' : '[ERROR]';
|
|
1302
|
+
// Lead with the result - more conversational
|
|
1303
|
+
if (isUpdate) {
|
|
1304
|
+
parts.push(`${statusMarker} **Updated: "${completion.title}"**`);
|
|
1305
|
+
}
|
|
1306
|
+
else {
|
|
1307
|
+
parts.push(`${statusMarker} **Created: "${completion.title}"**`);
|
|
1308
|
+
}
|
|
1309
|
+
// Build component insights with reasons when available
|
|
1310
|
+
const componentCount = completion.componentsUsed?.length || 0;
|
|
1311
|
+
if (componentCount > 0) {
|
|
1312
|
+
const componentList = completion.componentsUsed.slice(0, 5);
|
|
1313
|
+
// Check if we have meaningful reasons (not just "Used in composition")
|
|
1314
|
+
const componentsWithReasons = componentList.filter(c => c.reason && c.reason !== 'Used in composition');
|
|
1315
|
+
if (componentsWithReasons.length > 0) {
|
|
1316
|
+
// Show components with their reasons
|
|
1317
|
+
const insights = componentsWithReasons
|
|
1318
|
+
.slice(0, 3)
|
|
1319
|
+
.map(c => `\`${c.name}\` - ${c.reason?.toLowerCase()}`)
|
|
1320
|
+
.join(', ');
|
|
1321
|
+
parts.push(`\nUsed ${insights}${componentCount > 3 ? ` and ${componentCount - 3} more` : ''}.`);
|
|
1322
|
+
}
|
|
1323
|
+
else {
|
|
1324
|
+
// Fallback to simple list
|
|
1325
|
+
const names = componentList.map(c => `\`${c.name}\``).join(', ');
|
|
1326
|
+
parts.push(`\nBuilt with ${names}${componentCount > 5 ? '...' : ''}.`);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
// Add layout decisions with educational context
|
|
1330
|
+
if (completion.layoutChoices && completion.layoutChoices.length > 0) {
|
|
1331
|
+
const primaryLayout = completion.layoutChoices[0];
|
|
1332
|
+
parts.push(`\n\n**Layout:** ${primaryLayout.pattern} - ${primaryLayout.reason.charAt(0).toLowerCase()}${primaryLayout.reason.slice(1)}.`);
|
|
1333
|
+
}
|
|
1334
|
+
// Add style choices only if they add value
|
|
1335
|
+
if (completion.styleChoices && completion.styleChoices.length > 0) {
|
|
1336
|
+
const notableStyles = completion.styleChoices.filter(s => s.reason && s.reason !== 'Semantic color from design system');
|
|
1337
|
+
if (notableStyles.length > 0) {
|
|
1338
|
+
const styleInfo = notableStyles[0];
|
|
1339
|
+
parts.push(` Applied \`${styleInfo.value}\` for ${styleInfo.reason?.toLowerCase() || 'visual consistency'}.`);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
// Add validation fixes notice
|
|
1343
|
+
if (completion.validation?.autoFixApplied) {
|
|
1344
|
+
parts.push(`\n\n[WRENCH] **Auto-fixed:** Minor syntax issues were automatically corrected.`);
|
|
1345
|
+
}
|
|
1346
|
+
// Add suggestions only if meaningful
|
|
1347
|
+
if (completion.suggestions && completion.suggestions.length > 0) {
|
|
1348
|
+
const suggestion = completion.suggestions[0];
|
|
1349
|
+
// Only show if it's not the generic "review the generated code" message
|
|
1350
|
+
if (!suggestion.toLowerCase().includes('review the generated code')) {
|
|
1351
|
+
parts.push(`\n\n[TIP] **Tip:** ${suggestion}`);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
// Show refresh hint only once per session for new stories (local mode only)
|
|
1355
|
+
// In Edge mode, stories are stored in Durable Objects, not on filesystem
|
|
1356
|
+
if (!isUpdate && !hasShownRefreshHint.current) {
|
|
1357
|
+
if (isEdgeMode()) {
|
|
1358
|
+
parts.push(`\n\n_Story saved to cloud. View code in chat history above._`);
|
|
1359
|
+
}
|
|
1360
|
+
else {
|
|
1361
|
+
parts.push(`\n\n_Refresh Storybook (Cmd/Ctrl + R) to see new stories in the sidebar._`);
|
|
1362
|
+
}
|
|
1363
|
+
hasShownRefreshHint.current = true;
|
|
1364
|
+
}
|
|
1365
|
+
// Add metrics in a subtle way (if available)
|
|
1366
|
+
if (completion.metrics?.totalTimeMs) {
|
|
1367
|
+
const seconds = (completion.metrics.totalTimeMs / 1000).toFixed(1);
|
|
1368
|
+
parts.push(`\n\n_${seconds}s_`);
|
|
1369
|
+
}
|
|
1370
|
+
return parts.join('');
|
|
1371
|
+
};
|
|
1372
|
+
// Helper function to finalize conversation after streaming completes
|
|
1373
|
+
const finalizeStreamingConversation = useCallback((newConversation, completion, userInput) => {
|
|
1374
|
+
// Build conversational response using rich completion data
|
|
1375
|
+
const isUpdate = completion.summary.action === 'updated';
|
|
1376
|
+
const responseMessage = buildConversationalResponse(completion, isUpdate);
|
|
1377
|
+
const aiMsg = { role: 'ai', content: responseMessage };
|
|
1378
|
+
const updatedConversation = [...newConversation, aiMsg];
|
|
1379
|
+
setConversation(updatedConversation);
|
|
1380
|
+
// Update chat session
|
|
1381
|
+
const isExistingSession = activeChatId && conversation.length > 0;
|
|
1382
|
+
if (isExistingSession && activeChatId) {
|
|
1383
|
+
const updatedSession = {
|
|
1384
|
+
id: activeChatId,
|
|
1385
|
+
title: activeTitle,
|
|
1386
|
+
fileName: completion.fileName || activeChatId,
|
|
1387
|
+
conversation: updatedConversation,
|
|
1388
|
+
lastUpdated: Date.now(),
|
|
1389
|
+
};
|
|
1390
|
+
const chats = loadChats();
|
|
1391
|
+
const chatIndex = chats.findIndex(c => c.id === activeChatId);
|
|
1392
|
+
if (chatIndex !== -1) {
|
|
1393
|
+
chats[chatIndex] = updatedSession;
|
|
1394
|
+
}
|
|
1395
|
+
saveChats(chats);
|
|
1396
|
+
setRecentChats(chats);
|
|
1397
|
+
}
|
|
1398
|
+
else {
|
|
1399
|
+
const chatId = completion.storyId || completion.fileName || Date.now().toString();
|
|
1400
|
+
const chatTitle = completion.title || userInput;
|
|
1401
|
+
setActiveChatId(chatId);
|
|
1402
|
+
setActiveTitle(chatTitle);
|
|
1403
|
+
const newSession = {
|
|
1404
|
+
id: chatId,
|
|
1405
|
+
title: chatTitle,
|
|
1406
|
+
fileName: completion.fileName || '',
|
|
1407
|
+
conversation: updatedConversation,
|
|
1408
|
+
lastUpdated: Date.now(),
|
|
1409
|
+
};
|
|
1410
|
+
const chats = loadChats().filter(c => c.id !== chatId);
|
|
1411
|
+
chats.unshift(newSession);
|
|
1412
|
+
if (chats.length > MAX_RECENT_CHATS) {
|
|
1413
|
+
chats.splice(MAX_RECENT_CHATS);
|
|
1414
|
+
}
|
|
1415
|
+
saveChats(chats);
|
|
1416
|
+
setRecentChats(chats);
|
|
1417
|
+
}
|
|
1418
|
+
}, [activeChatId, activeTitle, conversation.length]);
|
|
1419
|
+
const handleSend = async (e) => {
|
|
1420
|
+
if (e)
|
|
1421
|
+
e.preventDefault();
|
|
1422
|
+
// Allow sending with either text or images
|
|
1423
|
+
if (!input.trim() && attachedImages.length === 0)
|
|
1424
|
+
return;
|
|
1425
|
+
// Use input text or default vision prompt if only images
|
|
1426
|
+
const userInput = input.trim() || (attachedImages.length > 0 ? 'Create a component that matches this design' : '');
|
|
1427
|
+
setError(null);
|
|
1428
|
+
setLoading(true);
|
|
1429
|
+
setStreamingState(null);
|
|
1430
|
+
// Test connection before sending
|
|
1431
|
+
const connectionTest = await testMCPConnection();
|
|
1432
|
+
setConnectionStatus(connectionTest);
|
|
1433
|
+
if (!connectionTest.connected) {
|
|
1434
|
+
setError(`Cannot connect to MCP server: ${connectionTest.error || 'Server not running'}`);
|
|
1435
|
+
setLoading(false);
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
// Capture images before clearing
|
|
1439
|
+
const imagesToSend = [...attachedImages];
|
|
1440
|
+
const hasImages = imagesToSend.length > 0;
|
|
1441
|
+
// Create user message with images
|
|
1442
|
+
const userMessage = {
|
|
1443
|
+
role: 'user',
|
|
1444
|
+
content: userInput,
|
|
1445
|
+
attachedImages: hasImages ? imagesToSend : undefined
|
|
1446
|
+
};
|
|
1447
|
+
const newConversation = [...conversation, userMessage];
|
|
1448
|
+
setConversation(newConversation);
|
|
1449
|
+
setInput('');
|
|
1450
|
+
clearAttachedImages();
|
|
1451
|
+
// Use streaming if enabled
|
|
1452
|
+
if (USE_STREAMING) {
|
|
1453
|
+
try {
|
|
1454
|
+
// Cancel any existing request
|
|
1455
|
+
if (abortControllerRef.current) {
|
|
1456
|
+
abortControllerRef.current.abort();
|
|
1457
|
+
}
|
|
1458
|
+
abortControllerRef.current = new AbortController();
|
|
1459
|
+
// Initialize streaming state
|
|
1460
|
+
setStreamingState({});
|
|
1461
|
+
// Prepare images for API request
|
|
1462
|
+
const imagePayload = hasImages
|
|
1463
|
+
? imagesToSend.map(img => ({
|
|
1464
|
+
type: 'base64',
|
|
1465
|
+
data: img.base64,
|
|
1466
|
+
mediaType: img.file.type,
|
|
1467
|
+
}))
|
|
1468
|
+
: undefined;
|
|
1469
|
+
const response = await fetch(MCP_STREAM_API, {
|
|
1470
|
+
method: 'POST',
|
|
1471
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1472
|
+
body: JSON.stringify({
|
|
1473
|
+
prompt: userInput,
|
|
1474
|
+
conversation: newConversation,
|
|
1475
|
+
fileName: activeChatId || undefined,
|
|
1476
|
+
isUpdate: activeChatId && conversation.length > 0,
|
|
1477
|
+
originalTitle: activeTitle || undefined,
|
|
1478
|
+
storyId: activeChatId || undefined,
|
|
1479
|
+
images: imagePayload,
|
|
1480
|
+
visionMode: hasImages ? 'screenshot_to_story' : undefined,
|
|
1481
|
+
provider: selectedProvider || undefined,
|
|
1482
|
+
model: selectedModel || undefined,
|
|
1483
|
+
considerations: considerations || undefined,
|
|
1484
|
+
}),
|
|
1485
|
+
signal: abortControllerRef.current.signal,
|
|
1486
|
+
});
|
|
1487
|
+
if (!response.ok) {
|
|
1488
|
+
throw new Error(`Streaming request failed: ${response.status}`);
|
|
1489
|
+
}
|
|
1490
|
+
const reader = response.body?.getReader();
|
|
1491
|
+
if (!reader) {
|
|
1492
|
+
throw new Error('No response body');
|
|
1493
|
+
}
|
|
1494
|
+
const decoder = new TextDecoder();
|
|
1495
|
+
let buffer = '';
|
|
1496
|
+
let completionData = null;
|
|
1497
|
+
let errorData = null;
|
|
1498
|
+
while (true) {
|
|
1499
|
+
const { done, value } = await reader.read();
|
|
1500
|
+
if (done)
|
|
1501
|
+
break;
|
|
1502
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1503
|
+
// Parse SSE events from buffer
|
|
1504
|
+
const lines = buffer.split('\n');
|
|
1505
|
+
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
1506
|
+
for (const line of lines) {
|
|
1507
|
+
if (line.startsWith('data: ')) {
|
|
1508
|
+
try {
|
|
1509
|
+
const event = JSON.parse(line.slice(6));
|
|
1510
|
+
// Update streaming state based on event type
|
|
1511
|
+
switch (event.type) {
|
|
1512
|
+
case 'intent':
|
|
1513
|
+
setStreamingState(prev => ({ ...prev, intent: event.data }));
|
|
1514
|
+
break;
|
|
1515
|
+
case 'progress':
|
|
1516
|
+
setStreamingState(prev => ({ ...prev, progress: event.data }));
|
|
1517
|
+
break;
|
|
1518
|
+
case 'validation':
|
|
1519
|
+
setStreamingState(prev => ({ ...prev, validation: event.data }));
|
|
1520
|
+
break;
|
|
1521
|
+
case 'retry':
|
|
1522
|
+
setStreamingState(prev => ({ ...prev, retry: event.data }));
|
|
1523
|
+
break;
|
|
1524
|
+
case 'completion':
|
|
1525
|
+
completionData = event.data;
|
|
1526
|
+
setStreamingState(prev => ({ ...prev, completion: event.data }));
|
|
1527
|
+
break;
|
|
1528
|
+
case 'error':
|
|
1529
|
+
errorData = event.data;
|
|
1530
|
+
setStreamingState(prev => ({ ...prev, error: event.data }));
|
|
1531
|
+
break;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
catch (parseError) {
|
|
1535
|
+
console.warn('Failed to parse SSE event:', line, parseError);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
// Handle completion or error
|
|
1541
|
+
if (completionData) {
|
|
1542
|
+
finalizeStreamingConversation(newConversation, completionData, userInput);
|
|
1543
|
+
}
|
|
1544
|
+
else if (errorData) {
|
|
1545
|
+
setError(errorData.message);
|
|
1546
|
+
const errorConversation = [...newConversation, { role: 'ai', content: `Error: ${errorData.message}\n\n${errorData.suggestion || ''}` }];
|
|
1547
|
+
setConversation(errorConversation);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
catch (err) {
|
|
1551
|
+
if (err.name === 'AbortError') {
|
|
1552
|
+
console.log('Request aborted');
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
// Fall back to non-streaming on error
|
|
1556
|
+
console.warn('Streaming failed, falling back to non-streaming:', err);
|
|
1557
|
+
setStreamingState(null);
|
|
1558
|
+
try {
|
|
1559
|
+
const data = await handleSendNonStreaming(userInput, newConversation);
|
|
1560
|
+
// Process non-streaming response (same as before)
|
|
1561
|
+
let responseMessage;
|
|
1562
|
+
const statusIcon = data.validation?.hasWarnings ? (data.validation.errors?.length > 0 ? '🔧' : '⚠️') : '✅';
|
|
1563
|
+
// Build conversational response for fallback
|
|
1564
|
+
if (data.isUpdate) {
|
|
1565
|
+
responseMessage = `${statusIcon} **Updated: "${data.title}"**\n\nI've made the requested changes to your component. You can view the updated version in Storybook.\n\n_Check the Docs tab to see both the rendered component and its code._`;
|
|
1566
|
+
}
|
|
1567
|
+
else {
|
|
1568
|
+
responseMessage = `${statusIcon} **Created: "${data.title}"**\n\nI've generated the component with the requested features. You can view it in Storybook where you'll see both the rendered component and its markup.\n\n💡 **Note**: If you don't see the story immediately, you may need to refresh your Storybook page (Cmd/Ctrl + R).`;
|
|
1569
|
+
}
|
|
1570
|
+
const aiMsg = { role: 'ai', content: responseMessage };
|
|
1571
|
+
const updatedConversation = [...newConversation, aiMsg];
|
|
1572
|
+
setConversation(updatedConversation);
|
|
1573
|
+
// Update chat session
|
|
1574
|
+
const isUpdate = activeChatId && conversation.length > 0;
|
|
1575
|
+
if (isUpdate && activeChatId) {
|
|
1576
|
+
const updatedSession = {
|
|
1577
|
+
id: activeChatId,
|
|
1578
|
+
title: activeTitle,
|
|
1579
|
+
fileName: data.fileName || activeChatId,
|
|
1580
|
+
conversation: updatedConversation,
|
|
1581
|
+
lastUpdated: Date.now(),
|
|
1582
|
+
};
|
|
1583
|
+
const chats = loadChats();
|
|
1584
|
+
const chatIndex = chats.findIndex(c => c.id === activeChatId);
|
|
1585
|
+
if (chatIndex !== -1)
|
|
1586
|
+
chats[chatIndex] = updatedSession;
|
|
1587
|
+
saveChats(chats);
|
|
1588
|
+
setRecentChats(chats);
|
|
1589
|
+
}
|
|
1590
|
+
else {
|
|
1591
|
+
const chatId = data.storyId || data.fileName || Date.now().toString();
|
|
1592
|
+
setActiveChatId(chatId);
|
|
1593
|
+
setActiveTitle(data.title || userInput);
|
|
1594
|
+
const newSession = {
|
|
1595
|
+
id: chatId,
|
|
1596
|
+
title: data.title || userInput,
|
|
1597
|
+
fileName: data.fileName || '',
|
|
1598
|
+
conversation: updatedConversation,
|
|
1599
|
+
lastUpdated: Date.now(),
|
|
1600
|
+
};
|
|
1601
|
+
const chats = loadChats().filter(c => c.id !== chatId);
|
|
1602
|
+
chats.unshift(newSession);
|
|
1603
|
+
if (chats.length > MAX_RECENT_CHATS)
|
|
1604
|
+
chats.splice(MAX_RECENT_CHATS);
|
|
1605
|
+
saveChats(chats);
|
|
1606
|
+
setRecentChats(chats);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
catch (fallbackErr) {
|
|
1610
|
+
const errorMessage = fallbackErr instanceof Error ? fallbackErr.message : 'Unknown error';
|
|
1611
|
+
setError(errorMessage);
|
|
1612
|
+
const errorConversation = [...newConversation, { role: 'ai', content: `Error: ${errorMessage}` }];
|
|
1613
|
+
setConversation(errorConversation);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
finally {
|
|
1617
|
+
setLoading(false);
|
|
1618
|
+
setStreamingState(null);
|
|
1619
|
+
abortControllerRef.current = null;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
else {
|
|
1623
|
+
// Non-streaming mode (original implementation)
|
|
1624
|
+
try {
|
|
1625
|
+
const data = await handleSendNonStreaming(userInput, newConversation);
|
|
1626
|
+
let responseMessage;
|
|
1627
|
+
const statusIcon = data.validation?.hasWarnings ? (data.validation.errors?.length > 0 ? '🔧' : '⚠️') : '✅';
|
|
1628
|
+
// Build conversational response for non-streaming mode
|
|
1629
|
+
if (data.isUpdate) {
|
|
1630
|
+
responseMessage = `${statusIcon} **Updated: "${data.title}"**\n\nI've made the requested changes to your component. You can view the updated version in Storybook.\n\n_Check the Docs tab to see both the rendered component and its code._`;
|
|
1631
|
+
}
|
|
1632
|
+
else {
|
|
1633
|
+
responseMessage = `${statusIcon} **Created: "${data.title}"**\n\nI've generated the component with the requested features. You can view it in Storybook where you'll see both the rendered component and its markup.\n\n💡 **Note**: If you don't see the story immediately, you may need to refresh your Storybook page (Cmd/Ctrl + R).`;
|
|
1634
|
+
}
|
|
1635
|
+
const aiMsg = { role: 'ai', content: responseMessage };
|
|
1636
|
+
const updatedConversation = [...newConversation, aiMsg];
|
|
1637
|
+
setConversation(updatedConversation);
|
|
1638
|
+
const isUpdate = activeChatId && conversation.length > 0;
|
|
1639
|
+
if (isUpdate && activeChatId) {
|
|
1640
|
+
const updatedSession = {
|
|
1641
|
+
id: activeChatId,
|
|
1642
|
+
title: activeTitle,
|
|
1643
|
+
fileName: data.fileName || activeChatId,
|
|
1644
|
+
conversation: updatedConversation,
|
|
1645
|
+
lastUpdated: Date.now(),
|
|
1646
|
+
};
|
|
1647
|
+
const chats = loadChats();
|
|
1648
|
+
const chatIndex = chats.findIndex(c => c.id === activeChatId);
|
|
1649
|
+
if (chatIndex !== -1)
|
|
1650
|
+
chats[chatIndex] = updatedSession;
|
|
1651
|
+
saveChats(chats);
|
|
1652
|
+
setRecentChats(chats);
|
|
1653
|
+
}
|
|
1654
|
+
else {
|
|
1655
|
+
const chatId = data.storyId || data.fileName || Date.now().toString();
|
|
1656
|
+
setActiveChatId(chatId);
|
|
1657
|
+
setActiveTitle(data.title || userInput);
|
|
1658
|
+
const newSession = {
|
|
1659
|
+
id: chatId,
|
|
1660
|
+
title: data.title || userInput,
|
|
1661
|
+
fileName: data.fileName || '',
|
|
1662
|
+
conversation: updatedConversation,
|
|
1663
|
+
lastUpdated: Date.now(),
|
|
1664
|
+
};
|
|
1665
|
+
const chats = loadChats().filter(c => c.id !== chatId);
|
|
1666
|
+
chats.unshift(newSession);
|
|
1667
|
+
if (chats.length > MAX_RECENT_CHATS)
|
|
1668
|
+
chats.splice(MAX_RECENT_CHATS);
|
|
1669
|
+
saveChats(chats);
|
|
1670
|
+
setRecentChats(chats);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
catch (err) {
|
|
1674
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
1675
|
+
setError(errorMessage);
|
|
1676
|
+
const errorConversation = [...newConversation, { role: 'ai', content: `Error: ${errorMessage}` }];
|
|
1677
|
+
setConversation(errorConversation);
|
|
1678
|
+
}
|
|
1679
|
+
finally {
|
|
1680
|
+
setLoading(false);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
const handleSelectChat = (chat) => {
|
|
1685
|
+
setConversation(chat.conversation);
|
|
1686
|
+
setActiveChatId(chat.id);
|
|
1687
|
+
setActiveTitle(chat.title);
|
|
1688
|
+
};
|
|
1689
|
+
const handleNewChat = () => {
|
|
1690
|
+
setConversation([]);
|
|
1691
|
+
setActiveChatId(null);
|
|
1692
|
+
setActiveTitle('');
|
|
1693
|
+
};
|
|
1694
|
+
const handleDeleteChat = async (chatId, e) => {
|
|
1695
|
+
e.stopPropagation(); // Prevent selecting the chat
|
|
1696
|
+
if (confirm('Delete this story and chat? This action cannot be undone.')) {
|
|
1697
|
+
const success = await deleteStoryAndChat(chatId);
|
|
1698
|
+
if (success) {
|
|
1699
|
+
// Update local state
|
|
1700
|
+
const updatedChats = recentChats.filter(chat => chat.id !== chatId);
|
|
1701
|
+
setRecentChats(updatedChats);
|
|
1702
|
+
// If we deleted the active chat, switch to another or clear
|
|
1703
|
+
if (activeChatId === chatId) {
|
|
1704
|
+
if (updatedChats.length > 0) {
|
|
1705
|
+
setConversation(updatedChats[0].conversation);
|
|
1706
|
+
setActiveChatId(updatedChats[0].id);
|
|
1707
|
+
setActiveTitle(updatedChats[0].title);
|
|
1708
|
+
}
|
|
1709
|
+
else {
|
|
1710
|
+
handleNewChat();
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
else {
|
|
1715
|
+
alert('Failed to delete story. Please try again.');
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
return (_jsxs("div", { style: STYLES.container, children: [_jsxs("div", { style: {
|
|
1720
|
+
...STYLES.sidebar,
|
|
1721
|
+
...(sidebarOpen ? {} : STYLES.sidebarCollapsed),
|
|
1722
|
+
}, children: [sidebarOpen && (_jsxs("div", { style: { flex: 1, overflowY: 'auto', padding: '16px' }, children: [_jsxs("button", { onClick: () => setSidebarOpen(false), style: STYLES.sidebarToggle, title: "Collapse sidebar", onMouseEnter: (e) => {
|
|
1723
|
+
e.currentTarget.style.transform = 'translateY(-1px)';
|
|
1724
|
+
e.currentTarget.style.boxShadow = '0 4px 16px rgba(59, 130, 246, 0.4)';
|
|
1725
|
+
}, onMouseLeave: (e) => {
|
|
1726
|
+
e.currentTarget.style.transform = 'translateY(0)';
|
|
1727
|
+
e.currentTarget.style.boxShadow = '0 2px 8px rgba(59, 130, 246, 0.3)';
|
|
1728
|
+
}, children: [_jsx("span", { style: { lineHeight: '0.5', display: 'inline-block', alignItems: 'center', width: '10px', height: '10px' }, children: "\u2630" }), _jsx("span", { children: "Chats" })] }), _jsxs("button", { onClick: handleNewChat, style: STYLES.newChatButton, onMouseEnter: (e) => {
|
|
1729
|
+
e.currentTarget.style.transform = 'translateY(-1px)';
|
|
1730
|
+
e.currentTarget.style.boxShadow = '0 4px 16px rgba(59, 130, 246, 0.4)';
|
|
1731
|
+
}, onMouseLeave: (e) => {
|
|
1732
|
+
e.currentTarget.style.transform = 'translateY(0)';
|
|
1733
|
+
e.currentTarget.style.boxShadow = '0 2px 8px rgba(59, 130, 246, 0.2)';
|
|
1734
|
+
}, children: [_jsx("span", { style: { lineHeight: '0.5', display: 'inline-block', alignItems: 'center', width: '10px', height: '10px' }, children: "+" }), _jsx("span", { children: "New Chat" })] }), recentChats.length > 0 && (_jsx("div", { style: {
|
|
1735
|
+
color: '#64748b',
|
|
1736
|
+
fontSize: '12px',
|
|
1737
|
+
marginBottom: '8px',
|
|
1738
|
+
fontWeight: '500',
|
|
1739
|
+
textTransform: 'uppercase',
|
|
1740
|
+
letterSpacing: '0.05em',
|
|
1741
|
+
}, children: "Recent Chats" })), recentChats.map(chat => (_jsxs("div", { onClick: () => handleSelectChat(chat), style: {
|
|
1742
|
+
...STYLES.chatItem,
|
|
1743
|
+
...(activeChatId === chat.id ? STYLES.chatItemActive : {}),
|
|
1744
|
+
}, onMouseEnter: (e) => {
|
|
1745
|
+
if (activeChatId !== chat.id) {
|
|
1746
|
+
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.12)';
|
|
1747
|
+
}
|
|
1748
|
+
const deleteBtn = e.currentTarget.querySelector('.delete-btn');
|
|
1749
|
+
if (deleteBtn)
|
|
1750
|
+
deleteBtn.style.opacity = '1';
|
|
1751
|
+
}, onMouseLeave: (e) => {
|
|
1752
|
+
if (activeChatId !== chat.id) {
|
|
1753
|
+
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.08)';
|
|
1754
|
+
}
|
|
1755
|
+
const deleteBtn = e.currentTarget.querySelector('.delete-btn');
|
|
1756
|
+
if (deleteBtn)
|
|
1757
|
+
deleteBtn.style.opacity = '0';
|
|
1758
|
+
}, children: [_jsx("div", { style: STYLES.chatItemTitle, children: chat.title }), _jsx("div", { style: STYLES.chatItemTime, children: formatTime(chat.lastUpdated) }), _jsx("button", { className: "delete-btn", onClick: (e) => handleDeleteChat(chat.id, e), style: STYLES.deleteButton, title: "Delete chat", children: "\u2715" })] }, chat.id)))] })), !sidebarOpen && (_jsx("div", { style: { padding: '8px', display: 'flex', justifyContent: 'center' }, children: _jsx("button", { onClick: () => setSidebarOpen(true), style: {
|
|
1759
|
+
...STYLES.sidebarToggle,
|
|
1760
|
+
width: '38px',
|
|
1761
|
+
height: '38px',
|
|
1762
|
+
padding: '0',
|
|
1763
|
+
fontSize: '16px',
|
|
1764
|
+
borderRadius: '8px',
|
|
1765
|
+
}, title: "Expand sidebar", onMouseEnter: (e) => {
|
|
1766
|
+
e.currentTarget.style.transform = 'scale(1.05)';
|
|
1767
|
+
e.currentTarget.style.background = '#2563eb';
|
|
1768
|
+
}, onMouseLeave: (e) => {
|
|
1769
|
+
e.currentTarget.style.transform = 'scale(1)';
|
|
1770
|
+
e.currentTarget.style.background = '#3b82f6';
|
|
1771
|
+
}, children: _jsx("span", { style: { lineHeight: '0.4', display: 'inline-block', height: '10px' }, children: "\u2630" }) }) }))] }), _jsxs("div", { style: { ...STYLES.mainContent, position: 'relative' }, onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, children: [isDragging && (_jsx("div", { style: STYLES.dropOverlay, children: _jsxs("div", { style: STYLES.dropOverlayText, children: [_jsx("svg", { width: 24, height: 24, viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z" }) }), "Drop images here"] }) })), _jsxs("div", { style: STYLES.chatHeader, children: [_jsx("h1", { style: {
|
|
1772
|
+
fontSize: '24px',
|
|
1773
|
+
margin: 0,
|
|
1774
|
+
fontWeight: '600',
|
|
1775
|
+
background: 'linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%)',
|
|
1776
|
+
WebkitBackgroundClip: 'text',
|
|
1777
|
+
WebkitTextFillColor: 'transparent',
|
|
1778
|
+
display: 'inline-block'
|
|
1779
|
+
}, children: "Story UI" }), _jsx("p", { style: { fontSize: '14px', margin: '4px 0 0 0', color: '#94a3b8' }, children: "Generate Storybook stories with AI" }), _jsxs("div", { style: {
|
|
1780
|
+
display: 'flex',
|
|
1781
|
+
alignItems: 'center',
|
|
1782
|
+
gap: '8px',
|
|
1783
|
+
marginTop: '8px',
|
|
1784
|
+
fontSize: '12px'
|
|
1785
|
+
}, children: [_jsx("div", { style: {
|
|
1786
|
+
width: '8px',
|
|
1787
|
+
height: '8px',
|
|
1788
|
+
borderRadius: '50%',
|
|
1789
|
+
backgroundColor: connectionStatus.connected ? '#10b981' : '#f87171'
|
|
1790
|
+
} }), _jsx("span", { style: { color: connectionStatus.connected ? '#10b981' : '#f87171' }, children: connectionStatus.connected
|
|
1791
|
+
? `Connected to ${getConnectionDisplayText()}`
|
|
1792
|
+
: `Disconnected: ${connectionStatus.error || 'Server not running'}` })] }), connectionStatus.connected && availableProviders.length > 0 && (_jsxs("div", { style: {
|
|
1793
|
+
display: 'flex',
|
|
1794
|
+
gap: '12px',
|
|
1795
|
+
marginTop: '12px',
|
|
1796
|
+
flexWrap: 'wrap'
|
|
1797
|
+
}, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '6px' }, children: [_jsx("label", { style: { fontSize: '12px', color: '#94a3b8' }, children: "Provider:" }), _jsx("select", { value: selectedProvider, onChange: (e) => {
|
|
1798
|
+
const newProvider = e.target.value;
|
|
1799
|
+
setSelectedProvider(newProvider);
|
|
1800
|
+
// Reset model to first available for new provider
|
|
1801
|
+
const provider = availableProviders.find(p => p.type === newProvider);
|
|
1802
|
+
if (provider && provider.models.length > 0) {
|
|
1803
|
+
setSelectedModel(provider.models[0]);
|
|
1804
|
+
}
|
|
1805
|
+
}, style: {
|
|
1806
|
+
background: '#1e293b',
|
|
1807
|
+
border: '1px solid #334155',
|
|
1808
|
+
borderRadius: '6px',
|
|
1809
|
+
color: '#e2e8f0',
|
|
1810
|
+
padding: '4px 8px',
|
|
1811
|
+
fontSize: '12px',
|
|
1812
|
+
cursor: 'pointer'
|
|
1813
|
+
}, children: availableProviders.map(p => (_jsx("option", { value: p.type, children: p.name }, p.type))) })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '6px' }, children: [_jsx("label", { style: { fontSize: '12px', color: '#94a3b8' }, children: "Model:" }), _jsx("select", { value: selectedModel, onChange: (e) => setSelectedModel(e.target.value), style: {
|
|
1814
|
+
background: '#1e293b',
|
|
1815
|
+
border: '1px solid #334155',
|
|
1816
|
+
borderRadius: '6px',
|
|
1817
|
+
color: '#e2e8f0',
|
|
1818
|
+
padding: '4px 8px',
|
|
1819
|
+
fontSize: '12px',
|
|
1820
|
+
cursor: 'pointer',
|
|
1821
|
+
maxWidth: '200px'
|
|
1822
|
+
}, children: availableProviders
|
|
1823
|
+
.find(p => p.type === selectedProvider)
|
|
1824
|
+
?.models.map(model => (_jsx("option", { value: model, children: model }, model))) })] })] }))] }), _jsxs("div", { style: STYLES.chatContainer, children: [error && (_jsx("div", { style: STYLES.errorMessage, children: error })), conversation.length === 0 && !loading && (_jsxs("div", { style: STYLES.emptyState, children: [_jsx("div", { style: STYLES.emptyStateTitle, children: "Start a new conversation" }), _jsx("div", { style: STYLES.emptyStateSubtitle, children: "Describe the UI component you'd like to create" })] })), conversation.map((msg, i) => (_jsx("div", { style: STYLES.messageContainer, children: _jsxs("div", { style: msg.role === 'user' ? STYLES.userMessage : STYLES.aiMessage, children: [msg.role === 'ai' ? renderMarkdown(msg.content) : msg.content, msg.role === 'user' && msg.attachedImages && msg.attachedImages.length > 0 && (_jsx("div", { style: STYLES.userMessageImages, children: msg.attachedImages.map((img) => (_jsx("img", { src: img.preview, alt: "attached", style: STYLES.userMessageImage }, img.id))) }))] }) }, i))), loading && (_jsx("div", { style: STYLES.messageContainer, children: streamingState ? (_jsx(StreamingProgressMessage, { streamingData: streamingState })) : (_jsxs("div", { style: STYLES.loadingMessage, children: [_jsx("span", { children: "Generating story" }), _jsx("span", { className: "loading-dots" })] })) })), _jsx("div", { ref: chatEndRef })] }), _jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", multiple: true, style: { display: 'none' }, onChange: handleFileSelect }), attachedImages.length > 0 && (_jsxs("div", { style: STYLES.imagePreviewContainer, children: [_jsxs("span", { style: STYLES.imagePreviewLabel, children: [_jsx("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z" }) }), attachedImages.length, " image", attachedImages.length > 1 ? 's' : '', " attached"] }), attachedImages.map((img) => (_jsxs("div", { style: STYLES.imagePreviewItem, children: [_jsx("img", { src: img.preview, alt: "preview", style: STYLES.imagePreviewImg }), _jsx("button", { type: "button", style: STYLES.imageRemoveButton, onClick: () => removeAttachedImage(img.id), title: "Remove image", children: "\u00D7" })] }, img.id)))] })), _jsxs("form", { onSubmit: handleSend, style: {
|
|
1825
|
+
...STYLES.inputForm,
|
|
1826
|
+
...(attachedImages.length > 0 ? {
|
|
1827
|
+
marginTop: 0,
|
|
1828
|
+
borderTopLeftRadius: 0,
|
|
1829
|
+
borderTopRightRadius: 0,
|
|
1830
|
+
} : {})
|
|
1831
|
+
}, children: [_jsx("button", { type: "button", onClick: () => fileInputRef.current?.click(), disabled: loading || attachedImages.length >= MAX_IMAGES, style: {
|
|
1832
|
+
...STYLES.uploadButton,
|
|
1833
|
+
...(attachedImages.length >= MAX_IMAGES ? {
|
|
1834
|
+
opacity: 0.5,
|
|
1835
|
+
cursor: 'not-allowed',
|
|
1836
|
+
} : {})
|
|
1837
|
+
}, title: attachedImages.length >= MAX_IMAGES
|
|
1838
|
+
? `Maximum ${MAX_IMAGES} images`
|
|
1839
|
+
: 'Attach images (screenshots, designs)', onMouseEnter: (e) => {
|
|
1840
|
+
if (attachedImages.length < MAX_IMAGES && !loading) {
|
|
1841
|
+
e.currentTarget.style.background = 'rgba(59, 130, 246, 0.2)';
|
|
1842
|
+
e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)';
|
|
1843
|
+
}
|
|
1844
|
+
}, onMouseLeave: (e) => {
|
|
1845
|
+
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.1)';
|
|
1846
|
+
e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.2)';
|
|
1847
|
+
}, children: _jsx("svg", { width: 20, height: 20, viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z" }) }) }), _jsx("input", { ref: inputRef, type: "text", value: input, onChange: e => setInput(e.target.value), onPaste: handlePaste, placeholder: attachedImages.length > 0
|
|
1848
|
+
? "Describe what to create from these images..."
|
|
1849
|
+
: "Describe a UI component...", style: STYLES.textInput, onFocus: (e) => {
|
|
1850
|
+
e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)';
|
|
1851
|
+
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(59, 130, 246, 0.1)';
|
|
1852
|
+
}, onBlur: (e) => {
|
|
1853
|
+
e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.2)';
|
|
1854
|
+
e.currentTarget.style.boxShadow = 'none';
|
|
1855
|
+
} }), _jsxs("button", { type: "submit", disabled: loading || (!input.trim() && attachedImages.length === 0), style: {
|
|
1856
|
+
...STYLES.sendButton,
|
|
1857
|
+
...(loading || (!input.trim() && attachedImages.length === 0) ? {
|
|
1858
|
+
opacity: 0.5,
|
|
1859
|
+
cursor: 'not-allowed',
|
|
1860
|
+
background: '#6b7280',
|
|
1861
|
+
boxShadow: 'none'
|
|
1862
|
+
} : {})
|
|
1863
|
+
}, onMouseEnter: (e) => {
|
|
1864
|
+
if (!loading && (input.trim() || attachedImages.length > 0)) {
|
|
1865
|
+
e.currentTarget.style.transform = 'scale(1.05)';
|
|
1866
|
+
e.currentTarget.style.boxShadow = '0 4px 16px rgba(16, 185, 129, 0.4)';
|
|
1867
|
+
}
|
|
1868
|
+
}, onMouseLeave: (e) => {
|
|
1869
|
+
e.currentTarget.style.transform = 'scale(1)';
|
|
1870
|
+
e.currentTarget.style.boxShadow = '0 2px 8px rgba(16, 185, 129, 0.3)';
|
|
1871
|
+
}, children: [_jsx("span", { children: "Send" }), _jsx("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" }) })] })] })] })] }));
|
|
1872
|
+
}
|
|
1873
|
+
export default StoryUIPanel;
|
|
1874
|
+
export { StoryUIPanel };
|