@kapeta/local-cluster-service 0.62.1 → 0.63.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,17 @@
1
+ # [0.63.0](https://github.com/kapetacom/local-cluster-service/compare/v0.62.2...v0.63.0) (2024-08-15)
2
+
3
+
4
+ ### Features
5
+
6
+ * Add reset endpoint for resetting localStorage in UI server ([#218](https://github.com/kapetacom/local-cluster-service/issues/218)) ([1dd1edc](https://github.com/kapetacom/local-cluster-service/commit/1dd1edcba982f40b853569fd408e6ed561495686))
7
+
8
+ ## [0.62.2](https://github.com/kapetacom/local-cluster-service/compare/v0.62.1...v0.62.2) (2024-08-14)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * Single page adjustments ([#217](https://github.com/kapetacom/local-cluster-service/issues/217)) ([8d0f7e6](https://github.com/kapetacom/local-cluster-service/commit/8d0f7e6b9a3fdd971559a4c9b06b78db38145b4d))
14
+
1
15
  ## [0.62.1](https://github.com/kapetacom/local-cluster-service/compare/v0.62.0...v0.62.1) (2024-08-14)
2
16
 
3
17
 
@@ -4,18 +4,20 @@
4
4
  */
5
5
  /// <reference types="node" />
6
6
  import { UIPagePrompt } from './stormClient';
7
- import { ReferenceClassification, StormEvent, StormEventPage } from './events';
7
+ import { ReferenceClassification, StormEvent, StormEventPage, UIShell } from './events';
8
8
  import { EventEmitter } from 'node:events';
9
9
  export declare class PageQueue extends EventEmitter {
10
10
  private readonly queue;
11
11
  private readonly systemId;
12
12
  private readonly references;
13
+ private uiShells;
13
14
  constructor(systemId: string, concurrency?: number);
14
15
  on(event: 'event', listener: (data: StormEvent) => void): this;
15
16
  on(event: 'page', listener: (data: StormEventPage) => void): this;
16
17
  emit(type: 'event', event: StormEvent): boolean;
17
18
  emit(type: 'page', event: StormEventPage): boolean;
18
- addPrompt(initialPrompt: UIPagePrompt): Promise<void>;
19
+ addUiShell(uiShell: UIShell): void;
20
+ addPrompt(initialPrompt: Omit<UIPagePrompt, 'shell_page'>, conversationId?: string, overwrite?: boolean): Promise<void>;
19
21
  private addPageGenerator;
20
22
  cancel(): void;
21
23
  wait(): Promise<void>;
@@ -12,10 +12,12 @@ const node_uuid_1 = __importDefault(require("node-uuid"));
12
12
  const stormClient_1 = require("./stormClient");
13
13
  const node_events_1 = require("node:events");
14
14
  const PromiseQueue_1 = require("./PromiseQueue");
15
+ const page_utils_1 = require("./page-utils");
15
16
  class PageQueue extends node_events_1.EventEmitter {
16
17
  queue;
17
18
  systemId;
18
19
  references = new Map();
20
+ uiShells = [];
19
21
  constructor(systemId, concurrency = 5) {
20
22
  super();
21
23
  this.systemId = systemId;
@@ -27,14 +29,24 @@ class PageQueue extends node_events_1.EventEmitter {
27
29
  emit(eventName, ...args) {
28
30
  return super.emit(eventName, ...args);
29
31
  }
30
- addPrompt(initialPrompt) {
31
- if (this.references.has(initialPrompt.path)) {
32
+ addUiShell(uiShell) {
33
+ this.uiShells.push(uiShell);
34
+ }
35
+ addPrompt(initialPrompt, conversationId = node_uuid_1.default.v4(), overwrite = false) {
36
+ if (!overwrite && this.references.has(initialPrompt.path)) {
32
37
  console.log('Ignoring duplicate prompt', initialPrompt.path);
33
38
  return Promise.resolve();
34
39
  }
35
- console.log('processing prompt', initialPrompt.path);
36
- const generator = new PageGenerator(initialPrompt);
37
- this.references.set(initialPrompt.path, generator);
40
+ if (!overwrite && (0, page_utils_1.hasPageOnDisk)(this.systemId, initialPrompt.method, initialPrompt.path)) {
41
+ console.log('Ignoring prompt with existing page', initialPrompt.path);
42
+ return Promise.resolve();
43
+ }
44
+ const prompt = {
45
+ ...initialPrompt,
46
+ shell_page: this.uiShells.find((shell) => shell.screens.includes(initialPrompt.name))?.content,
47
+ };
48
+ const generator = new PageGenerator(prompt, conversationId);
49
+ this.references.set(prompt.path, generator);
38
50
  return this.addPageGenerator(generator);
39
51
  }
40
52
  async addPageGenerator(generator) {
@@ -8,4 +8,5 @@ export declare class UIServer {
8
8
  start(): Promise<void>;
9
9
  close(): void;
10
10
  resolveUrl(screenData: StormEventPage): string;
11
+ resolveUrlFromPath(path: string): string;
11
12
  }
@@ -22,6 +22,13 @@ class UIServer {
22
22
  }
23
23
  async start() {
24
24
  this.port = await clusterService_1.clusterService.getNextAvailablePort(this.port);
25
+ this.express.get('/_reset', (req, res) => {
26
+ res.send(`
27
+ <script>
28
+ window.localStorage.clear();
29
+ window.sessionStorage.clear();
30
+ </script>`);
31
+ });
25
32
  this.express.all('/*', async (req, res) => {
26
33
  (0, page_utils_1.readPageFromDisk)(this.systemId, req.params[0], req.method, res);
27
34
  });
@@ -40,8 +47,11 @@ class UIServer {
40
47
  }
41
48
  }
42
49
  resolveUrl(screenData) {
43
- const path = screenData.payload.path.startsWith('/') ? screenData.payload.path : `/${screenData.payload.path}`;
44
- return `http://localhost:${this.port}${path}`;
50
+ return this.resolveUrlFromPath(screenData.payload.path);
51
+ }
52
+ resolveUrlFromPath(path) {
53
+ const resolvedPath = path.startsWith('/') ? path : `/${path}`;
54
+ return `http://localhost:${this.port}${resolvedPath}`;
45
55
  }
46
56
  }
47
57
  exports.UIServer = UIServer;
@@ -375,5 +375,14 @@ export interface StormEventReferenceClassification {
375
375
  created: number;
376
376
  payload: ReferenceClassification;
377
377
  }
378
- 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 | StormEventLandingPage | StormEventReferenceClassification | StormEventApiBase;
378
+ export interface StormEventUIStarted {
379
+ type: 'UI_SERVER_STARTED';
380
+ reason: string;
381
+ created: number;
382
+ payload: {
383
+ conversationId: string;
384
+ resetUrl: string;
385
+ };
386
+ }
387
+ 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 | StormEventLandingPage | StormEventReferenceClassification | StormEventApiBase | StormEventUIStarted;
379
388
  export {};
@@ -9,6 +9,7 @@ export declare const SystemIdHeader = "System-Id";
9
9
  export declare function writePageToDisk(systemId: string, event: StormEventPage): Promise<{
10
10
  path: string;
11
11
  }>;
12
+ export declare function hasPageOnDisk(systemId: string, method: string, path: string): boolean;
12
13
  export declare function resolveReadPath(systemId: string, path: string, method: string): string | null;
13
14
  export declare function readPageFromDiskAsString(systemId: string, path: string, method: string): string | null;
14
15
  export declare function readPageFromDisk(systemId: string, path: string, method: string, res: Response): void;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.writeConversationToFile = exports.readConversationFromFile = exports.readPageFromDisk = exports.readPageFromDiskAsString = exports.resolveReadPath = exports.writePageToDisk = exports.SystemIdHeader = void 0;
6
+ exports.writeConversationToFile = exports.readConversationFromFile = exports.readPageFromDisk = exports.readPageFromDiskAsString = exports.resolveReadPath = exports.hasPageOnDisk = exports.writePageToDisk = exports.SystemIdHeader = void 0;
7
7
  const node_os_1 = __importDefault(require("node:os"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const fs_extra_1 = __importDefault(require("fs-extra"));
@@ -26,6 +26,13 @@ async function writePageToDisk(systemId, event) {
26
26
  };
27
27
  }
28
28
  exports.writePageToDisk = writePageToDisk;
29
+ function hasPageOnDisk(systemId, method, path) {
30
+ const baseDir = getBaseDir(systemId);
31
+ const filePath = getFilePath(method);
32
+ const fullPath = path_1.default.join(baseDir, normalizePath(path), filePath);
33
+ return fs_extra_1.default.existsSync(fullPath);
34
+ }
35
+ exports.hasPageOnDisk = hasPageOnDisk;
29
36
  function getBaseDir(systemId) {
30
37
  return path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId);
31
38
  }
@@ -71,19 +71,13 @@ router.post('/ui/screen', async (req, res) => {
71
71
  onRequestAborted(req, res, () => {
72
72
  queue.cancel();
73
73
  });
74
- await queue.addPrompt(aiRequest);
75
74
  const promises = [];
76
75
  queue.on('page', (data) => {
77
- switch (data.type) {
78
- case 'PAGE':
79
- console.log('Processing page event', data);
80
- if (systemId) {
81
- promises.push(sendPageEvent(systemId, data, res));
82
- }
83
- break;
76
+ if (systemId) {
77
+ promises.push(sendPageEvent(systemId, data, res));
84
78
  }
85
- sendEvent(res, data);
86
79
  });
80
+ await queue.addPrompt(aiRequest, conversationId, true);
87
81
  await queue.wait();
88
82
  await Promise.allSettled(promises);
89
83
  sendDone(res);
@@ -233,7 +227,7 @@ router.post('/:handle/ui', async (req, res) => {
233
227
  onRequestAborted(req, res, () => {
234
228
  shellsStream.abort();
235
229
  });
236
- const uiShells = [];
230
+ const queue = new PageGenerator_1.PageQueue(outerConversationId, 5);
237
231
  shellsStream.on('data', (data) => {
238
232
  console.log('Processing shell event', data);
239
233
  sendEvent(res, data);
@@ -243,7 +237,7 @@ router.post('/:handle/ui', async (req, res) => {
243
237
  if (shellsStream.isAborted()) {
244
238
  return;
245
239
  }
246
- uiShells.push(data.payload);
240
+ queue.addUiShell(data.payload);
247
241
  });
248
242
  shellsStream.on('error', (error) => {
249
243
  console.error('Error on shellsStream', error);
@@ -253,8 +247,16 @@ router.post('/:handle/ui', async (req, res) => {
253
247
  await waitForStormStream(shellsStream);
254
248
  UI_SERVERS[outerConversationId] = new UIServer_1.UIServer(outerConversationId);
255
249
  await UI_SERVERS[outerConversationId].start();
250
+ sendEvent(res, {
251
+ type: 'UI_SERVER_STARTED',
252
+ reason: '',
253
+ payload: {
254
+ conversationId: outerConversationId,
255
+ resetUrl: UI_SERVERS[outerConversationId].resolveUrlFromPath('/_reset'),
256
+ },
257
+ created: Date.now(),
258
+ });
256
259
  // Get the pages (5 at a time)
257
- const queue = new PageGenerator_1.PageQueue(outerConversationId, 5);
258
260
  const pagePromises = [];
259
261
  onRequestAborted(req, res, () => {
260
262
  queue.cancel();
@@ -276,7 +278,6 @@ router.post('/:handle/ui', async (req, res) => {
276
278
  title: screen.title,
277
279
  filename: screen.filename,
278
280
  storage_prefix: outerConversationId + '_',
279
- shell_page: uiShells.find((shell) => shell.screens.includes(screen.name))?.content,
280
281
  }));
281
282
  }
282
283
  await queue.wait();
@@ -536,7 +537,7 @@ function streamStormPartialResponse(result, res) {
536
537
  switch (data.type) {
537
538
  // todo: temporarily (for demo purposes) disable error messages when codegen fails
538
539
  case 'ERROR_INTERNAL':
539
- console.log("Error internal", data);
540
+ console.log('Error internal', data);
540
541
  return;
541
542
  }
542
543
  sendEvent(res, data);
@@ -555,13 +556,13 @@ function onRequestAborted(req, res, onAborted) {
555
556
  onAborted();
556
557
  });
557
558
  }
558
- function sendPageEvent(mainConversationId, data, res) {
559
- return (0, page_utils_1.writePageToDisk)(mainConversationId, data)
560
- .catch((err) => {
559
+ async function sendPageEvent(mainConversationId, data, res) {
560
+ try {
561
+ await (0, page_utils_1.writePageToDisk)(mainConversationId, data);
562
+ }
563
+ catch (err) {
561
564
  console.error('Failed to write page to disk', err);
562
- })
563
- .then(() => {
564
- sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
565
- });
565
+ }
566
+ sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
566
567
  }
567
568
  exports.default = router;
@@ -4,18 +4,20 @@
4
4
  */
5
5
  /// <reference types="node" />
6
6
  import { UIPagePrompt } from './stormClient';
7
- import { ReferenceClassification, StormEvent, StormEventPage } from './events';
7
+ import { ReferenceClassification, StormEvent, StormEventPage, UIShell } from './events';
8
8
  import { EventEmitter } from 'node:events';
9
9
  export declare class PageQueue extends EventEmitter {
10
10
  private readonly queue;
11
11
  private readonly systemId;
12
12
  private readonly references;
13
+ private uiShells;
13
14
  constructor(systemId: string, concurrency?: number);
14
15
  on(event: 'event', listener: (data: StormEvent) => void): this;
15
16
  on(event: 'page', listener: (data: StormEventPage) => void): this;
16
17
  emit(type: 'event', event: StormEvent): boolean;
17
18
  emit(type: 'page', event: StormEventPage): boolean;
18
- addPrompt(initialPrompt: UIPagePrompt): Promise<void>;
19
+ addUiShell(uiShell: UIShell): void;
20
+ addPrompt(initialPrompt: Omit<UIPagePrompt, 'shell_page'>, conversationId?: string, overwrite?: boolean): Promise<void>;
19
21
  private addPageGenerator;
20
22
  cancel(): void;
21
23
  wait(): Promise<void>;
@@ -12,10 +12,12 @@ const node_uuid_1 = __importDefault(require("node-uuid"));
12
12
  const stormClient_1 = require("./stormClient");
13
13
  const node_events_1 = require("node:events");
14
14
  const PromiseQueue_1 = require("./PromiseQueue");
15
+ const page_utils_1 = require("./page-utils");
15
16
  class PageQueue extends node_events_1.EventEmitter {
16
17
  queue;
17
18
  systemId;
18
19
  references = new Map();
20
+ uiShells = [];
19
21
  constructor(systemId, concurrency = 5) {
20
22
  super();
21
23
  this.systemId = systemId;
@@ -27,14 +29,24 @@ class PageQueue extends node_events_1.EventEmitter {
27
29
  emit(eventName, ...args) {
28
30
  return super.emit(eventName, ...args);
29
31
  }
30
- addPrompt(initialPrompt) {
31
- if (this.references.has(initialPrompt.path)) {
32
+ addUiShell(uiShell) {
33
+ this.uiShells.push(uiShell);
34
+ }
35
+ addPrompt(initialPrompt, conversationId = node_uuid_1.default.v4(), overwrite = false) {
36
+ if (!overwrite && this.references.has(initialPrompt.path)) {
32
37
  console.log('Ignoring duplicate prompt', initialPrompt.path);
33
38
  return Promise.resolve();
34
39
  }
35
- console.log('processing prompt', initialPrompt.path);
36
- const generator = new PageGenerator(initialPrompt);
37
- this.references.set(initialPrompt.path, generator);
40
+ if (!overwrite && (0, page_utils_1.hasPageOnDisk)(this.systemId, initialPrompt.method, initialPrompt.path)) {
41
+ console.log('Ignoring prompt with existing page', initialPrompt.path);
42
+ return Promise.resolve();
43
+ }
44
+ const prompt = {
45
+ ...initialPrompt,
46
+ shell_page: this.uiShells.find((shell) => shell.screens.includes(initialPrompt.name))?.content,
47
+ };
48
+ const generator = new PageGenerator(prompt, conversationId);
49
+ this.references.set(prompt.path, generator);
38
50
  return this.addPageGenerator(generator);
39
51
  }
40
52
  async addPageGenerator(generator) {
@@ -8,4 +8,5 @@ export declare class UIServer {
8
8
  start(): Promise<void>;
9
9
  close(): void;
10
10
  resolveUrl(screenData: StormEventPage): string;
11
+ resolveUrlFromPath(path: string): string;
11
12
  }
@@ -22,6 +22,13 @@ class UIServer {
22
22
  }
23
23
  async start() {
24
24
  this.port = await clusterService_1.clusterService.getNextAvailablePort(this.port);
25
+ this.express.get('/_reset', (req, res) => {
26
+ res.send(`
27
+ <script>
28
+ window.localStorage.clear();
29
+ window.sessionStorage.clear();
30
+ </script>`);
31
+ });
25
32
  this.express.all('/*', async (req, res) => {
26
33
  (0, page_utils_1.readPageFromDisk)(this.systemId, req.params[0], req.method, res);
27
34
  });
@@ -40,8 +47,11 @@ class UIServer {
40
47
  }
41
48
  }
42
49
  resolveUrl(screenData) {
43
- const path = screenData.payload.path.startsWith('/') ? screenData.payload.path : `/${screenData.payload.path}`;
44
- return `http://localhost:${this.port}${path}`;
50
+ return this.resolveUrlFromPath(screenData.payload.path);
51
+ }
52
+ resolveUrlFromPath(path) {
53
+ const resolvedPath = path.startsWith('/') ? path : `/${path}`;
54
+ return `http://localhost:${this.port}${resolvedPath}`;
45
55
  }
46
56
  }
47
57
  exports.UIServer = UIServer;
@@ -375,5 +375,14 @@ export interface StormEventReferenceClassification {
375
375
  created: number;
376
376
  payload: ReferenceClassification;
377
377
  }
378
- 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 | StormEventLandingPage | StormEventReferenceClassification | StormEventApiBase;
378
+ export interface StormEventUIStarted {
379
+ type: 'UI_SERVER_STARTED';
380
+ reason: string;
381
+ created: number;
382
+ payload: {
383
+ conversationId: string;
384
+ resetUrl: string;
385
+ };
386
+ }
387
+ 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 | StormEventLandingPage | StormEventReferenceClassification | StormEventApiBase | StormEventUIStarted;
379
388
  export {};
@@ -9,6 +9,7 @@ export declare const SystemIdHeader = "System-Id";
9
9
  export declare function writePageToDisk(systemId: string, event: StormEventPage): Promise<{
10
10
  path: string;
11
11
  }>;
12
+ export declare function hasPageOnDisk(systemId: string, method: string, path: string): boolean;
12
13
  export declare function resolveReadPath(systemId: string, path: string, method: string): string | null;
13
14
  export declare function readPageFromDiskAsString(systemId: string, path: string, method: string): string | null;
14
15
  export declare function readPageFromDisk(systemId: string, path: string, method: string, res: Response): void;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.writeConversationToFile = exports.readConversationFromFile = exports.readPageFromDisk = exports.readPageFromDiskAsString = exports.resolveReadPath = exports.writePageToDisk = exports.SystemIdHeader = void 0;
6
+ exports.writeConversationToFile = exports.readConversationFromFile = exports.readPageFromDisk = exports.readPageFromDiskAsString = exports.resolveReadPath = exports.hasPageOnDisk = exports.writePageToDisk = exports.SystemIdHeader = void 0;
7
7
  const node_os_1 = __importDefault(require("node:os"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const fs_extra_1 = __importDefault(require("fs-extra"));
@@ -26,6 +26,13 @@ async function writePageToDisk(systemId, event) {
26
26
  };
27
27
  }
28
28
  exports.writePageToDisk = writePageToDisk;
29
+ function hasPageOnDisk(systemId, method, path) {
30
+ const baseDir = getBaseDir(systemId);
31
+ const filePath = getFilePath(method);
32
+ const fullPath = path_1.default.join(baseDir, normalizePath(path), filePath);
33
+ return fs_extra_1.default.existsSync(fullPath);
34
+ }
35
+ exports.hasPageOnDisk = hasPageOnDisk;
29
36
  function getBaseDir(systemId) {
30
37
  return path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId);
31
38
  }
@@ -71,19 +71,13 @@ router.post('/ui/screen', async (req, res) => {
71
71
  onRequestAborted(req, res, () => {
72
72
  queue.cancel();
73
73
  });
74
- await queue.addPrompt(aiRequest);
75
74
  const promises = [];
76
75
  queue.on('page', (data) => {
77
- switch (data.type) {
78
- case 'PAGE':
79
- console.log('Processing page event', data);
80
- if (systemId) {
81
- promises.push(sendPageEvent(systemId, data, res));
82
- }
83
- break;
76
+ if (systemId) {
77
+ promises.push(sendPageEvent(systemId, data, res));
84
78
  }
85
- sendEvent(res, data);
86
79
  });
80
+ await queue.addPrompt(aiRequest, conversationId, true);
87
81
  await queue.wait();
88
82
  await Promise.allSettled(promises);
89
83
  sendDone(res);
@@ -233,7 +227,7 @@ router.post('/:handle/ui', async (req, res) => {
233
227
  onRequestAborted(req, res, () => {
234
228
  shellsStream.abort();
235
229
  });
236
- const uiShells = [];
230
+ const queue = new PageGenerator_1.PageQueue(outerConversationId, 5);
237
231
  shellsStream.on('data', (data) => {
238
232
  console.log('Processing shell event', data);
239
233
  sendEvent(res, data);
@@ -243,7 +237,7 @@ router.post('/:handle/ui', async (req, res) => {
243
237
  if (shellsStream.isAborted()) {
244
238
  return;
245
239
  }
246
- uiShells.push(data.payload);
240
+ queue.addUiShell(data.payload);
247
241
  });
248
242
  shellsStream.on('error', (error) => {
249
243
  console.error('Error on shellsStream', error);
@@ -253,8 +247,16 @@ router.post('/:handle/ui', async (req, res) => {
253
247
  await waitForStormStream(shellsStream);
254
248
  UI_SERVERS[outerConversationId] = new UIServer_1.UIServer(outerConversationId);
255
249
  await UI_SERVERS[outerConversationId].start();
250
+ sendEvent(res, {
251
+ type: 'UI_SERVER_STARTED',
252
+ reason: '',
253
+ payload: {
254
+ conversationId: outerConversationId,
255
+ resetUrl: UI_SERVERS[outerConversationId].resolveUrlFromPath('/_reset'),
256
+ },
257
+ created: Date.now(),
258
+ });
256
259
  // Get the pages (5 at a time)
257
- const queue = new PageGenerator_1.PageQueue(outerConversationId, 5);
258
260
  const pagePromises = [];
259
261
  onRequestAborted(req, res, () => {
260
262
  queue.cancel();
@@ -276,7 +278,6 @@ router.post('/:handle/ui', async (req, res) => {
276
278
  title: screen.title,
277
279
  filename: screen.filename,
278
280
  storage_prefix: outerConversationId + '_',
279
- shell_page: uiShells.find((shell) => shell.screens.includes(screen.name))?.content,
280
281
  }));
281
282
  }
282
283
  await queue.wait();
@@ -536,7 +537,7 @@ function streamStormPartialResponse(result, res) {
536
537
  switch (data.type) {
537
538
  // todo: temporarily (for demo purposes) disable error messages when codegen fails
538
539
  case 'ERROR_INTERNAL':
539
- console.log("Error internal", data);
540
+ console.log('Error internal', data);
540
541
  return;
541
542
  }
542
543
  sendEvent(res, data);
@@ -555,13 +556,13 @@ function onRequestAborted(req, res, onAborted) {
555
556
  onAborted();
556
557
  });
557
558
  }
558
- function sendPageEvent(mainConversationId, data, res) {
559
- return (0, page_utils_1.writePageToDisk)(mainConversationId, data)
560
- .catch((err) => {
559
+ async function sendPageEvent(mainConversationId, data, res) {
560
+ try {
561
+ await (0, page_utils_1.writePageToDisk)(mainConversationId, data);
562
+ }
563
+ catch (err) {
561
564
  console.error('Failed to write page to disk', err);
562
- })
563
- .then(() => {
564
- sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
565
- });
565
+ }
566
+ sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
566
567
  }
567
568
  exports.default = router;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.62.1",
3
+ "version": "0.63.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -5,14 +5,22 @@
5
5
 
6
6
  import uuid from 'node-uuid';
7
7
  import { stormClient, UIPagePrompt } from './stormClient';
8
- import { ReferenceClassification, StormEvent, StormEventPage, StormEventReferenceClassification } from './events';
8
+ import {
9
+ ReferenceClassification,
10
+ StormEvent,
11
+ StormEventPage,
12
+ StormEventReferenceClassification,
13
+ UIShell,
14
+ } from './events';
9
15
  import { EventEmitter } from 'node:events';
10
16
  import { PromiseQueue } from './PromiseQueue';
17
+ import { hasPageOnDisk } from './page-utils';
11
18
 
12
19
  export class PageQueue extends EventEmitter {
13
20
  private readonly queue: PromiseQueue;
14
21
  private readonly systemId: string;
15
22
  private readonly references: Map<string, PageGenerator> = new Map();
23
+ private uiShells: UIShell[] = [];
16
24
 
17
25
  constructor(systemId: string, concurrency: number = 5) {
18
26
  super();
@@ -33,14 +41,33 @@ export class PageQueue extends EventEmitter {
33
41
  return super.emit(eventName, ...args);
34
42
  }
35
43
 
36
- public addPrompt(initialPrompt: UIPagePrompt) {
37
- if (this.references.has(initialPrompt.path)) {
44
+ public addUiShell(uiShell: UIShell) {
45
+ this.uiShells.push(uiShell);
46
+ }
47
+
48
+ public addPrompt(
49
+ initialPrompt: Omit<UIPagePrompt, 'shell_page'>,
50
+ conversationId: string = uuid.v4(),
51
+ overwrite: boolean = false
52
+ ) {
53
+ if (!overwrite && this.references.has(initialPrompt.path)) {
38
54
  console.log('Ignoring duplicate prompt', initialPrompt.path);
39
55
  return Promise.resolve();
40
56
  }
41
- console.log('processing prompt', initialPrompt.path);
42
- const generator = new PageGenerator(initialPrompt);
43
- this.references.set(initialPrompt.path, generator);
57
+
58
+ if (!overwrite && hasPageOnDisk(this.systemId, initialPrompt.method, initialPrompt.path)) {
59
+ console.log('Ignoring prompt with existing page', initialPrompt.path);
60
+ return Promise.resolve();
61
+ }
62
+
63
+ const prompt: UIPagePrompt = {
64
+ ...initialPrompt,
65
+ shell_page: this.uiShells.find((shell) => shell.screens.includes(initialPrompt.name))?.content,
66
+ };
67
+
68
+ const generator = new PageGenerator(prompt, conversationId);
69
+ this.references.set(prompt.path, generator);
70
+
44
71
  return this.addPageGenerator(generator);
45
72
  }
46
73
 
@@ -23,6 +23,16 @@ export class UIServer {
23
23
  public async start() {
24
24
  this.port = await clusterService.getNextAvailablePort(this.port);
25
25
 
26
+ this.express.get('/_reset', (req: Request, res: Response) => {
27
+ res.send(
28
+ `
29
+ <script>
30
+ window.localStorage.clear();
31
+ window.sessionStorage.clear();
32
+ </script>`
33
+ );
34
+ });
35
+
26
36
  this.express.all('/*', async (req: Request, res: Response) => {
27
37
  readPageFromDisk(this.systemId, req.params[0], req.method, res);
28
38
  });
@@ -44,7 +54,11 @@ export class UIServer {
44
54
  }
45
55
 
46
56
  resolveUrl(screenData: StormEventPage) {
47
- const path = screenData.payload.path.startsWith('/') ? screenData.payload.path : `/${screenData.payload.path}`;
48
- return `http://localhost:${this.port}${path}`;
57
+ return this.resolveUrlFromPath(screenData.payload.path);
58
+ }
59
+
60
+ resolveUrlFromPath(path: string) {
61
+ const resolvedPath = path.startsWith('/') ? path : `/${path}`;
62
+ return `http://localhost:${this.port}${resolvedPath}`;
49
63
  }
50
64
  }
@@ -264,7 +264,13 @@ export interface StormEventFileChunk extends StormEventFileBase {
264
264
  }
265
265
 
266
266
  export interface StormEventApiBase {
267
- type: 'API_STREAM_CHUNK' | 'API_STREAM_DONE' | 'API_STREAM_FAILED' | 'API_STREAM_STATE' | 'API_STREAM_START' | 'API_STREAM_CHUNK_RESET';
267
+ type:
268
+ | 'API_STREAM_CHUNK'
269
+ | 'API_STREAM_DONE'
270
+ | 'API_STREAM_FAILED'
271
+ | 'API_STREAM_STATE'
272
+ | 'API_STREAM_START'
273
+ | 'API_STREAM_CHUNK_RESET';
268
274
  payload: StormEventFileBasePayload;
269
275
  }
270
276
 
@@ -445,6 +451,16 @@ export interface StormEventReferenceClassification {
445
451
  payload: ReferenceClassification;
446
452
  }
447
453
 
454
+ export interface StormEventUIStarted {
455
+ type: 'UI_SERVER_STARTED';
456
+ reason: string;
457
+ created: number;
458
+ payload: {
459
+ conversationId: string;
460
+ resetUrl: string;
461
+ };
462
+ }
463
+
448
464
  export type StormEvent =
449
465
  | StormEventCreateBlock
450
466
  | StormEventCreateConnection
@@ -477,4 +493,5 @@ export type StormEvent =
477
493
  | StormEventPromptImprove
478
494
  | StormEventLandingPage
479
495
  | StormEventReferenceClassification
480
- | StormEventApiBase;
496
+ | StormEventApiBase
497
+ | StormEventUIStarted;
@@ -34,6 +34,13 @@ export async function writePageToDisk(systemId: string, event: StormEventPage) {
34
34
  };
35
35
  }
36
36
 
37
+ export function hasPageOnDisk(systemId: string, method: string, path: string) {
38
+ const baseDir = getBaseDir(systemId);
39
+ const filePath = getFilePath(method);
40
+ const fullPath = Path.join(baseDir, normalizePath(path), filePath);
41
+ return FS.existsSync(fullPath);
42
+ }
43
+
37
44
  function getBaseDir(systemId: string) {
38
45
  return Path.join(os.tmpdir(), 'ai-systems', systemId);
39
46
  }
@@ -106,22 +106,16 @@ router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
106
106
  queue.cancel();
107
107
  });
108
108
 
109
- await queue.addPrompt(aiRequest);
110
-
111
109
  const promises: Promise<void>[] = [];
112
- queue.on('page', (data) => {
113
- switch (data.type) {
114
- case 'PAGE':
115
- console.log('Processing page event', data);
116
110
 
117
- if (systemId) {
118
- promises.push(sendPageEvent(systemId, data, res));
119
- }
120
- break;
111
+ queue.on('page', (data) => {
112
+ if (systemId) {
113
+ promises.push(sendPageEvent(systemId, data, res));
121
114
  }
122
- sendEvent(res, data);
123
115
  });
124
116
 
117
+ await queue.addPrompt(aiRequest, conversationId, true);
118
+
125
119
  await queue.wait();
126
120
  await Promise.allSettled(promises);
127
121
 
@@ -304,8 +298,7 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
304
298
  shellsStream.abort();
305
299
  });
306
300
 
307
- const uiShells: UIShell[] = [];
308
-
301
+ const queue = new PageQueue(outerConversationId, 5);
309
302
  shellsStream.on('data', (data: StormEvent) => {
310
303
  console.log('Processing shell event', data);
311
304
  sendEvent(res, data);
@@ -318,7 +311,7 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
318
311
  return;
319
312
  }
320
313
 
321
- uiShells.push(data.payload);
314
+ queue.addUiShell(data.payload);
322
315
  });
323
316
 
324
317
  shellsStream.on('error', (error) => {
@@ -332,8 +325,18 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
332
325
  UI_SERVERS[outerConversationId] = new UIServer(outerConversationId);
333
326
  await UI_SERVERS[outerConversationId].start();
334
327
 
328
+ sendEvent(res, {
329
+ type: 'UI_SERVER_STARTED',
330
+ reason: '',
331
+ payload: {
332
+ conversationId: outerConversationId,
333
+ resetUrl: UI_SERVERS[outerConversationId].resolveUrlFromPath('/_reset'),
334
+ },
335
+ created: Date.now(),
336
+ });
337
+
335
338
  // Get the pages (5 at a time)
336
- const queue = new PageQueue(outerConversationId, 5);
339
+
337
340
  const pagePromises: Promise<void>[] = [];
338
341
  onRequestAborted(req, res, () => {
339
342
  queue.cancel();
@@ -359,7 +362,6 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
359
362
  title: screen.title,
360
363
  filename: screen.filename,
361
364
  storage_prefix: outerConversationId + '_',
362
- shell_page: uiShells.find((shell) => shell.screens.includes(screen.name))?.content,
363
365
  })
364
366
  );
365
367
  }
@@ -668,7 +670,7 @@ function streamStormPartialResponse(result: StormStream, res: Response) {
668
670
  switch (data.type) {
669
671
  // todo: temporarily (for demo purposes) disable error messages when codegen fails
670
672
  case 'ERROR_INTERNAL':
671
- console.log("Error internal", data);
673
+ console.log('Error internal', data);
672
674
  return;
673
675
  }
674
676
  sendEvent(res, data);
@@ -691,14 +693,13 @@ function onRequestAborted(req: KapetaBodyRequest, res: Response, onAborted: () =
691
693
  });
692
694
  }
693
695
 
694
- function sendPageEvent(mainConversationId: string, data: StormEventPage, res: Response) {
695
- return writePageToDisk(mainConversationId, data)
696
- .catch((err) => {
697
- console.error('Failed to write page to disk', err);
698
- })
699
- .then(() => {
700
- sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
701
- });
696
+ async function sendPageEvent(mainConversationId: string, data: StormEventPage, res: Response) {
697
+ try {
698
+ await writePageToDisk(mainConversationId, data);
699
+ } catch (err) {
700
+ console.error('Failed to write page to disk', err);
701
+ }
702
+ sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
702
703
  }
703
704
 
704
705
  export default router;