@kapeta/local-cluster-service 0.58.6 → 0.60.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.
@@ -20,14 +20,48 @@ const codegen_1 = require("./codegen");
20
20
  const assetManager_1 = require("../assetManager");
21
21
  const node_uuid_1 = __importDefault(require("node-uuid"));
22
22
  const PromiseQueue_1 = require("./PromiseQueue");
23
+ const page_utils_1 = require("./page-utils");
24
+ const UIServer_1 = require("./UIServer");
25
+ const UI_SERVERS = {};
23
26
  const router = (0, express_promise_router_1.default)();
24
27
  router.use('/', cors_1.corsHandler);
25
28
  router.use('/', stringBody_1.stringBody);
26
- router.post('/:handle/ui/screen', async (req, res) => {
27
- const handle = req.params.handle;
29
+ function convertPageEvent(screenData, innerConversationId, mainConversationId) {
30
+ if (screenData.type === 'PAGE') {
31
+ const server = UI_SERVERS[mainConversationId];
32
+ if (!server) {
33
+ console.warn('No server found for conversation', mainConversationId);
34
+ }
35
+ screenData.payload.conversationId = innerConversationId;
36
+ return {
37
+ type: 'PAGE_URL',
38
+ reason: screenData.reason,
39
+ created: screenData.created,
40
+ payload: {
41
+ id: node_uuid_1.default.v4(),
42
+ name: screenData.payload.name,
43
+ title: screenData.payload.title,
44
+ filename: screenData.payload.filename,
45
+ description: screenData.payload.description,
46
+ prompt: screenData.payload.prompt,
47
+ path: screenData.payload.path,
48
+ url: server ? server.resolveUrl(screenData) : '',
49
+ method: screenData.payload.method,
50
+ conversationId: innerConversationId,
51
+ },
52
+ };
53
+ }
54
+ return screenData;
55
+ }
56
+ router.all('/ui/:systemId/serve/:method/*', async (req, res) => {
57
+ (0, page_utils_1.readPageFromDisk)(req.params.systemId, req.params[0], req.params.method, res);
58
+ });
59
+ router.post('/ui/screen', async (req, res) => {
28
60
  try {
29
61
  const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
62
+ const systemId = req.headers[page_utils_1.SystemIdHeader.toLowerCase()];
30
63
  const aiRequest = JSON.parse(req.stringBody ?? '{}');
64
+ aiRequest.storage_prefix = systemId ? systemId + '_' : 'mock_';
31
65
  const screenStream = await stormClient_1.stormClient.createUIPage(aiRequest, conversationId);
32
66
  onRequestAborted(req, res, () => {
33
67
  screenStream.abort();
@@ -35,11 +69,21 @@ router.post('/:handle/ui/screen', async (req, res) => {
35
69
  res.set('Content-Type', 'application/x-ndjson');
36
70
  res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
37
71
  res.set(stormClient_1.ConversationIdHeader, screenStream.getConversationId());
72
+ const promises = [];
38
73
  screenStream.on('data', (data) => {
39
- console.log('Processing screen event', data);
74
+ switch (data.type) {
75
+ case 'PAGE':
76
+ console.log('Processing page event', data);
77
+ data.payload.conversationId = screenStream.getConversationId();
78
+ if (systemId) {
79
+ promises.push(sendPageEvent(systemId, data, res));
80
+ }
81
+ break;
82
+ }
40
83
  sendEvent(res, data);
41
84
  });
42
85
  await waitForStormStream(screenStream);
86
+ await Promise.allSettled(promises);
43
87
  sendDone(res);
44
88
  }
45
89
  catch (err) {
@@ -49,6 +93,18 @@ router.post('/:handle/ui/screen', async (req, res) => {
49
93
  }
50
94
  }
51
95
  });
96
+ router.delete('/:handle/ui', async (req, res) => {
97
+ const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
98
+ if (!conversationId) {
99
+ res.status(400).send('Missing conversation id');
100
+ return;
101
+ }
102
+ const server = UI_SERVERS[conversationId];
103
+ if (server) {
104
+ server.close();
105
+ delete UI_SERVERS[conversationId];
106
+ }
107
+ });
52
108
  router.post('/:handle/ui', async (req, res) => {
53
109
  const handle = req.params.handle;
54
110
  try {
@@ -62,10 +118,13 @@ router.post('/:handle/ui', async (req, res) => {
62
118
  res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
63
119
  res.set(stormClient_1.ConversationIdHeader, userJourneysStream.getConversationId());
64
120
  const promises = {};
65
- const queue = new PromiseQueue_1.PromiseQueue(10);
121
+ const queue = new PromiseQueue_1.PromiseQueue(5);
66
122
  onRequestAborted(req, res, () => {
67
123
  queue.cancel();
68
124
  });
125
+ const systemId = userJourneysStream.getConversationId();
126
+ UI_SERVERS[systemId] = new UIServer_1.UIServer(systemId);
127
+ await UI_SERVERS[systemId].start();
69
128
  userJourneysStream.on('data', async (data) => {
70
129
  try {
71
130
  console.log('Processing user journey event', data);
@@ -91,15 +150,20 @@ router.post('/:handle/ui', async (req, res) => {
91
150
  name: screen.name,
92
151
  title: screen.title,
93
152
  filename: screen.filename,
153
+ storage_prefix: userJourneysStream.getConversationId() + '_',
94
154
  }, innerConversationId);
155
+ const promises = [];
95
156
  screenStream.on('data', (screenData) => {
96
157
  if (screenData.type === 'PAGE') {
97
158
  screenData.payload.conversationId = innerConversationId;
159
+ promises.push(sendPageEvent(userJourneysStream.getConversationId(), screenData, res));
160
+ }
161
+ else {
162
+ sendEvent(res, screenData);
98
163
  }
99
- sendEvent(res, screenData);
100
164
  });
101
165
  screenStream.on('end', () => {
102
- resolve();
166
+ Promise.allSettled(promises).finally(resolve);
103
167
  });
104
168
  }
105
169
  catch (e) {
@@ -131,16 +195,45 @@ router.post('/ui/edit', async (req, res) => {
131
195
  try {
132
196
  const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
133
197
  const aiRequest = JSON.parse(req.stringBody ?? '{}');
134
- const editStream = await stormClient_1.stormClient.editPages(aiRequest.prompt, conversationId);
198
+ const pages = aiRequest.prompt.pages
199
+ .map((page) => {
200
+ const content = (0, page_utils_1.readPageFromDiskAsString)(conversationId, page.path, page.method);
201
+ if (!content) {
202
+ console.warn('Page not found', page);
203
+ return undefined;
204
+ }
205
+ return {
206
+ filename: page.filename,
207
+ path: page.path,
208
+ method: page.method,
209
+ title: page.title,
210
+ conversationId: page.conversationId,
211
+ prompt: page.prompt,
212
+ name: page.name,
213
+ description: page.description,
214
+ content,
215
+ };
216
+ })
217
+ .filter((page) => !!page);
218
+ const editStream = await stormClient_1.stormClient.editPages({
219
+ ...aiRequest.prompt,
220
+ pages,
221
+ }, conversationId);
135
222
  onRequestAborted(req, res, () => {
136
223
  editStream.abort();
137
224
  });
138
225
  res.set('Content-Type', 'application/x-ndjson');
139
226
  res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
140
227
  res.set(stormClient_1.ConversationIdHeader, editStream.getConversationId());
228
+ const promises = [];
141
229
  editStream.on('data', (data) => {
142
230
  try {
143
- sendEvent(res, data);
231
+ if (data.type === 'PAGE') {
232
+ promises.push(sendPageEvent(editStream.getConversationId(), data, res));
233
+ }
234
+ else {
235
+ sendEvent(res, data);
236
+ }
144
237
  }
145
238
  catch (e) {
146
239
  console.error('Failed to process event', e);
@@ -150,6 +243,7 @@ router.post('/ui/edit', async (req, res) => {
150
243
  if (editStream.isAborted()) {
151
244
  return;
152
245
  }
246
+ await Promise.all(promises);
153
247
  sendDone(res);
154
248
  }
155
249
  catch (err) {
@@ -351,4 +445,13 @@ function onRequestAborted(req, res, onAborted) {
351
445
  onAborted();
352
446
  });
353
447
  }
448
+ function sendPageEvent(mainConversationId, data, res) {
449
+ return (0, page_utils_1.writePageToDisk)(mainConversationId, data)
450
+ .catch((err) => {
451
+ console.error('Failed to write page to disk', err);
452
+ })
453
+ .then(() => {
454
+ sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
455
+ });
456
+ }
354
457
  exports.default = router;
@@ -1,4 +1,5 @@
1
1
  import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
2
+ import { Page, StormEventPageUrl } from './events';
2
3
  export declare const STORM_ID = "storm";
3
4
  export declare const ConversationIdHeader = "Conversation-Id";
4
5
  export interface UIPagePrompt {
@@ -9,14 +10,18 @@ export interface UIPagePrompt {
9
10
  path: string;
10
11
  method: string;
11
12
  description: string;
13
+ storage_prefix: string;
12
14
  }
13
15
  export interface UIPageEditPrompt {
14
16
  planDescription: string;
15
17
  blockDescription: string;
16
- pages: {
17
- filename: string;
18
- content: string;
19
- }[];
18
+ pages: Page[];
19
+ prompt: string;
20
+ }
21
+ export interface UIPageEditRequest {
22
+ planDescription: string;
23
+ blockDescription: string;
24
+ pages: StormEventPageUrl['payload'][];
20
25
  prompt: string;
21
26
  }
22
27
  declare class StormClient {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.58.6",
3
+ "version": "0.60.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -56,15 +56,24 @@ class ClusterService {
56
56
 
57
57
  /**
58
58
  * Gets next available port
59
- * @return {Promise<number>}
60
59
  */
61
- async getNextAvailablePort() {
60
+ public async getNextAvailablePort(startPort: number = -1) {
61
+ let receivedStartPort = startPort > 0;
62
+ if (!receivedStartPort) {
63
+ startPort = this._currentPort;
64
+ }
62
65
  while (true) {
63
- while (this._reservedPorts.indexOf(this._currentPort) > -1) {
64
- this._currentPort++;
66
+ while (this._reservedPorts.indexOf(startPort) > -1) {
67
+ startPort++;
68
+ if (!receivedStartPort) {
69
+ this._currentPort = startPort;
70
+ }
65
71
  }
66
72
 
67
- const nextPort = this._currentPort++;
73
+ const nextPort = startPort++;
74
+ if (!receivedStartPort) {
75
+ this._currentPort = startPort;
76
+ }
68
77
  const isUsed = await this._checkIfPortIsUsed(nextPort);
69
78
  if (!isUsed) {
70
79
  return nextPort;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+ import express, { Express, Request, Response } from 'express';
6
+ import { readPageFromDisk } from './page-utils';
7
+ import { clusterService } from '../clusterService';
8
+ import { Server } from 'http';
9
+ import { StormEventPage } from './events';
10
+
11
+ export class UIServer {
12
+ private readonly express: Express;
13
+ private readonly systemId: string;
14
+
15
+ private port: number = 50000;
16
+ private server: Server | undefined;
17
+
18
+ constructor(systemId: string) {
19
+ this.systemId = systemId;
20
+ this.express = express();
21
+ }
22
+
23
+ public async start() {
24
+ this.port = await clusterService.getNextAvailablePort(this.port);
25
+
26
+ this.express.all('/*', async (req: Request, res: Response) => {
27
+ readPageFromDisk(this.systemId, req.params[0], req.method, res);
28
+ });
29
+
30
+ return new Promise<void>((resolve) => {
31
+ this.server = this.express.listen(this.port, () => {
32
+ console.log(`UI Server started on port ${this.port}`);
33
+ resolve();
34
+ });
35
+ });
36
+ }
37
+
38
+ public close() {
39
+ if (this.server) {
40
+ console.log('UI Server closed on port: %s', this.port);
41
+ this.server.close();
42
+ this.server = undefined;
43
+ }
44
+ }
45
+
46
+ resolveUrl(screenData: StormEventPage) {
47
+ const path = screenData.payload.path.startsWith('/') ? screenData.payload.path : `/${screenData.payload.path}`;
48
+ return `http://localhost:${this.port}${path}`;
49
+ }
50
+ }
@@ -324,12 +324,14 @@ export interface StormEventPhases {
324
324
 
325
325
  export interface Page {
326
326
  name: string;
327
+ filename: string;
327
328
  title: string;
328
329
  description: string;
329
330
  content: string;
330
331
  path: string;
331
332
  method: string;
332
333
  conversationId: string;
334
+ prompt: string;
333
335
  }
334
336
 
335
337
  // Event for creating a page
@@ -340,6 +342,25 @@ export interface StormEventPage {
340
342
  payload: Page;
341
343
  }
342
344
 
345
+ // Event for creating a page
346
+ export interface StormEventPageUrl {
347
+ type: 'PAGE_URL';
348
+ reason: string;
349
+ created: number;
350
+ payload: {
351
+ id: string;
352
+ name: string;
353
+ filename: string;
354
+ title: string;
355
+ description: string;
356
+ path: string;
357
+ url: string;
358
+ method: string;
359
+ conversationId: string;
360
+ prompt: string;
361
+ };
362
+ }
363
+
343
364
  export interface UserJourneyScreen {
344
365
  name: string;
345
366
  title: string;
@@ -389,4 +410,5 @@ export type StormEvent =
389
410
  | StormEventBlockStatus
390
411
  | StormEventCreateDSLRetry
391
412
  | StormEventUserJourney
392
- | StormEventPage;
413
+ | StormEventPage
414
+ | StormEventPageUrl;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+ import { StormEventPage } from './events';
6
+ import { Response } from 'express';
7
+ import os from 'node:os';
8
+ import Path from 'path';
9
+ import FS from 'fs-extra';
10
+
11
+ export const SystemIdHeader = 'System-Id';
12
+
13
+ export async function writePageToDisk(systemId: string, event: StormEventPage) {
14
+ const path = Path.join(
15
+ os.tmpdir(),
16
+ 'ai-systems',
17
+ systemId,
18
+ event.payload.path,
19
+ event.payload.method.toLowerCase(),
20
+ 'index.html'
21
+ );
22
+ await FS.ensureDir(Path.dirname(path));
23
+ await FS.writeFile(path, event.payload.content);
24
+
25
+ console.log(`Page written to disk: ${event.payload.title} > ${path}`);
26
+
27
+ return {
28
+ path,
29
+ };
30
+ }
31
+
32
+ export function readPageFromDiskAsString(systemId: string, path: string, method: string) {
33
+ const filePath = Path.join(os.tmpdir(), 'ai-systems', systemId, path, method.toLowerCase(), 'index.html');
34
+ if (!FS.existsSync(filePath)) {
35
+ return null;
36
+ }
37
+
38
+ return FS.readFileSync(filePath, 'utf8');
39
+ }
40
+
41
+ export function readPageFromDisk(systemId: string, path: string, method: string, res: Response) {
42
+ const filePath = Path.join(os.tmpdir(), 'ai-systems', systemId, path, method.toLowerCase(), 'index.html');
43
+ if (!FS.existsSync(filePath)) {
44
+ res.status(404).send('Page not found');
45
+ return;
46
+ }
47
+
48
+ res.type(filePath.split('.').pop() as string);
49
+
50
+ const content = FS.readFileSync(filePath, 'utf8');
51
+ res.write(content);
52
+ res.end();
53
+ }
@@ -12,8 +12,8 @@ import { corsHandler } from '../middleware/cors';
12
12
  import { stringBody } from '../middleware/stringBody';
13
13
  import { KapetaBodyRequest } from '../types';
14
14
  import { StormCodegenRequest, StormContextRequest, StormCreateBlockRequest, StormStream } from './stream';
15
- import { ConversationIdHeader, stormClient, UIPagePrompt, UIPageEditPrompt } from './stormClient';
16
- import { StormEvent, StormEventPhaseType } from './events';
15
+ import { ConversationIdHeader, stormClient, UIPagePrompt, UIPageEditPrompt, UIPageEditRequest } from './stormClient';
16
+ import { Page, StormEvent, StormEventPage, StormEventPhaseType } from './events';
17
17
  import {
18
18
  createPhaseEndEvent,
19
19
  createPhaseStartEvent,
@@ -25,18 +25,55 @@ import { StormCodegen } from './codegen';
25
25
  import { assetManager } from '../assetManager';
26
26
  import uuid from 'node-uuid';
27
27
  import { PromiseQueue } from './PromiseQueue';
28
+ import { readPageFromDisk, readPageFromDiskAsString, SystemIdHeader, writePageToDisk } from './page-utils';
29
+ import { UIServer } from './UIServer';
28
30
 
31
+ const UI_SERVERS: { [key: string]: UIServer } = {};
29
32
  const router = Router();
30
33
 
31
34
  router.use('/', corsHandler);
32
35
  router.use('/', stringBody);
33
36
 
34
- router.post('/:handle/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
35
- const handle = req.params.handle as string;
37
+ function convertPageEvent(screenData: StormEvent, innerConversationId: string, mainConversationId: string): StormEvent {
38
+ if (screenData.type === 'PAGE') {
39
+ const server: UIServer | undefined = UI_SERVERS[mainConversationId];
40
+ if (!server) {
41
+ console.warn('No server found for conversation', mainConversationId);
42
+ }
43
+ screenData.payload.conversationId = innerConversationId;
44
+ return {
45
+ type: 'PAGE_URL',
46
+ reason: screenData.reason,
47
+ created: screenData.created,
48
+ payload: {
49
+ id: uuid.v4(),
50
+ name: screenData.payload.name,
51
+ title: screenData.payload.title,
52
+ filename: screenData.payload.filename,
53
+ description: screenData.payload.description,
54
+ prompt: screenData.payload.prompt,
55
+ path: screenData.payload.path,
56
+ url: server ? server.resolveUrl(screenData) : '',
57
+ method: screenData.payload.method,
58
+ conversationId: innerConversationId,
59
+ },
60
+ };
61
+ }
62
+
63
+ return screenData;
64
+ }
65
+
66
+ router.all('/ui/:systemId/serve/:method/*', async (req: KapetaBodyRequest, res: Response) => {
67
+ readPageFromDisk(req.params.systemId, req.params[0], req.params.method, res);
68
+ });
69
+
70
+ router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
36
71
  try {
37
72
  const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
73
+ const systemId = req.headers[SystemIdHeader.toLowerCase()] as string | undefined;
38
74
 
39
75
  const aiRequest: UIPagePrompt = JSON.parse(req.stringBody ?? '{}');
76
+ aiRequest.storage_prefix = systemId ? systemId + '_' : 'mock_';
40
77
 
41
78
  const screenStream = await stormClient.createUIPage(aiRequest, conversationId);
42
79
 
@@ -48,12 +85,22 @@ router.post('/:handle/ui/screen', async (req: KapetaBodyRequest, res: Response)
48
85
  res.set('Access-Control-Expose-Headers', ConversationIdHeader);
49
86
  res.set(ConversationIdHeader, screenStream.getConversationId());
50
87
 
88
+ const promises: Promise<void>[] = [];
51
89
  screenStream.on('data', (data: StormEvent) => {
52
- console.log('Processing screen event', data);
90
+ switch (data.type) {
91
+ case 'PAGE':
92
+ console.log('Processing page event', data);
93
+ data.payload.conversationId = screenStream.getConversationId();
94
+ if (systemId) {
95
+ promises.push(sendPageEvent(systemId, data, res));
96
+ }
97
+ break;
98
+ }
53
99
  sendEvent(res, data);
54
100
  });
55
101
 
56
102
  await waitForStormStream(screenStream);
103
+ await Promise.allSettled(promises);
57
104
 
58
105
  sendDone(res);
59
106
  } catch (err: any) {
@@ -64,6 +111,20 @@ router.post('/:handle/ui/screen', async (req: KapetaBodyRequest, res: Response)
64
111
  }
65
112
  });
66
113
 
114
+ router.delete('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
115
+ const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
116
+ if (!conversationId) {
117
+ res.status(400).send('Missing conversation id');
118
+ return;
119
+ }
120
+
121
+ const server = UI_SERVERS[conversationId];
122
+ if (server) {
123
+ server.close();
124
+ delete UI_SERVERS[conversationId];
125
+ }
126
+ });
127
+
67
128
  router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
68
129
  const handle = req.params.handle as string;
69
130
  try {
@@ -83,11 +144,16 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
83
144
 
84
145
  const promises: { [key: string]: Promise<void> } = {};
85
146
 
86
- const queue = new PromiseQueue(10);
147
+ const queue = new PromiseQueue(5);
87
148
  onRequestAborted(req, res, () => {
88
149
  queue.cancel();
89
150
  });
90
151
 
152
+ const systemId = userJourneysStream.getConversationId();
153
+
154
+ UI_SERVERS[systemId] = new UIServer(systemId);
155
+ await UI_SERVERS[systemId].start();
156
+
91
157
  userJourneysStream.on('data', async (data: StormEvent) => {
92
158
  try {
93
159
  console.log('Processing user journey event', data);
@@ -118,18 +184,23 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
118
184
  name: screen.name,
119
185
  title: screen.title,
120
186
  filename: screen.filename,
187
+ storage_prefix: userJourneysStream.getConversationId() + '_',
121
188
  },
122
189
  innerConversationId
123
190
  );
191
+ const promises: Promise<void>[] = [];
124
192
  screenStream.on('data', (screenData: StormEvent) => {
125
193
  if (screenData.type === 'PAGE') {
126
194
  screenData.payload.conversationId = innerConversationId;
195
+ promises.push(
196
+ sendPageEvent(userJourneysStream.getConversationId(), screenData, res)
197
+ );
198
+ } else {
199
+ sendEvent(res, screenData);
127
200
  }
128
-
129
- sendEvent(res, screenData);
130
201
  });
131
202
  screenStream.on('end', () => {
132
- resolve();
203
+ Promise.allSettled(promises).finally(resolve);
133
204
  });
134
205
  } catch (e: any) {
135
206
  console.error('Failed to process screen', e);
@@ -163,9 +234,37 @@ router.post('/ui/edit', async (req: KapetaBodyRequest, res: Response) => {
163
234
  try {
164
235
  const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
165
236
 
166
- const aiRequest: StormContextRequest<UIPageEditPrompt> = JSON.parse(req.stringBody ?? '{}');
237
+ const aiRequest: StormContextRequest<UIPageEditRequest> = JSON.parse(req.stringBody ?? '{}');
238
+
239
+ const pages = aiRequest.prompt.pages
240
+ .map((page) => {
241
+ const content = readPageFromDiskAsString(conversationId!, page.path, page.method);
242
+ if (!content) {
243
+ console.warn('Page not found', page);
244
+ return undefined;
245
+ }
167
246
 
168
- const editStream = await stormClient.editPages(aiRequest.prompt, conversationId);
247
+ return {
248
+ filename: page.filename,
249
+ path: page.path,
250
+ method: page.method,
251
+ title: page.title,
252
+ conversationId: page.conversationId,
253
+ prompt: page.prompt,
254
+ name: page.name,
255
+ description: page.description,
256
+ content,
257
+ } satisfies Page;
258
+ })
259
+ .filter((page: Page | undefined) => !!page) as UIPageEditPrompt['pages'];
260
+
261
+ const editStream = await stormClient.editPages(
262
+ {
263
+ ...aiRequest.prompt,
264
+ pages,
265
+ },
266
+ conversationId
267
+ );
169
268
 
170
269
  onRequestAborted(req, res, () => {
171
270
  editStream.abort();
@@ -175,19 +274,24 @@ router.post('/ui/edit', async (req: KapetaBodyRequest, res: Response) => {
175
274
  res.set('Access-Control-Expose-Headers', ConversationIdHeader);
176
275
  res.set(ConversationIdHeader, editStream.getConversationId());
177
276
 
277
+ const promises: Promise<void>[] = [];
178
278
  editStream.on('data', (data: StormEvent) => {
179
279
  try {
180
- sendEvent(res, data);
280
+ if (data.type === 'PAGE') {
281
+ promises.push(sendPageEvent(editStream.getConversationId(), data, res));
282
+ } else {
283
+ sendEvent(res, data);
284
+ }
181
285
  } catch (e) {
182
286
  console.error('Failed to process event', e);
183
287
  }
184
288
  });
185
289
 
186
290
  await waitForStormStream(editStream);
187
-
188
291
  if (editStream.isAborted()) {
189
292
  return;
190
293
  }
294
+ await Promise.all(promises);
191
295
 
192
296
  sendDone(res);
193
297
  } catch (err: any) {
@@ -428,4 +532,14 @@ function onRequestAborted(req: KapetaBodyRequest, res: Response, onAborted: () =
428
532
  });
429
533
  }
430
534
 
535
+ function sendPageEvent(mainConversationId: string, data: StormEventPage, res: Response) {
536
+ return writePageToDisk(mainConversationId, data)
537
+ .catch((err) => {
538
+ console.error('Failed to write page to disk', err);
539
+ })
540
+ .then(() => {
541
+ sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
542
+ });
543
+ }
544
+
431
545
  export default router;