@kapeta/local-cluster-service 0.60.3 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ # [0.61.0](https://github.com/kapetacom/local-cluster-service/compare/v0.60.3...v0.61.0) (2024-08-09)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Move UI server start to later ([fb75cb1](https://github.com/kapetacom/local-cluster-service/commit/fb75cb1b44e8047b9f451a12219d3c8295d9394e))
7
+
8
+
9
+ ### Features
10
+
11
+ * Get UI shells and use them when creating pages ([de6dc6f](https://github.com/kapetacom/local-cluster-service/commit/de6dc6fcebeaad1522a9d280ee95cf163f73fb7f))
12
+
1
13
  ## [0.60.3](https://github.com/kapetacom/local-cluster-service/compare/v0.60.2...v0.60.3) (2024-08-07)
2
14
 
3
15
 
@@ -280,6 +280,7 @@ export interface Page {
280
280
  method: string;
281
281
  conversationId: string;
282
282
  prompt: string;
283
+ shellPage?: string;
283
284
  }
284
285
  export interface StormEventPage {
285
286
  type: 'PAGE';
@@ -323,5 +324,23 @@ export interface StormEventUserJourney {
323
324
  created: number;
324
325
  payload: UserJourney;
325
326
  }
326
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry | StormEventUserJourney | StormEventPage | StormEventPageUrl;
327
+ export interface UIShell {
328
+ name: string;
329
+ content: string;
330
+ screens: string[];
331
+ }
332
+ export interface StormEventUIShell {
333
+ type: 'UI_SHELL';
334
+ reason: string;
335
+ created: number;
336
+ payload: UIShell;
337
+ }
338
+ export interface StormEventPromptImprove {
339
+ type: 'PROMPT_IMPROVE';
340
+ reason: string;
341
+ payload: {
342
+ prompt: string;
343
+ };
344
+ }
345
+ export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry | StormEventUserJourney | StormEventUIShell | StormEventPage | StormEventPageUrl | StormEventPromptImprove;
327
346
  export {};
@@ -110,24 +110,18 @@ router.post('/:handle/ui', async (req, res) => {
110
110
  try {
111
111
  const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
112
112
  const aiRequest = JSON.parse(req.stringBody ?? '{}');
113
+ // Get user journeys
113
114
  const userJourneysStream = await stormClient_1.stormClient.createUIUserJourneys(aiRequest.prompt, conversationId);
115
+ const outerConversationId = userJourneysStream.getConversationId();
114
116
  onRequestAborted(req, res, () => {
115
117
  userJourneysStream.abort();
116
118
  });
117
119
  res.set('Content-Type', 'application/x-ndjson');
118
120
  res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
119
- res.set(stormClient_1.ConversationIdHeader, userJourneysStream.getConversationId());
120
- const promises = {};
121
- const queue = new PromiseQueue_1.PromiseQueue(5);
122
- onRequestAborted(req, res, () => {
123
- queue.cancel();
124
- });
125
- const systemId = userJourneysStream.getConversationId();
126
- UI_SERVERS[systemId] = new UIServer_1.UIServer(systemId);
127
- await UI_SERVERS[systemId].start();
128
- userJourneysStream.on('data', async (data) => {
121
+ res.set(stormClient_1.ConversationIdHeader, outerConversationId);
122
+ const uniqueUserJourneyScreens = {};
123
+ userJourneysStream.on('data', (data) => {
129
124
  try {
130
- console.log('Processing user journey event', data);
131
125
  sendEvent(res, data);
132
126
  if (data.type !== 'USER_JOURNEY') {
133
127
  return;
@@ -136,48 +130,109 @@ router.post('/:handle/ui', async (req, res) => {
136
130
  return;
137
131
  }
138
132
  data.payload.screens.forEach((screen) => {
139
- if (screen.name in promises) {
140
- return;
133
+ if (!uniqueUserJourneyScreens[screen.name]) {
134
+ uniqueUserJourneyScreens[screen.name] = screen;
141
135
  }
142
- promises[screen.name] = queue.add(() => new Promise(async (resolve, reject) => {
143
- try {
144
- const innerConversationId = node_uuid_1.default.v4();
145
- const screenStream = await stormClient_1.stormClient.createUIPage({
146
- prompt: screen.requirements,
147
- method: screen.method,
148
- path: screen.path,
149
- description: screen.requirements,
150
- name: screen.name,
151
- title: screen.title,
152
- filename: screen.filename,
153
- storage_prefix: userJourneysStream.getConversationId() + '_',
154
- }, innerConversationId);
155
- const promises = [];
156
- screenStream.on('data', (screenData) => {
157
- if (screenData.type === 'PAGE') {
158
- screenData.payload.conversationId = innerConversationId;
159
- promises.push(sendPageEvent(userJourneysStream.getConversationId(), screenData, res));
160
- }
161
- else {
162
- sendEvent(res, screenData);
163
- }
164
- });
165
- screenStream.on('end', () => {
166
- Promise.allSettled(promises).finally(resolve);
167
- });
168
- }
169
- catch (e) {
170
- console.error('Failed to process screen', e);
171
- reject(e);
172
- }
173
- }));
174
136
  });
175
137
  }
176
138
  catch (e) {
177
139
  console.error('Failed to process event', e);
178
140
  }
179
141
  });
142
+ userJourneysStream.on('error', (error) => {
143
+ console.error('Error on userJourneysStream', error);
144
+ userJourneysStream.abort();
145
+ sendError(error, res);
146
+ });
180
147
  await waitForStormStream(userJourneysStream);
148
+ // Get the UI shells
149
+ const shellsStream = await stormClient_1.stormClient.createUIShells({
150
+ pages: Object.values(uniqueUserJourneyScreens).map((screen) => ({
151
+ name: screen.name,
152
+ title: screen.title,
153
+ filename: screen.filename,
154
+ path: screen.path,
155
+ method: screen.method,
156
+ requirements: screen.requirements,
157
+ })),
158
+ }, conversationId);
159
+ onRequestAborted(req, res, () => {
160
+ shellsStream.abort();
161
+ });
162
+ const uiShells = [];
163
+ shellsStream.on('data', (data) => {
164
+ console.log('Processing shell event', data);
165
+ sendEvent(res, data);
166
+ if (data.type !== 'UI_SHELL') {
167
+ return;
168
+ }
169
+ if (shellsStream.isAborted()) {
170
+ return;
171
+ }
172
+ uiShells.push(data.payload);
173
+ });
174
+ shellsStream.on('error', (error) => {
175
+ console.error('Error on shellsStream', error);
176
+ shellsStream.abort();
177
+ sendError(error, res);
178
+ });
179
+ await waitForStormStream(shellsStream);
180
+ UI_SERVERS[outerConversationId] = new UIServer_1.UIServer(outerConversationId);
181
+ await UI_SERVERS[outerConversationId].start();
182
+ // Get the pages (5 at a time)
183
+ const queue = new PromiseQueue_1.PromiseQueue(5);
184
+ onRequestAborted(req, res, () => {
185
+ queue.cancel();
186
+ });
187
+ for (const screen of Object.values(uniqueUserJourneyScreens)) {
188
+ await queue.add(() => new Promise(async (resolve, reject) => {
189
+ try {
190
+ const innerConversationId = node_uuid_1.default.v4();
191
+ const screenStream = await stormClient_1.stormClient.createUIPage({
192
+ prompt: screen.requirements,
193
+ method: screen.method,
194
+ path: screen.path,
195
+ description: screen.requirements,
196
+ name: screen.name,
197
+ title: screen.title,
198
+ filename: screen.filename,
199
+ storage_prefix: outerConversationId + '_',
200
+ shell_page: uiShells.find((shell) => shell.screens.includes(screen.name))?.content,
201
+ }, innerConversationId);
202
+ const promiseList = [];
203
+ screenStream.on('data', (screenData) => {
204
+ if (screenData.type === 'PAGE') {
205
+ promiseList.push(sendPageEvent(outerConversationId, {
206
+ ...screenData,
207
+ payload: {
208
+ ...screenData.payload,
209
+ conversationId: innerConversationId,
210
+ },
211
+ }, res));
212
+ }
213
+ else {
214
+ sendEvent(res, screenData);
215
+ }
216
+ });
217
+ screenStream.on('end', async () => {
218
+ try {
219
+ await Promise.allSettled(promiseList).finally(() => resolve(true));
220
+ }
221
+ catch (error) {
222
+ console.error('Failed to process screen', error);
223
+ }
224
+ });
225
+ screenStream.on('error', (error) => {
226
+ console.error('Error on screenStream', error);
227
+ screenStream.abort();
228
+ });
229
+ }
230
+ catch (e) {
231
+ console.error('Failed to process screen', e);
232
+ reject(e);
233
+ }
234
+ }));
235
+ }
181
236
  await queue.wait();
182
237
  if (userJourneysStream.isAborted()) {
183
238
  return;
@@ -2,6 +2,16 @@ import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIIm
2
2
  import { Page, StormEventPageUrl } from './events';
3
3
  export declare const STORM_ID = "storm";
4
4
  export declare const ConversationIdHeader = "Conversation-Id";
5
+ export interface UIShellsPrompt {
6
+ pages: {
7
+ name: string;
8
+ title: string;
9
+ filename: string;
10
+ path: string;
11
+ method: string;
12
+ requirements: string;
13
+ }[];
14
+ }
5
15
  export interface UIPagePrompt {
6
16
  name: string;
7
17
  title: string;
@@ -11,6 +21,7 @@ export interface UIPagePrompt {
11
21
  method: string;
12
22
  description: string;
13
23
  storage_prefix: string;
24
+ shell_page?: string;
14
25
  }
15
26
  export interface UIPageEditPrompt {
16
27
  planDescription: string;
@@ -32,6 +43,7 @@ declare class StormClient {
32
43
  createMetadata(prompt: string, conversationId?: string): Promise<StormStream>;
33
44
  createUIPages(prompt: string, conversationId?: string): Promise<StormStream>;
34
45
  createUIUserJourneys(prompt: string, conversationId?: string): Promise<StormStream>;
46
+ createUIShells(prompt: UIShellsPrompt, conversationId?: string): Promise<StormStream>;
35
47
  createUIPage(prompt: UIPagePrompt, conversationId?: string): Promise<StormStream>;
36
48
  editPages(prompt: UIPageEditPrompt, conversationId?: string): Promise<StormStream>;
37
49
  listScreens(prompt: StormUIListPrompt, conversationId?: string): Promise<StormStream>;
@@ -92,6 +92,12 @@ class StormClient {
92
92
  conversationId,
93
93
  });
94
94
  }
95
+ createUIShells(prompt, conversationId) {
96
+ return this.send('/v2/ui/shells', {
97
+ prompt: JSON.stringify(prompt),
98
+ conversationId,
99
+ });
100
+ }
95
101
  createUIPage(prompt, conversationId) {
96
102
  return this.send('/v2/ui/page', {
97
103
  prompt: prompt,
@@ -280,6 +280,7 @@ export interface Page {
280
280
  method: string;
281
281
  conversationId: string;
282
282
  prompt: string;
283
+ shellPage?: string;
283
284
  }
284
285
  export interface StormEventPage {
285
286
  type: 'PAGE';
@@ -323,5 +324,23 @@ export interface StormEventUserJourney {
323
324
  created: number;
324
325
  payload: UserJourney;
325
326
  }
326
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry | StormEventUserJourney | StormEventPage | StormEventPageUrl;
327
+ export interface UIShell {
328
+ name: string;
329
+ content: string;
330
+ screens: string[];
331
+ }
332
+ export interface StormEventUIShell {
333
+ type: 'UI_SHELL';
334
+ reason: string;
335
+ created: number;
336
+ payload: UIShell;
337
+ }
338
+ export interface StormEventPromptImprove {
339
+ type: 'PROMPT_IMPROVE';
340
+ reason: string;
341
+ payload: {
342
+ prompt: string;
343
+ };
344
+ }
345
+ export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry | StormEventUserJourney | StormEventUIShell | StormEventPage | StormEventPageUrl | StormEventPromptImprove;
327
346
  export {};
@@ -110,24 +110,18 @@ router.post('/:handle/ui', async (req, res) => {
110
110
  try {
111
111
  const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
112
112
  const aiRequest = JSON.parse(req.stringBody ?? '{}');
113
+ // Get user journeys
113
114
  const userJourneysStream = await stormClient_1.stormClient.createUIUserJourneys(aiRequest.prompt, conversationId);
115
+ const outerConversationId = userJourneysStream.getConversationId();
114
116
  onRequestAborted(req, res, () => {
115
117
  userJourneysStream.abort();
116
118
  });
117
119
  res.set('Content-Type', 'application/x-ndjson');
118
120
  res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
119
- res.set(stormClient_1.ConversationIdHeader, userJourneysStream.getConversationId());
120
- const promises = {};
121
- const queue = new PromiseQueue_1.PromiseQueue(5);
122
- onRequestAborted(req, res, () => {
123
- queue.cancel();
124
- });
125
- const systemId = userJourneysStream.getConversationId();
126
- UI_SERVERS[systemId] = new UIServer_1.UIServer(systemId);
127
- await UI_SERVERS[systemId].start();
128
- userJourneysStream.on('data', async (data) => {
121
+ res.set(stormClient_1.ConversationIdHeader, outerConversationId);
122
+ const uniqueUserJourneyScreens = {};
123
+ userJourneysStream.on('data', (data) => {
129
124
  try {
130
- console.log('Processing user journey event', data);
131
125
  sendEvent(res, data);
132
126
  if (data.type !== 'USER_JOURNEY') {
133
127
  return;
@@ -136,48 +130,109 @@ router.post('/:handle/ui', async (req, res) => {
136
130
  return;
137
131
  }
138
132
  data.payload.screens.forEach((screen) => {
139
- if (screen.name in promises) {
140
- return;
133
+ if (!uniqueUserJourneyScreens[screen.name]) {
134
+ uniqueUserJourneyScreens[screen.name] = screen;
141
135
  }
142
- promises[screen.name] = queue.add(() => new Promise(async (resolve, reject) => {
143
- try {
144
- const innerConversationId = node_uuid_1.default.v4();
145
- const screenStream = await stormClient_1.stormClient.createUIPage({
146
- prompt: screen.requirements,
147
- method: screen.method,
148
- path: screen.path,
149
- description: screen.requirements,
150
- name: screen.name,
151
- title: screen.title,
152
- filename: screen.filename,
153
- storage_prefix: userJourneysStream.getConversationId() + '_',
154
- }, innerConversationId);
155
- const promises = [];
156
- screenStream.on('data', (screenData) => {
157
- if (screenData.type === 'PAGE') {
158
- screenData.payload.conversationId = innerConversationId;
159
- promises.push(sendPageEvent(userJourneysStream.getConversationId(), screenData, res));
160
- }
161
- else {
162
- sendEvent(res, screenData);
163
- }
164
- });
165
- screenStream.on('end', () => {
166
- Promise.allSettled(promises).finally(resolve);
167
- });
168
- }
169
- catch (e) {
170
- console.error('Failed to process screen', e);
171
- reject(e);
172
- }
173
- }));
174
136
  });
175
137
  }
176
138
  catch (e) {
177
139
  console.error('Failed to process event', e);
178
140
  }
179
141
  });
142
+ userJourneysStream.on('error', (error) => {
143
+ console.error('Error on userJourneysStream', error);
144
+ userJourneysStream.abort();
145
+ sendError(error, res);
146
+ });
180
147
  await waitForStormStream(userJourneysStream);
148
+ // Get the UI shells
149
+ const shellsStream = await stormClient_1.stormClient.createUIShells({
150
+ pages: Object.values(uniqueUserJourneyScreens).map((screen) => ({
151
+ name: screen.name,
152
+ title: screen.title,
153
+ filename: screen.filename,
154
+ path: screen.path,
155
+ method: screen.method,
156
+ requirements: screen.requirements,
157
+ })),
158
+ }, conversationId);
159
+ onRequestAborted(req, res, () => {
160
+ shellsStream.abort();
161
+ });
162
+ const uiShells = [];
163
+ shellsStream.on('data', (data) => {
164
+ console.log('Processing shell event', data);
165
+ sendEvent(res, data);
166
+ if (data.type !== 'UI_SHELL') {
167
+ return;
168
+ }
169
+ if (shellsStream.isAborted()) {
170
+ return;
171
+ }
172
+ uiShells.push(data.payload);
173
+ });
174
+ shellsStream.on('error', (error) => {
175
+ console.error('Error on shellsStream', error);
176
+ shellsStream.abort();
177
+ sendError(error, res);
178
+ });
179
+ await waitForStormStream(shellsStream);
180
+ UI_SERVERS[outerConversationId] = new UIServer_1.UIServer(outerConversationId);
181
+ await UI_SERVERS[outerConversationId].start();
182
+ // Get the pages (5 at a time)
183
+ const queue = new PromiseQueue_1.PromiseQueue(5);
184
+ onRequestAborted(req, res, () => {
185
+ queue.cancel();
186
+ });
187
+ for (const screen of Object.values(uniqueUserJourneyScreens)) {
188
+ await queue.add(() => new Promise(async (resolve, reject) => {
189
+ try {
190
+ const innerConversationId = node_uuid_1.default.v4();
191
+ const screenStream = await stormClient_1.stormClient.createUIPage({
192
+ prompt: screen.requirements,
193
+ method: screen.method,
194
+ path: screen.path,
195
+ description: screen.requirements,
196
+ name: screen.name,
197
+ title: screen.title,
198
+ filename: screen.filename,
199
+ storage_prefix: outerConversationId + '_',
200
+ shell_page: uiShells.find((shell) => shell.screens.includes(screen.name))?.content,
201
+ }, innerConversationId);
202
+ const promiseList = [];
203
+ screenStream.on('data', (screenData) => {
204
+ if (screenData.type === 'PAGE') {
205
+ promiseList.push(sendPageEvent(outerConversationId, {
206
+ ...screenData,
207
+ payload: {
208
+ ...screenData.payload,
209
+ conversationId: innerConversationId,
210
+ },
211
+ }, res));
212
+ }
213
+ else {
214
+ sendEvent(res, screenData);
215
+ }
216
+ });
217
+ screenStream.on('end', async () => {
218
+ try {
219
+ await Promise.allSettled(promiseList).finally(() => resolve(true));
220
+ }
221
+ catch (error) {
222
+ console.error('Failed to process screen', error);
223
+ }
224
+ });
225
+ screenStream.on('error', (error) => {
226
+ console.error('Error on screenStream', error);
227
+ screenStream.abort();
228
+ });
229
+ }
230
+ catch (e) {
231
+ console.error('Failed to process screen', e);
232
+ reject(e);
233
+ }
234
+ }));
235
+ }
181
236
  await queue.wait();
182
237
  if (userJourneysStream.isAborted()) {
183
238
  return;
@@ -2,6 +2,16 @@ import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIIm
2
2
  import { Page, StormEventPageUrl } from './events';
3
3
  export declare const STORM_ID = "storm";
4
4
  export declare const ConversationIdHeader = "Conversation-Id";
5
+ export interface UIShellsPrompt {
6
+ pages: {
7
+ name: string;
8
+ title: string;
9
+ filename: string;
10
+ path: string;
11
+ method: string;
12
+ requirements: string;
13
+ }[];
14
+ }
5
15
  export interface UIPagePrompt {
6
16
  name: string;
7
17
  title: string;
@@ -11,6 +21,7 @@ export interface UIPagePrompt {
11
21
  method: string;
12
22
  description: string;
13
23
  storage_prefix: string;
24
+ shell_page?: string;
14
25
  }
15
26
  export interface UIPageEditPrompt {
16
27
  planDescription: string;
@@ -32,6 +43,7 @@ declare class StormClient {
32
43
  createMetadata(prompt: string, conversationId?: string): Promise<StormStream>;
33
44
  createUIPages(prompt: string, conversationId?: string): Promise<StormStream>;
34
45
  createUIUserJourneys(prompt: string, conversationId?: string): Promise<StormStream>;
46
+ createUIShells(prompt: UIShellsPrompt, conversationId?: string): Promise<StormStream>;
35
47
  createUIPage(prompt: UIPagePrompt, conversationId?: string): Promise<StormStream>;
36
48
  editPages(prompt: UIPageEditPrompt, conversationId?: string): Promise<StormStream>;
37
49
  listScreens(prompt: StormUIListPrompt, conversationId?: string): Promise<StormStream>;
@@ -92,6 +92,12 @@ class StormClient {
92
92
  conversationId,
93
93
  });
94
94
  }
95
+ createUIShells(prompt, conversationId) {
96
+ return this.send('/v2/ui/shells', {
97
+ prompt: JSON.stringify(prompt),
98
+ conversationId,
99
+ });
100
+ }
95
101
  createUIPage(prompt, conversationId) {
96
102
  return this.send('/v2/ui/page', {
97
103
  prompt: prompt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.60.3",
3
+ "version": "0.61.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -332,6 +332,7 @@ export interface Page {
332
332
  method: string;
333
333
  conversationId: string;
334
334
  prompt: string;
335
+ shellPage?: string;
335
336
  }
336
337
 
337
338
  // Event for creating a page
@@ -384,6 +385,27 @@ export interface StormEventUserJourney {
384
385
  payload: UserJourney;
385
386
  }
386
387
 
388
+ export interface UIShell {
389
+ name: string;
390
+ content: string;
391
+ screens: string[];
392
+ }
393
+
394
+ export interface StormEventUIShell {
395
+ type: 'UI_SHELL';
396
+ reason: string;
397
+ created: number;
398
+ payload: UIShell;
399
+ }
400
+
401
+ export interface StormEventPromptImprove {
402
+ type: 'PROMPT_IMPROVE';
403
+ reason: string;
404
+ payload: {
405
+ prompt: string;
406
+ };
407
+ }
408
+
387
409
  export type StormEvent =
388
410
  | StormEventCreateBlock
389
411
  | StormEventCreateConnection
@@ -410,5 +432,7 @@ export type StormEvent =
410
432
  | StormEventBlockStatus
411
433
  | StormEventCreateDSLRetry
412
434
  | StormEventUserJourney
435
+ | StormEventUIShell
413
436
  | StormEventPage
414
- | StormEventPageUrl;
437
+ | StormEventPageUrl
438
+ | StormEventPromptImprove;
@@ -13,7 +13,7 @@ import { stringBody } from '../middleware/stringBody';
13
13
  import { KapetaBodyRequest } from '../types';
14
14
  import { StormCodegenRequest, StormContextRequest, StormCreateBlockRequest, StormStream } from './stream';
15
15
  import { ConversationIdHeader, stormClient, UIPagePrompt, UIPageEditPrompt, UIPageEditRequest } from './stormClient';
16
- import { Page, StormEvent, StormEventPage, StormEventPhaseType } from './events';
16
+ import { Page, StormEvent, StormEventPage, StormEventPhaseType, UIShell, UserJourneyScreen } from './events';
17
17
  import {
18
18
  createPhaseEndEvent,
19
19
  createPhaseStartEvent,
@@ -132,7 +132,9 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
132
132
 
133
133
  const aiRequest: StormContextRequest = JSON.parse(req.stringBody ?? '{}');
134
134
 
135
+ // Get user journeys
135
136
  const userJourneysStream = await stormClient.createUIUserJourneys(aiRequest.prompt, conversationId);
137
+ const outerConversationId = userJourneysStream.getConversationId();
136
138
 
137
139
  onRequestAborted(req, res, () => {
138
140
  userJourneysStream.abort();
@@ -140,23 +142,12 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
140
142
 
141
143
  res.set('Content-Type', 'application/x-ndjson');
142
144
  res.set('Access-Control-Expose-Headers', ConversationIdHeader);
143
- res.set(ConversationIdHeader, userJourneysStream.getConversationId());
145
+ res.set(ConversationIdHeader, outerConversationId);
144
146
 
145
- const promises: { [key: string]: Promise<void> } = {};
147
+ const uniqueUserJourneyScreens: Record<string, UserJourneyScreen> = {};
146
148
 
147
- const queue = new PromiseQueue(5);
148
- onRequestAborted(req, res, () => {
149
- queue.cancel();
150
- });
151
-
152
- const systemId = userJourneysStream.getConversationId();
153
-
154
- UI_SERVERS[systemId] = new UIServer(systemId);
155
- await UI_SERVERS[systemId].start();
156
-
157
- userJourneysStream.on('data', async (data: StormEvent) => {
149
+ userJourneysStream.on('data', (data: StormEvent) => {
158
150
  try {
159
- console.log('Processing user journey event', data);
160
151
  sendEvent(res, data);
161
152
  if (data.type !== 'USER_JOURNEY') {
162
153
  return;
@@ -167,54 +158,139 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
167
158
  }
168
159
 
169
160
  data.payload.screens.forEach((screen) => {
170
- if (screen.name in promises) {
171
- return;
161
+ if (!uniqueUserJourneyScreens[screen.name]) {
162
+ uniqueUserJourneyScreens[screen.name] = screen;
172
163
  }
173
- promises[screen.name] = queue.add(
174
- () =>
175
- new Promise(async (resolve, reject) => {
176
- try {
177
- const innerConversationId = uuid.v4();
178
- const screenStream = await stormClient.createUIPage(
179
- {
180
- prompt: screen.requirements,
181
- method: screen.method,
182
- path: screen.path,
183
- description: screen.requirements,
184
- name: screen.name,
185
- title: screen.title,
186
- filename: screen.filename,
187
- storage_prefix: userJourneysStream.getConversationId() + '_',
188
- },
189
- innerConversationId
190
- );
191
- const promises: Promise<void>[] = [];
192
- screenStream.on('data', (screenData: StormEvent) => {
193
- if (screenData.type === 'PAGE') {
194
- screenData.payload.conversationId = innerConversationId;
195
- promises.push(
196
- sendPageEvent(userJourneysStream.getConversationId(), screenData, res)
197
- );
198
- } else {
199
- sendEvent(res, screenData);
200
- }
201
- });
202
- screenStream.on('end', () => {
203
- Promise.allSettled(promises).finally(resolve);
204
- });
205
- } catch (e: any) {
206
- console.error('Failed to process screen', e);
207
- reject(e);
208
- }
209
- })
210
- );
211
164
  });
212
165
  } catch (e) {
213
166
  console.error('Failed to process event', e);
214
167
  }
215
168
  });
216
169
 
170
+ userJourneysStream.on('error', (error) => {
171
+ console.error('Error on userJourneysStream', error);
172
+ userJourneysStream.abort();
173
+ sendError(error, res);
174
+ });
175
+
217
176
  await waitForStormStream(userJourneysStream);
177
+
178
+ // Get the UI shells
179
+ const shellsStream = await stormClient.createUIShells(
180
+ {
181
+ pages: Object.values(uniqueUserJourneyScreens).map((screen) => ({
182
+ name: screen.name,
183
+ title: screen.title,
184
+ filename: screen.filename,
185
+ path: screen.path,
186
+ method: screen.method,
187
+ requirements: screen.requirements,
188
+ })),
189
+ },
190
+ conversationId
191
+ );
192
+
193
+ onRequestAborted(req, res, () => {
194
+ shellsStream.abort();
195
+ });
196
+
197
+ const uiShells: UIShell[] = [];
198
+
199
+ shellsStream.on('data', (data: StormEvent) => {
200
+ console.log('Processing shell event', data);
201
+ sendEvent(res, data);
202
+
203
+ if (data.type !== 'UI_SHELL') {
204
+ return;
205
+ }
206
+
207
+ if (shellsStream.isAborted()) {
208
+ return;
209
+ }
210
+
211
+ uiShells.push(data.payload);
212
+ });
213
+
214
+ shellsStream.on('error', (error) => {
215
+ console.error('Error on shellsStream', error);
216
+ shellsStream.abort();
217
+ sendError(error, res);
218
+ });
219
+
220
+ await waitForStormStream(shellsStream);
221
+
222
+ UI_SERVERS[outerConversationId] = new UIServer(outerConversationId);
223
+ await UI_SERVERS[outerConversationId].start();
224
+
225
+ // Get the pages (5 at a time)
226
+ const queue = new PromiseQueue(5);
227
+
228
+ onRequestAborted(req, res, () => {
229
+ queue.cancel();
230
+ });
231
+
232
+ for (const screen of Object.values(uniqueUserJourneyScreens)) {
233
+ await queue.add(
234
+ () =>
235
+ new Promise(async (resolve, reject) => {
236
+ try {
237
+ const innerConversationId = uuid.v4();
238
+ const screenStream = await stormClient.createUIPage(
239
+ {
240
+ prompt: screen.requirements,
241
+ method: screen.method,
242
+ path: screen.path,
243
+ description: screen.requirements,
244
+ name: screen.name,
245
+ title: screen.title,
246
+ filename: screen.filename,
247
+ storage_prefix: outerConversationId + '_',
248
+ shell_page: uiShells.find((shell) => shell.screens.includes(screen.name))?.content,
249
+ },
250
+ innerConversationId
251
+ );
252
+
253
+ const promiseList: Promise<void>[] = [];
254
+ screenStream.on('data', (screenData: StormEvent) => {
255
+ if (screenData.type === 'PAGE') {
256
+ promiseList.push(
257
+ sendPageEvent(
258
+ outerConversationId,
259
+ {
260
+ ...screenData,
261
+ payload: {
262
+ ...screenData.payload,
263
+ conversationId: innerConversationId,
264
+ },
265
+ },
266
+ res
267
+ )
268
+ );
269
+ } else {
270
+ sendEvent(res, screenData);
271
+ }
272
+ });
273
+
274
+ screenStream.on('end', async () => {
275
+ try {
276
+ await Promise.allSettled(promiseList).finally(() => resolve(true));
277
+ } catch (error) {
278
+ console.error('Failed to process screen', error);
279
+ }
280
+ });
281
+
282
+ screenStream.on('error', (error) => {
283
+ console.error('Error on screenStream', error);
284
+ screenStream.abort();
285
+ });
286
+ } catch (e) {
287
+ console.error('Failed to process screen', e);
288
+ reject(e);
289
+ }
290
+ })
291
+ );
292
+ }
293
+
218
294
  await queue.wait();
219
295
 
220
296
  if (userJourneysStream.isAborted()) {
@@ -222,8 +298,8 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
222
298
  }
223
299
 
224
300
  sendDone(res);
225
- } catch (err: any) {
226
- sendError(err, res);
301
+ } catch (err) {
302
+ sendError(err as Error, res);
227
303
  if (!res.closed) {
228
304
  res.end();
229
305
  }
@@ -21,6 +21,17 @@ export const STORM_ID = 'storm';
21
21
 
22
22
  export const ConversationIdHeader = 'Conversation-Id';
23
23
 
24
+ export interface UIShellsPrompt {
25
+ pages: {
26
+ name: string;
27
+ title: string;
28
+ filename: string;
29
+ path: string;
30
+ method: string;
31
+ requirements: string;
32
+ }[];
33
+ }
34
+
24
35
  export interface UIPagePrompt {
25
36
  name: string;
26
37
  title: string;
@@ -30,6 +41,7 @@ export interface UIPagePrompt {
30
41
  method: string;
31
42
  description: string;
32
43
  storage_prefix: string;
44
+ shell_page?: string;
33
45
  }
34
46
 
35
47
  export interface UIPageEditPrompt {
@@ -153,6 +165,13 @@ class StormClient {
153
165
  });
154
166
  }
155
167
 
168
+ public createUIShells(prompt: UIShellsPrompt, conversationId?: string) {
169
+ return this.send('/v2/ui/shells', {
170
+ prompt: JSON.stringify(prompt),
171
+ conversationId,
172
+ });
173
+ }
174
+
156
175
  public createUIPage(prompt: UIPagePrompt, conversationId?: string) {
157
176
  return this.send('/v2/ui/page', {
158
177
  prompt: prompt,