@tpitre/story-ui 3.4.2 → 3.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp-server/routes/storyHelpers.d.ts +51 -0
- package/dist/mcp-server/routes/storyHelpers.d.ts.map +1 -0
- package/dist/mcp-server/routes/storyHelpers.js +185 -0
- package/dist/story-generator/generateStory.d.ts.map +1 -1
- package/dist/story-generator/generateStory.js +0 -11
- package/dist/templates/StoryUI/StoryUIPanel.d.ts.map +1 -1
- package/dist/templates/StoryUI/StoryUIPanel.js +6 -1
- package/package.json +1 -1
- package/templates/StoryUI/StoryUIPanel.tsx +7 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper functions for story generation routes.
|
|
3
|
+
* These utilities are used by both generateStory.ts and generateStoryStream.ts
|
|
4
|
+
* to eliminate code duplication and ensure consistent behavior.
|
|
5
|
+
*/
|
|
6
|
+
import { ImageContent } from '../../story-generator/llm-providers/types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Slugify a string for use in filenames and identifiers.
|
|
9
|
+
*/
|
|
10
|
+
export declare function slugify(str: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Extract code block from LLM response text.
|
|
13
|
+
* Accepts various language identifiers (tsx, jsx, typescript, etc.)
|
|
14
|
+
*/
|
|
15
|
+
export declare function extractCodeBlock(text: string): string | null;
|
|
16
|
+
/**
|
|
17
|
+
* Call the LLM service with optional vision support.
|
|
18
|
+
* Automatically handles provider configuration and image attachment.
|
|
19
|
+
*/
|
|
20
|
+
export declare function callLLM(messages: {
|
|
21
|
+
role: 'user' | 'assistant';
|
|
22
|
+
content: string;
|
|
23
|
+
}[], images?: ImageContent[]): Promise<string>;
|
|
24
|
+
/**
|
|
25
|
+
* Clean a user prompt to create a readable title.
|
|
26
|
+
* Removes common leading phrases and normalizes formatting.
|
|
27
|
+
*/
|
|
28
|
+
export declare function cleanPromptForTitle(prompt: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Generate a title using the LLM service.
|
|
31
|
+
* Falls back to empty string on failure.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getLLMTitle(userPrompt: string): Promise<string>;
|
|
34
|
+
/**
|
|
35
|
+
* Escape a title string for use in TypeScript string literals.
|
|
36
|
+
*/
|
|
37
|
+
export declare function escapeTitleForTS(title: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Extract component imports from code for a specific import path.
|
|
40
|
+
*/
|
|
41
|
+
export declare function extractImportsFromCode(code: string, importPath: string): string[];
|
|
42
|
+
/**
|
|
43
|
+
* Generate a filename from a story title and hash.
|
|
44
|
+
*/
|
|
45
|
+
export declare function fileNameFromTitle(title: string, hash: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* Find a similar icon name from the allowed icons set.
|
|
48
|
+
* Used for providing suggestions when an invalid icon is used.
|
|
49
|
+
*/
|
|
50
|
+
export declare function findSimilarIcon(iconName: string, allowedIcons: Set<string>): string | null;
|
|
51
|
+
//# sourceMappingURL=storyHelpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storyHelpers.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/storyHelpers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAWH,OAAO,EAAE,YAAY,EAAE,MAAM,8CAA8C,CAAC;AAE5E;;GAEG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQ3C;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG5D;AAED;;;GAGG;AACH,wBAAsB,OAAO,CAC3B,QAAQ,EAAE;IAAE,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAAE,EAC3D,MAAM,CAAC,EAAE,YAAY,EAAE,GACtB,OAAO,CAAC,MAAM,CAAC,CA6BjB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAkC1D;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAOrE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAStD;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAajF;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAgBrE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAwB1F"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper functions for story generation routes.
|
|
3
|
+
* These utilities are used by both generateStory.ts and generateStoryStream.ts
|
|
4
|
+
* to eliminate code duplication and ensure consistent behavior.
|
|
5
|
+
*/
|
|
6
|
+
import { logger } from '../../story-generator/logger.js';
|
|
7
|
+
import { chatCompletion, generateTitle as llmGenerateTitle, isProviderConfigured, getProviderInfo, chatCompletionWithImages, buildMessageWithImages } from '../../story-generator/llm-providers/story-llm-service.js';
|
|
8
|
+
/**
|
|
9
|
+
* Slugify a string for use in filenames and identifiers.
|
|
10
|
+
*/
|
|
11
|
+
export function slugify(str) {
|
|
12
|
+
if (!str || typeof str !== 'string') {
|
|
13
|
+
return 'untitled';
|
|
14
|
+
}
|
|
15
|
+
return str
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
18
|
+
.replace(/^-+|-+$/g, '');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Extract code block from LLM response text.
|
|
22
|
+
* Accepts various language identifiers (tsx, jsx, typescript, etc.)
|
|
23
|
+
*/
|
|
24
|
+
export function extractCodeBlock(text) {
|
|
25
|
+
const codeBlock = text.match(/```(?:tsx|jsx|typescript|ts|js|javascript)?([\s\S]*?)```/i);
|
|
26
|
+
return codeBlock ? codeBlock[1].trim() : null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Call the LLM service with optional vision support.
|
|
30
|
+
* Automatically handles provider configuration and image attachment.
|
|
31
|
+
*/
|
|
32
|
+
export async function callLLM(messages, images) {
|
|
33
|
+
if (!isProviderConfigured()) {
|
|
34
|
+
throw new Error('No LLM provider configured. Please set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY.');
|
|
35
|
+
}
|
|
36
|
+
const providerInfo = getProviderInfo();
|
|
37
|
+
logger.debug(`Using ${providerInfo.currentProvider} (${providerInfo.currentModel}) for story generation`);
|
|
38
|
+
if (images && images.length > 0) {
|
|
39
|
+
if (!providerInfo.supportsVision) {
|
|
40
|
+
throw new Error(`${providerInfo.currentProvider} does not support vision. Please configure a vision-capable provider.`);
|
|
41
|
+
}
|
|
42
|
+
logger.log(`🖼️ Using vision-capable chat with ${images.length} image(s)`);
|
|
43
|
+
const messagesWithImages = messages.map((msg, index) => {
|
|
44
|
+
if (msg.role === 'user' && index === 0) {
|
|
45
|
+
return {
|
|
46
|
+
role: msg.role,
|
|
47
|
+
content: buildMessageWithImages(msg.content, images)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return msg;
|
|
51
|
+
});
|
|
52
|
+
return await chatCompletionWithImages(messagesWithImages, { maxTokens: 8192 });
|
|
53
|
+
}
|
|
54
|
+
return await chatCompletion(messages, { maxTokens: 8192 });
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Clean a user prompt to create a readable title.
|
|
58
|
+
* Removes common leading phrases and normalizes formatting.
|
|
59
|
+
*/
|
|
60
|
+
export function cleanPromptForTitle(prompt) {
|
|
61
|
+
if (!prompt || typeof prompt !== 'string') {
|
|
62
|
+
return 'Untitled Story';
|
|
63
|
+
}
|
|
64
|
+
const leadingPhrases = [
|
|
65
|
+
/^generate (a|an|the)? /i,
|
|
66
|
+
/^build (a|an|the)? /i,
|
|
67
|
+
/^create (a|an|the)? /i,
|
|
68
|
+
/^make (a|an|the)? /i,
|
|
69
|
+
/^design (a|an|the)? /i,
|
|
70
|
+
/^show (me )?(a|an|the)? /i,
|
|
71
|
+
/^write (a|an|the)? /i,
|
|
72
|
+
/^produce (a|an|the)? /i,
|
|
73
|
+
/^construct (a|an|the)? /i,
|
|
74
|
+
/^draft (a|an|the)? /i,
|
|
75
|
+
/^compose (a|an|the)? /i,
|
|
76
|
+
/^implement (a|an|the)? /i,
|
|
77
|
+
/^build out (a|an|the)? /i,
|
|
78
|
+
/^add (a|an|the)? /i,
|
|
79
|
+
/^render (a|an|the)? /i,
|
|
80
|
+
/^display (a|an|the)? /i,
|
|
81
|
+
];
|
|
82
|
+
let cleaned = prompt.trim();
|
|
83
|
+
for (const regex of leadingPhrases) {
|
|
84
|
+
cleaned = cleaned.replace(regex, '');
|
|
85
|
+
}
|
|
86
|
+
return cleaned
|
|
87
|
+
.replace(/[^\w\s'"?!-]/g, ' ')
|
|
88
|
+
.replace(/\s+/g, ' ')
|
|
89
|
+
.trim()
|
|
90
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Generate a title using the LLM service.
|
|
94
|
+
* Falls back to empty string on failure.
|
|
95
|
+
*/
|
|
96
|
+
export async function getLLMTitle(userPrompt) {
|
|
97
|
+
try {
|
|
98
|
+
return await llmGenerateTitle(userPrompt);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
logger.warn('Failed to generate title via LLM, using fallback', { error });
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Escape a title string for use in TypeScript string literals.
|
|
107
|
+
*/
|
|
108
|
+
export function escapeTitleForTS(title) {
|
|
109
|
+
return title
|
|
110
|
+
.replace(/\\/g, '\\\\')
|
|
111
|
+
.replace(/"/g, '\\"')
|
|
112
|
+
.replace(/'/g, "\\'")
|
|
113
|
+
.replace(/`/g, '\\`')
|
|
114
|
+
.replace(/\n/g, '\\n')
|
|
115
|
+
.replace(/\r/g, '\\r')
|
|
116
|
+
.replace(/\t/g, '\\t');
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Extract component imports from code for a specific import path.
|
|
120
|
+
*/
|
|
121
|
+
export function extractImportsFromCode(code, importPath) {
|
|
122
|
+
const imports = [];
|
|
123
|
+
const escapedPath = importPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
124
|
+
const importRegex = new RegExp(`import\\s*{([^}]+)}\\s*from\\s*['"]${escapedPath}['"]`, 'g');
|
|
125
|
+
let match;
|
|
126
|
+
while ((match = importRegex.exec(code)) !== null) {
|
|
127
|
+
const importList = match[1];
|
|
128
|
+
const components = importList.split(',').map(comp => comp.trim());
|
|
129
|
+
imports.push(...components);
|
|
130
|
+
}
|
|
131
|
+
return imports;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Generate a filename from a story title and hash.
|
|
135
|
+
*/
|
|
136
|
+
export function fileNameFromTitle(title, hash) {
|
|
137
|
+
if (!title || typeof title !== 'string') {
|
|
138
|
+
title = 'untitled';
|
|
139
|
+
}
|
|
140
|
+
if (!hash || typeof hash !== 'string') {
|
|
141
|
+
hash = 'default';
|
|
142
|
+
}
|
|
143
|
+
const base = title
|
|
144
|
+
.toLowerCase()
|
|
145
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
146
|
+
.replace(/^-+|-+$/g, '')
|
|
147
|
+
.replace(/"|'/g, '')
|
|
148
|
+
.slice(0, 60);
|
|
149
|
+
return `${base}-${hash}.stories.tsx`;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Find a similar icon name from the allowed icons set.
|
|
153
|
+
* Used for providing suggestions when an invalid icon is used.
|
|
154
|
+
*/
|
|
155
|
+
export function findSimilarIcon(iconName, allowedIcons) {
|
|
156
|
+
if (!iconName || typeof iconName !== 'string') {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const iconLower = iconName.toLowerCase();
|
|
160
|
+
for (const allowed of allowedIcons) {
|
|
161
|
+
const allowedLower = allowed.toLowerCase();
|
|
162
|
+
// Check if core words match
|
|
163
|
+
if (iconLower.includes('commit') && allowedLower.includes('commit'))
|
|
164
|
+
return allowed;
|
|
165
|
+
if (iconLower.includes('branch') && allowedLower.includes('branch'))
|
|
166
|
+
return allowed;
|
|
167
|
+
if (iconLower.includes('merge') && allowedLower.includes('merge'))
|
|
168
|
+
return allowed;
|
|
169
|
+
if (iconLower.includes('pull') && allowedLower.includes('pull'))
|
|
170
|
+
return allowed;
|
|
171
|
+
if (iconLower.includes('push') && allowedLower.includes('push'))
|
|
172
|
+
return allowed;
|
|
173
|
+
if (iconLower.includes('star') && allowedLower.includes('star'))
|
|
174
|
+
return allowed;
|
|
175
|
+
if (iconLower.includes('user') && allowedLower.includes('user'))
|
|
176
|
+
return allowed;
|
|
177
|
+
if (iconLower.includes('search') && allowedLower.includes('search'))
|
|
178
|
+
return allowed;
|
|
179
|
+
if (iconLower.includes('settings') && allowedLower.includes('settings'))
|
|
180
|
+
return allowed;
|
|
181
|
+
if (iconLower.includes('home') && allowedLower.includes('home'))
|
|
182
|
+
return allowed;
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generateStory.d.ts","sourceRoot":"","sources":["../../story-generator/generateStory.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"generateStory.d.ts","sourceRoot":"","sources":["../../story-generator/generateStory.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAsBtD,wBAAgB,aAAa,CAAC,EAC5B,YAAY,EACZ,QAAQ,EACR,MAAM,EACP,EAAE;IACD,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,CAAC;CACvB,UAmBA"}
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
function slugify(str) {
|
|
4
|
-
return str
|
|
5
|
-
.toLowerCase()
|
|
6
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
7
|
-
.replace(/^-+|-+$/g, '');
|
|
8
|
-
}
|
|
9
3
|
/**
|
|
10
4
|
* Check if the current working directory is the Story UI package itself.
|
|
11
5
|
* This prevents accidentally generating stories in the package source code.
|
|
@@ -41,8 +35,3 @@ export function generateStory({ fileContents, fileName, config }) {
|
|
|
41
35
|
fs.writeFileSync(outPath, fileContents, 'utf-8');
|
|
42
36
|
return outPath;
|
|
43
37
|
}
|
|
44
|
-
// Mock usage:
|
|
45
|
-
// generateStory({
|
|
46
|
-
// title: 'Login Form',
|
|
47
|
-
// jsx: '<al-input label="Email"></al-input>\n<al-input label="Password" type="password"></al-input>\n<al-button>Login</al-button>'
|
|
48
|
-
// });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAkTA,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,2BAA2B,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACtD;CACF;AA6oCD,iBAAS,YAAY,4CAmtCpB;AAED,eAAe,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -1104,6 +1104,7 @@ function StoryUIPanel() {
|
|
|
1104
1104
|
file,
|
|
1105
1105
|
preview,
|
|
1106
1106
|
base64,
|
|
1107
|
+
mediaType: file.type || 'image/png',
|
|
1107
1108
|
});
|
|
1108
1109
|
}
|
|
1109
1110
|
catch (err) {
|
|
@@ -1182,6 +1183,7 @@ function StoryUIPanel() {
|
|
|
1182
1183
|
file,
|
|
1183
1184
|
preview,
|
|
1184
1185
|
base64,
|
|
1186
|
+
mediaType: file.type || 'image/png',
|
|
1185
1187
|
});
|
|
1186
1188
|
}
|
|
1187
1189
|
catch (err) {
|
|
@@ -1236,6 +1238,7 @@ function StoryUIPanel() {
|
|
|
1236
1238
|
file: renamedFile,
|
|
1237
1239
|
preview,
|
|
1238
1240
|
base64,
|
|
1241
|
+
mediaType: file.type || 'image/png',
|
|
1239
1242
|
});
|
|
1240
1243
|
}
|
|
1241
1244
|
catch (err) {
|
|
@@ -1874,7 +1877,9 @@ function StoryUIPanel() {
|
|
|
1874
1877
|
maxWidth: '200px'
|
|
1875
1878
|
}, children: availableProviders
|
|
1876
1879
|
.find(p => p.type === selectedProvider)
|
|
1877
|
-
?.models.map(model => (_jsx("option", { value: model, children: model }, model))) })] })] }))] }), _jsxs("div", { style: STYLES.chatContainer, children: [error && (_jsx("div", { style: STYLES.errorMessage, children: error })), conversation.length === 0 && !loading && (_jsxs("div", { style: STYLES.emptyState, children: [_jsx("div", { style: STYLES.emptyStateTitle, children: "Start a new conversation" }), _jsx("div", { style: STYLES.emptyStateSubtitle, children: "Describe the UI component you'd like to create" })] })), conversation.map((msg, i) => (_jsx("div", { style: STYLES.messageContainer, children: _jsxs("div", { style: msg.role === 'user' ? STYLES.userMessage : STYLES.aiMessage, children: [msg.role === 'ai' ? renderMarkdown(msg.content) : msg.content, msg.role === 'user' && msg.attachedImages && msg.attachedImages.length > 0 && (_jsx("div", { style: STYLES.userMessageImages, children: msg.attachedImages.map((img) => (_jsx("img", { src: img.
|
|
1880
|
+
?.models.map(model => (_jsx("option", { value: model, children: model }, model))) })] })] }))] }), _jsxs("div", { style: STYLES.chatContainer, children: [error && (_jsx("div", { style: STYLES.errorMessage, children: error })), conversation.length === 0 && !loading && (_jsxs("div", { style: STYLES.emptyState, children: [_jsx("div", { style: STYLES.emptyStateTitle, children: "Start a new conversation" }), _jsx("div", { style: STYLES.emptyStateSubtitle, children: "Describe the UI component you'd like to create" })] })), conversation.map((msg, i) => (_jsx("div", { style: STYLES.messageContainer, children: _jsxs("div", { style: msg.role === 'user' ? STYLES.userMessage : STYLES.aiMessage, children: [msg.role === 'ai' ? renderMarkdown(msg.content) : msg.content, msg.role === 'user' && msg.attachedImages && msg.attachedImages.length > 0 && (_jsx("div", { style: STYLES.userMessageImages, children: msg.attachedImages.map((img) => (_jsx("img", { src: img.base64
|
|
1881
|
+
? `data:${img.mediaType || 'image/png'};base64,${img.base64}`
|
|
1882
|
+
: img.preview, alt: "attached", style: STYLES.userMessageImage }, img.id))) }))] }) }, i))), loading && (_jsx("div", { style: STYLES.messageContainer, children: streamingState ? (_jsx(StreamingProgressMessage, { streamingData: streamingState })) : (_jsxs("div", { style: STYLES.loadingMessage, children: [_jsx("span", { children: "Generating story" }), _jsx("span", { className: "loading-dots" })] })) })), _jsx("div", { ref: chatEndRef })] }), _jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", multiple: true, style: { display: 'none' }, onChange: handleFileSelect }), attachedImages.length > 0 && (_jsxs("div", { style: STYLES.imagePreviewContainer, children: [_jsxs("span", { style: STYLES.imagePreviewLabel, children: [_jsx("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z" }) }), attachedImages.length, " image", attachedImages.length > 1 ? 's' : '', " attached"] }), attachedImages.map((img) => (_jsxs("div", { style: STYLES.imagePreviewItem, children: [_jsx("img", { src: img.preview, alt: "preview", style: STYLES.imagePreviewImg }), _jsx("button", { type: "button", style: STYLES.imageRemoveButton, onClick: () => removeAttachedImage(img.id), title: "Remove image", children: "\u00D7" })] }, img.id)))] })), _jsxs("form", { onSubmit: handleSend, style: {
|
|
1878
1883
|
...STYLES.inputForm,
|
|
1879
1884
|
...(attachedImages.length > 0 ? {
|
|
1880
1885
|
marginTop: 0,
|
package/package.json
CHANGED
|
@@ -138,6 +138,7 @@ interface AttachedImage {
|
|
|
138
138
|
file: File;
|
|
139
139
|
preview: string;
|
|
140
140
|
base64?: string;
|
|
141
|
+
mediaType?: string; // Store MIME type for data URL reconstruction after localStorage restore
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
// Provider info from /story-ui/providers API
|
|
@@ -1544,6 +1545,7 @@ function StoryUIPanel() {
|
|
|
1544
1545
|
file,
|
|
1545
1546
|
preview,
|
|
1546
1547
|
base64,
|
|
1548
|
+
mediaType: file.type || 'image/png',
|
|
1547
1549
|
});
|
|
1548
1550
|
} catch (err) {
|
|
1549
1551
|
errors.push(`${file.name}: Failed to process`);
|
|
@@ -1638,6 +1640,7 @@ function StoryUIPanel() {
|
|
|
1638
1640
|
file,
|
|
1639
1641
|
preview,
|
|
1640
1642
|
base64,
|
|
1643
|
+
mediaType: file.type || 'image/png',
|
|
1641
1644
|
});
|
|
1642
1645
|
} catch (err) {
|
|
1643
1646
|
errors.push(`${file.name}: Failed to process`);
|
|
@@ -1703,6 +1706,7 @@ function StoryUIPanel() {
|
|
|
1703
1706
|
file: renamedFile,
|
|
1704
1707
|
preview,
|
|
1705
1708
|
base64,
|
|
1709
|
+
mediaType: file.type || 'image/png',
|
|
1706
1710
|
});
|
|
1707
1711
|
} catch (err) {
|
|
1708
1712
|
errors.push('Failed to process pasted image');
|
|
@@ -2548,7 +2552,9 @@ function StoryUIPanel() {
|
|
|
2548
2552
|
{msg.attachedImages.map((img) => (
|
|
2549
2553
|
<img
|
|
2550
2554
|
key={img.id}
|
|
2551
|
-
src={img.
|
|
2555
|
+
src={img.base64
|
|
2556
|
+
? `data:${img.mediaType || 'image/png'};base64,${img.base64}`
|
|
2557
|
+
: img.preview}
|
|
2552
2558
|
alt="attached"
|
|
2553
2559
|
style={STYLES.userMessageImage}
|
|
2554
2560
|
/>
|