@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
|
@@ -1,9 +1,239 @@
|
|
|
1
|
-
import React, { useState, useRef, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useRef, useEffect, useCallback, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
// Simple markdown renderer for AI messages with icon marker support
|
|
4
|
+
const renderMarkdown = (text: string): ReactNode => {
|
|
5
|
+
// Split by double newlines to get paragraphs
|
|
6
|
+
const paragraphs = text.split(/\n\n+/);
|
|
7
|
+
|
|
8
|
+
// Parse inline formatting within text
|
|
9
|
+
const parseInline = (str: string, paragraphIndex: number): ReactNode[] => {
|
|
10
|
+
const parts: ReactNode[] = [];
|
|
11
|
+
let remaining = str;
|
|
12
|
+
let keyIndex = 0;
|
|
13
|
+
|
|
14
|
+
while (remaining.length > 0) {
|
|
15
|
+
// Icon markers: [SUCCESS], [ERROR], [TIP], [WRENCH]
|
|
16
|
+
const iconMatch = remaining.match(/^\[(SUCCESS|ERROR|TIP|WRENCH)\]/);
|
|
17
|
+
if (iconMatch) {
|
|
18
|
+
const iconType = iconMatch[1].toLowerCase() as keyof typeof StatusIcons;
|
|
19
|
+
parts.push(<span key={`icon-${paragraphIndex}-${keyIndex++}`}>{StatusIcons[iconType]}</span>);
|
|
20
|
+
remaining = remaining.slice(iconMatch[0].length);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Bold: **text**
|
|
25
|
+
const boldMatch = remaining.match(/^\*\*(.+?)\*\*/);
|
|
26
|
+
if (boldMatch) {
|
|
27
|
+
parts.push(<strong key={`b-${paragraphIndex}-${keyIndex++}`}>{boldMatch[1]}</strong>);
|
|
28
|
+
remaining = remaining.slice(boldMatch[0].length);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Italic: _text_
|
|
33
|
+
const italicMatch = remaining.match(/^_(.+?)_/);
|
|
34
|
+
if (italicMatch) {
|
|
35
|
+
parts.push(<em key={`i-${paragraphIndex}-${keyIndex++}`} style={{ opacity: 0.7, fontSize: '0.9em' }}>{italicMatch[1]}</em>);
|
|
36
|
+
remaining = remaining.slice(italicMatch[0].length);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Code: `text`
|
|
41
|
+
const codeMatch = remaining.match(/^`([^`]+)`/);
|
|
42
|
+
if (codeMatch) {
|
|
43
|
+
parts.push(
|
|
44
|
+
<code
|
|
45
|
+
key={`c-${paragraphIndex}-${keyIndex++}`}
|
|
46
|
+
style={{
|
|
47
|
+
background: 'rgba(0,0,0,0.08)',
|
|
48
|
+
padding: '1px 5px',
|
|
49
|
+
borderRadius: '3px',
|
|
50
|
+
fontFamily: 'ui-monospace, monospace',
|
|
51
|
+
fontSize: '0.88em'
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
{codeMatch[1]}
|
|
55
|
+
</code>
|
|
56
|
+
);
|
|
57
|
+
remaining = remaining.slice(codeMatch[0].length);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Single newline within paragraph - convert to space or line break
|
|
62
|
+
if (remaining.startsWith('\n')) {
|
|
63
|
+
parts.push(' ');
|
|
64
|
+
remaining = remaining.slice(1);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Regular text - consume until next special character or bracket
|
|
69
|
+
const nextSpecial = remaining.search(/[*_`\[\n]/);
|
|
70
|
+
if (nextSpecial === -1) {
|
|
71
|
+
parts.push(remaining);
|
|
72
|
+
remaining = '';
|
|
73
|
+
} else if (nextSpecial === 0) {
|
|
74
|
+
// Special char that didn't match a pattern, treat as regular text
|
|
75
|
+
parts.push(remaining[0]);
|
|
76
|
+
remaining = remaining.slice(1);
|
|
77
|
+
} else {
|
|
78
|
+
parts.push(remaining.slice(0, nextSpecial));
|
|
79
|
+
remaining = remaining.slice(nextSpecial);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return parts;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<>
|
|
88
|
+
{paragraphs.map((paragraph, index) => (
|
|
89
|
+
<div key={`p-${index}`} style={{ marginBottom: index < paragraphs.length - 1 ? '12px' : 0 }}>
|
|
90
|
+
{parseInline(paragraph.trim(), index)}
|
|
91
|
+
</div>
|
|
92
|
+
))}
|
|
93
|
+
</>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Inline SVG icons for status indicators (avoiding emojis)
|
|
98
|
+
const StatusIcons = {
|
|
99
|
+
success: (
|
|
100
|
+
<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' }}>
|
|
101
|
+
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
|
102
|
+
<polyline points="22,4 12,14.01 9,11.01"/>
|
|
103
|
+
</svg>
|
|
104
|
+
),
|
|
105
|
+
error: (
|
|
106
|
+
<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' }}>
|
|
107
|
+
<circle cx="12" cy="12" r="10"/>
|
|
108
|
+
<line x1="15" y1="9" x2="9" y2="15"/>
|
|
109
|
+
<line x1="9" y1="9" x2="15" y2="15"/>
|
|
110
|
+
</svg>
|
|
111
|
+
),
|
|
112
|
+
tip: (
|
|
113
|
+
<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' }}>
|
|
114
|
+
<circle cx="12" cy="12" r="10"/>
|
|
115
|
+
<line x1="12" y1="16" x2="12" y2="12"/>
|
|
116
|
+
<line x1="12" y1="8" x2="12.01" y2="8"/>
|
|
117
|
+
</svg>
|
|
118
|
+
),
|
|
119
|
+
wrench: (
|
|
120
|
+
<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' }}>
|
|
121
|
+
<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"/>
|
|
122
|
+
</svg>
|
|
123
|
+
)
|
|
124
|
+
};
|
|
2
125
|
|
|
3
126
|
// Message type
|
|
4
127
|
interface Message {
|
|
5
128
|
role: 'user' | 'ai';
|
|
6
129
|
content: string;
|
|
130
|
+
isStreaming?: boolean;
|
|
131
|
+
streamingData?: StreamingState;
|
|
132
|
+
attachedImages?: AttachedImage[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Attached image type for upload
|
|
136
|
+
interface AttachedImage {
|
|
137
|
+
id: string;
|
|
138
|
+
file: File;
|
|
139
|
+
preview: string;
|
|
140
|
+
base64?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Provider info from /story-ui/providers API
|
|
144
|
+
interface ProviderInfo {
|
|
145
|
+
type: string;
|
|
146
|
+
name: string;
|
|
147
|
+
configured: boolean;
|
|
148
|
+
models: string[];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface ProvidersResponse {
|
|
152
|
+
providers: ProviderInfo[];
|
|
153
|
+
current: {
|
|
154
|
+
provider: string;
|
|
155
|
+
model: string;
|
|
156
|
+
supportsVision: boolean;
|
|
157
|
+
supportsStreaming: boolean;
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Streaming event types (matching backend streamTypes.ts)
|
|
162
|
+
type StreamEventType = 'intent' | 'progress' | 'validation' | 'retry' | 'completion' | 'error';
|
|
163
|
+
|
|
164
|
+
interface IntentPreview {
|
|
165
|
+
requestType: 'new' | 'modification';
|
|
166
|
+
framework: string;
|
|
167
|
+
detectedDesignSystem: string | null;
|
|
168
|
+
strategy: string;
|
|
169
|
+
estimatedComponents: string[];
|
|
170
|
+
promptAnalysis: {
|
|
171
|
+
hasVisionInput: boolean;
|
|
172
|
+
hasConversationContext: boolean;
|
|
173
|
+
hasPreviousCode: boolean;
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface ProgressUpdate {
|
|
178
|
+
step: number;
|
|
179
|
+
totalSteps: number;
|
|
180
|
+
phase: 'config_loaded' | 'components_discovered' | 'prompt_built' | 'llm_thinking' | 'code_extracted' | 'validating' | 'post_processing' | 'saving';
|
|
181
|
+
message: string;
|
|
182
|
+
details?: Record<string, unknown>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
interface ValidationFeedback {
|
|
186
|
+
isValid: boolean;
|
|
187
|
+
errors: string[];
|
|
188
|
+
warnings: string[];
|
|
189
|
+
autoFixApplied: boolean;
|
|
190
|
+
fixDetails?: string[];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
interface RetryInfo {
|
|
194
|
+
attempt: number;
|
|
195
|
+
maxAttempts: number;
|
|
196
|
+
reason: string;
|
|
197
|
+
errors: string[];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
interface CompletionFeedback {
|
|
201
|
+
success: boolean;
|
|
202
|
+
title: string;
|
|
203
|
+
fileName: string;
|
|
204
|
+
storyId: string;
|
|
205
|
+
summary: { action: 'created' | 'updated' | 'failed'; description: string };
|
|
206
|
+
componentsUsed: { name: string; reason?: string }[];
|
|
207
|
+
layoutChoices: { pattern: string; reason: string }[];
|
|
208
|
+
styleChoices: { property: string; value: string; reason?: string }[];
|
|
209
|
+
suggestions?: string[];
|
|
210
|
+
validation: ValidationFeedback;
|
|
211
|
+
code: string;
|
|
212
|
+
metrics: { totalTimeMs: number; llmCallsCount: number; tokensUsed?: number };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
interface ErrorFeedback {
|
|
216
|
+
code: string;
|
|
217
|
+
message: string;
|
|
218
|
+
details?: string;
|
|
219
|
+
recoverable: boolean;
|
|
220
|
+
suggestion?: string;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
interface StreamEvent {
|
|
224
|
+
type: StreamEventType;
|
|
225
|
+
timestamp: number;
|
|
226
|
+
data: IntentPreview | ProgressUpdate | ValidationFeedback | RetryInfo | CompletionFeedback | ErrorFeedback;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// State for tracking streaming progress
|
|
230
|
+
interface StreamingState {
|
|
231
|
+
intent?: IntentPreview;
|
|
232
|
+
progress?: ProgressUpdate;
|
|
233
|
+
validation?: ValidationFeedback;
|
|
234
|
+
retry?: RetryInfo;
|
|
235
|
+
completion?: CompletionFeedback;
|
|
236
|
+
error?: ErrorFeedback;
|
|
7
237
|
}
|
|
8
238
|
|
|
9
239
|
// Session type
|
|
@@ -15,33 +245,91 @@ interface ChatSession {
|
|
|
15
245
|
lastUpdated: number;
|
|
16
246
|
}
|
|
17
247
|
|
|
18
|
-
// Determine the MCP API
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
248
|
+
// Determine the MCP API base URL.
|
|
249
|
+
// Priority order:
|
|
250
|
+
// 1. VITE_STORY_UI_EDGE_URL - Edge Worker URL for cloud deployments
|
|
251
|
+
// 2. window.__STORY_UI_EDGE_URL__ - Runtime override for edge URL
|
|
252
|
+
// 3. VITE_STORY_UI_PORT - Custom port for localhost
|
|
253
|
+
// 4. window.__STORY_UI_PORT__ - Legacy port override
|
|
254
|
+
// 5. window.STORY_UI_MCP_PORT - MCP port override
|
|
255
|
+
// 6. Default to localhost:4001
|
|
256
|
+
const getApiBaseUrl = () => {
|
|
257
|
+
// Check for Edge Worker URL (cloud deployment)
|
|
258
|
+
const edgeUrl = (import.meta as any).env?.VITE_STORY_UI_EDGE_URL;
|
|
259
|
+
if (edgeUrl) return edgeUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
260
|
+
|
|
261
|
+
// Check for window override for edge URL (support both naming conventions)
|
|
262
|
+
const windowEdgeUrl = (window as any).__STORY_UI_EDGE_URL__ || (window as any).STORY_UI_EDGE_URL;
|
|
263
|
+
if (windowEdgeUrl) return windowEdgeUrl.replace(/\/$/, '');
|
|
264
|
+
|
|
265
|
+
// Check for Vite port environment variable
|
|
25
266
|
const vitePort = (import.meta as any).env?.VITE_STORY_UI_PORT;
|
|
26
|
-
if (vitePort) return
|
|
27
|
-
|
|
267
|
+
if (vitePort) return `http://localhost:${vitePort}`;
|
|
268
|
+
|
|
28
269
|
// Check for window override (legacy support)
|
|
29
270
|
const windowOverride = (window as any).__STORY_UI_PORT__;
|
|
30
|
-
if (windowOverride) return
|
|
31
|
-
|
|
271
|
+
if (windowOverride) return `http://localhost:${windowOverride}`;
|
|
272
|
+
|
|
32
273
|
// Check for MCP port override set by stories file
|
|
33
274
|
const mcpOverride = (window as any).STORY_UI_MCP_PORT;
|
|
34
|
-
if (mcpOverride) return
|
|
35
|
-
|
|
36
|
-
return '4001';
|
|
275
|
+
if (mcpOverride) return `http://localhost:${mcpOverride}`;
|
|
276
|
+
|
|
277
|
+
return 'http://localhost:4001';
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// Helper to check if we're using Edge mode (cloud deployment)
|
|
281
|
+
const isEdgeMode = () => {
|
|
282
|
+
const baseUrl = getApiBaseUrl();
|
|
283
|
+
return baseUrl.includes('workers.dev') || baseUrl.includes('pages.dev') ||
|
|
284
|
+
baseUrl.startsWith('https://') && !baseUrl.includes('localhost');
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Legacy helper for backwards compatibility
|
|
288
|
+
const getApiPort = () => {
|
|
289
|
+
const baseUrl = getApiBaseUrl();
|
|
290
|
+
const match = baseUrl.match(/:(\d+)$/);
|
|
291
|
+
return match ? match[1] : '4001';
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Get connection display text
|
|
295
|
+
const getConnectionDisplayText = () => {
|
|
296
|
+
const baseUrl = getApiBaseUrl();
|
|
297
|
+
if (isEdgeMode()) {
|
|
298
|
+
// Extract domain for Edge URL
|
|
299
|
+
try {
|
|
300
|
+
const url = new URL(baseUrl);
|
|
301
|
+
return `Edge Worker (${url.hostname})`;
|
|
302
|
+
} catch {
|
|
303
|
+
return 'Edge Worker';
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return `MCP server (port ${getApiPort()})`;
|
|
37
307
|
};
|
|
38
308
|
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
309
|
+
const API_BASE = getApiBaseUrl();
|
|
310
|
+
const MCP_API = `${API_BASE}/story-ui/generate`;
|
|
311
|
+
const MCP_STREAM_API = `${API_BASE}/story-ui/generate-stream`;
|
|
312
|
+
const STORIES_API = `${API_BASE}/story-ui/stories`;
|
|
313
|
+
const DELETE_API_BASE = `${API_BASE}/story-ui/stories`;
|
|
314
|
+
const PROVIDERS_API = `${API_BASE}/story-ui/providers`;
|
|
315
|
+
// Considerations API URL - includes storybookOrigin param for Edge mode
|
|
316
|
+
const getConsiderationsApiUrl = () => {
|
|
317
|
+
const baseUrl = `${API_BASE}/story-ui/considerations`;
|
|
318
|
+
if (isEdgeMode()) {
|
|
319
|
+
// In Edge mode, tell the Edge Worker where to fetch considerations from
|
|
320
|
+
// The Storybook origin is where the panel is running (window.location.origin)
|
|
321
|
+
const storybookOrigin = window.location.origin;
|
|
322
|
+
return `${baseUrl}?storybookOrigin=${encodeURIComponent(storybookOrigin)}`;
|
|
323
|
+
}
|
|
324
|
+
return baseUrl;
|
|
325
|
+
};
|
|
326
|
+
const CONSIDERATIONS_API = getConsiderationsApiUrl();
|
|
42
327
|
const STORAGE_KEY = `story-ui-chats-${window.location.port}`;
|
|
43
328
|
const MAX_RECENT_CHATS = 20;
|
|
44
329
|
|
|
330
|
+
// Feature flag: Enable streaming mode (can be toggled for testing)
|
|
331
|
+
const USE_STREAMING = true;
|
|
332
|
+
|
|
45
333
|
// Load from localStorage
|
|
46
334
|
const loadChats = (): ChatSession[] => {
|
|
47
335
|
try {
|
|
@@ -157,47 +445,71 @@ const deleteStoryAndChat = async (chatId: string): Promise<boolean> => {
|
|
|
157
445
|
// Remove .stories.tsx extension if present to get the actual story ID
|
|
158
446
|
const storyId = chatId.replace(/\.stories\.tsx$/, '');
|
|
159
447
|
console.log(`Attempting to delete story: chatId="${chatId}", storyId="${storyId}"`);
|
|
160
|
-
|
|
161
|
-
// First try to delete from backend
|
|
162
|
-
const response = await fetch(`${DELETE_API_BASE}/${storyId}`, {
|
|
163
|
-
method: 'DELETE',
|
|
164
|
-
headers: { 'Content-Type': 'application/json' }
|
|
165
|
-
});
|
|
166
448
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
method: '
|
|
173
|
-
headers: { 'Content-Type': 'application/json' }
|
|
174
|
-
body: JSON.stringify({
|
|
175
|
-
chatId: storyId,
|
|
176
|
-
storyId: storyId
|
|
177
|
-
})
|
|
449
|
+
let serverDeleteSucceeded = false;
|
|
450
|
+
|
|
451
|
+
// First try to delete from backend
|
|
452
|
+
try {
|
|
453
|
+
const response = await fetch(`${DELETE_API_BASE}/${storyId}`, {
|
|
454
|
+
method: 'DELETE',
|
|
455
|
+
headers: { 'Content-Type': 'application/json' }
|
|
178
456
|
});
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
457
|
+
|
|
458
|
+
// 404 means story doesn't exist on server - that's OK, we can still clean up localStorage
|
|
459
|
+
if (response.ok || response.status === 404) {
|
|
460
|
+
serverDeleteSucceeded = true;
|
|
461
|
+
if (response.status === 404) {
|
|
462
|
+
console.log('Story not found on server (may have been a failed generation), cleaning up localStorage');
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
console.warn(`Backend delete returned ${response.status}, trying legacy endpoint`);
|
|
183
466
|
}
|
|
467
|
+
} catch (fetchError) {
|
|
468
|
+
console.warn('Backend delete request failed, trying legacy endpoint:', fetchError);
|
|
184
469
|
}
|
|
185
470
|
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
471
|
+
// Try legacy endpoint as fallback only if primary didn't succeed
|
|
472
|
+
if (!serverDeleteSucceeded) {
|
|
473
|
+
try {
|
|
474
|
+
const legacyResponse = await fetch(`${API_BASE}/story-ui/delete`, {
|
|
475
|
+
method: 'POST',
|
|
476
|
+
headers: { 'Content-Type': 'application/json' },
|
|
477
|
+
body: JSON.stringify({
|
|
478
|
+
chatId: storyId,
|
|
479
|
+
storyId: storyId
|
|
480
|
+
})
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// 404 is also OK for legacy endpoint
|
|
484
|
+
if (legacyResponse.ok || legacyResponse.status === 404) {
|
|
485
|
+
serverDeleteSucceeded = true;
|
|
486
|
+
} else {
|
|
487
|
+
console.warn('Legacy delete endpoint also returned non-success status');
|
|
488
|
+
}
|
|
489
|
+
} catch (legacyError) {
|
|
490
|
+
console.warn('Legacy delete request failed:', legacyError);
|
|
491
|
+
}
|
|
191
492
|
}
|
|
192
493
|
|
|
193
|
-
//
|
|
494
|
+
// Always clean up localStorage - the chat/story data is primarily client-side
|
|
495
|
+
// Even if server delete failed, we should allow users to clean up their chat history
|
|
194
496
|
const chats = loadChats().filter(chat => chat.id !== chatId);
|
|
195
497
|
saveChats(chats);
|
|
498
|
+
console.log('Cleaned up localStorage chat entry');
|
|
196
499
|
|
|
197
500
|
return true;
|
|
198
501
|
} catch (error) {
|
|
199
502
|
console.error('Error deleting story:', error);
|
|
200
|
-
|
|
503
|
+
// Still try to clean up localStorage even on error
|
|
504
|
+
try {
|
|
505
|
+
const chats = loadChats().filter(chat => chat.id !== chatId);
|
|
506
|
+
saveChats(chats);
|
|
507
|
+
console.log('Cleaned up localStorage despite error');
|
|
508
|
+
return true;
|
|
509
|
+
} catch (localError) {
|
|
510
|
+
console.error('Failed to clean up localStorage:', localError);
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
201
513
|
}
|
|
202
514
|
};
|
|
203
515
|
|
|
@@ -238,9 +550,9 @@ const STYLES = {
|
|
|
238
550
|
|
|
239
551
|
// Sidebar
|
|
240
552
|
sidebar: {
|
|
241
|
-
width: '
|
|
242
|
-
background: 'rgba(255, 255, 255, 0.
|
|
243
|
-
borderRight: '1px solid rgba(255, 255, 255, 0.
|
|
553
|
+
width: '240px',
|
|
554
|
+
background: 'rgba(255, 255, 255, 0.03)',
|
|
555
|
+
borderRight: '1px solid rgba(255, 255, 255, 0.08)',
|
|
244
556
|
display: 'flex',
|
|
245
557
|
flexDirection: 'column' as const,
|
|
246
558
|
backdropFilter: 'blur(10px)',
|
|
@@ -249,70 +561,76 @@ const STYLES = {
|
|
|
249
561
|
},
|
|
250
562
|
|
|
251
563
|
sidebarCollapsed: {
|
|
252
|
-
width: '
|
|
564
|
+
width: '56px',
|
|
253
565
|
},
|
|
254
566
|
|
|
255
567
|
sidebarToggle: {
|
|
256
568
|
width: '100%',
|
|
257
|
-
padding: '
|
|
258
|
-
background: '
|
|
569
|
+
padding: '8px 12px',
|
|
570
|
+
background: '#3b82f6',
|
|
259
571
|
color: 'white',
|
|
260
572
|
border: 'none',
|
|
261
|
-
borderRadius: '
|
|
262
|
-
fontSize: '
|
|
573
|
+
borderRadius: '6px',
|
|
574
|
+
fontSize: '13px',
|
|
263
575
|
fontWeight: '500',
|
|
264
576
|
cursor: 'pointer',
|
|
265
|
-
marginBottom: '
|
|
577
|
+
marginBottom: '6px',
|
|
266
578
|
transition: 'all 0.2s ease',
|
|
267
|
-
boxShadow: '
|
|
579
|
+
boxShadow: 'none',
|
|
268
580
|
display: 'flex',
|
|
269
581
|
alignItems: 'center',
|
|
270
582
|
justifyContent: 'center',
|
|
271
|
-
gap: '
|
|
583
|
+
gap: '6px',
|
|
584
|
+
lineHeight: '1',
|
|
272
585
|
},
|
|
273
586
|
|
|
274
587
|
newChatButton: {
|
|
275
588
|
width: '100%',
|
|
276
|
-
padding: '
|
|
277
|
-
background: '
|
|
589
|
+
padding: '8px 12px',
|
|
590
|
+
background: '#3b82f6',
|
|
278
591
|
color: 'white',
|
|
279
592
|
border: 'none',
|
|
280
|
-
borderRadius: '
|
|
281
|
-
fontSize: '
|
|
593
|
+
borderRadius: '6px',
|
|
594
|
+
fontSize: '13px',
|
|
282
595
|
fontWeight: '500',
|
|
283
596
|
cursor: 'pointer',
|
|
284
|
-
marginBottom: '
|
|
597
|
+
marginBottom: '12px',
|
|
285
598
|
transition: 'all 0.2s ease',
|
|
286
|
-
boxShadow: '
|
|
599
|
+
boxShadow: 'none',
|
|
600
|
+
display: 'flex',
|
|
601
|
+
alignItems: 'center',
|
|
602
|
+
justifyContent: 'center',
|
|
603
|
+
gap: '6px',
|
|
604
|
+
lineHeight: '1',
|
|
287
605
|
},
|
|
288
606
|
|
|
289
607
|
chatItem: {
|
|
290
|
-
padding: '
|
|
291
|
-
marginBottom: '
|
|
292
|
-
background: 'rgba(255, 255, 255, 0.
|
|
293
|
-
borderRadius: '
|
|
608
|
+
padding: '8px 10px',
|
|
609
|
+
marginBottom: '4px',
|
|
610
|
+
background: 'rgba(255, 255, 255, 0.05)',
|
|
611
|
+
borderRadius: '6px',
|
|
294
612
|
cursor: 'pointer',
|
|
295
|
-
transition: 'all 0.
|
|
613
|
+
transition: 'all 0.15s ease',
|
|
296
614
|
position: 'relative' as const,
|
|
297
|
-
paddingRight: '
|
|
615
|
+
paddingRight: '32px',
|
|
298
616
|
},
|
|
299
617
|
|
|
300
618
|
chatItemActive: {
|
|
301
|
-
background: 'rgba(59, 130, 246, 0.
|
|
302
|
-
borderLeft: '
|
|
619
|
+
background: 'rgba(59, 130, 246, 0.15)',
|
|
620
|
+
borderLeft: '2px solid #3b82f6',
|
|
303
621
|
},
|
|
304
622
|
|
|
305
623
|
chatItemTitle: {
|
|
306
|
-
fontSize: '
|
|
624
|
+
fontSize: '13px',
|
|
307
625
|
fontWeight: '500',
|
|
308
|
-
marginBottom: '
|
|
626
|
+
marginBottom: '2px',
|
|
309
627
|
whiteSpace: 'nowrap' as const,
|
|
310
628
|
overflow: 'hidden',
|
|
311
629
|
textOverflow: 'ellipsis',
|
|
312
630
|
},
|
|
313
631
|
|
|
314
632
|
chatItemTime: {
|
|
315
|
-
fontSize: '
|
|
633
|
+
fontSize: '11px',
|
|
316
634
|
color: '#94a3b8',
|
|
317
635
|
},
|
|
318
636
|
|
|
@@ -341,15 +659,15 @@ const STYLES = {
|
|
|
341
659
|
},
|
|
342
660
|
|
|
343
661
|
chatHeader: {
|
|
344
|
-
padding: '
|
|
345
|
-
borderBottom: '1px solid rgba(255, 255, 255, 0.
|
|
346
|
-
background: 'rgba(255, 255, 255, 0.
|
|
662
|
+
padding: '12px 16px',
|
|
663
|
+
borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
|
|
664
|
+
background: 'rgba(255, 255, 255, 0.03)',
|
|
347
665
|
backdropFilter: 'blur(10px)',
|
|
348
666
|
},
|
|
349
667
|
|
|
350
668
|
chatContainer: {
|
|
351
669
|
flex: 1,
|
|
352
|
-
padding: '
|
|
670
|
+
padding: '16px',
|
|
353
671
|
overflowY: 'auto' as const,
|
|
354
672
|
scrollBehavior: 'smooth' as const,
|
|
355
673
|
},
|
|
@@ -376,33 +694,33 @@ const STYLES = {
|
|
|
376
694
|
// Message bubbles
|
|
377
695
|
messageContainer: {
|
|
378
696
|
display: 'flex',
|
|
379
|
-
marginBottom: '
|
|
697
|
+
marginBottom: '10px',
|
|
380
698
|
},
|
|
381
699
|
|
|
382
700
|
userMessage: {
|
|
383
|
-
background: '
|
|
701
|
+
background: '#3b82f6',
|
|
384
702
|
color: '#ffffff',
|
|
385
|
-
borderRadius: '
|
|
386
|
-
padding: '
|
|
387
|
-
maxWidth: '
|
|
703
|
+
borderRadius: '16px 16px 4px 16px',
|
|
704
|
+
padding: '10px 14px',
|
|
705
|
+
maxWidth: '85%',
|
|
388
706
|
marginLeft: 'auto',
|
|
389
707
|
fontSize: '14px',
|
|
390
708
|
lineHeight: '1.5',
|
|
391
709
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
|
392
|
-
boxShadow: '
|
|
710
|
+
boxShadow: 'none',
|
|
393
711
|
wordWrap: 'break-word' as const,
|
|
394
712
|
},
|
|
395
713
|
|
|
396
714
|
aiMessage: {
|
|
397
715
|
background: 'rgba(255, 255, 255, 0.95)',
|
|
398
716
|
color: '#1f2937',
|
|
399
|
-
borderRadius: '
|
|
400
|
-
padding: '
|
|
401
|
-
maxWidth: '
|
|
717
|
+
borderRadius: '16px 16px 16px 4px',
|
|
718
|
+
padding: '10px 14px',
|
|
719
|
+
maxWidth: '85%',
|
|
402
720
|
fontSize: '14px',
|
|
403
721
|
lineHeight: '1.5',
|
|
404
722
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
|
405
|
-
boxShadow: '0
|
|
723
|
+
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.08)',
|
|
406
724
|
wordWrap: 'break-word' as const,
|
|
407
725
|
whiteSpace: 'pre-wrap' as const,
|
|
408
726
|
},
|
|
@@ -410,67 +728,67 @@ const STYLES = {
|
|
|
410
728
|
loadingMessage: {
|
|
411
729
|
background: 'rgba(255, 255, 255, 0.9)',
|
|
412
730
|
color: '#6b7280',
|
|
413
|
-
borderRadius: '
|
|
414
|
-
padding: '
|
|
731
|
+
borderRadius: '16px 16px 16px 4px',
|
|
732
|
+
padding: '10px 14px',
|
|
415
733
|
fontSize: '14px',
|
|
416
734
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
|
417
735
|
display: 'flex',
|
|
418
736
|
alignItems: 'center',
|
|
419
|
-
gap: '
|
|
737
|
+
gap: '6px',
|
|
420
738
|
},
|
|
421
739
|
|
|
422
740
|
// Input form
|
|
423
741
|
inputForm: {
|
|
424
742
|
display: 'flex',
|
|
425
743
|
alignItems: 'center',
|
|
426
|
-
gap: '
|
|
427
|
-
margin: '0
|
|
428
|
-
padding: '
|
|
429
|
-
background: 'rgba(255, 255, 255, 0.
|
|
430
|
-
borderRadius: '
|
|
431
|
-
border: '1px solid rgba(255, 255, 255, 0.
|
|
744
|
+
gap: '10px',
|
|
745
|
+
margin: '0 16px 16px 16px',
|
|
746
|
+
padding: '10px',
|
|
747
|
+
background: 'rgba(255, 255, 255, 0.03)',
|
|
748
|
+
borderRadius: '10px',
|
|
749
|
+
border: '1px solid rgba(255, 255, 255, 0.08)',
|
|
432
750
|
backdropFilter: 'blur(10px)',
|
|
433
751
|
},
|
|
434
752
|
|
|
435
753
|
textInput: {
|
|
436
754
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
|
437
755
|
flex: 1,
|
|
438
|
-
padding: '
|
|
439
|
-
borderRadius: '
|
|
440
|
-
border: '1px solid rgba(255, 255, 255, 0.
|
|
441
|
-
fontSize: '
|
|
756
|
+
padding: '10px 14px',
|
|
757
|
+
borderRadius: '6px',
|
|
758
|
+
border: '1px solid rgba(255, 255, 255, 0.15)',
|
|
759
|
+
fontSize: '13px',
|
|
442
760
|
color: '#1f2937',
|
|
443
761
|
background: '#ffffff',
|
|
444
762
|
outline: 'none',
|
|
445
|
-
transition: 'all 0.
|
|
763
|
+
transition: 'all 0.15s ease',
|
|
446
764
|
boxSizing: 'border-box' as const,
|
|
447
765
|
},
|
|
448
766
|
|
|
449
767
|
sendButton: {
|
|
450
768
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
|
451
|
-
padding: '
|
|
452
|
-
borderRadius: '
|
|
769
|
+
padding: '10px 16px',
|
|
770
|
+
borderRadius: '6px',
|
|
453
771
|
border: 'none',
|
|
454
|
-
background: '
|
|
772
|
+
background: '#10b981',
|
|
455
773
|
color: '#ffffff',
|
|
456
|
-
fontSize: '
|
|
774
|
+
fontSize: '13px',
|
|
457
775
|
fontWeight: '500',
|
|
458
776
|
cursor: 'pointer',
|
|
459
777
|
display: 'flex',
|
|
460
778
|
alignItems: 'center',
|
|
461
|
-
gap: '
|
|
462
|
-
transition: 'all 0.
|
|
463
|
-
boxShadow: '
|
|
779
|
+
gap: '5px',
|
|
780
|
+
transition: 'all 0.15s ease',
|
|
781
|
+
boxShadow: 'none',
|
|
464
782
|
},
|
|
465
783
|
|
|
466
784
|
errorMessage: {
|
|
467
785
|
background: 'rgba(248, 113, 113, 0.1)',
|
|
468
786
|
color: '#f87171',
|
|
469
|
-
padding: '12px
|
|
470
|
-
borderRadius: '
|
|
471
|
-
fontSize: '
|
|
472
|
-
marginBottom: '
|
|
473
|
-
border: '1px solid rgba(248, 113, 113, 0.
|
|
787
|
+
padding: '8px 12px',
|
|
788
|
+
borderRadius: '6px',
|
|
789
|
+
fontSize: '13px',
|
|
790
|
+
marginBottom: '10px',
|
|
791
|
+
border: '1px solid rgba(248, 113, 113, 0.2)',
|
|
474
792
|
},
|
|
475
793
|
|
|
476
794
|
loadingDots: {
|
|
@@ -487,299 +805,1376 @@ const STYLES = {
|
|
|
487
805
|
|
|
488
806
|
codeBlock: {
|
|
489
807
|
background: '#1e293b',
|
|
490
|
-
padding: '12px
|
|
491
|
-
borderRadius: '
|
|
808
|
+
padding: '10px 12px',
|
|
809
|
+
borderRadius: '6px',
|
|
492
810
|
fontFamily: 'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace',
|
|
493
|
-
fontSize: '
|
|
494
|
-
lineHeight: '1.
|
|
811
|
+
fontSize: '12px',
|
|
812
|
+
lineHeight: '1.5',
|
|
495
813
|
overflowX: 'auto' as const,
|
|
496
|
-
marginTop: '
|
|
497
|
-
border: '1px solid rgba(255, 255, 255, 0.
|
|
814
|
+
marginTop: '6px',
|
|
815
|
+
border: '1px solid rgba(255, 255, 255, 0.08)',
|
|
498
816
|
},
|
|
499
|
-
};
|
|
500
817
|
|
|
501
|
-
//
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}
|
|
818
|
+
// Streaming progress styles
|
|
819
|
+
streamingContainer: {
|
|
820
|
+
background: 'rgba(255, 255, 255, 0.95)',
|
|
821
|
+
borderRadius: '16px 16px 16px 4px',
|
|
822
|
+
padding: '12px',
|
|
823
|
+
maxWidth: '85%',
|
|
824
|
+
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.08)',
|
|
825
|
+
},
|
|
509
826
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
827
|
+
intentPreview: {
|
|
828
|
+
background: 'rgba(59, 130, 246, 0.08)',
|
|
829
|
+
borderRadius: '8px',
|
|
830
|
+
padding: '10px',
|
|
831
|
+
marginBottom: '10px',
|
|
832
|
+
border: '1px solid rgba(59, 130, 246, 0.15)',
|
|
833
|
+
},
|
|
516
834
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
835
|
+
intentTitle: {
|
|
836
|
+
fontSize: '13px',
|
|
837
|
+
fontWeight: '600',
|
|
838
|
+
color: '#1e40af',
|
|
839
|
+
marginBottom: '6px',
|
|
840
|
+
display: 'flex',
|
|
841
|
+
alignItems: 'center',
|
|
842
|
+
gap: '5px',
|
|
843
|
+
},
|
|
525
844
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
};
|
|
845
|
+
intentStrategy: {
|
|
846
|
+
fontSize: '12px',
|
|
847
|
+
color: '#4b5563',
|
|
848
|
+
marginBottom: '4px',
|
|
849
|
+
},
|
|
532
850
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const [recentChats, setRecentChats] = useState<ChatSession[]>([]);
|
|
540
|
-
const [activeChatId, setActiveChatId] = useState<string | null>(null);
|
|
541
|
-
const [activeTitle, setActiveTitle] = useState<string>('');
|
|
542
|
-
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
543
|
-
const [connectionStatus, setConnectionStatus] = useState<{ connected: boolean; error?: string }>({ connected: false });
|
|
544
|
-
const chatEndRef = useRef<HTMLDivElement | null>(null);
|
|
545
|
-
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
851
|
+
intentComponents: {
|
|
852
|
+
display: 'flex',
|
|
853
|
+
flexWrap: 'wrap' as const,
|
|
854
|
+
gap: '4px',
|
|
855
|
+
marginTop: '6px',
|
|
856
|
+
},
|
|
546
857
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const syncedChats = await syncWithActualStories();
|
|
556
|
-
const sortedChats = syncedChats.sort((a, b) => b.lastUpdated - a.lastUpdated).slice(0, MAX_RECENT_CHATS);
|
|
557
|
-
setRecentChats(sortedChats);
|
|
858
|
+
componentTag: {
|
|
859
|
+
background: 'rgba(59, 130, 246, 0.12)',
|
|
860
|
+
color: '#1d4ed8',
|
|
861
|
+
fontSize: '10px',
|
|
862
|
+
padding: '2px 6px',
|
|
863
|
+
borderRadius: '10px',
|
|
864
|
+
fontWeight: '500',
|
|
865
|
+
},
|
|
558
866
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
setRecentChats(localChats);
|
|
568
|
-
}
|
|
569
|
-
};
|
|
867
|
+
progressBar: {
|
|
868
|
+
background: 'rgba(0, 0, 0, 0.08)',
|
|
869
|
+
borderRadius: '3px',
|
|
870
|
+
height: '4px',
|
|
871
|
+
marginTop: '10px',
|
|
872
|
+
marginBottom: '6px',
|
|
873
|
+
overflow: 'hidden',
|
|
874
|
+
},
|
|
570
875
|
|
|
571
|
-
|
|
572
|
-
|
|
876
|
+
progressFill: {
|
|
877
|
+
background: '#3b82f6',
|
|
878
|
+
height: '100%',
|
|
879
|
+
borderRadius: '3px',
|
|
880
|
+
transition: 'width 0.3s ease',
|
|
881
|
+
},
|
|
573
882
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
883
|
+
progressPhase: {
|
|
884
|
+
fontSize: '11px',
|
|
885
|
+
color: '#6b7280',
|
|
886
|
+
display: 'flex',
|
|
887
|
+
alignItems: 'center',
|
|
888
|
+
gap: '5px',
|
|
889
|
+
},
|
|
580
890
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
891
|
+
phaseIcon: {
|
|
892
|
+
fontSize: '12px',
|
|
893
|
+
},
|
|
894
|
+
|
|
895
|
+
validationBox: {
|
|
896
|
+
marginTop: '8px',
|
|
897
|
+
padding: '8px',
|
|
898
|
+
borderRadius: '6px',
|
|
899
|
+
fontSize: '11px',
|
|
900
|
+
},
|
|
901
|
+
|
|
902
|
+
validationSuccess: {
|
|
903
|
+
background: 'rgba(16, 185, 129, 0.08)',
|
|
904
|
+
border: '1px solid rgba(16, 185, 129, 0.15)',
|
|
905
|
+
color: '#047857',
|
|
906
|
+
},
|
|
907
|
+
|
|
908
|
+
validationWarning: {
|
|
909
|
+
background: 'rgba(245, 158, 11, 0.08)',
|
|
910
|
+
border: '1px solid rgba(245, 158, 11, 0.15)',
|
|
911
|
+
color: '#b45309',
|
|
912
|
+
},
|
|
913
|
+
|
|
914
|
+
validationError: {
|
|
915
|
+
background: 'rgba(239, 68, 68, 0.08)',
|
|
916
|
+
border: '1px solid rgba(239, 68, 68, 0.15)',
|
|
917
|
+
color: '#dc2626',
|
|
918
|
+
},
|
|
919
|
+
|
|
920
|
+
retryBadge: {
|
|
921
|
+
background: 'rgba(245, 158, 11, 0.12)',
|
|
922
|
+
color: '#b45309',
|
|
923
|
+
fontSize: '10px',
|
|
924
|
+
padding: '2px 8px',
|
|
925
|
+
borderRadius: '10px',
|
|
926
|
+
display: 'inline-flex',
|
|
927
|
+
alignItems: 'center',
|
|
928
|
+
gap: '3px',
|
|
929
|
+
marginTop: '6px',
|
|
930
|
+
},
|
|
931
|
+
|
|
932
|
+
completionSummary: {
|
|
933
|
+
marginTop: '10px',
|
|
934
|
+
paddingTop: '10px',
|
|
935
|
+
borderTop: '1px solid rgba(0, 0, 0, 0.08)',
|
|
936
|
+
},
|
|
937
|
+
|
|
938
|
+
summaryTitle: {
|
|
939
|
+
fontSize: '14px',
|
|
940
|
+
fontWeight: '600',
|
|
941
|
+
color: '#111827',
|
|
942
|
+
marginBottom: '6px',
|
|
943
|
+
display: 'flex',
|
|
944
|
+
alignItems: 'center',
|
|
945
|
+
gap: '6px',
|
|
946
|
+
},
|
|
947
|
+
|
|
948
|
+
summaryDescription: {
|
|
949
|
+
fontSize: '12px',
|
|
950
|
+
color: '#4b5563',
|
|
951
|
+
lineHeight: '1.5',
|
|
952
|
+
},
|
|
953
|
+
|
|
954
|
+
metricsRow: {
|
|
955
|
+
display: 'flex',
|
|
956
|
+
gap: '12px',
|
|
957
|
+
marginTop: '8px',
|
|
958
|
+
fontSize: '10px',
|
|
959
|
+
color: '#6b7280',
|
|
960
|
+
},
|
|
961
|
+
|
|
962
|
+
metric: {
|
|
963
|
+
display: 'flex',
|
|
964
|
+
alignItems: 'center',
|
|
965
|
+
gap: '3px',
|
|
966
|
+
},
|
|
967
|
+
|
|
968
|
+
// Code viewer styles for generated stories
|
|
969
|
+
codeViewerContainer: {
|
|
970
|
+
marginTop: '12px',
|
|
971
|
+
borderTop: '1px solid rgba(0, 0, 0, 0.08)',
|
|
972
|
+
paddingTop: '12px',
|
|
973
|
+
},
|
|
974
|
+
|
|
975
|
+
codeViewerToggle: {
|
|
976
|
+
display: 'flex',
|
|
977
|
+
alignItems: 'center',
|
|
978
|
+
justifyContent: 'space-between',
|
|
979
|
+
padding: '8px 12px',
|
|
980
|
+
background: 'rgba(59, 130, 246, 0.08)',
|
|
981
|
+
borderRadius: '6px',
|
|
982
|
+
cursor: 'pointer',
|
|
983
|
+
border: '1px solid rgba(59, 130, 246, 0.15)',
|
|
984
|
+
transition: 'all 0.2s ease',
|
|
985
|
+
fontSize: '13px',
|
|
986
|
+
fontWeight: '500',
|
|
987
|
+
color: '#1e40af',
|
|
988
|
+
},
|
|
989
|
+
|
|
990
|
+
codeViewerToggleHover: {
|
|
991
|
+
background: 'rgba(59, 130, 246, 0.15)',
|
|
992
|
+
},
|
|
993
|
+
|
|
994
|
+
codeViewerContent: {
|
|
995
|
+
marginTop: '10px',
|
|
996
|
+
background: '#1e293b',
|
|
997
|
+
borderRadius: '8px',
|
|
998
|
+
overflow: 'hidden',
|
|
999
|
+
border: '1px solid rgba(255, 255, 255, 0.08)',
|
|
1000
|
+
},
|
|
1001
|
+
|
|
1002
|
+
codeViewerHeader: {
|
|
1003
|
+
display: 'flex',
|
|
1004
|
+
alignItems: 'center',
|
|
1005
|
+
justifyContent: 'space-between',
|
|
1006
|
+
padding: '8px 12px',
|
|
1007
|
+
background: 'rgba(0, 0, 0, 0.2)',
|
|
1008
|
+
borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
|
|
1009
|
+
},
|
|
1010
|
+
|
|
1011
|
+
codeViewerFileName: {
|
|
1012
|
+
fontSize: '12px',
|
|
1013
|
+
color: '#94a3b8',
|
|
1014
|
+
fontFamily: 'ui-monospace, monospace',
|
|
1015
|
+
},
|
|
1016
|
+
|
|
1017
|
+
copyButton: {
|
|
1018
|
+
padding: '4px 10px',
|
|
1019
|
+
fontSize: '11px',
|
|
1020
|
+
fontWeight: '500',
|
|
1021
|
+
color: '#e2e8f0',
|
|
1022
|
+
background: 'rgba(59, 130, 246, 0.3)',
|
|
1023
|
+
border: '1px solid rgba(59, 130, 246, 0.5)',
|
|
1024
|
+
borderRadius: '4px',
|
|
1025
|
+
cursor: 'pointer',
|
|
1026
|
+
transition: 'all 0.2s ease',
|
|
1027
|
+
},
|
|
1028
|
+
|
|
1029
|
+
copyButtonSuccess: {
|
|
1030
|
+
background: 'rgba(34, 197, 94, 0.3)',
|
|
1031
|
+
borderColor: 'rgba(34, 197, 94, 0.5)',
|
|
1032
|
+
color: '#86efac',
|
|
1033
|
+
},
|
|
1034
|
+
|
|
1035
|
+
codeViewerPre: {
|
|
1036
|
+
margin: 0,
|
|
1037
|
+
padding: '12px',
|
|
1038
|
+
fontSize: '11px',
|
|
1039
|
+
lineHeight: '1.5',
|
|
1040
|
+
fontFamily: 'ui-monospace, Consolas, Monaco, monospace',
|
|
1041
|
+
color: '#e2e8f0',
|
|
1042
|
+
overflowX: 'auto' as const,
|
|
1043
|
+
maxHeight: '400px',
|
|
1044
|
+
overflowY: 'auto' as const,
|
|
1045
|
+
},
|
|
1046
|
+
|
|
1047
|
+
// Image upload styles
|
|
1048
|
+
uploadButton: {
|
|
1049
|
+
display: 'flex',
|
|
1050
|
+
alignItems: 'center',
|
|
1051
|
+
justifyContent: 'center',
|
|
1052
|
+
width: '36px',
|
|
1053
|
+
height: '36px',
|
|
1054
|
+
borderRadius: '6px',
|
|
1055
|
+
border: '1px solid rgba(255, 255, 255, 0.15)',
|
|
1056
|
+
background: 'rgba(255, 255, 255, 0.08)',
|
|
1057
|
+
color: '#e2e8f0',
|
|
1058
|
+
cursor: 'pointer',
|
|
1059
|
+
transition: 'all 0.2s ease',
|
|
1060
|
+
flexShrink: 0,
|
|
1061
|
+
},
|
|
1062
|
+
|
|
1063
|
+
uploadButtonHover: {
|
|
1064
|
+
background: 'rgba(59, 130, 246, 0.2)',
|
|
1065
|
+
borderColor: 'rgba(59, 130, 246, 0.5)',
|
|
1066
|
+
},
|
|
1067
|
+
|
|
1068
|
+
imagePreviewContainer: {
|
|
1069
|
+
display: 'flex',
|
|
1070
|
+
flexWrap: 'wrap' as const,
|
|
1071
|
+
gap: '6px',
|
|
1072
|
+
padding: '8px 12px',
|
|
1073
|
+
background: 'rgba(255, 255, 255, 0.03)',
|
|
1074
|
+
borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
|
|
1075
|
+
margin: '0 16px',
|
|
1076
|
+
borderRadius: '6px 6px 0 0',
|
|
1077
|
+
},
|
|
1078
|
+
|
|
1079
|
+
imagePreviewItem: {
|
|
1080
|
+
position: 'relative' as const,
|
|
1081
|
+
width: '56px',
|
|
1082
|
+
height: '56px',
|
|
1083
|
+
borderRadius: '6px',
|
|
1084
|
+
overflow: 'hidden',
|
|
1085
|
+
border: '1px solid rgba(255, 255, 255, 0.15)',
|
|
1086
|
+
background: '#1e293b',
|
|
1087
|
+
},
|
|
1088
|
+
|
|
1089
|
+
imagePreviewImg: {
|
|
1090
|
+
width: '100%',
|
|
1091
|
+
height: '100%',
|
|
1092
|
+
objectFit: 'cover' as const,
|
|
1093
|
+
},
|
|
1094
|
+
|
|
1095
|
+
imageRemoveButton: {
|
|
1096
|
+
position: 'absolute' as const,
|
|
1097
|
+
top: '2px',
|
|
1098
|
+
right: '2px',
|
|
1099
|
+
width: '18px',
|
|
1100
|
+
height: '18px',
|
|
1101
|
+
borderRadius: '50%',
|
|
1102
|
+
background: 'rgba(239, 68, 68, 0.9)',
|
|
1103
|
+
color: 'white',
|
|
1104
|
+
border: 'none',
|
|
1105
|
+
fontSize: '12px',
|
|
1106
|
+
cursor: 'pointer',
|
|
1107
|
+
display: 'flex',
|
|
1108
|
+
alignItems: 'center',
|
|
1109
|
+
justifyContent: 'center',
|
|
1110
|
+
lineHeight: 1,
|
|
1111
|
+
},
|
|
1112
|
+
|
|
1113
|
+
imagePreviewLabel: {
|
|
1114
|
+
display: 'flex',
|
|
1115
|
+
alignItems: 'center',
|
|
1116
|
+
gap: '6px',
|
|
1117
|
+
fontSize: '12px',
|
|
1118
|
+
color: '#94a3b8',
|
|
1119
|
+
marginRight: 'auto',
|
|
1120
|
+
},
|
|
1121
|
+
|
|
1122
|
+
userMessageImages: {
|
|
1123
|
+
display: 'flex',
|
|
1124
|
+
gap: '6px',
|
|
1125
|
+
marginTop: '6px',
|
|
1126
|
+
flexWrap: 'wrap' as const,
|
|
1127
|
+
},
|
|
1128
|
+
|
|
1129
|
+
userMessageImage: {
|
|
1130
|
+
width: '40px',
|
|
1131
|
+
height: '40px',
|
|
1132
|
+
borderRadius: '6px',
|
|
1133
|
+
objectFit: 'cover' as const,
|
|
1134
|
+
border: '1px solid rgba(255, 255, 255, 0.25)',
|
|
1135
|
+
},
|
|
1136
|
+
|
|
1137
|
+
// Drag and drop overlay
|
|
1138
|
+
dropOverlay: {
|
|
1139
|
+
position: 'absolute' as const,
|
|
1140
|
+
top: 0,
|
|
1141
|
+
left: 0,
|
|
1142
|
+
right: 0,
|
|
1143
|
+
bottom: 0,
|
|
1144
|
+
background: 'rgba(59, 130, 246, 0.12)',
|
|
1145
|
+
border: '2px dashed rgba(59, 130, 246, 0.4)',
|
|
1146
|
+
borderRadius: '10px',
|
|
1147
|
+
display: 'flex',
|
|
1148
|
+
alignItems: 'center',
|
|
1149
|
+
justifyContent: 'center',
|
|
1150
|
+
zIndex: 100,
|
|
1151
|
+
backdropFilter: 'blur(3px)',
|
|
1152
|
+
},
|
|
1153
|
+
|
|
1154
|
+
dropOverlayText: {
|
|
1155
|
+
background: 'rgba(59, 130, 246, 0.85)',
|
|
1156
|
+
color: 'white',
|
|
1157
|
+
padding: '12px 24px',
|
|
1158
|
+
borderRadius: '8px',
|
|
1159
|
+
fontSize: '14px',
|
|
1160
|
+
fontWeight: '500',
|
|
1161
|
+
display: 'flex',
|
|
1162
|
+
alignItems: 'center',
|
|
1163
|
+
gap: '8px',
|
|
1164
|
+
boxShadow: '0 2px 8px rgba(59, 130, 246, 0.3)',
|
|
1165
|
+
},
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
// Add custom style for loading animation
|
|
1169
|
+
const styleSheet = document.createElement('style');
|
|
1170
|
+
styleSheet.textContent = `
|
|
1171
|
+
@keyframes loadingDots {
|
|
1172
|
+
0%, 20% { content: "."; }
|
|
1173
|
+
40% { content: ".."; }
|
|
1174
|
+
60%, 100% { content: "..."; }
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
.loading-dots::after {
|
|
1178
|
+
content: ".";
|
|
1179
|
+
animation: loadingDots 1.4s infinite;
|
|
1180
|
+
}
|
|
1181
|
+
`;
|
|
1182
|
+
document.head.appendChild(styleSheet);
|
|
1183
|
+
|
|
1184
|
+
// Helper function to format timestamp
|
|
1185
|
+
const formatTime = (timestamp: number): string => {
|
|
1186
|
+
const date = new Date(timestamp);
|
|
1187
|
+
const now = new Date();
|
|
1188
|
+
const diffMs = now.getTime() - date.getTime();
|
|
1189
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
1190
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
1191
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
1192
|
+
|
|
1193
|
+
if (diffMins < 1) return 'just now';
|
|
1194
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
1195
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
1196
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
1197
|
+
return date.toLocaleDateString();
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
// Helper to get phase icon and text
|
|
1201
|
+
const getPhaseInfo = (phase: ProgressUpdate['phase']): { icon: string; text: string } => {
|
|
1202
|
+
const phases: Record<ProgressUpdate['phase'], { icon: string; text: string }> = {
|
|
1203
|
+
config_loaded: { icon: '⚙️', text: 'Loading configuration' },
|
|
1204
|
+
components_discovered: { icon: '🔍', text: 'Discovering components' },
|
|
1205
|
+
prompt_built: { icon: '📝', text: 'Building prompt' },
|
|
1206
|
+
llm_thinking: { icon: '🤔', text: 'AI is thinking' },
|
|
1207
|
+
code_extracted: { icon: '📦', text: 'Extracting code' },
|
|
1208
|
+
validating: { icon: '✅', text: 'Validating output' },
|
|
1209
|
+
post_processing: { icon: '🔧', text: 'Processing' },
|
|
1210
|
+
saving: { icon: '💾', text: 'Saving story' },
|
|
1211
|
+
};
|
|
1212
|
+
return phases[phase] || { icon: '⏳', text: 'Working' };
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
// Streaming Progress Message Component
|
|
1216
|
+
const StreamingProgressMessage: React.FC<{ streamingData: StreamingState }> = ({ streamingData }) => {
|
|
1217
|
+
const { intent, progress, validation, retry, completion, error } = streamingData;
|
|
1218
|
+
const [showCode, setShowCode] = useState(true); // Show code by default for better UX
|
|
1219
|
+
const [copyStatus, setCopyStatus] = useState<'idle' | 'copied'>('idle');
|
|
1220
|
+
|
|
1221
|
+
// Handle copy to clipboard
|
|
1222
|
+
const handleCopyCode = async (code: string) => {
|
|
600
1223
|
try {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
1224
|
+
await navigator.clipboard.writeText(code);
|
|
1225
|
+
setCopyStatus('copied');
|
|
1226
|
+
setTimeout(() => setCopyStatus('idle'), 2000);
|
|
1227
|
+
} catch (err) {
|
|
1228
|
+
console.error('Failed to copy:', err);
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
// If completed, show completion summary
|
|
1233
|
+
if (completion) {
|
|
1234
|
+
return (
|
|
1235
|
+
<div style={STYLES.streamingContainer}>
|
|
1236
|
+
<div style={STYLES.completionSummary}>
|
|
1237
|
+
<div style={STYLES.summaryTitle}>
|
|
1238
|
+
{completion.success ? '✅' : '❌'} {completion.title}
|
|
1239
|
+
</div>
|
|
1240
|
+
<div style={STYLES.summaryDescription}>
|
|
1241
|
+
{completion.summary.description}
|
|
1242
|
+
</div>
|
|
1243
|
+
|
|
1244
|
+
{/* Components Used */}
|
|
1245
|
+
{completion.componentsUsed.length > 0 && (
|
|
1246
|
+
<div style={{ marginTop: '10px' }}>
|
|
1247
|
+
<div style={{ fontSize: '12px', color: '#6b7280', marginBottom: '6px' }}>Components used:</div>
|
|
1248
|
+
<div style={STYLES.intentComponents}>
|
|
1249
|
+
{completion.componentsUsed.map((comp, i) => (
|
|
1250
|
+
<span key={i} style={STYLES.componentTag}>{comp.name}</span>
|
|
1251
|
+
))}
|
|
1252
|
+
</div>
|
|
1253
|
+
</div>
|
|
1254
|
+
)}
|
|
1255
|
+
|
|
1256
|
+
{/* Layout Choices */}
|
|
1257
|
+
{completion.layoutChoices.length > 0 && (
|
|
1258
|
+
<div style={{ marginTop: '10px' }}>
|
|
1259
|
+
<div style={{ fontSize: '12px', color: '#6b7280', marginBottom: '6px' }}>Layout:</div>
|
|
1260
|
+
<div style={{ fontSize: '12px', color: '#4b5563' }}>
|
|
1261
|
+
{completion.layoutChoices.map(l => l.pattern).join(', ')}
|
|
1262
|
+
</div>
|
|
1263
|
+
</div>
|
|
1264
|
+
)}
|
|
1265
|
+
|
|
1266
|
+
{/* Validation Status */}
|
|
1267
|
+
{completion.validation && !completion.validation.isValid && (
|
|
1268
|
+
<div style={{ ...STYLES.validationBox, ...STYLES.validationWarning }}>
|
|
1269
|
+
⚠️ {completion.validation.autoFixApplied ? 'Auto-fixed issues' : 'Minor issues detected'}
|
|
1270
|
+
</div>
|
|
1271
|
+
)}
|
|
1272
|
+
|
|
1273
|
+
{/* Suggestions */}
|
|
1274
|
+
{completion.suggestions && completion.suggestions.length > 0 && (
|
|
1275
|
+
<div style={{ marginTop: '10px', fontSize: '12px', color: '#6b7280' }}>
|
|
1276
|
+
💡 {completion.suggestions[0]}
|
|
1277
|
+
</div>
|
|
1278
|
+
)}
|
|
1279
|
+
|
|
1280
|
+
{/* Metrics */}
|
|
1281
|
+
{completion.metrics && (
|
|
1282
|
+
<div style={STYLES.metricsRow}>
|
|
1283
|
+
<span style={STYLES.metric}>⏱️ {(completion.metrics.totalTimeMs / 1000).toFixed(1)}s</span>
|
|
1284
|
+
<span style={STYLES.metric}>🔄 {completion.metrics.llmCallsCount} LLM calls</span>
|
|
1285
|
+
</div>
|
|
1286
|
+
)}
|
|
1287
|
+
|
|
1288
|
+
{/* Code Viewer - Show the generated story code */}
|
|
1289
|
+
{completion.code && (
|
|
1290
|
+
<div style={STYLES.codeViewerContainer}>
|
|
1291
|
+
<div
|
|
1292
|
+
style={STYLES.codeViewerToggle}
|
|
1293
|
+
onClick={() => setShowCode(!showCode)}
|
|
1294
|
+
role="button"
|
|
1295
|
+
tabIndex={0}
|
|
1296
|
+
onKeyDown={(e) => e.key === 'Enter' && setShowCode(!showCode)}
|
|
1297
|
+
>
|
|
1298
|
+
<span>{showCode ? '▼' : '▶'} View Generated Code</span>
|
|
1299
|
+
<span style={{ fontSize: '11px', color: '#6366f1' }}>{completion.fileName}</span>
|
|
1300
|
+
</div>
|
|
1301
|
+
{showCode && (
|
|
1302
|
+
<div style={STYLES.codeViewerContent}>
|
|
1303
|
+
<div style={STYLES.codeViewerHeader}>
|
|
1304
|
+
<span style={STYLES.codeViewerFileName}>{completion.fileName}</span>
|
|
1305
|
+
<button
|
|
1306
|
+
style={{
|
|
1307
|
+
...STYLES.copyButton,
|
|
1308
|
+
...(copyStatus === 'copied' ? STYLES.copyButtonSuccess : {})
|
|
1309
|
+
}}
|
|
1310
|
+
onClick={() => handleCopyCode(completion.code)}
|
|
1311
|
+
>
|
|
1312
|
+
{copyStatus === 'copied' ? '✓ Copied!' : 'Copy Code'}
|
|
1313
|
+
</button>
|
|
1314
|
+
</div>
|
|
1315
|
+
<pre style={STYLES.codeViewerPre}>
|
|
1316
|
+
<code>{completion.code}</code>
|
|
1317
|
+
</pre>
|
|
1318
|
+
</div>
|
|
1319
|
+
)}
|
|
1320
|
+
</div>
|
|
1321
|
+
)}
|
|
1322
|
+
</div>
|
|
1323
|
+
</div>
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// If error, show error
|
|
1328
|
+
if (error) {
|
|
1329
|
+
return (
|
|
1330
|
+
<div style={STYLES.streamingContainer}>
|
|
1331
|
+
<div style={{ ...STYLES.validationBox, ...STYLES.validationError }}>
|
|
1332
|
+
<strong>❌ {error.message}</strong>
|
|
1333
|
+
{error.details && <div style={{ marginTop: '4px' }}>{error.details}</div>}
|
|
1334
|
+
{error.suggestion && <div style={{ marginTop: '8px' }}>💡 {error.suggestion}</div>}
|
|
1335
|
+
</div>
|
|
1336
|
+
</div>
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// Show progress
|
|
1341
|
+
return (
|
|
1342
|
+
<div style={STYLES.streamingContainer}>
|
|
1343
|
+
{/* Intent Preview */}
|
|
1344
|
+
{intent && (
|
|
1345
|
+
<div style={STYLES.intentPreview}>
|
|
1346
|
+
<div style={STYLES.intentTitle}>
|
|
1347
|
+
{intent.requestType === 'modification' ? '✏️' : '✨'}
|
|
1348
|
+
{intent.requestType === 'modification' ? ' Modifying Story' : ' Creating New Story'}
|
|
1349
|
+
</div>
|
|
1350
|
+
<div style={STYLES.intentStrategy}>{intent.strategy}</div>
|
|
1351
|
+
{intent.detectedDesignSystem && (
|
|
1352
|
+
<div style={{ fontSize: '12px', color: '#6b7280' }}>
|
|
1353
|
+
Design system: <strong>{intent.detectedDesignSystem}</strong>
|
|
1354
|
+
</div>
|
|
1355
|
+
)}
|
|
1356
|
+
{intent.estimatedComponents.length > 0 && (
|
|
1357
|
+
<div style={STYLES.intentComponents}>
|
|
1358
|
+
{intent.estimatedComponents.map((comp, i) => (
|
|
1359
|
+
<span key={i} style={STYLES.componentTag}>{comp}</span>
|
|
1360
|
+
))}
|
|
1361
|
+
</div>
|
|
1362
|
+
)}
|
|
1363
|
+
</div>
|
|
1364
|
+
)}
|
|
1365
|
+
|
|
1366
|
+
{/* Progress Bar */}
|
|
1367
|
+
{progress && (
|
|
1368
|
+
<>
|
|
1369
|
+
<div style={STYLES.progressBar}>
|
|
1370
|
+
<div
|
|
1371
|
+
style={{
|
|
1372
|
+
...STYLES.progressFill,
|
|
1373
|
+
width: `${(progress.step / progress.totalSteps) * 100}%`
|
|
1374
|
+
}}
|
|
1375
|
+
/>
|
|
1376
|
+
</div>
|
|
1377
|
+
<div style={STYLES.progressPhase}>
|
|
1378
|
+
<span style={STYLES.phaseIcon}>{getPhaseInfo(progress.phase).icon}</span>
|
|
1379
|
+
<span>{progress.message || getPhaseInfo(progress.phase).text}</span>
|
|
1380
|
+
<span style={{ marginLeft: 'auto', color: '#9ca3af' }}>
|
|
1381
|
+
{progress.step}/{progress.totalSteps}
|
|
1382
|
+
</span>
|
|
1383
|
+
</div>
|
|
1384
|
+
</>
|
|
1385
|
+
)}
|
|
1386
|
+
|
|
1387
|
+
{/* Retry Badge */}
|
|
1388
|
+
{retry && (
|
|
1389
|
+
<div style={STYLES.retryBadge}>
|
|
1390
|
+
🔄 Retry {retry.attempt}/{retry.maxAttempts}: {retry.reason}
|
|
1391
|
+
</div>
|
|
1392
|
+
)}
|
|
1393
|
+
|
|
1394
|
+
{/* Validation Feedback */}
|
|
1395
|
+
{validation && !validation.isValid && (
|
|
1396
|
+
<div style={{ ...STYLES.validationBox, ...STYLES.validationWarning }}>
|
|
1397
|
+
{validation.autoFixApplied ? '🔧 Auto-fixing issues...' : '⚠️ Validation issues found'}
|
|
1398
|
+
{validation.errors.slice(0, 2).map((err, i) => (
|
|
1399
|
+
<div key={i} style={{ marginTop: '4px', fontSize: '11px' }}>• {err}</div>
|
|
1400
|
+
))}
|
|
1401
|
+
</div>
|
|
1402
|
+
)}
|
|
1403
|
+
|
|
1404
|
+
{/* Loading indicator when no specific phase */}
|
|
1405
|
+
{!progress && !intent && (
|
|
1406
|
+
<div style={STYLES.progressPhase}>
|
|
1407
|
+
<span className="loading-dots">Connecting</span>
|
|
1408
|
+
</div>
|
|
1409
|
+
)}
|
|
1410
|
+
</div>
|
|
1411
|
+
);
|
|
1412
|
+
};
|
|
1413
|
+
|
|
1414
|
+
// Main component
|
|
1415
|
+
function StoryUIPanel() {
|
|
1416
|
+
const [input, setInput] = useState('');
|
|
1417
|
+
const [conversation, setConversation] = useState<Message[]>([]);
|
|
1418
|
+
const [loading, setLoading] = useState(false);
|
|
1419
|
+
const [error, setError] = useState<string | null>(null);
|
|
1420
|
+
const [recentChats, setRecentChats] = useState<ChatSession[]>([]);
|
|
1421
|
+
const [activeChatId, setActiveChatId] = useState<string | null>(null);
|
|
1422
|
+
const [activeTitle, setActiveTitle] = useState<string>('');
|
|
1423
|
+
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
1424
|
+
const [connectionStatus, setConnectionStatus] = useState<{ connected: boolean; error?: string }>({ connected: false });
|
|
1425
|
+
const [availableProviders, setAvailableProviders] = useState<ProviderInfo[]>([]);
|
|
1426
|
+
const [selectedProvider, setSelectedProvider] = useState<string>('');
|
|
1427
|
+
const [selectedModel, setSelectedModel] = useState<string>('');
|
|
1428
|
+
const [streamingState, setStreamingState] = useState<StreamingState | null>(null);
|
|
1429
|
+
const [attachedImages, setAttachedImages] = useState<AttachedImage[]>([]);
|
|
1430
|
+
const [considerations, setConsiderations] = useState<string>('');
|
|
1431
|
+
const chatEndRef = useRef<HTMLDivElement | null>(null);
|
|
1432
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
1433
|
+
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
1434
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
1435
|
+
|
|
1436
|
+
// Maximum images allowed
|
|
1437
|
+
const MAX_IMAGES = 4;
|
|
1438
|
+
const MAX_IMAGE_SIZE_MB = 20;
|
|
1439
|
+
|
|
1440
|
+
// Helper to convert file to base64
|
|
1441
|
+
const fileToBase64 = (file: File): Promise<string> => {
|
|
1442
|
+
return new Promise((resolve, reject) => {
|
|
1443
|
+
const reader = new FileReader();
|
|
1444
|
+
reader.readAsDataURL(file);
|
|
1445
|
+
reader.onload = () => {
|
|
1446
|
+
const result = reader.result as string;
|
|
1447
|
+
// Extract base64 data (remove data:image/...;base64, prefix)
|
|
1448
|
+
const base64 = result.split(',')[1];
|
|
1449
|
+
resolve(base64);
|
|
1450
|
+
};
|
|
1451
|
+
reader.onerror = error => reject(error);
|
|
1452
|
+
});
|
|
1453
|
+
};
|
|
1454
|
+
|
|
1455
|
+
// Handle file selection
|
|
1456
|
+
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
1457
|
+
const files = e.target.files;
|
|
1458
|
+
if (!files) return;
|
|
1459
|
+
|
|
1460
|
+
const newImages: AttachedImage[] = [];
|
|
1461
|
+
const errors: string[] = [];
|
|
1462
|
+
|
|
1463
|
+
for (let i = 0; i < files.length && (attachedImages.length + newImages.length) < MAX_IMAGES; i++) {
|
|
1464
|
+
const file = files[i];
|
|
1465
|
+
|
|
1466
|
+
// Validate file type
|
|
1467
|
+
if (!file.type.startsWith('image/')) {
|
|
1468
|
+
errors.push(`${file.name}: Not an image file`);
|
|
1469
|
+
continue;
|
|
616
1470
|
}
|
|
617
|
-
|
|
618
|
-
const data = await res.json();
|
|
619
|
-
if (!res.ok || !data.success) throw new Error(data.error || 'Story generation failed');
|
|
620
1471
|
|
|
621
|
-
//
|
|
622
|
-
|
|
623
|
-
|
|
1472
|
+
// Validate file size
|
|
1473
|
+
if (file.size > MAX_IMAGE_SIZE_MB * 1024 * 1024) {
|
|
1474
|
+
errors.push(`${file.name}: File too large (max ${MAX_IMAGE_SIZE_MB}MB)`);
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
624
1477
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
1478
|
+
try {
|
|
1479
|
+
const base64 = await fileToBase64(file);
|
|
1480
|
+
const preview = URL.createObjectURL(file);
|
|
1481
|
+
|
|
1482
|
+
newImages.push({
|
|
1483
|
+
id: `${Date.now()}-${i}`,
|
|
1484
|
+
file,
|
|
1485
|
+
preview,
|
|
1486
|
+
base64,
|
|
1487
|
+
});
|
|
1488
|
+
} catch (err) {
|
|
1489
|
+
errors.push(`${file.name}: Failed to process`);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
630
1492
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
1493
|
+
if (errors.length > 0) {
|
|
1494
|
+
setError(errors.join('\n'));
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
setAttachedImages(prev => [...prev, ...newImages]);
|
|
1498
|
+
|
|
1499
|
+
// Reset file input
|
|
1500
|
+
if (fileInputRef.current) {
|
|
1501
|
+
fileInputRef.current.value = '';
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
|
|
1505
|
+
// Remove attached image
|
|
1506
|
+
const removeAttachedImage = (id: string) => {
|
|
1507
|
+
setAttachedImages(prev => {
|
|
1508
|
+
const removed = prev.find(img => img.id === id);
|
|
1509
|
+
if (removed) {
|
|
1510
|
+
URL.revokeObjectURL(removed.preview);
|
|
634
1511
|
}
|
|
1512
|
+
return prev.filter(img => img.id !== id);
|
|
1513
|
+
});
|
|
1514
|
+
};
|
|
635
1515
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
1516
|
+
// Clear all attached images
|
|
1517
|
+
const clearAttachedImages = () => {
|
|
1518
|
+
attachedImages.forEach(img => URL.revokeObjectURL(img.preview));
|
|
1519
|
+
setAttachedImages([]);
|
|
1520
|
+
};
|
|
1521
|
+
|
|
1522
|
+
// Drag and drop state
|
|
1523
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
1524
|
+
|
|
1525
|
+
// Handle drag events
|
|
1526
|
+
const handleDragEnter = useCallback((e: React.DragEvent) => {
|
|
1527
|
+
e.preventDefault();
|
|
1528
|
+
e.stopPropagation();
|
|
1529
|
+
if (e.dataTransfer.types.includes('Files')) {
|
|
1530
|
+
setIsDragging(true);
|
|
1531
|
+
}
|
|
1532
|
+
}, []);
|
|
1533
|
+
|
|
1534
|
+
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
|
1535
|
+
e.preventDefault();
|
|
1536
|
+
e.stopPropagation();
|
|
1537
|
+
// Only set to false if we're leaving the main container
|
|
1538
|
+
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
|
1539
|
+
setIsDragging(false);
|
|
1540
|
+
}
|
|
1541
|
+
}, []);
|
|
1542
|
+
|
|
1543
|
+
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
1544
|
+
e.preventDefault();
|
|
1545
|
+
e.stopPropagation();
|
|
1546
|
+
}, []);
|
|
1547
|
+
|
|
1548
|
+
const handleDrop = useCallback(async (e: React.DragEvent) => {
|
|
1549
|
+
e.preventDefault();
|
|
1550
|
+
e.stopPropagation();
|
|
1551
|
+
setIsDragging(false);
|
|
1552
|
+
|
|
1553
|
+
const files = Array.from(e.dataTransfer.files);
|
|
1554
|
+
const imageFiles = files.filter(f => f.type.startsWith('image/'));
|
|
1555
|
+
|
|
1556
|
+
if (imageFiles.length === 0) {
|
|
1557
|
+
setError('Please drop image files only');
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
const newImages: AttachedImage[] = [];
|
|
1562
|
+
const errors: string[] = [];
|
|
1563
|
+
|
|
1564
|
+
for (let i = 0; i < imageFiles.length && (attachedImages.length + newImages.length) < MAX_IMAGES; i++) {
|
|
1565
|
+
const file = imageFiles[i];
|
|
1566
|
+
|
|
1567
|
+
if (file.size > MAX_IMAGE_SIZE_MB * 1024 * 1024) {
|
|
1568
|
+
errors.push(`${file.name}: File too large (max ${MAX_IMAGE_SIZE_MB}MB)`);
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
try {
|
|
1573
|
+
const base64 = await fileToBase64(file);
|
|
1574
|
+
const preview = URL.createObjectURL(file);
|
|
1575
|
+
|
|
1576
|
+
newImages.push({
|
|
1577
|
+
id: `${Date.now()}-${i}`,
|
|
1578
|
+
file,
|
|
1579
|
+
preview,
|
|
1580
|
+
base64,
|
|
1581
|
+
});
|
|
1582
|
+
} catch (err) {
|
|
1583
|
+
errors.push(`${file.name}: Failed to process`);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
if (errors.length > 0) {
|
|
1588
|
+
setError(errors.join('\n'));
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
setAttachedImages(prev => [...prev, ...newImages]);
|
|
1592
|
+
}, [attachedImages.length, fileToBase64]);
|
|
1593
|
+
|
|
1594
|
+
// Handle clipboard paste for images
|
|
1595
|
+
const handlePaste = useCallback(async (e: React.ClipboardEvent) => {
|
|
1596
|
+
const items = e.clipboardData?.items;
|
|
1597
|
+
if (!items) return;
|
|
640
1598
|
|
|
641
|
-
|
|
642
|
-
|
|
1599
|
+
const imageItems: DataTransferItem[] = [];
|
|
1600
|
+
for (let i = 0; i < items.length; i++) {
|
|
1601
|
+
if (items[i].type.startsWith('image/')) {
|
|
1602
|
+
imageItems.push(items[i]);
|
|
643
1603
|
}
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
if (imageItems.length === 0) return;
|
|
1607
|
+
|
|
1608
|
+
// Prevent default text paste behavior when pasting images
|
|
1609
|
+
e.preventDefault();
|
|
1610
|
+
|
|
1611
|
+
if (attachedImages.length >= MAX_IMAGES) {
|
|
1612
|
+
setError(`Maximum ${MAX_IMAGES} images allowed`);
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
const newImages: AttachedImage[] = [];
|
|
1617
|
+
const errors: string[] = [];
|
|
1618
|
+
|
|
1619
|
+
for (let i = 0; i < imageItems.length && (attachedImages.length + newImages.length) < MAX_IMAGES; i++) {
|
|
1620
|
+
const item = imageItems[i];
|
|
1621
|
+
const file = item.getAsFile();
|
|
1622
|
+
|
|
1623
|
+
if (!file) {
|
|
1624
|
+
errors.push('Failed to get image from clipboard');
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
if (file.size > MAX_IMAGE_SIZE_MB * 1024 * 1024) {
|
|
1629
|
+
errors.push(`Pasted image too large (max ${MAX_IMAGE_SIZE_MB}MB)`);
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
try {
|
|
1634
|
+
const base64 = await fileToBase64(file);
|
|
1635
|
+
const preview = URL.createObjectURL(file);
|
|
1636
|
+
|
|
1637
|
+
// Create a meaningful name for pasted images
|
|
1638
|
+
const timestamp = new Date().toISOString().slice(11, 19).replace(/:/g, '-');
|
|
1639
|
+
const renamedFile = new File([file], `pasted-image-${timestamp}.${file.type.split('/')[1] || 'png'}`, { type: file.type });
|
|
1640
|
+
|
|
1641
|
+
newImages.push({
|
|
1642
|
+
id: `paste-${Date.now()}-${i}`,
|
|
1643
|
+
file: renamedFile,
|
|
1644
|
+
preview,
|
|
1645
|
+
base64,
|
|
1646
|
+
});
|
|
1647
|
+
} catch (err) {
|
|
1648
|
+
errors.push('Failed to process pasted image');
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
644
1651
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
1652
|
+
if (errors.length > 0) {
|
|
1653
|
+
setError(errors.join('\n'));
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
if (newImages.length > 0) {
|
|
1657
|
+
setAttachedImages(prev => [...prev, ...newImages]);
|
|
1658
|
+
// Clear any existing error on successful paste
|
|
1659
|
+
if (errors.length === 0) {
|
|
1660
|
+
setError(null);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}, [attachedImages.length, fileToBase64]);
|
|
648
1664
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
1665
|
+
// Load and sync chats on mount
|
|
1666
|
+
useEffect(() => {
|
|
1667
|
+
const initializeChats = async () => {
|
|
1668
|
+
// Test connection first
|
|
1669
|
+
const connectionTest = await testMCPConnection();
|
|
1670
|
+
setConnectionStatus(connectionTest);
|
|
1671
|
+
|
|
1672
|
+
if (connectionTest.connected) {
|
|
1673
|
+
// Fetch available providers
|
|
1674
|
+
try {
|
|
1675
|
+
const providersRes = await fetch(PROVIDERS_API);
|
|
1676
|
+
if (providersRes.ok) {
|
|
1677
|
+
const providersData: ProvidersResponse = await providersRes.json();
|
|
1678
|
+
setAvailableProviders(providersData.providers.filter(p => p.configured));
|
|
1679
|
+
// Set initial selection from server defaults
|
|
1680
|
+
if (providersData.current) {
|
|
1681
|
+
setSelectedProvider(providersData.current.provider.toLowerCase());
|
|
1682
|
+
setSelectedModel(providersData.current.model);
|
|
1683
|
+
}
|
|
654
1684
|
}
|
|
655
|
-
|
|
1685
|
+
} catch (e) {
|
|
1686
|
+
console.error('Failed to fetch providers:', e);
|
|
656
1687
|
}
|
|
657
1688
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
1689
|
+
// Fetch design system considerations for environment parity
|
|
1690
|
+
// This ensures production gets the same considerations as local development
|
|
1691
|
+
try {
|
|
1692
|
+
const considerationsRes = await fetch(CONSIDERATIONS_API);
|
|
1693
|
+
if (considerationsRes.ok) {
|
|
1694
|
+
const considerationsData = await considerationsRes.json();
|
|
1695
|
+
if (considerationsData.hasConsiderations && considerationsData.considerations) {
|
|
1696
|
+
setConsiderations(considerationsData.considerations);
|
|
1697
|
+
console.log(`Loaded considerations from ${considerationsData.source}`);
|
|
1698
|
+
}
|
|
663
1699
|
}
|
|
1700
|
+
} catch (e) {
|
|
1701
|
+
console.error('Failed to fetch considerations:', e);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
const syncedChats = await syncWithActualStories();
|
|
1705
|
+
const sortedChats = syncedChats.sort((a, b) => b.lastUpdated - a.lastUpdated).slice(0, MAX_RECENT_CHATS);
|
|
1706
|
+
setRecentChats(sortedChats);
|
|
1707
|
+
|
|
1708
|
+
if (sortedChats.length > 0) {
|
|
1709
|
+
setConversation(sortedChats[0].conversation);
|
|
1710
|
+
setActiveChatId(sortedChats[0].id);
|
|
1711
|
+
setActiveTitle(sortedChats[0].title);
|
|
664
1712
|
}
|
|
1713
|
+
} else {
|
|
1714
|
+
// Load from local storage if server is not available
|
|
1715
|
+
const localChats = loadChats();
|
|
1716
|
+
setRecentChats(localChats);
|
|
665
1717
|
}
|
|
1718
|
+
};
|
|
1719
|
+
|
|
1720
|
+
initializeChats();
|
|
1721
|
+
}, []);
|
|
666
1722
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
1723
|
+
// Scroll to bottom on new message
|
|
1724
|
+
useEffect(() => {
|
|
1725
|
+
if (chatEndRef.current) {
|
|
1726
|
+
chatEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
|
1727
|
+
}
|
|
1728
|
+
}, [conversation, loading]);
|
|
1729
|
+
|
|
1730
|
+
// Helper function for non-streaming fallback
|
|
1731
|
+
const handleSendNonStreaming = async (userInput: string, newConversation: Message[]) => {
|
|
1732
|
+
const res = await fetch(MCP_API, {
|
|
1733
|
+
method: 'POST',
|
|
1734
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1735
|
+
body: JSON.stringify({
|
|
1736
|
+
prompt: userInput,
|
|
1737
|
+
conversation: newConversation,
|
|
1738
|
+
fileName: activeChatId || undefined,
|
|
1739
|
+
provider: selectedProvider || undefined,
|
|
1740
|
+
model: selectedModel || undefined,
|
|
1741
|
+
considerations: considerations || undefined,
|
|
1742
|
+
}),
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1745
|
+
const contentType = res.headers.get('content-type');
|
|
1746
|
+
if (!contentType || !contentType.includes('application/json')) {
|
|
1747
|
+
const text = await res.text();
|
|
1748
|
+
throw new Error(`Server returned non-JSON response. Response: ${text.substring(0, 200)}...`);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const data = await res.json();
|
|
1752
|
+
if (!res.ok || !data.success) throw new Error(data.error || 'Story generation failed');
|
|
1753
|
+
|
|
1754
|
+
return data;
|
|
1755
|
+
};
|
|
1756
|
+
|
|
1757
|
+
// Helper function to build a conversational response from completion data
|
|
1758
|
+
// Uses special markers [SUCCESS], [ERROR], [TIP], [WRENCH] that renderMarkdown converts to icons
|
|
1759
|
+
// Track whether we've shown the refresh hint in this session
|
|
1760
|
+
const hasShownRefreshHint = useRef(false);
|
|
1761
|
+
|
|
1762
|
+
const buildConversationalResponse = (completion: CompletionFeedback, isUpdate: boolean): string => {
|
|
1763
|
+
const parts: string[] = [];
|
|
1764
|
+
const statusMarker = completion.success ? '[SUCCESS]' : '[ERROR]';
|
|
1765
|
+
|
|
1766
|
+
// Lead with the result - more conversational
|
|
1767
|
+
if (isUpdate) {
|
|
1768
|
+
parts.push(`${statusMarker} **Updated: "${completion.title}"**`);
|
|
1769
|
+
} else {
|
|
1770
|
+
parts.push(`${statusMarker} **Created: "${completion.title}"**`);
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// Build component insights with reasons when available
|
|
1774
|
+
const componentCount = completion.componentsUsed?.length || 0;
|
|
1775
|
+
if (componentCount > 0) {
|
|
1776
|
+
const componentList = completion.componentsUsed!.slice(0, 5);
|
|
1777
|
+
|
|
1778
|
+
// Check if we have meaningful reasons (not just "Used in composition")
|
|
1779
|
+
const componentsWithReasons = componentList.filter(c =>
|
|
1780
|
+
c.reason && c.reason !== 'Used in composition'
|
|
678
1781
|
);
|
|
679
1782
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
1783
|
+
if (componentsWithReasons.length > 0) {
|
|
1784
|
+
// Show components with their reasons
|
|
1785
|
+
const insights = componentsWithReasons
|
|
1786
|
+
.slice(0, 3)
|
|
1787
|
+
.map(c => `\`${c.name}\` - ${c.reason?.toLowerCase()}`)
|
|
1788
|
+
.join(', ');
|
|
1789
|
+
parts.push(`\nUsed ${insights}${componentCount > 3 ? ` and ${componentCount - 3} more` : ''}.`);
|
|
1790
|
+
} else {
|
|
1791
|
+
// Fallback to simple list
|
|
1792
|
+
const names = componentList.map(c => `\`${c.name}\``).join(', ');
|
|
1793
|
+
parts.push(`\nBuilt with ${names}${componentCount > 5 ? '...' : ''}.`);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
687
1796
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
title: chatTitle,
|
|
694
|
-
fileName: data.fileName || activeChatId,
|
|
695
|
-
conversation: updatedConversation,
|
|
696
|
-
lastUpdated: Date.now(),
|
|
697
|
-
};
|
|
1797
|
+
// Add layout decisions with educational context
|
|
1798
|
+
if (completion.layoutChoices && completion.layoutChoices.length > 0) {
|
|
1799
|
+
const primaryLayout = completion.layoutChoices[0];
|
|
1800
|
+
parts.push(`\n\n**Layout:** ${primaryLayout.pattern} - ${primaryLayout.reason.charAt(0).toLowerCase()}${primaryLayout.reason.slice(1)}.`);
|
|
1801
|
+
}
|
|
698
1802
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
1803
|
+
// Add style choices only if they add value
|
|
1804
|
+
if (completion.styleChoices && completion.styleChoices.length > 0) {
|
|
1805
|
+
const notableStyles = completion.styleChoices.filter(s =>
|
|
1806
|
+
s.reason && s.reason !== 'Semantic color from design system'
|
|
1807
|
+
);
|
|
1808
|
+
if (notableStyles.length > 0) {
|
|
1809
|
+
const styleInfo = notableStyles[0];
|
|
1810
|
+
parts.push(` Applied \`${styleInfo.value}\` for ${styleInfo.reason?.toLowerCase() || 'visual consistency'}.`);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
// Add validation fixes notice
|
|
1815
|
+
if (completion.validation?.autoFixApplied) {
|
|
1816
|
+
parts.push(`\n\n[WRENCH] **Auto-fixed:** Minor syntax issues were automatically corrected.`);
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
// Add suggestions only if meaningful
|
|
1820
|
+
if (completion.suggestions && completion.suggestions.length > 0) {
|
|
1821
|
+
const suggestion = completion.suggestions[0];
|
|
1822
|
+
// Only show if it's not the generic "review the generated code" message
|
|
1823
|
+
if (!suggestion.toLowerCase().includes('review the generated code')) {
|
|
1824
|
+
parts.push(`\n\n[TIP] **Tip:** ${suggestion}`);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
// Show refresh hint only once per session for new stories (local mode only)
|
|
1829
|
+
// In Edge mode, stories are stored in Durable Objects, not on filesystem
|
|
1830
|
+
if (!isUpdate && !hasShownRefreshHint.current) {
|
|
1831
|
+
if (isEdgeMode()) {
|
|
1832
|
+
parts.push(`\n\n_Story saved to cloud. View code in chat history above._`);
|
|
707
1833
|
} else {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
setActiveTitle(chatTitle);
|
|
713
|
-
|
|
714
|
-
const newSession: ChatSession = {
|
|
715
|
-
id: chatId,
|
|
716
|
-
title: chatTitle,
|
|
717
|
-
fileName: data.fileName || '',
|
|
718
|
-
conversation: updatedConversation,
|
|
719
|
-
lastUpdated: Date.now(),
|
|
720
|
-
};
|
|
1834
|
+
parts.push(`\n\n_Refresh Storybook (Cmd/Ctrl + R) to see new stories in the sidebar._`);
|
|
1835
|
+
}
|
|
1836
|
+
hasShownRefreshHint.current = true;
|
|
1837
|
+
}
|
|
721
1838
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
1839
|
+
// Add metrics in a subtle way (if available)
|
|
1840
|
+
if (completion.metrics?.totalTimeMs) {
|
|
1841
|
+
const seconds = (completion.metrics.totalTimeMs / 1000).toFixed(1);
|
|
1842
|
+
parts.push(`\n\n_${seconds}s_`);
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
return parts.join('');
|
|
1846
|
+
};
|
|
1847
|
+
|
|
1848
|
+
// Helper function to finalize conversation after streaming completes
|
|
1849
|
+
const finalizeStreamingConversation = useCallback((
|
|
1850
|
+
newConversation: Message[],
|
|
1851
|
+
completion: CompletionFeedback,
|
|
1852
|
+
userInput: string
|
|
1853
|
+
) => {
|
|
1854
|
+
// Build conversational response using rich completion data
|
|
1855
|
+
const isUpdate = completion.summary.action === 'updated';
|
|
1856
|
+
const responseMessage = buildConversationalResponse(completion, isUpdate);
|
|
1857
|
+
|
|
1858
|
+
const aiMsg: Message = { role: 'ai', content: responseMessage };
|
|
1859
|
+
const updatedConversation = [...newConversation, aiMsg];
|
|
1860
|
+
setConversation(updatedConversation);
|
|
1861
|
+
|
|
1862
|
+
// Update chat session
|
|
1863
|
+
const isExistingSession = activeChatId && conversation.length > 0;
|
|
1864
|
+
|
|
1865
|
+
if (isExistingSession && activeChatId) {
|
|
1866
|
+
const updatedSession: ChatSession = {
|
|
1867
|
+
id: activeChatId,
|
|
1868
|
+
title: activeTitle,
|
|
1869
|
+
fileName: completion.fileName || activeChatId,
|
|
1870
|
+
conversation: updatedConversation,
|
|
1871
|
+
lastUpdated: Date.now(),
|
|
1872
|
+
};
|
|
1873
|
+
|
|
1874
|
+
const chats = loadChats();
|
|
1875
|
+
const chatIndex = chats.findIndex(c => c.id === activeChatId);
|
|
1876
|
+
if (chatIndex !== -1) {
|
|
1877
|
+
chats[chatIndex] = updatedSession;
|
|
730
1878
|
}
|
|
1879
|
+
saveChats(chats);
|
|
1880
|
+
setRecentChats(chats);
|
|
1881
|
+
} else {
|
|
1882
|
+
const chatId = completion.storyId || completion.fileName || Date.now().toString();
|
|
1883
|
+
const chatTitle = completion.title || userInput;
|
|
1884
|
+
setActiveChatId(chatId);
|
|
1885
|
+
setActiveTitle(chatTitle);
|
|
1886
|
+
|
|
1887
|
+
const newSession: ChatSession = {
|
|
1888
|
+
id: chatId,
|
|
1889
|
+
title: chatTitle,
|
|
1890
|
+
fileName: completion.fileName || '',
|
|
1891
|
+
conversation: updatedConversation,
|
|
1892
|
+
lastUpdated: Date.now(),
|
|
1893
|
+
};
|
|
1894
|
+
|
|
1895
|
+
const chats = loadChats().filter(c => c.id !== chatId);
|
|
1896
|
+
chats.unshift(newSession);
|
|
1897
|
+
if (chats.length > MAX_RECENT_CHATS) {
|
|
1898
|
+
chats.splice(MAX_RECENT_CHATS);
|
|
1899
|
+
}
|
|
1900
|
+
saveChats(chats);
|
|
1901
|
+
setRecentChats(chats);
|
|
1902
|
+
}
|
|
1903
|
+
}, [activeChatId, activeTitle, conversation.length]);
|
|
731
1904
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1905
|
+
const handleSend = async (e?: React.FormEvent) => {
|
|
1906
|
+
if (e) e.preventDefault();
|
|
1907
|
+
// Allow sending with either text or images
|
|
1908
|
+
if (!input.trim() && attachedImages.length === 0) return;
|
|
1909
|
+
|
|
1910
|
+
// Use input text or default vision prompt if only images
|
|
1911
|
+
const userInput = input.trim() || (attachedImages.length > 0 ? 'Create a component that matches this design' : '');
|
|
1912
|
+
setError(null);
|
|
1913
|
+
setLoading(true);
|
|
1914
|
+
setStreamingState(null);
|
|
1915
|
+
|
|
1916
|
+
// Test connection before sending
|
|
1917
|
+
const connectionTest = await testMCPConnection();
|
|
1918
|
+
setConnectionStatus(connectionTest);
|
|
1919
|
+
|
|
1920
|
+
if (!connectionTest.connected) {
|
|
1921
|
+
setError(`Cannot connect to MCP server: ${connectionTest.error || 'Server not running'}`);
|
|
1922
|
+
setLoading(false);
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
// Capture images before clearing
|
|
1927
|
+
const imagesToSend = [...attachedImages];
|
|
1928
|
+
const hasImages = imagesToSend.length > 0;
|
|
750
1929
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1930
|
+
// Create user message with images
|
|
1931
|
+
const userMessage: Message = {
|
|
1932
|
+
role: 'user',
|
|
1933
|
+
content: userInput,
|
|
1934
|
+
attachedImages: hasImages ? imagesToSend : undefined
|
|
1935
|
+
};
|
|
1936
|
+
const newConversation: Message[] = [...conversation, userMessage];
|
|
1937
|
+
setConversation(newConversation);
|
|
1938
|
+
setInput('');
|
|
1939
|
+
clearAttachedImages();
|
|
1940
|
+
|
|
1941
|
+
// Use streaming if enabled
|
|
1942
|
+
if (USE_STREAMING) {
|
|
1943
|
+
try {
|
|
1944
|
+
// Cancel any existing request
|
|
1945
|
+
if (abortControllerRef.current) {
|
|
1946
|
+
abortControllerRef.current.abort();
|
|
1947
|
+
}
|
|
1948
|
+
abortControllerRef.current = new AbortController();
|
|
1949
|
+
|
|
1950
|
+
// Initialize streaming state
|
|
1951
|
+
setStreamingState({});
|
|
1952
|
+
|
|
1953
|
+
// Prepare images for API request
|
|
1954
|
+
const imagePayload = hasImages
|
|
1955
|
+
? imagesToSend.map(img => ({
|
|
1956
|
+
type: 'base64' as const,
|
|
1957
|
+
data: img.base64,
|
|
1958
|
+
mediaType: img.file.type,
|
|
1959
|
+
}))
|
|
1960
|
+
: undefined;
|
|
1961
|
+
|
|
1962
|
+
const response = await fetch(MCP_STREAM_API, {
|
|
1963
|
+
method: 'POST',
|
|
1964
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1965
|
+
body: JSON.stringify({
|
|
1966
|
+
prompt: userInput,
|
|
1967
|
+
conversation: newConversation,
|
|
1968
|
+
fileName: activeChatId || undefined,
|
|
1969
|
+
isUpdate: activeChatId && conversation.length > 0,
|
|
1970
|
+
originalTitle: activeTitle || undefined,
|
|
1971
|
+
storyId: activeChatId || undefined,
|
|
1972
|
+
images: imagePayload,
|
|
1973
|
+
visionMode: hasImages ? 'screenshot_to_story' : undefined,
|
|
1974
|
+
provider: selectedProvider || undefined,
|
|
1975
|
+
model: selectedModel || undefined,
|
|
1976
|
+
considerations: considerations || undefined,
|
|
1977
|
+
}),
|
|
1978
|
+
signal: abortControllerRef.current.signal,
|
|
1979
|
+
});
|
|
1980
|
+
|
|
1981
|
+
if (!response.ok) {
|
|
1982
|
+
throw new Error(`Streaming request failed: ${response.status}`);
|
|
755
1983
|
}
|
|
756
|
-
saveChats(chats);
|
|
757
|
-
setRecentChats(chats);
|
|
758
|
-
} else {
|
|
759
|
-
// Create new chat session for error (so retries can continue it)
|
|
760
|
-
const chatId = `error-${Date.now()}`;
|
|
761
|
-
const chatTitle = input.length > 30 ? input.substring(0, 30) + '...' : input;
|
|
762
|
-
setActiveChatId(chatId);
|
|
763
|
-
setActiveTitle(chatTitle);
|
|
764
|
-
|
|
765
|
-
const newSession: ChatSession = {
|
|
766
|
-
id: chatId,
|
|
767
|
-
title: chatTitle,
|
|
768
|
-
fileName: '',
|
|
769
|
-
conversation: errorConversation,
|
|
770
|
-
lastUpdated: Date.now(),
|
|
771
|
-
};
|
|
772
1984
|
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
chats.splice(MAX_RECENT_CHATS);
|
|
1985
|
+
const reader = response.body?.getReader();
|
|
1986
|
+
if (!reader) {
|
|
1987
|
+
throw new Error('No response body');
|
|
777
1988
|
}
|
|
778
|
-
|
|
779
|
-
|
|
1989
|
+
|
|
1990
|
+
const decoder = new TextDecoder();
|
|
1991
|
+
let buffer = '';
|
|
1992
|
+
let completionData: CompletionFeedback | null = null;
|
|
1993
|
+
let errorData: ErrorFeedback | null = null;
|
|
1994
|
+
|
|
1995
|
+
while (true) {
|
|
1996
|
+
const { done, value } = await reader.read();
|
|
1997
|
+
if (done) break;
|
|
1998
|
+
|
|
1999
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2000
|
+
|
|
2001
|
+
// Parse SSE events from buffer
|
|
2002
|
+
const lines = buffer.split('\n');
|
|
2003
|
+
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
2004
|
+
|
|
2005
|
+
for (const line of lines) {
|
|
2006
|
+
if (line.startsWith('data: ')) {
|
|
2007
|
+
try {
|
|
2008
|
+
const event: StreamEvent = JSON.parse(line.slice(6));
|
|
2009
|
+
|
|
2010
|
+
// Update streaming state based on event type
|
|
2011
|
+
switch (event.type) {
|
|
2012
|
+
case 'intent':
|
|
2013
|
+
setStreamingState(prev => ({ ...prev, intent: event.data as IntentPreview }));
|
|
2014
|
+
break;
|
|
2015
|
+
case 'progress':
|
|
2016
|
+
setStreamingState(prev => ({ ...prev, progress: event.data as ProgressUpdate }));
|
|
2017
|
+
break;
|
|
2018
|
+
case 'validation':
|
|
2019
|
+
setStreamingState(prev => ({ ...prev, validation: event.data as ValidationFeedback }));
|
|
2020
|
+
break;
|
|
2021
|
+
case 'retry':
|
|
2022
|
+
setStreamingState(prev => ({ ...prev, retry: event.data as RetryInfo }));
|
|
2023
|
+
break;
|
|
2024
|
+
case 'completion':
|
|
2025
|
+
completionData = event.data as CompletionFeedback;
|
|
2026
|
+
setStreamingState(prev => ({ ...prev, completion: event.data as CompletionFeedback }));
|
|
2027
|
+
break;
|
|
2028
|
+
case 'error':
|
|
2029
|
+
errorData = event.data as ErrorFeedback;
|
|
2030
|
+
setStreamingState(prev => ({ ...prev, error: event.data as ErrorFeedback }));
|
|
2031
|
+
break;
|
|
2032
|
+
}
|
|
2033
|
+
} catch (parseError) {
|
|
2034
|
+
console.warn('Failed to parse SSE event:', line, parseError);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// Handle completion or error
|
|
2041
|
+
if (completionData) {
|
|
2042
|
+
finalizeStreamingConversation(newConversation, completionData, userInput);
|
|
2043
|
+
} else if (errorData) {
|
|
2044
|
+
setError(errorData.message);
|
|
2045
|
+
const errorConversation = [...newConversation, { role: 'ai' as const, content: `Error: ${errorData.message}\n\n${errorData.suggestion || ''}` }];
|
|
2046
|
+
setConversation(errorConversation);
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
} catch (err: unknown) {
|
|
2050
|
+
if ((err as Error).name === 'AbortError') {
|
|
2051
|
+
console.log('Request aborted');
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
// Fall back to non-streaming on error
|
|
2056
|
+
console.warn('Streaming failed, falling back to non-streaming:', err);
|
|
2057
|
+
setStreamingState(null);
|
|
2058
|
+
|
|
2059
|
+
try {
|
|
2060
|
+
const data = await handleSendNonStreaming(userInput, newConversation);
|
|
2061
|
+
|
|
2062
|
+
// Process non-streaming response (same as before)
|
|
2063
|
+
let responseMessage: string;
|
|
2064
|
+
const statusIcon = data.validation?.hasWarnings ? (data.validation.errors?.length > 0 ? '🔧' : '⚠️') : '✅';
|
|
2065
|
+
|
|
2066
|
+
// Build conversational response for fallback
|
|
2067
|
+
if (data.isUpdate) {
|
|
2068
|
+
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._`;
|
|
2069
|
+
} else {
|
|
2070
|
+
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).`;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
const aiMsg: Message = { role: 'ai', content: responseMessage };
|
|
2074
|
+
const updatedConversation = [...newConversation, aiMsg];
|
|
2075
|
+
setConversation(updatedConversation);
|
|
2076
|
+
|
|
2077
|
+
// Update chat session
|
|
2078
|
+
const isUpdate = activeChatId && conversation.length > 0;
|
|
2079
|
+
if (isUpdate && activeChatId) {
|
|
2080
|
+
const updatedSession: ChatSession = {
|
|
2081
|
+
id: activeChatId,
|
|
2082
|
+
title: activeTitle,
|
|
2083
|
+
fileName: data.fileName || activeChatId,
|
|
2084
|
+
conversation: updatedConversation,
|
|
2085
|
+
lastUpdated: Date.now(),
|
|
2086
|
+
};
|
|
2087
|
+
const chats = loadChats();
|
|
2088
|
+
const chatIndex = chats.findIndex(c => c.id === activeChatId);
|
|
2089
|
+
if (chatIndex !== -1) chats[chatIndex] = updatedSession;
|
|
2090
|
+
saveChats(chats);
|
|
2091
|
+
setRecentChats(chats);
|
|
2092
|
+
} else {
|
|
2093
|
+
const chatId = data.storyId || data.fileName || Date.now().toString();
|
|
2094
|
+
setActiveChatId(chatId);
|
|
2095
|
+
setActiveTitle(data.title || userInput);
|
|
2096
|
+
const newSession: ChatSession = {
|
|
2097
|
+
id: chatId,
|
|
2098
|
+
title: data.title || userInput,
|
|
2099
|
+
fileName: data.fileName || '',
|
|
2100
|
+
conversation: updatedConversation,
|
|
2101
|
+
lastUpdated: Date.now(),
|
|
2102
|
+
};
|
|
2103
|
+
const chats = loadChats().filter(c => c.id !== chatId);
|
|
2104
|
+
chats.unshift(newSession);
|
|
2105
|
+
if (chats.length > MAX_RECENT_CHATS) chats.splice(MAX_RECENT_CHATS);
|
|
2106
|
+
saveChats(chats);
|
|
2107
|
+
setRecentChats(chats);
|
|
2108
|
+
}
|
|
2109
|
+
} catch (fallbackErr: unknown) {
|
|
2110
|
+
const errorMessage = fallbackErr instanceof Error ? fallbackErr.message : 'Unknown error';
|
|
2111
|
+
setError(errorMessage);
|
|
2112
|
+
const errorConversation = [...newConversation, { role: 'ai' as const, content: `Error: ${errorMessage}` }];
|
|
2113
|
+
setConversation(errorConversation);
|
|
2114
|
+
}
|
|
2115
|
+
} finally {
|
|
2116
|
+
setLoading(false);
|
|
2117
|
+
setStreamingState(null);
|
|
2118
|
+
abortControllerRef.current = null;
|
|
2119
|
+
}
|
|
2120
|
+
} else {
|
|
2121
|
+
// Non-streaming mode (original implementation)
|
|
2122
|
+
try {
|
|
2123
|
+
const data = await handleSendNonStreaming(userInput, newConversation);
|
|
2124
|
+
|
|
2125
|
+
let responseMessage: string;
|
|
2126
|
+
const statusIcon = data.validation?.hasWarnings ? (data.validation.errors?.length > 0 ? '🔧' : '⚠️') : '✅';
|
|
2127
|
+
|
|
2128
|
+
// Build conversational response for non-streaming mode
|
|
2129
|
+
if (data.isUpdate) {
|
|
2130
|
+
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._`;
|
|
2131
|
+
} else {
|
|
2132
|
+
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).`;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
const aiMsg: Message = { role: 'ai', content: responseMessage };
|
|
2136
|
+
const updatedConversation = [...newConversation, aiMsg];
|
|
2137
|
+
setConversation(updatedConversation);
|
|
2138
|
+
|
|
2139
|
+
const isUpdate = activeChatId && conversation.length > 0;
|
|
2140
|
+
if (isUpdate && activeChatId) {
|
|
2141
|
+
const updatedSession: ChatSession = {
|
|
2142
|
+
id: activeChatId,
|
|
2143
|
+
title: activeTitle,
|
|
2144
|
+
fileName: data.fileName || activeChatId,
|
|
2145
|
+
conversation: updatedConversation,
|
|
2146
|
+
lastUpdated: Date.now(),
|
|
2147
|
+
};
|
|
2148
|
+
const chats = loadChats();
|
|
2149
|
+
const chatIndex = chats.findIndex(c => c.id === activeChatId);
|
|
2150
|
+
if (chatIndex !== -1) chats[chatIndex] = updatedSession;
|
|
2151
|
+
saveChats(chats);
|
|
2152
|
+
setRecentChats(chats);
|
|
2153
|
+
} else {
|
|
2154
|
+
const chatId = data.storyId || data.fileName || Date.now().toString();
|
|
2155
|
+
setActiveChatId(chatId);
|
|
2156
|
+
setActiveTitle(data.title || userInput);
|
|
2157
|
+
const newSession: ChatSession = {
|
|
2158
|
+
id: chatId,
|
|
2159
|
+
title: data.title || userInput,
|
|
2160
|
+
fileName: data.fileName || '',
|
|
2161
|
+
conversation: updatedConversation,
|
|
2162
|
+
lastUpdated: Date.now(),
|
|
2163
|
+
};
|
|
2164
|
+
const chats = loadChats().filter(c => c.id !== chatId);
|
|
2165
|
+
chats.unshift(newSession);
|
|
2166
|
+
if (chats.length > MAX_RECENT_CHATS) chats.splice(MAX_RECENT_CHATS);
|
|
2167
|
+
saveChats(chats);
|
|
2168
|
+
setRecentChats(chats);
|
|
2169
|
+
}
|
|
2170
|
+
} catch (err: unknown) {
|
|
2171
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
2172
|
+
setError(errorMessage);
|
|
2173
|
+
const errorConversation = [...newConversation, { role: 'ai' as const, content: `Error: ${errorMessage}` }];
|
|
2174
|
+
setConversation(errorConversation);
|
|
2175
|
+
} finally {
|
|
2176
|
+
setLoading(false);
|
|
780
2177
|
}
|
|
781
|
-
} finally {
|
|
782
|
-
setLoading(false);
|
|
783
2178
|
}
|
|
784
2179
|
};
|
|
785
2180
|
|
|
@@ -844,7 +2239,8 @@ export function StoryUIPanel() {
|
|
|
844
2239
|
e.currentTarget.style.boxShadow = '0 2px 8px rgba(59, 130, 246, 0.3)';
|
|
845
2240
|
}}
|
|
846
2241
|
>
|
|
847
|
-
|
|
2242
|
+
<span style={{ lineHeight: '0.5', display: 'inline-block', alignItems: 'center', width: '10px', height: '10px' }}>☰</span>
|
|
2243
|
+
<span>Chats</span>
|
|
848
2244
|
</button>
|
|
849
2245
|
<button
|
|
850
2246
|
onClick={handleNewChat}
|
|
@@ -858,7 +2254,8 @@ export function StoryUIPanel() {
|
|
|
858
2254
|
e.currentTarget.style.boxShadow = '0 2px 8px rgba(59, 130, 246, 0.2)';
|
|
859
2255
|
}}
|
|
860
2256
|
>
|
|
861
|
-
|
|
2257
|
+
<span style={{ lineHeight: '0.5', display: 'inline-block', alignItems: 'center', width: '10px', height: '10px' }}>+</span>
|
|
2258
|
+
<span>New Chat</span>
|
|
862
2259
|
</button>
|
|
863
2260
|
{recentChats.length > 0 && (
|
|
864
2261
|
<div style={{
|
|
@@ -910,34 +2307,53 @@ export function StoryUIPanel() {
|
|
|
910
2307
|
</div>
|
|
911
2308
|
)}
|
|
912
2309
|
{!sidebarOpen && (
|
|
913
|
-
<div style={{ padding: '
|
|
2310
|
+
<div style={{ padding: '8px', display: 'flex', justifyContent: 'center' }}>
|
|
914
2311
|
<button
|
|
915
2312
|
onClick={() => setSidebarOpen(true)}
|
|
916
2313
|
style={{
|
|
917
2314
|
...STYLES.sidebarToggle,
|
|
918
|
-
width: '
|
|
919
|
-
height: '
|
|
2315
|
+
width: '38px',
|
|
2316
|
+
height: '38px',
|
|
920
2317
|
padding: '0',
|
|
921
2318
|
fontSize: '16px',
|
|
2319
|
+
borderRadius: '8px',
|
|
922
2320
|
}}
|
|
923
2321
|
title="Expand sidebar"
|
|
924
2322
|
onMouseEnter={(e) => {
|
|
925
2323
|
e.currentTarget.style.transform = 'scale(1.05)';
|
|
926
|
-
e.currentTarget.style.
|
|
2324
|
+
e.currentTarget.style.background = '#2563eb';
|
|
927
2325
|
}}
|
|
928
2326
|
onMouseLeave={(e) => {
|
|
929
2327
|
e.currentTarget.style.transform = 'scale(1)';
|
|
930
|
-
e.currentTarget.style.
|
|
2328
|
+
e.currentTarget.style.background = '#3b82f6';
|
|
931
2329
|
}}
|
|
932
2330
|
>
|
|
933
|
-
|
|
2331
|
+
<span style={{ lineHeight: '0.4', display: 'inline-block', height: '10px' }}>☰</span>
|
|
934
2332
|
</button>
|
|
935
2333
|
</div>
|
|
936
2334
|
)}
|
|
937
2335
|
</div>
|
|
938
2336
|
|
|
939
2337
|
{/* Main content */}
|
|
940
|
-
<div
|
|
2338
|
+
<div
|
|
2339
|
+
style={{ ...STYLES.mainContent, position: 'relative' as const }}
|
|
2340
|
+
onDragEnter={handleDragEnter}
|
|
2341
|
+
onDragLeave={handleDragLeave}
|
|
2342
|
+
onDragOver={handleDragOver}
|
|
2343
|
+
onDrop={handleDrop}
|
|
2344
|
+
>
|
|
2345
|
+
{/* Drop zone overlay */}
|
|
2346
|
+
{isDragging && (
|
|
2347
|
+
<div style={STYLES.dropOverlay}>
|
|
2348
|
+
<div style={STYLES.dropOverlayText}>
|
|
2349
|
+
<svg width={24} height={24} viewBox="0 0 24 24" fill="currentColor">
|
|
2350
|
+
<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"/>
|
|
2351
|
+
</svg>
|
|
2352
|
+
Drop images here
|
|
2353
|
+
</div>
|
|
2354
|
+
</div>
|
|
2355
|
+
)}
|
|
2356
|
+
|
|
941
2357
|
<div style={STYLES.chatHeader}>
|
|
942
2358
|
<h1 style={{
|
|
943
2359
|
fontSize: '24px',
|
|
@@ -967,12 +2383,74 @@ export function StoryUIPanel() {
|
|
|
967
2383
|
backgroundColor: connectionStatus.connected ? '#10b981' : '#f87171'
|
|
968
2384
|
}}></div>
|
|
969
2385
|
<span style={{ color: connectionStatus.connected ? '#10b981' : '#f87171' }}>
|
|
970
|
-
{connectionStatus.connected
|
|
971
|
-
? `Connected to
|
|
2386
|
+
{connectionStatus.connected
|
|
2387
|
+
? `Connected to ${getConnectionDisplayText()}`
|
|
972
2388
|
: `Disconnected: ${connectionStatus.error || 'Server not running'}`
|
|
973
2389
|
}
|
|
974
2390
|
</span>
|
|
975
2391
|
</div>
|
|
2392
|
+
|
|
2393
|
+
{/* LLM Provider/Model Selection */}
|
|
2394
|
+
{connectionStatus.connected && availableProviders.length > 0 && (
|
|
2395
|
+
<div style={{
|
|
2396
|
+
display: 'flex',
|
|
2397
|
+
gap: '12px',
|
|
2398
|
+
marginTop: '12px',
|
|
2399
|
+
flexWrap: 'wrap'
|
|
2400
|
+
}}>
|
|
2401
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
2402
|
+
<label style={{ fontSize: '12px', color: '#94a3b8' }}>Provider:</label>
|
|
2403
|
+
<select
|
|
2404
|
+
value={selectedProvider}
|
|
2405
|
+
onChange={(e) => {
|
|
2406
|
+
const newProvider = e.target.value;
|
|
2407
|
+
setSelectedProvider(newProvider);
|
|
2408
|
+
// Reset model to first available for new provider
|
|
2409
|
+
const provider = availableProviders.find(p => p.type === newProvider);
|
|
2410
|
+
if (provider && provider.models.length > 0) {
|
|
2411
|
+
setSelectedModel(provider.models[0]);
|
|
2412
|
+
}
|
|
2413
|
+
}}
|
|
2414
|
+
style={{
|
|
2415
|
+
background: '#1e293b',
|
|
2416
|
+
border: '1px solid #334155',
|
|
2417
|
+
borderRadius: '6px',
|
|
2418
|
+
color: '#e2e8f0',
|
|
2419
|
+
padding: '4px 8px',
|
|
2420
|
+
fontSize: '12px',
|
|
2421
|
+
cursor: 'pointer'
|
|
2422
|
+
}}
|
|
2423
|
+
>
|
|
2424
|
+
{availableProviders.map(p => (
|
|
2425
|
+
<option key={p.type} value={p.type}>{p.name}</option>
|
|
2426
|
+
))}
|
|
2427
|
+
</select>
|
|
2428
|
+
</div>
|
|
2429
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
2430
|
+
<label style={{ fontSize: '12px', color: '#94a3b8' }}>Model:</label>
|
|
2431
|
+
<select
|
|
2432
|
+
value={selectedModel}
|
|
2433
|
+
onChange={(e) => setSelectedModel(e.target.value)}
|
|
2434
|
+
style={{
|
|
2435
|
+
background: '#1e293b',
|
|
2436
|
+
border: '1px solid #334155',
|
|
2437
|
+
borderRadius: '6px',
|
|
2438
|
+
color: '#e2e8f0',
|
|
2439
|
+
padding: '4px 8px',
|
|
2440
|
+
fontSize: '12px',
|
|
2441
|
+
cursor: 'pointer',
|
|
2442
|
+
maxWidth: '200px'
|
|
2443
|
+
}}
|
|
2444
|
+
>
|
|
2445
|
+
{availableProviders
|
|
2446
|
+
.find(p => p.type === selectedProvider)
|
|
2447
|
+
?.models.map(model => (
|
|
2448
|
+
<option key={model} value={model}>{model}</option>
|
|
2449
|
+
))}
|
|
2450
|
+
</select>
|
|
2451
|
+
</div>
|
|
2452
|
+
</div>
|
|
2453
|
+
)}
|
|
976
2454
|
</div>
|
|
977
2455
|
|
|
978
2456
|
<div style={STYLES.chatContainer}>
|
|
@@ -994,30 +2472,125 @@ export function StoryUIPanel() {
|
|
|
994
2472
|
{conversation.map((msg, i) => (
|
|
995
2473
|
<div key={i} style={STYLES.messageContainer}>
|
|
996
2474
|
<div style={msg.role === 'user' ? STYLES.userMessage : STYLES.aiMessage}>
|
|
997
|
-
{msg.content}
|
|
2475
|
+
{msg.role === 'ai' ? renderMarkdown(msg.content) : msg.content}
|
|
2476
|
+
{/* Show attached images in user messages */}
|
|
2477
|
+
{msg.role === 'user' && msg.attachedImages && msg.attachedImages.length > 0 && (
|
|
2478
|
+
<div style={STYLES.userMessageImages}>
|
|
2479
|
+
{msg.attachedImages.map((img) => (
|
|
2480
|
+
<img
|
|
2481
|
+
key={img.id}
|
|
2482
|
+
src={img.preview}
|
|
2483
|
+
alt="attached"
|
|
2484
|
+
style={STYLES.userMessageImage}
|
|
2485
|
+
/>
|
|
2486
|
+
))}
|
|
2487
|
+
</div>
|
|
2488
|
+
)}
|
|
998
2489
|
</div>
|
|
999
2490
|
</div>
|
|
1000
2491
|
))}
|
|
1001
2492
|
|
|
1002
2493
|
{loading && (
|
|
1003
2494
|
<div style={STYLES.messageContainer}>
|
|
1004
|
-
|
|
1005
|
-
<
|
|
1006
|
-
|
|
1007
|
-
|
|
2495
|
+
{streamingState ? (
|
|
2496
|
+
<StreamingProgressMessage streamingData={streamingState} />
|
|
2497
|
+
) : (
|
|
2498
|
+
<div style={STYLES.loadingMessage}>
|
|
2499
|
+
<span>Generating story</span>
|
|
2500
|
+
<span className="loading-dots"></span>
|
|
2501
|
+
</div>
|
|
2502
|
+
)}
|
|
1008
2503
|
</div>
|
|
1009
2504
|
)}
|
|
1010
2505
|
|
|
1011
2506
|
<div ref={chatEndRef} />
|
|
1012
2507
|
</div>
|
|
1013
2508
|
|
|
1014
|
-
|
|
2509
|
+
{/* Hidden file input */}
|
|
2510
|
+
<input
|
|
2511
|
+
ref={fileInputRef}
|
|
2512
|
+
type="file"
|
|
2513
|
+
accept="image/*"
|
|
2514
|
+
multiple
|
|
2515
|
+
style={{ display: 'none' }}
|
|
2516
|
+
onChange={handleFileSelect}
|
|
2517
|
+
/>
|
|
2518
|
+
|
|
2519
|
+
{/* Image preview area */}
|
|
2520
|
+
{attachedImages.length > 0 && (
|
|
2521
|
+
<div style={STYLES.imagePreviewContainer}>
|
|
2522
|
+
<span style={STYLES.imagePreviewLabel}>
|
|
2523
|
+
<svg width={14} height={14} viewBox="0 0 24 24" fill="currentColor">
|
|
2524
|
+
<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"/>
|
|
2525
|
+
</svg>
|
|
2526
|
+
{attachedImages.length} image{attachedImages.length > 1 ? 's' : ''} attached
|
|
2527
|
+
</span>
|
|
2528
|
+
{attachedImages.map((img) => (
|
|
2529
|
+
<div key={img.id} style={STYLES.imagePreviewItem}>
|
|
2530
|
+
<img src={img.preview} alt="preview" style={STYLES.imagePreviewImg} />
|
|
2531
|
+
<button
|
|
2532
|
+
type="button"
|
|
2533
|
+
style={STYLES.imageRemoveButton}
|
|
2534
|
+
onClick={() => removeAttachedImage(img.id)}
|
|
2535
|
+
title="Remove image"
|
|
2536
|
+
>
|
|
2537
|
+
×
|
|
2538
|
+
</button>
|
|
2539
|
+
</div>
|
|
2540
|
+
))}
|
|
2541
|
+
</div>
|
|
2542
|
+
)}
|
|
2543
|
+
|
|
2544
|
+
<form onSubmit={handleSend} style={{
|
|
2545
|
+
...STYLES.inputForm,
|
|
2546
|
+
...(attachedImages.length > 0 ? {
|
|
2547
|
+
marginTop: 0,
|
|
2548
|
+
borderTopLeftRadius: 0,
|
|
2549
|
+
borderTopRightRadius: 0,
|
|
2550
|
+
} : {})
|
|
2551
|
+
}}>
|
|
2552
|
+
{/* Upload button */}
|
|
2553
|
+
<button
|
|
2554
|
+
type="button"
|
|
2555
|
+
onClick={() => fileInputRef.current?.click()}
|
|
2556
|
+
disabled={loading || attachedImages.length >= MAX_IMAGES}
|
|
2557
|
+
style={{
|
|
2558
|
+
...STYLES.uploadButton,
|
|
2559
|
+
...(attachedImages.length >= MAX_IMAGES ? {
|
|
2560
|
+
opacity: 0.5,
|
|
2561
|
+
cursor: 'not-allowed',
|
|
2562
|
+
} : {})
|
|
2563
|
+
}}
|
|
2564
|
+
title={attachedImages.length >= MAX_IMAGES
|
|
2565
|
+
? `Maximum ${MAX_IMAGES} images`
|
|
2566
|
+
: 'Attach images (screenshots, designs)'
|
|
2567
|
+
}
|
|
2568
|
+
onMouseEnter={(e) => {
|
|
2569
|
+
if (attachedImages.length < MAX_IMAGES && !loading) {
|
|
2570
|
+
e.currentTarget.style.background = 'rgba(59, 130, 246, 0.2)';
|
|
2571
|
+
e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)';
|
|
2572
|
+
}
|
|
2573
|
+
}}
|
|
2574
|
+
onMouseLeave={(e) => {
|
|
2575
|
+
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.1)';
|
|
2576
|
+
e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.2)';
|
|
2577
|
+
}}
|
|
2578
|
+
>
|
|
2579
|
+
<svg width={20} height={20} viewBox="0 0 24 24" fill="currentColor">
|
|
2580
|
+
<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"/>
|
|
2581
|
+
</svg>
|
|
2582
|
+
</button>
|
|
2583
|
+
|
|
1015
2584
|
<input
|
|
1016
2585
|
ref={inputRef}
|
|
1017
2586
|
type="text"
|
|
1018
2587
|
value={input}
|
|
1019
2588
|
onChange={e => setInput(e.target.value)}
|
|
1020
|
-
|
|
2589
|
+
onPaste={handlePaste}
|
|
2590
|
+
placeholder={attachedImages.length > 0
|
|
2591
|
+
? "Describe what to create from these images..."
|
|
2592
|
+
: "Describe a UI component..."
|
|
2593
|
+
}
|
|
1021
2594
|
style={STYLES.textInput}
|
|
1022
2595
|
onFocus={(e) => {
|
|
1023
2596
|
e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)';
|
|
@@ -1030,10 +2603,10 @@ export function StoryUIPanel() {
|
|
|
1030
2603
|
/>
|
|
1031
2604
|
<button
|
|
1032
2605
|
type="submit"
|
|
1033
|
-
disabled={loading || !input.trim()}
|
|
2606
|
+
disabled={loading || (!input.trim() && attachedImages.length === 0)}
|
|
1034
2607
|
style={{
|
|
1035
2608
|
...STYLES.sendButton,
|
|
1036
|
-
...(loading || !input.trim() ? {
|
|
2609
|
+
...(loading || (!input.trim() && attachedImages.length === 0) ? {
|
|
1037
2610
|
opacity: 0.5,
|
|
1038
2611
|
cursor: 'not-allowed',
|
|
1039
2612
|
background: '#6b7280',
|
|
@@ -1041,7 +2614,7 @@ export function StoryUIPanel() {
|
|
|
1041
2614
|
} : {})
|
|
1042
2615
|
}}
|
|
1043
2616
|
onMouseEnter={(e) => {
|
|
1044
|
-
if (!loading && input.trim()) {
|
|
2617
|
+
if (!loading && (input.trim() || attachedImages.length > 0)) {
|
|
1045
2618
|
e.currentTarget.style.transform = 'scale(1.05)';
|
|
1046
2619
|
e.currentTarget.style.boxShadow = '0 4px 16px rgba(16, 185, 129, 0.4)';
|
|
1047
2620
|
}
|
|
@@ -1061,3 +2634,6 @@ export function StoryUIPanel() {
|
|
|
1061
2634
|
</div>
|
|
1062
2635
|
);
|
|
1063
2636
|
}
|
|
2637
|
+
|
|
2638
|
+
export default StoryUIPanel;
|
|
2639
|
+
export { StoryUIPanel };
|