@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.
Files changed (188) hide show
  1. package/.env.sample +82 -11
  2. package/README.md +89 -0
  3. package/dist/cli/deploy.d.ts +17 -0
  4. package/dist/cli/deploy.d.ts.map +1 -0
  5. package/dist/cli/deploy.js +696 -0
  6. package/dist/cli/index.d.ts +3 -0
  7. package/dist/cli/index.d.ts.map +1 -0
  8. package/dist/cli/index.js +26 -2
  9. package/dist/cli/setup.d.ts +11 -0
  10. package/dist/cli/setup.d.ts.map +1 -0
  11. package/dist/cli/setup.js +437 -110
  12. package/dist/mcp-server/index.d.ts +2 -0
  13. package/dist/mcp-server/index.d.ts.map +1 -0
  14. package/dist/mcp-server/index.js +120 -2
  15. package/dist/mcp-server/mcp-stdio-server.d.ts +3 -0
  16. package/dist/mcp-server/mcp-stdio-server.d.ts.map +1 -0
  17. package/dist/mcp-server/mcp-stdio-server.js +8 -1
  18. package/dist/mcp-server/routes/claude.d.ts +3 -0
  19. package/dist/mcp-server/routes/claude.d.ts.map +1 -0
  20. package/dist/mcp-server/routes/claude.js +60 -23
  21. package/dist/mcp-server/routes/components.d.ts +4 -0
  22. package/dist/mcp-server/routes/components.d.ts.map +1 -0
  23. package/dist/mcp-server/routes/frameworks.d.ts +38 -0
  24. package/dist/mcp-server/routes/frameworks.d.ts.map +1 -0
  25. package/dist/mcp-server/routes/frameworks.js +183 -0
  26. package/dist/mcp-server/routes/generateStory.d.ts +3 -0
  27. package/dist/mcp-server/routes/generateStory.d.ts.map +1 -0
  28. package/dist/mcp-server/routes/generateStory.js +160 -76
  29. package/dist/mcp-server/routes/generateStoryStream.d.ts +12 -0
  30. package/dist/mcp-server/routes/generateStoryStream.d.ts.map +1 -0
  31. package/dist/mcp-server/routes/generateStoryStream.js +947 -0
  32. package/dist/mcp-server/routes/hybridStories.d.ts +18 -0
  33. package/dist/mcp-server/routes/hybridStories.d.ts.map +1 -0
  34. package/dist/mcp-server/routes/mcpRemote.d.ts +14 -0
  35. package/dist/mcp-server/routes/mcpRemote.d.ts.map +1 -0
  36. package/dist/mcp-server/routes/mcpRemote.js +489 -0
  37. package/dist/mcp-server/routes/memoryStories.d.ts +26 -0
  38. package/dist/mcp-server/routes/memoryStories.d.ts.map +1 -0
  39. package/dist/mcp-server/routes/providers.d.ts +89 -0
  40. package/dist/mcp-server/routes/providers.d.ts.map +1 -0
  41. package/dist/mcp-server/routes/providers.js +369 -0
  42. package/dist/mcp-server/routes/storySync.d.ts +26 -0
  43. package/dist/mcp-server/routes/storySync.d.ts.map +1 -0
  44. package/dist/mcp-server/routes/streamTypes.d.ts +110 -0
  45. package/dist/mcp-server/routes/streamTypes.d.ts.map +1 -0
  46. package/dist/mcp-server/routes/streamTypes.js +18 -0
  47. package/dist/mcp-server/sessionManager.d.ts +50 -0
  48. package/dist/mcp-server/sessionManager.d.ts.map +1 -0
  49. package/dist/story-generator/componentBlacklist.d.ts +21 -0
  50. package/dist/story-generator/componentBlacklist.d.ts.map +1 -0
  51. package/dist/story-generator/componentDiscovery.d.ts +28 -0
  52. package/dist/story-generator/componentDiscovery.d.ts.map +1 -0
  53. package/dist/story-generator/componentRegistryGenerator.d.ts +49 -0
  54. package/dist/story-generator/componentRegistryGenerator.d.ts.map +1 -0
  55. package/dist/story-generator/componentRegistryGenerator.js +205 -0
  56. package/dist/story-generator/configLoader.d.ts +33 -0
  57. package/dist/story-generator/configLoader.d.ts.map +1 -0
  58. package/dist/story-generator/considerationsLoader.d.ts +32 -0
  59. package/dist/story-generator/considerationsLoader.d.ts.map +1 -0
  60. package/dist/story-generator/documentation-sources.d.ts +28 -0
  61. package/dist/story-generator/documentation-sources.d.ts.map +1 -0
  62. package/dist/story-generator/documentationLoader.d.ts +64 -0
  63. package/dist/story-generator/documentationLoader.d.ts.map +1 -0
  64. package/dist/story-generator/dynamicPackageDiscovery.d.ts +97 -0
  65. package/dist/story-generator/dynamicPackageDiscovery.d.ts.map +1 -0
  66. package/dist/story-generator/enhancedComponentDiscovery.d.ts +125 -0
  67. package/dist/story-generator/enhancedComponentDiscovery.d.ts.map +1 -0
  68. package/dist/story-generator/enhancedComponentDiscovery.js +111 -11
  69. package/dist/story-generator/framework-adapters/angular-adapter.d.ts +40 -0
  70. package/dist/story-generator/framework-adapters/angular-adapter.d.ts.map +1 -0
  71. package/dist/story-generator/framework-adapters/angular-adapter.js +427 -0
  72. package/dist/story-generator/framework-adapters/base-adapter.d.ts +75 -0
  73. package/dist/story-generator/framework-adapters/base-adapter.d.ts.map +1 -0
  74. package/dist/story-generator/framework-adapters/base-adapter.js +147 -0
  75. package/dist/story-generator/framework-adapters/framework-detector.d.ts +55 -0
  76. package/dist/story-generator/framework-adapters/framework-detector.d.ts.map +1 -0
  77. package/dist/story-generator/framework-adapters/framework-detector.js +323 -0
  78. package/dist/story-generator/framework-adapters/index.d.ts +97 -0
  79. package/dist/story-generator/framework-adapters/index.d.ts.map +1 -0
  80. package/dist/story-generator/framework-adapters/index.js +198 -0
  81. package/dist/story-generator/framework-adapters/react-adapter.d.ts +40 -0
  82. package/dist/story-generator/framework-adapters/react-adapter.d.ts.map +1 -0
  83. package/dist/story-generator/framework-adapters/react-adapter.js +316 -0
  84. package/dist/story-generator/framework-adapters/svelte-adapter.d.ts +40 -0
  85. package/dist/story-generator/framework-adapters/svelte-adapter.d.ts.map +1 -0
  86. package/dist/story-generator/framework-adapters/svelte-adapter.js +372 -0
  87. package/dist/story-generator/framework-adapters/types.d.ts +182 -0
  88. package/dist/story-generator/framework-adapters/types.d.ts.map +1 -0
  89. package/dist/story-generator/framework-adapters/types.js +8 -0
  90. package/dist/story-generator/framework-adapters/vue-adapter.d.ts +36 -0
  91. package/dist/story-generator/framework-adapters/vue-adapter.d.ts.map +1 -0
  92. package/dist/story-generator/framework-adapters/vue-adapter.js +336 -0
  93. package/dist/story-generator/framework-adapters/web-components-adapter.d.ts +54 -0
  94. package/dist/story-generator/framework-adapters/web-components-adapter.d.ts.map +1 -0
  95. package/dist/story-generator/framework-adapters/web-components-adapter.js +387 -0
  96. package/dist/story-generator/generateStory.d.ts +7 -0
  97. package/dist/story-generator/generateStory.d.ts.map +1 -0
  98. package/dist/story-generator/gitignoreManager.d.ts +50 -0
  99. package/dist/story-generator/gitignoreManager.d.ts.map +1 -0
  100. package/dist/story-generator/imageProcessor.d.ts +80 -0
  101. package/dist/story-generator/imageProcessor.d.ts.map +1 -0
  102. package/dist/story-generator/imageProcessor.js +391 -0
  103. package/dist/story-generator/inMemoryStoryService.d.ts +89 -0
  104. package/dist/story-generator/inMemoryStoryService.d.ts.map +1 -0
  105. package/dist/story-generator/llm-providers/base-provider.d.ts +36 -0
  106. package/dist/story-generator/llm-providers/base-provider.d.ts.map +1 -0
  107. package/dist/story-generator/llm-providers/base-provider.js +135 -0
  108. package/dist/story-generator/llm-providers/claude-provider.d.ts +23 -0
  109. package/dist/story-generator/llm-providers/claude-provider.d.ts.map +1 -0
  110. package/dist/story-generator/llm-providers/claude-provider.js +414 -0
  111. package/dist/story-generator/llm-providers/gemini-provider.d.ts +24 -0
  112. package/dist/story-generator/llm-providers/gemini-provider.d.ts.map +1 -0
  113. package/dist/story-generator/llm-providers/gemini-provider.js +406 -0
  114. package/dist/story-generator/llm-providers/index.d.ts +63 -0
  115. package/dist/story-generator/llm-providers/index.d.ts.map +1 -0
  116. package/dist/story-generator/llm-providers/index.js +169 -0
  117. package/dist/story-generator/llm-providers/openai-provider.d.ts +24 -0
  118. package/dist/story-generator/llm-providers/openai-provider.d.ts.map +1 -0
  119. package/dist/story-generator/llm-providers/openai-provider.js +458 -0
  120. package/dist/story-generator/llm-providers/settings-manager.d.ts +75 -0
  121. package/dist/story-generator/llm-providers/settings-manager.d.ts.map +1 -0
  122. package/dist/story-generator/llm-providers/settings-manager.js +173 -0
  123. package/dist/story-generator/llm-providers/story-llm-service.d.ts +79 -0
  124. package/dist/story-generator/llm-providers/story-llm-service.d.ts.map +1 -0
  125. package/dist/story-generator/llm-providers/story-llm-service.js +240 -0
  126. package/dist/story-generator/llm-providers/types.d.ts +153 -0
  127. package/dist/story-generator/llm-providers/types.d.ts.map +1 -0
  128. package/dist/story-generator/llm-providers/types.js +8 -0
  129. package/dist/story-generator/logger.d.ts +14 -0
  130. package/dist/story-generator/logger.d.ts.map +1 -0
  131. package/dist/story-generator/logger.js +96 -29
  132. package/dist/story-generator/postProcessStory.d.ts +6 -0
  133. package/dist/story-generator/postProcessStory.d.ts.map +1 -0
  134. package/dist/story-generator/productionGitignoreManager.d.ts +91 -0
  135. package/dist/story-generator/productionGitignoreManager.d.ts.map +1 -0
  136. package/dist/story-generator/promptGenerator.d.ts +48 -0
  137. package/dist/story-generator/promptGenerator.d.ts.map +1 -0
  138. package/dist/story-generator/promptGenerator.js +186 -1
  139. package/dist/story-generator/storyHistory.d.ts +44 -0
  140. package/dist/story-generator/storyHistory.d.ts.map +1 -0
  141. package/dist/story-generator/storySync.d.ts +68 -0
  142. package/dist/story-generator/storySync.d.ts.map +1 -0
  143. package/dist/story-generator/storyTracker.d.ts +48 -0
  144. package/dist/story-generator/storyTracker.d.ts.map +1 -0
  145. package/dist/story-generator/storyValidator.d.ts +6 -0
  146. package/dist/story-generator/storyValidator.d.ts.map +1 -0
  147. package/dist/story-generator/universalDesignSystemAdapter.d.ts +68 -0
  148. package/dist/story-generator/universalDesignSystemAdapter.d.ts.map +1 -0
  149. package/dist/story-generator/universalDesignSystemAdapter.js +138 -1
  150. package/dist/story-generator/urlRedirectService.d.ts +21 -0
  151. package/dist/story-generator/urlRedirectService.d.ts.map +1 -0
  152. package/dist/story-generator/validateStory.d.ts +19 -0
  153. package/dist/story-generator/validateStory.d.ts.map +1 -0
  154. package/dist/story-generator/validateStory.js +6 -2
  155. package/dist/story-generator/visionPrompts.d.ts +88 -0
  156. package/dist/story-generator/visionPrompts.d.ts.map +1 -0
  157. package/dist/story-generator/visionPrompts.js +462 -0
  158. package/dist/story-ui.config.d.ts +78 -0
  159. package/dist/story-ui.config.d.ts.map +1 -0
  160. package/dist/templates/StoryUI/StoryUIPanel.d.ts +4 -0
  161. package/dist/templates/StoryUI/StoryUIPanel.d.ts.map +1 -0
  162. package/dist/templates/StoryUI/StoryUIPanel.js +1874 -0
  163. package/dist/templates/StoryUI/StoryUIPanel.stories.d.ts +18 -0
  164. package/dist/templates/StoryUI/StoryUIPanel.stories.d.ts.map +1 -0
  165. package/dist/templates/StoryUI/StoryUIPanel.stories.js +37 -0
  166. package/dist/templates/StoryUI/index.d.ts +3 -0
  167. package/dist/templates/StoryUI/index.d.ts.map +1 -0
  168. package/dist/templates/StoryUI/index.js +2 -0
  169. package/package.json +17 -3
  170. package/templates/StoryUI/StoryUIPanel.tsx +1960 -384
  171. package/templates/StoryUI/index.tsx +1 -1
  172. package/templates/StoryUI/manager.tsx +264 -0
  173. package/templates/production-app/.env.example +11 -0
  174. package/templates/production-app/index.html +66 -0
  175. package/templates/production-app/package.json +30 -0
  176. package/templates/production-app/public/favicon.svg +5 -0
  177. package/templates/production-app/src/App.tsx +1560 -0
  178. package/templates/production-app/src/LivePreviewRenderer.tsx +420 -0
  179. package/templates/production-app/src/componentRegistry.ts +315 -0
  180. package/templates/production-app/src/considerations.ts +16 -0
  181. package/templates/production-app/src/index.css +284 -0
  182. package/templates/production-app/src/main.tsx +25 -0
  183. package/templates/production-app/tsconfig.json +32 -0
  184. package/templates/production-app/tsconfig.node.json +11 -0
  185. package/templates/production-app/vite.config.ts +83 -0
  186. package/templates/react-import-rule.json +2 -2
  187. package/dist/index.js +0 -12
  188. package/dist/story-ui.config.loader.js +0 -205
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Image Processing Module for StoryUI Vision Feature
3
+ *
4
+ * This module handles all image processing operations including:
5
+ * - Validation of image inputs (size, format, data integrity)
6
+ * - Conversion between different image formats (URL, base64, buffer)
7
+ * - Media type detection from magic bytes and file extensions
8
+ * - Preparation of images for LLM provider consumption
9
+ *
10
+ * Supports: PNG, JPEG, GIF, WebP formats
11
+ * Max size: 20MB for base64 encoded images
12
+ */
13
+ import { logger } from './logger.js';
14
+ // Maximum image size in bytes (20MB)
15
+ const MAX_IMAGE_SIZE_BYTES = 20 * 1024 * 1024;
16
+ // Size warning threshold (5MB)
17
+ const SIZE_WARNING_THRESHOLD = 5 * 1024 * 1024;
18
+ // Supported MIME types
19
+ const SUPPORTED_MEDIA_TYPES = [
20
+ 'image/png',
21
+ 'image/jpeg',
22
+ 'image/jpg',
23
+ 'image/gif',
24
+ 'image/webp',
25
+ ];
26
+ // Magic bytes for image format detection
27
+ const MAGIC_BYTES = {
28
+ png: { bytes: [0x89, 0x50, 0x4e, 0x47], type: 'image/png' },
29
+ jpeg: { bytes: [0xff, 0xd8, 0xff], type: 'image/jpeg' },
30
+ gif87a: { bytes: [0x47, 0x49, 0x46, 0x38, 0x37, 0x61], type: 'image/gif' },
31
+ gif89a: { bytes: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61], type: 'image/gif' },
32
+ webp: { bytes: [0x52, 0x49, 0x46, 0x46], type: 'image/webp' }, // RIFF header (WebP also has WEBP at offset 8)
33
+ };
34
+ // File extension to MIME type mapping
35
+ const EXTENSION_TO_MIME = {
36
+ png: 'image/png',
37
+ jpg: 'image/jpeg',
38
+ jpeg: 'image/jpeg',
39
+ gif: 'image/gif',
40
+ webp: 'image/webp',
41
+ };
42
+ /**
43
+ * Get media type from buffer by checking magic bytes
44
+ *
45
+ * @param buffer - Image buffer to analyze
46
+ * @returns Detected MIME type or undefined if not recognized
47
+ */
48
+ export function getMediaTypeFromBuffer(buffer) {
49
+ // Check each magic byte signature
50
+ for (const [format, { bytes, type }] of Object.entries(MAGIC_BYTES)) {
51
+ if (buffer.length < bytes.length)
52
+ continue;
53
+ // Compare the first N bytes
54
+ const matches = bytes.every((byte, index) => buffer[index] === byte);
55
+ if (matches) {
56
+ // Special case for WebP: verify WEBP signature at offset 8
57
+ if (format === 'webp') {
58
+ if (buffer.length < 12)
59
+ continue;
60
+ const webpSignature = buffer.slice(8, 12).toString('ascii');
61
+ if (webpSignature === 'WEBP') {
62
+ logger.debug(`Detected image format from magic bytes: ${type}`);
63
+ return type;
64
+ }
65
+ continue;
66
+ }
67
+ logger.debug(`Detected image format from magic bytes: ${type}`);
68
+ return type;
69
+ }
70
+ }
71
+ logger.warn('Unable to detect image format from magic bytes');
72
+ return undefined;
73
+ }
74
+ /**
75
+ * Get media type from URL file extension
76
+ *
77
+ * @param url - URL to extract extension from
78
+ * @returns MIME type or undefined if not recognized
79
+ */
80
+ function getMediaTypeFromUrl(url) {
81
+ try {
82
+ const urlObj = new URL(url);
83
+ const pathname = urlObj.pathname.toLowerCase();
84
+ const extension = pathname.split('.').pop();
85
+ if (extension && EXTENSION_TO_MIME[extension]) {
86
+ const mediaType = EXTENSION_TO_MIME[extension];
87
+ logger.debug(`Detected media type from URL extension: ${mediaType}`);
88
+ return mediaType;
89
+ }
90
+ }
91
+ catch (error) {
92
+ logger.warn(`Failed to parse URL for media type detection: ${url}`);
93
+ }
94
+ return undefined;
95
+ }
96
+ /**
97
+ * Normalize base64 data by removing data URI prefix if present
98
+ *
99
+ * @param data - Base64 string (with or without data URI prefix)
100
+ * @returns Pure base64 string without prefix
101
+ */
102
+ function normalizeBase64(data) {
103
+ // Remove data URI prefix if present (e.g., "data:image/png;base64,")
104
+ const dataUriPattern = /^data:([^;]+);base64,/;
105
+ const match = data.match(dataUriPattern);
106
+ if (match) {
107
+ logger.debug(`Removing data URI prefix: ${match[1]}`);
108
+ return data.replace(dataUriPattern, '');
109
+ }
110
+ return data;
111
+ }
112
+ /**
113
+ * Extract media type from data URI prefix if present
114
+ *
115
+ * @param data - Base64 string that may contain data URI prefix
116
+ * @returns Media type from prefix or undefined
117
+ */
118
+ function extractMediaTypeFromDataUri(data) {
119
+ const dataUriPattern = /^data:([^;]+);base64,/;
120
+ const match = data.match(dataUriPattern);
121
+ if (match) {
122
+ const mediaType = match[1].toLowerCase();
123
+ if (SUPPORTED_MEDIA_TYPES.includes(mediaType)) {
124
+ logger.debug(`Extracted media type from data URI: ${mediaType}`);
125
+ return mediaType;
126
+ }
127
+ }
128
+ return undefined;
129
+ }
130
+ /**
131
+ * Validate an image input
132
+ *
133
+ * Checks:
134
+ * - Has either data or url
135
+ * - File size is within limits
136
+ * - Media type is supported
137
+ * - Base64 data is valid
138
+ *
139
+ * @param image - Image input to validate
140
+ * @returns Validation result with error/warning messages
141
+ */
142
+ export function validateImage(image) {
143
+ const name = image.name || 'unknown';
144
+ // Check that either data or url is provided
145
+ if (!image.data && !image.url) {
146
+ return {
147
+ valid: false,
148
+ error: `Image "${name}" must have either 'data' or 'url' property`,
149
+ };
150
+ }
151
+ // Both data and url provided - prefer data
152
+ if (image.data && image.url) {
153
+ logger.warn(`Image "${name}" has both data and url, will use data`);
154
+ }
155
+ // Validate media type if provided
156
+ if (image.mediaType) {
157
+ const mediaType = image.mediaType.toLowerCase();
158
+ if (!SUPPORTED_MEDIA_TYPES.includes(mediaType)) {
159
+ return {
160
+ valid: false,
161
+ error: `Unsupported media type "${mediaType}". Supported types: ${SUPPORTED_MEDIA_TYPES.join(', ')}`,
162
+ };
163
+ }
164
+ }
165
+ // Validate base64 data if provided
166
+ if (image.data) {
167
+ try {
168
+ const normalizedData = normalizeBase64(image.data);
169
+ // Check if valid base64
170
+ const base64Pattern = /^[A-Za-z0-9+/]*={0,2}$/;
171
+ if (!base64Pattern.test(normalizedData)) {
172
+ return {
173
+ valid: false,
174
+ error: `Invalid base64 data for image "${name}"`,
175
+ };
176
+ }
177
+ // Calculate decoded size
178
+ const padding = (normalizedData.match(/=/g) || []).length;
179
+ const sizeBytes = (normalizedData.length * 3 / 4) - padding;
180
+ logger.debug(`Image "${name}" size: ${(sizeBytes / 1024 / 1024).toFixed(2)}MB`);
181
+ // Check size limits
182
+ if (sizeBytes > MAX_IMAGE_SIZE_BYTES) {
183
+ return {
184
+ valid: false,
185
+ error: `Image "${name}" exceeds maximum size of ${MAX_IMAGE_SIZE_BYTES / 1024 / 1024}MB (size: ${(sizeBytes / 1024 / 1024).toFixed(2)}MB)`,
186
+ };
187
+ }
188
+ // Warn about large images
189
+ if (sizeBytes > SIZE_WARNING_THRESHOLD) {
190
+ return {
191
+ valid: true,
192
+ warning: `Image "${name}" is large (${(sizeBytes / 1024 / 1024).toFixed(2)}MB). Consider optimizing for better performance.`,
193
+ };
194
+ }
195
+ }
196
+ catch (error) {
197
+ return {
198
+ valid: false,
199
+ error: `Failed to validate base64 data for image "${name}": ${error instanceof Error ? error.message : String(error)}`,
200
+ };
201
+ }
202
+ }
203
+ // If URL is provided and no data, we can't validate size until fetch
204
+ if (image.url && !image.data) {
205
+ try {
206
+ // Basic URL validation
207
+ new URL(image.url);
208
+ }
209
+ catch (error) {
210
+ return {
211
+ valid: false,
212
+ error: `Invalid URL for image "${name}": ${image.url}`,
213
+ };
214
+ }
215
+ }
216
+ return { valid: true };
217
+ }
218
+ /**
219
+ * Fetch an image from a URL and convert to base64
220
+ *
221
+ * @param url - URL to fetch image from
222
+ * @returns Object containing base64 data and detected media type
223
+ * @throws Error if fetch fails or image is invalid
224
+ */
225
+ export async function fetchImageAsBase64(url) {
226
+ logger.info(`Fetching image from URL: ${url}`);
227
+ try {
228
+ const response = await fetch(url);
229
+ if (!response.ok) {
230
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
231
+ }
232
+ // Get content type from response headers
233
+ const contentType = response.headers.get('content-type')?.toLowerCase();
234
+ logger.debug(`Response content-type: ${contentType}`);
235
+ // Get the image buffer
236
+ const arrayBuffer = await response.arrayBuffer();
237
+ const buffer = Buffer.from(arrayBuffer);
238
+ // Check size
239
+ if (buffer.length > MAX_IMAGE_SIZE_BYTES) {
240
+ throw new Error(`Image exceeds maximum size of ${MAX_IMAGE_SIZE_BYTES / 1024 / 1024}MB (size: ${(buffer.length / 1024 / 1024).toFixed(2)}MB)`);
241
+ }
242
+ // Detect media type (priority: magic bytes > content-type header > URL extension)
243
+ let mediaType = getMediaTypeFromBuffer(buffer);
244
+ if (!mediaType && contentType && SUPPORTED_MEDIA_TYPES.includes(contentType)) {
245
+ mediaType = contentType;
246
+ logger.debug(`Using media type from content-type header: ${mediaType}`);
247
+ }
248
+ if (!mediaType) {
249
+ mediaType = getMediaTypeFromUrl(url);
250
+ }
251
+ if (!mediaType) {
252
+ throw new Error('Unable to determine image media type from URL, headers, or content');
253
+ }
254
+ // Convert to base64
255
+ const base64Data = buffer.toString('base64');
256
+ logger.info(`Successfully fetched and encoded image: ${(buffer.length / 1024).toFixed(2)}KB, type: ${mediaType}`);
257
+ return {
258
+ data: base64Data,
259
+ mediaType,
260
+ };
261
+ }
262
+ catch (error) {
263
+ const errorMessage = error instanceof Error ? error.message : String(error);
264
+ logger.error(`Failed to fetch image from URL ${url}: ${errorMessage}`);
265
+ throw new Error(`Failed to fetch image: ${errorMessage}`);
266
+ }
267
+ }
268
+ /**
269
+ * Process a single image input into the internal ProcessedImage format
270
+ *
271
+ * @param image - Image input to process
272
+ * @returns Processed image with validated data and media type
273
+ * @throws Error if processing fails
274
+ */
275
+ async function processSingleImage(image) {
276
+ const name = image.name || 'unknown';
277
+ logger.debug(`Processing image: ${name}`);
278
+ // Validate image first
279
+ const validation = validateImage(image);
280
+ if (!validation.valid) {
281
+ throw new Error(validation.error || 'Image validation failed');
282
+ }
283
+ // Log warnings
284
+ if (validation.warning) {
285
+ logger.warn(validation.warning);
286
+ }
287
+ let data;
288
+ let mediaType;
289
+ // Process based on input type
290
+ if (image.data) {
291
+ // Extract media type from data URI if present
292
+ mediaType = extractMediaTypeFromDataUri(image.data);
293
+ // Normalize base64 data (remove data URI prefix)
294
+ data = normalizeBase64(image.data);
295
+ // If no media type from data URI, try to detect from base64 data
296
+ if (!mediaType) {
297
+ const buffer = Buffer.from(data, 'base64');
298
+ mediaType = getMediaTypeFromBuffer(buffer);
299
+ }
300
+ // Use provided media type as fallback
301
+ if (!mediaType && image.mediaType) {
302
+ const providedType = image.mediaType.toLowerCase();
303
+ if (SUPPORTED_MEDIA_TYPES.includes(providedType)) {
304
+ mediaType = providedType;
305
+ logger.debug(`Using provided media type: ${mediaType}`);
306
+ }
307
+ }
308
+ if (!mediaType) {
309
+ throw new Error(`Unable to determine media type for image "${name}". Please provide mediaType property.`);
310
+ }
311
+ }
312
+ else if (image.url) {
313
+ // Fetch image from URL
314
+ const fetched = await fetchImageAsBase64(image.url);
315
+ data = fetched.data;
316
+ mediaType = fetched.mediaType;
317
+ // Override with provided media type if specified
318
+ if (image.mediaType) {
319
+ const providedType = image.mediaType.toLowerCase();
320
+ if (SUPPORTED_MEDIA_TYPES.includes(providedType)) {
321
+ logger.debug(`Overriding detected media type ${mediaType} with provided type ${providedType}`);
322
+ mediaType = providedType;
323
+ }
324
+ }
325
+ }
326
+ else {
327
+ throw new Error(`Image "${name}" must have either 'data' or 'url' property`);
328
+ }
329
+ logger.info(`Successfully processed image "${name}": ${mediaType}`);
330
+ return {
331
+ data,
332
+ mediaType,
333
+ name: image.name,
334
+ };
335
+ }
336
+ /**
337
+ * Convert processed images to ImageContent format for LLM providers
338
+ *
339
+ * @param processed - Array of processed images
340
+ * @returns Array of ImageContent objects ready for LLM consumption
341
+ */
342
+ function convertToImageContent(processed) {
343
+ return processed.map(img => ({
344
+ type: 'image',
345
+ source: {
346
+ type: 'base64',
347
+ mediaType: img.mediaType,
348
+ data: img.data,
349
+ },
350
+ }));
351
+ }
352
+ /**
353
+ * Process multiple image inputs and convert to ImageContent format
354
+ *
355
+ * This is the main entry point for image processing. It:
356
+ * 1. Validates each image input
357
+ * 2. Fetches URL images and converts to base64
358
+ * 3. Detects media types from magic bytes, headers, or extensions
359
+ * 4. Returns array of ImageContent objects ready for LLM providers
360
+ *
361
+ * @param images - Array of image inputs to process
362
+ * @returns Promise resolving to array of ImageContent objects
363
+ * @throws Error if any image fails to process
364
+ */
365
+ export async function processImageInputs(images) {
366
+ if (!images || images.length === 0) {
367
+ logger.debug('No images to process');
368
+ return [];
369
+ }
370
+ logger.info(`Processing ${images.length} image(s)`);
371
+ try {
372
+ // Process all images (can be done in parallel for better performance)
373
+ const processed = await Promise.all(images.map((img, index) => {
374
+ // Add index to name for better error messages if name not provided
375
+ const imageWithName = {
376
+ ...img,
377
+ name: img.name || `image-${index + 1}`,
378
+ };
379
+ return processSingleImage(imageWithName);
380
+ }));
381
+ // Convert to ImageContent format
382
+ const imageContents = convertToImageContent(processed);
383
+ logger.info(`Successfully processed ${imageContents.length} image(s)`);
384
+ return imageContents;
385
+ }
386
+ catch (error) {
387
+ const errorMessage = error instanceof Error ? error.message : String(error);
388
+ logger.error(`Failed to process images: ${errorMessage}`);
389
+ throw new Error(`Image processing failed: ${errorMessage}`);
390
+ }
391
+ }
@@ -0,0 +1,89 @@
1
+ import { StoryUIConfig } from '../story-ui.config.js';
2
+ /**
3
+ * In-memory story service for production environments
4
+ * Stores generated stories in memory and serves them via API
5
+ */
6
+ export declare class InMemoryStoryService {
7
+ private stories;
8
+ private config;
9
+ constructor(config: StoryUIConfig);
10
+ /**
11
+ * Stores a generated story in memory
12
+ */
13
+ storeStory(story: GeneratedStory): void;
14
+ /**
15
+ * Retrieves a story by ID
16
+ */
17
+ getStory(id: string): GeneratedStory | null;
18
+ /**
19
+ * Gets all stored stories
20
+ */
21
+ getAllStories(): GeneratedStory[];
22
+ /**
23
+ * Deletes a story by ID
24
+ */
25
+ deleteStory(id: string): boolean;
26
+ /**
27
+ * Clears all stories
28
+ */
29
+ clearAllStories(): void;
30
+ /**
31
+ * Gets story content for Storybook integration
32
+ */
33
+ getStoryContent(id: string): string | null;
34
+ /**
35
+ * Gets story metadata for listing
36
+ */
37
+ getStoryMetadata(): StoryMetadata[];
38
+ /**
39
+ * Cleans up old stories to prevent memory leaks
40
+ */
41
+ private cleanupOldStories;
42
+ /**
43
+ * Counts components used in a story
44
+ */
45
+ private countComponents;
46
+ /**
47
+ * Gets memory usage statistics
48
+ */
49
+ getMemoryStats(): MemoryStats;
50
+ }
51
+ /**
52
+ * Generated story interface
53
+ */
54
+ export interface GeneratedStory {
55
+ id: string;
56
+ title: string;
57
+ description: string;
58
+ content: string;
59
+ createdAt: Date;
60
+ lastAccessed: Date;
61
+ prompt?: string;
62
+ components?: string[];
63
+ }
64
+ /**
65
+ * Story metadata for listing
66
+ */
67
+ export interface StoryMetadata {
68
+ id: string;
69
+ title: string;
70
+ description: string;
71
+ createdAt: Date;
72
+ lastAccessed: Date;
73
+ componentCount: number;
74
+ }
75
+ /**
76
+ * Memory usage statistics
77
+ */
78
+ export interface MemoryStats {
79
+ storyCount: number;
80
+ totalSizeBytes: number;
81
+ averageSizeBytes: number;
82
+ oldestStory: Date | null;
83
+ newestStory: Date | null;
84
+ }
85
+ /**
86
+ * Gets or creates the global story service instance
87
+ */
88
+ export declare function getInMemoryStoryService(config: StoryUIConfig): InMemoryStoryService;
89
+ //# sourceMappingURL=inMemoryStoryService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inMemoryStoryService.d.ts","sourceRoot":"","sources":["../../story-generator/inMemoryStoryService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAEtD;;;GAGG;AACH,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,OAAO,CAA0C;IACzD,OAAO,CAAC,MAAM,CAAgB;gBAElB,MAAM,EAAE,aAAa;IAIjC;;OAEG;IACH,UAAU,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI;IAWvC;;OAEG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI;IAS3C;;OAEG;IACH,aAAa,IAAI,cAAc,EAAE;IAMjC;;OAEG;IACH,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIhC;;OAEG;IACH,eAAe,IAAI,IAAI;IAIvB;;OAEG;IACH,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAK1C;;OAEG;IACH,gBAAgB,IAAI,aAAa,EAAE;IAWnC;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAwBzB;;OAEG;IACH,OAAO,CAAC,eAAe;IAKvB;;OAEG;IACH,cAAc,IAAI,WAAW;CAgB9B;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,YAAY,EAAE,IAAI,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,IAAI,CAAC;IAChB,YAAY,EAAE,IAAI,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,IAAI,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,IAAI,GAAG,IAAI,CAAC;CAC1B;AAOD;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,aAAa,GAAG,oBAAoB,CAKnF"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Base LLM Provider
3
+ *
4
+ * Abstract base class for LLM providers with shared functionality.
5
+ */
6
+ import { LLMProvider, ProviderType, ProviderConfig, ModelInfo, ChatMessage, ChatOptions, ChatResponse, StreamChunk, ImageContent, ImageAnalysis, ValidationResult } from './types.js';
7
+ export declare abstract class BaseLLMProvider implements LLMProvider {
8
+ abstract readonly name: string;
9
+ abstract readonly type: ProviderType;
10
+ abstract readonly supportedModels: ModelInfo[];
11
+ protected config: ProviderConfig;
12
+ constructor(config?: Partial<ProviderConfig>);
13
+ /**
14
+ * Called after construction to set the provider type
15
+ * Subclasses should call this in their constructor after super()
16
+ */
17
+ protected setProviderType(): void;
18
+ abstract chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse>;
19
+ abstract validateApiKey(apiKey: string): Promise<ValidationResult>;
20
+ supportsVision(): boolean;
21
+ supportsDocuments(): boolean;
22
+ supportsFunctionCalling(): boolean;
23
+ supportsStreaming(): boolean;
24
+ configure(config: ProviderConfig): void;
25
+ getConfig(): ProviderConfig;
26
+ isConfigured(): boolean;
27
+ protected getSelectedModel(): ModelInfo | undefined;
28
+ protected validateMessages(messages: ChatMessage[]): void;
29
+ protected buildSystemPrompt(options?: ChatOptions): string | undefined;
30
+ chatStream(messages: ChatMessage[], options?: ChatOptions): AsyncIterable<StreamChunk>;
31
+ analyzeImage(image: ImageContent, prompt?: string): Promise<ImageAnalysis>;
32
+ estimateTokens(text: string): number;
33
+ protected logRequest(messages: ChatMessage[], options?: ChatOptions): void;
34
+ protected logResponse(response: ChatResponse): void;
35
+ }
36
+ //# sourceMappingURL=base-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-provider.d.ts","sourceRoot":"","sources":["../../../story-generator/llm-providers/base-provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,WAAW,EACX,YAAY,EACZ,cAAc,EACd,SAAS,EACT,WAAW,EACX,WAAW,EACX,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,aAAa,EACb,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAGpB,8BAAsB,eAAgB,YAAW,WAAW;IAC1D,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IACrC,QAAQ,CAAC,QAAQ,CAAC,eAAe,EAAE,SAAS,EAAE,CAAC;IAE/C,SAAS,CAAC,MAAM,EAAE,cAAc,CAAC;gBAErB,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC;IAU5C;;;OAGG;IACH,SAAS,CAAC,eAAe,IAAI,IAAI;IAKjC,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IACpF,QAAQ,CAAC,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAGlE,cAAc,IAAI,OAAO;IAKzB,iBAAiB,IAAI,OAAO;IAK5B,uBAAuB,IAAI,OAAO;IAKlC,iBAAiB,IAAI,OAAO;IAM5B,SAAS,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAQvC,SAAS,IAAI,cAAc;IAI3B,YAAY,IAAI,OAAO;IAKvB,SAAS,CAAC,gBAAgB,IAAI,SAAS,GAAG,SAAS;IAInD,SAAS,CAAC,gBAAgB,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,IAAI;IAYzD,SAAS,CAAC,iBAAiB,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS;IAK/D,UAAU,CACf,QAAQ,EAAE,WAAW,EAAE,EACvB,OAAO,CAAC,EAAE,WAAW,GACpB,aAAa,CAAC,WAAW,CAAC;IAqBvB,YAAY,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IA2BhF,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAMpC,SAAS,CAAC,UAAU,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI;IAQ1E,SAAS,CAAC,WAAW,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI;CAOpD"}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Base LLM Provider
3
+ *
4
+ * Abstract base class for LLM providers with shared functionality.
5
+ */
6
+ import { logger } from '../logger.js';
7
+ export class BaseLLMProvider {
8
+ constructor(config) {
9
+ // Note: provider type will be set by subclass after construction
10
+ this.config = {
11
+ provider: 'custom', // Placeholder, will be set when configure() is called
12
+ model: '',
13
+ timeout: 120000, // 2 minutes default
14
+ ...config,
15
+ };
16
+ }
17
+ /**
18
+ * Called after construction to set the provider type
19
+ * Subclasses should call this in their constructor after super()
20
+ */
21
+ setProviderType() {
22
+ this.config.provider = this.type;
23
+ }
24
+ // Default capability checks (can be overridden)
25
+ supportsVision() {
26
+ const model = this.getSelectedModel();
27
+ return model?.supportsVision ?? false;
28
+ }
29
+ supportsDocuments() {
30
+ const model = this.getSelectedModel();
31
+ return model?.supportsDocuments ?? false;
32
+ }
33
+ supportsFunctionCalling() {
34
+ const model = this.getSelectedModel();
35
+ return model?.supportsFunctionCalling ?? false;
36
+ }
37
+ supportsStreaming() {
38
+ const model = this.getSelectedModel();
39
+ return model?.supportsStreaming ?? false;
40
+ }
41
+ // Configuration methods
42
+ configure(config) {
43
+ this.config = { ...this.config, ...config };
44
+ logger.debug(`${this.name} provider configured`, {
45
+ model: config.model,
46
+ hasApiKey: !!config.apiKey,
47
+ });
48
+ }
49
+ getConfig() {
50
+ return { ...this.config };
51
+ }
52
+ isConfigured() {
53
+ return !!this.config.apiKey && !!this.config.model;
54
+ }
55
+ // Helper methods
56
+ getSelectedModel() {
57
+ return this.supportedModels.find(m => m.id === this.config.model);
58
+ }
59
+ validateMessages(messages) {
60
+ if (!messages || messages.length === 0) {
61
+ throw new Error('Messages array cannot be empty');
62
+ }
63
+ for (const msg of messages) {
64
+ if (!['user', 'assistant', 'system'].includes(msg.role)) {
65
+ throw new Error(`Invalid message role: ${msg.role}`);
66
+ }
67
+ }
68
+ }
69
+ buildSystemPrompt(options) {
70
+ return options?.systemPrompt;
71
+ }
72
+ // Default streaming implementation (providers can override)
73
+ async *chatStream(messages, options) {
74
+ // Default non-streaming implementation
75
+ try {
76
+ const response = await this.chat(messages, options);
77
+ yield {
78
+ type: 'text',
79
+ content: response.content,
80
+ };
81
+ yield {
82
+ type: 'done',
83
+ usage: response.usage,
84
+ };
85
+ }
86
+ catch (error) {
87
+ yield {
88
+ type: 'error',
89
+ error: error instanceof Error ? error.message : String(error),
90
+ };
91
+ }
92
+ }
93
+ // Default image analysis (providers can override)
94
+ async analyzeImage(image, prompt) {
95
+ if (!this.supportsVision()) {
96
+ throw new Error(`${this.name} provider does not support image analysis`);
97
+ }
98
+ const analysisPrompt = prompt || 'Analyze this image and describe what you see, including any UI components, layout structure, colors, and design patterns.';
99
+ const messages = [
100
+ {
101
+ role: 'user',
102
+ content: [
103
+ { type: 'text', text: analysisPrompt },
104
+ image,
105
+ ],
106
+ },
107
+ ];
108
+ const response = await this.chat(messages);
109
+ // Parse the response to extract structured information
110
+ return {
111
+ description: response.content,
112
+ // Additional parsing could be done here based on the prompt
113
+ };
114
+ }
115
+ // Simple token estimation (providers can override with more accurate methods)
116
+ estimateTokens(text) {
117
+ // Rough estimate: ~4 characters per token for English text
118
+ return Math.ceil(text.length / 4);
119
+ }
120
+ // Request logging
121
+ logRequest(messages, options) {
122
+ logger.debug(`${this.name} API request`, {
123
+ model: options?.model || this.config.model,
124
+ messageCount: messages.length,
125
+ hasSystemPrompt: !!options?.systemPrompt,
126
+ });
127
+ }
128
+ logResponse(response) {
129
+ logger.debug(`${this.name} API response`, {
130
+ model: response.model,
131
+ finishReason: response.finishReason,
132
+ usage: response.usage,
133
+ });
134
+ }
135
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Claude LLM Provider
3
+ *
4
+ * Implementation of the LLM provider interface for Anthropic's Claude models.
5
+ */
6
+ import { ProviderType, ProviderConfig, ModelInfo, ChatMessage, ChatOptions, ChatResponse, StreamChunk, ValidationResult } from './types.js';
7
+ import { BaseLLMProvider } from './base-provider.js';
8
+ export declare class ClaudeProvider extends BaseLLMProvider {
9
+ readonly name = "Claude";
10
+ readonly type: ProviderType;
11
+ readonly supportedModels: ModelInfo[];
12
+ constructor(config?: Partial<ProviderConfig>);
13
+ chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse>;
14
+ chatStream(messages: ChatMessage[], options?: ChatOptions): AsyncIterable<StreamChunk>;
15
+ validateApiKey(apiKey: string): Promise<ValidationResult>;
16
+ private convertMessages;
17
+ private convertContent;
18
+ private convertResponse;
19
+ private mapStopReason;
20
+ estimateTokens(text: string): number;
21
+ }
22
+ export declare function createClaudeProvider(config?: Partial<ProviderConfig>): ClaudeProvider;
23
+ //# sourceMappingURL=claude-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-provider.d.ts","sourceRoot":"","sources":["../../../story-generator/llm-providers/claude-provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,YAAY,EACZ,cAAc,EACd,SAAS,EACT,WAAW,EACX,WAAW,EACX,YAAY,EACZ,WAAW,EACX,gBAAgB,EAGjB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAwJrD,qBAAa,cAAe,SAAQ,eAAe;IACjD,QAAQ,CAAC,IAAI,YAAY;IACzB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAY;IACvC,QAAQ,CAAC,eAAe,cAAiB;gBAE7B,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC;IAUtC,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAkE1E,UAAU,CACf,QAAQ,EAAE,WAAW,EAAE,EACvB,OAAO,CAAC,EAAE,WAAW,GACpB,aAAa,CAAC,WAAW,CAAC;IAuGvB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAgD/D,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,cAAc;IAgCtB,OAAO,CAAC,eAAe;IAoBvB,OAAO,CAAC,aAAa;IAiBrB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;CAMrC;AAGD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAErF"}