@kapeta/local-cluster-service 0.61.2 → 0.62.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [0.62.1](https://github.com/kapetacom/local-cluster-service/compare/v0.62.0...v0.62.1) (2024-08-14)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * add openapi spec streaming for a sense of progress ([95eff45](https://github.com/kapetacom/local-cluster-service/commit/95eff4504fd83a2440f4343f608383299fa50326))
7
+
8
+ # [0.62.0](https://github.com/kapetacom/local-cluster-service/compare/v0.61.2...v0.62.0) (2024-08-13)
9
+
10
+
11
+ ### Features
12
+
13
+ * Add iterative reference resolving ([#215](https://github.com/kapetacom/local-cluster-service/issues/215)) ([8bf5e23](https://github.com/kapetacom/local-cluster-service/commit/8bf5e23ce36031ca10df6c407a8a3e73a3e1ac46))
14
+
1
15
  ## [0.61.2](https://github.com/kapetacom/local-cluster-service/compare/v0.61.1...v0.61.2) (2024-08-13)
2
16
 
3
17
 
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+ /// <reference types="node" />
6
+ import { UIPagePrompt } from './stormClient';
7
+ import { ReferenceClassification, StormEvent, StormEventPage } from './events';
8
+ import { EventEmitter } from 'node:events';
9
+ export declare class PageQueue extends EventEmitter {
10
+ private readonly queue;
11
+ private readonly systemId;
12
+ private readonly references;
13
+ constructor(systemId: string, concurrency?: number);
14
+ on(event: 'event', listener: (data: StormEvent) => void): this;
15
+ on(event: 'page', listener: (data: StormEventPage) => void): this;
16
+ emit(type: 'event', event: StormEvent): boolean;
17
+ emit(type: 'page', event: StormEventPage): boolean;
18
+ addPrompt(initialPrompt: UIPagePrompt): Promise<void>;
19
+ private addPageGenerator;
20
+ cancel(): void;
21
+ wait(): Promise<void>;
22
+ }
23
+ export declare class PageGenerator extends EventEmitter {
24
+ private readonly conversationId;
25
+ private prompt;
26
+ constructor(prompt: UIPagePrompt, conversationId?: string);
27
+ on(event: 'event', listener: (data: StormEvent) => void): this;
28
+ on(event: 'page_refs', listener: (data: {
29
+ event: StormEventPage;
30
+ references: ReferenceClassification[];
31
+ }) => void): this;
32
+ emit(type: 'event', event: StormEvent): boolean;
33
+ emit(type: 'page_refs', event: {
34
+ event: StormEventPage;
35
+ references: ReferenceClassification[];
36
+ }): boolean;
37
+ generate(): Promise<void>;
38
+ private resolveReferences;
39
+ }
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright 2023 Kapeta Inc.
4
+ * SPDX-License-Identifier: BUSL-1.1
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.PageGenerator = exports.PageQueue = void 0;
11
+ const node_uuid_1 = __importDefault(require("node-uuid"));
12
+ const stormClient_1 = require("./stormClient");
13
+ const node_events_1 = require("node:events");
14
+ const PromiseQueue_1 = require("./PromiseQueue");
15
+ class PageQueue extends node_events_1.EventEmitter {
16
+ queue;
17
+ systemId;
18
+ references = new Map();
19
+ constructor(systemId, concurrency = 5) {
20
+ super();
21
+ this.systemId = systemId;
22
+ this.queue = new PromiseQueue_1.PromiseQueue(concurrency);
23
+ }
24
+ on(event, listener) {
25
+ return super.on(event, listener);
26
+ }
27
+ emit(eventName, ...args) {
28
+ return super.emit(eventName, ...args);
29
+ }
30
+ addPrompt(initialPrompt) {
31
+ if (this.references.has(initialPrompt.path)) {
32
+ console.log('Ignoring duplicate prompt', initialPrompt.path);
33
+ return Promise.resolve();
34
+ }
35
+ console.log('processing prompt', initialPrompt.path);
36
+ const generator = new PageGenerator(initialPrompt);
37
+ this.references.set(initialPrompt.path, generator);
38
+ return this.addPageGenerator(generator);
39
+ }
40
+ async addPageGenerator(generator) {
41
+ generator.on('event', (event) => this.emit('event', event));
42
+ generator.on('page_refs', ({ event, references }) => {
43
+ this.emit('page', event);
44
+ references.forEach((reference) => {
45
+ if (reference.url.startsWith('#') ||
46
+ reference.url.startsWith('javascript:') ||
47
+ reference.url.startsWith('http://') ||
48
+ reference.url.startsWith('https://')) {
49
+ return;
50
+ }
51
+ switch (reference.type) {
52
+ case 'image':
53
+ console.log('Ignoring image reference', reference);
54
+ break;
55
+ case 'css':
56
+ case 'javascript':
57
+ //console.log('Ignoring reference', reference);
58
+ break;
59
+ case 'html':
60
+ console.log('Adding page generator for', reference);
61
+ const paths = Array.from(this.references.keys());
62
+ this.addPrompt({
63
+ name: reference.name,
64
+ title: reference.title,
65
+ path: reference.url,
66
+ method: 'GET',
67
+ storage_prefix: this.systemId + '_',
68
+ prompt: `Implement a page for ${reference.name} at ${reference.url} with the following description: ${reference.description}.\n` +
69
+ `The page was referenced from this page: \`\`\`html\n${event.payload.content}\n\`\`\`\n` +
70
+ (paths.length > 0
71
+ ? `\nThese paths are already implemented:\n- ${paths.join('\n - ')}\n\n`
72
+ : ''),
73
+ description: reference.description,
74
+ filename: '',
75
+ });
76
+ break;
77
+ }
78
+ });
79
+ });
80
+ return this.queue.add(() => generator.generate());
81
+ }
82
+ cancel() {
83
+ this.queue.cancel();
84
+ }
85
+ wait() {
86
+ return this.queue.wait();
87
+ }
88
+ }
89
+ exports.PageQueue = PageQueue;
90
+ class PageGenerator extends node_events_1.EventEmitter {
91
+ conversationId;
92
+ prompt;
93
+ constructor(prompt, conversationId = node_uuid_1.default.v4()) {
94
+ super();
95
+ this.conversationId = conversationId;
96
+ this.prompt = prompt;
97
+ }
98
+ on(event, listener) {
99
+ return super.on(event, listener);
100
+ }
101
+ emit(eventName, ...args) {
102
+ return super.emit(eventName, ...args);
103
+ }
104
+ async generate() {
105
+ return new Promise(async (resolve) => {
106
+ const screenStream = await stormClient_1.stormClient.createUIPage(this.prompt, this.conversationId);
107
+ const promises = [];
108
+ screenStream.on('data', (event) => {
109
+ if (event.type === 'PAGE') {
110
+ event.payload.conversationId = this.conversationId;
111
+ promises.push((async () => {
112
+ const references = await this.resolveReferences(event.payload.content);
113
+ //console.log('Resolved references for page', references, event.payload);
114
+ this.emit('page_refs', {
115
+ event,
116
+ references,
117
+ });
118
+ })());
119
+ return;
120
+ }
121
+ this.emit('event', event);
122
+ });
123
+ screenStream.on('end', () => {
124
+ Promise.allSettled(promises).finally(resolve);
125
+ });
126
+ await screenStream.waitForDone();
127
+ });
128
+ }
129
+ async resolveReferences(content) {
130
+ const referenceStream = await stormClient_1.stormClient.classifyUIReferences(content);
131
+ const references = [];
132
+ referenceStream.on('data', (referenceData) => {
133
+ if (referenceData.type !== 'REF_CLASSIFICATION') {
134
+ return;
135
+ }
136
+ //console.log('Processing reference classification', referenceData);
137
+ references.push(referenceData.payload);
138
+ });
139
+ await referenceStream.waitForDone();
140
+ return references;
141
+ }
142
+ }
143
+ exports.PageGenerator = PageGenerator;
@@ -236,6 +236,17 @@ class StormEventParser {
236
236
  this.blocks[evt.payload.blockName].models = [];
237
237
  }
238
238
  break;
239
+ case 'API_STREAM_CHUNK':
240
+ case 'API_STREAM_CHUNK_RESET':
241
+ case 'API_STREAM_DONE':
242
+ case 'API_STREAM_FAILED':
243
+ case 'API_STREAM_STATE':
244
+ case 'API_STREAM_START':
245
+ if ('blockName' in evt.payload) {
246
+ evt.payload.blockRef = StormEventParser.toRef(handle, evt.payload.blockName).toNormalizedString();
247
+ evt.payload.instanceId = StormEventParser.toInstanceIdFromRef(evt.payload.blockRef);
248
+ }
249
+ break;
239
250
  }
240
251
  return await this.toResult(handle);
241
252
  }
@@ -218,6 +218,10 @@ export interface StormEventFileChunk extends StormEventFileBase {
218
218
  lineNumber: number;
219
219
  };
220
220
  }
221
+ export interface StormEventApiBase {
222
+ type: 'API_STREAM_CHUNK' | 'API_STREAM_DONE' | 'API_STREAM_FAILED' | 'API_STREAM_STATE' | 'API_STREAM_START' | 'API_STREAM_CHUNK_RESET';
223
+ payload: StormEventFileBasePayload;
224
+ }
221
225
  export interface StormEventBlockReady {
222
226
  type: 'BLOCK_READY';
223
227
  reason: string;
@@ -342,5 +346,34 @@ export interface StormEventPromptImprove {
342
346
  prompt: string;
343
347
  };
344
348
  }
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;
349
+ export interface LandingPage {
350
+ name: string;
351
+ title: string;
352
+ filename: string;
353
+ create_prompt: string;
354
+ path: string;
355
+ archetype: string;
356
+ requires_authentication: boolean;
357
+ }
358
+ export interface StormEventLandingPage {
359
+ type: 'LANDING_PAGE';
360
+ reason: string;
361
+ created: number;
362
+ payload: LandingPage;
363
+ }
364
+ export interface ReferenceClassification {
365
+ name: string;
366
+ title: string;
367
+ url: string;
368
+ description: string;
369
+ type: 'image' | 'css' | 'javascript' | 'html';
370
+ source: 'local' | 'cdn' | 'example';
371
+ }
372
+ export interface StormEventReferenceClassification {
373
+ type: 'REF_CLASSIFICATION';
374
+ reason: string;
375
+ created: number;
376
+ payload: ReferenceClassification;
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;
346
379
  export {};
@@ -4,9 +4,19 @@
4
4
  */
5
5
  import { StormEventPage } from './events';
6
6
  import { Response } from 'express';
7
+ import { ConversationItem } from './stream';
7
8
  export declare const SystemIdHeader = "System-Id";
8
9
  export declare function writePageToDisk(systemId: string, event: StormEventPage): Promise<{
9
10
  path: string;
10
11
  }>;
12
+ export declare function resolveReadPath(systemId: string, path: string, method: string): string | null;
11
13
  export declare function readPageFromDiskAsString(systemId: string, path: string, method: string): string | null;
12
14
  export declare function readPageFromDisk(systemId: string, path: string, method: string, res: Response): void;
15
+ export interface Conversation {
16
+ messages: ConversationItem[];
17
+ variantId: string;
18
+ type: 'page';
19
+ filename: string;
20
+ }
21
+ export declare function readConversationFromFile(filename: string): Conversation[];
22
+ export declare function writeConversationToFile(filename: string, conversations: Conversation[]): void;
@@ -3,13 +3,21 @@ 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.readPageFromDisk = exports.readPageFromDiskAsString = exports.writePageToDisk = exports.SystemIdHeader = void 0;
6
+ exports.writeConversationToFile = exports.readConversationFromFile = exports.readPageFromDisk = exports.readPageFromDiskAsString = exports.resolveReadPath = 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"));
10
10
  exports.SystemIdHeader = 'System-Id';
11
+ function normalizePath(path) {
12
+ return path
13
+ .replace(/\?.*$/gi, '')
14
+ .replace(/:[a-z][a-z_]*\b/gi, '*')
15
+ .replace(/\{[a-z]+}/gi, '*');
16
+ }
11
17
  async function writePageToDisk(systemId, event) {
12
- const path = path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId, event.payload.path, event.payload.method.toLowerCase(), 'index.html');
18
+ const baseDir = getBaseDir(systemId);
19
+ const filePath = getFilePath(event.payload.method);
20
+ const path = path_1.default.join(baseDir, normalizePath(event.payload.path), filePath);
13
21
  await fs_extra_1.default.ensureDir(path_1.default.dirname(path));
14
22
  await fs_extra_1.default.writeFile(path, event.payload.content);
15
23
  console.log(`Page written to disk: ${event.payload.title} > ${path}`);
@@ -18,17 +26,53 @@ async function writePageToDisk(systemId, event) {
18
26
  };
19
27
  }
20
28
  exports.writePageToDisk = writePageToDisk;
29
+ function getBaseDir(systemId) {
30
+ return path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId);
31
+ }
32
+ function getFilePath(method) {
33
+ return path_1.default.join(method.toLowerCase(), 'index.html');
34
+ }
35
+ function resolveReadPath(systemId, path, method) {
36
+ const baseDir = getBaseDir(systemId);
37
+ path = normalizePath(path);
38
+ const filePath = getFilePath(method);
39
+ const fullPath = path_1.default.join(baseDir, path, filePath);
40
+ if (fs_extra_1.default.existsSync(fullPath)) {
41
+ return fullPath;
42
+ }
43
+ const parts = path.split(/\*/g);
44
+ let currentPath = '';
45
+ for (let part in parts) {
46
+ const thisPath = path_1.default.join(currentPath, part);
47
+ const starPath = path_1.default.join(currentPath, '*');
48
+ const thisPathDir = path_1.default.join(baseDir, thisPath);
49
+ const starPathDir = path_1.default.join(baseDir, starPath);
50
+ if (fs_extra_1.default.existsSync(thisPathDir)) {
51
+ currentPath = thisPath;
52
+ continue;
53
+ }
54
+ if (fs_extra_1.default.existsSync(starPathDir)) {
55
+ currentPath = starPath;
56
+ continue;
57
+ }
58
+ console.log('Path not found', thisPathDir, starPathDir);
59
+ // Path not found
60
+ return null;
61
+ }
62
+ return path_1.default.join(baseDir, currentPath, filePath);
63
+ }
64
+ exports.resolveReadPath = resolveReadPath;
21
65
  function readPageFromDiskAsString(systemId, path, method) {
22
- const filePath = path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId, path, method.toLowerCase(), 'index.html');
23
- if (!fs_extra_1.default.existsSync(filePath)) {
66
+ const filePath = resolveReadPath(systemId, path, method);
67
+ if (!filePath || !fs_extra_1.default.existsSync(filePath)) {
24
68
  return null;
25
69
  }
26
70
  return fs_extra_1.default.readFileSync(filePath, 'utf8');
27
71
  }
28
72
  exports.readPageFromDiskAsString = readPageFromDiskAsString;
29
73
  function readPageFromDisk(systemId, path, method, res) {
30
- const filePath = path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId, path, method.toLowerCase(), 'index.html');
31
- if (!fs_extra_1.default.existsSync(filePath)) {
74
+ const filePath = resolveReadPath(systemId, path, method);
75
+ if (!filePath || !fs_extra_1.default.existsSync(filePath)) {
32
76
  res.status(404).send('Page not found');
33
77
  return;
34
78
  }
@@ -38,3 +82,18 @@ function readPageFromDisk(systemId, path, method, res) {
38
82
  res.end();
39
83
  }
40
84
  exports.readPageFromDisk = readPageFromDisk;
85
+ function readConversationFromFile(filename) {
86
+ if (!fs_extra_1.default.existsSync(filename)) {
87
+ return [];
88
+ }
89
+ const content = fs_extra_1.default.readFileSync(filename).toString();
90
+ if (!content.trim()) {
91
+ return [];
92
+ }
93
+ return content.split(/\n/g).map((line) => JSON.parse(line));
94
+ }
95
+ exports.readConversationFromFile = readConversationFromFile;
96
+ function writeConversationToFile(filename, conversations) {
97
+ fs_extra_1.default.writeFileSync(filename, conversations.map((conversation) => JSON.stringify(conversation)).join('\n'));
98
+ }
99
+ exports.writeConversationToFile = writeConversationToFile;
@@ -19,13 +19,14 @@ const event_parser_1 = require("./event-parser");
19
19
  const codegen_1 = require("./codegen");
20
20
  const assetManager_1 = require("../assetManager");
21
21
  const node_uuid_1 = __importDefault(require("node-uuid"));
22
- const PromiseQueue_1 = require("./PromiseQueue");
23
22
  const page_utils_1 = require("./page-utils");
24
23
  const UIServer_1 = require("./UIServer");
24
+ const PageGenerator_1 = require("./PageGenerator");
25
25
  const UI_SERVERS = {};
26
26
  const router = (0, express_promise_router_1.default)();
27
27
  router.use('/', cors_1.corsHandler);
28
28
  router.use('/', stringBody_1.stringBody);
29
+ const samplesBaseDir = path_1.default.join(__dirname, 'samples');
29
30
  function convertPageEvent(screenData, innerConversationId, mainConversationId) {
30
31
  if (screenData.type === 'PAGE') {
31
32
  const server = UI_SERVERS[mainConversationId];
@@ -62,19 +63,20 @@ router.post('/ui/screen', async (req, res) => {
62
63
  const systemId = req.headers[page_utils_1.SystemIdHeader.toLowerCase()];
63
64
  const aiRequest = JSON.parse(req.stringBody ?? '{}');
64
65
  aiRequest.storage_prefix = systemId ? systemId + '_' : 'mock_';
65
- const screenStream = await stormClient_1.stormClient.createUIPage(aiRequest, conversationId);
66
- onRequestAborted(req, res, () => {
67
- screenStream.abort();
68
- });
69
66
  res.set('Content-Type', 'application/x-ndjson');
70
67
  res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
71
- res.set(stormClient_1.ConversationIdHeader, screenStream.getConversationId());
68
+ res.set(stormClient_1.ConversationIdHeader, conversationId);
69
+ const parentConversationId = systemId ?? '';
70
+ const queue = new PageGenerator_1.PageQueue(parentConversationId, 5);
71
+ onRequestAborted(req, res, () => {
72
+ queue.cancel();
73
+ });
74
+ await queue.addPrompt(aiRequest);
72
75
  const promises = [];
73
- screenStream.on('data', (data) => {
76
+ queue.on('page', (data) => {
74
77
  switch (data.type) {
75
78
  case 'PAGE':
76
79
  console.log('Processing page event', data);
77
- data.payload.conversationId = screenStream.getConversationId();
78
80
  if (systemId) {
79
81
  promises.push(sendPageEvent(systemId, data, res));
80
82
  }
@@ -82,7 +84,7 @@ router.post('/ui/screen', async (req, res) => {
82
84
  }
83
85
  sendEvent(res, data);
84
86
  });
85
- await waitForStormStream(screenStream);
87
+ await queue.wait();
86
88
  await Promise.allSettled(promises);
87
89
  sendDone(res);
88
90
  }
@@ -105,6 +107,78 @@ router.delete('/:handle/ui', async (req, res) => {
105
107
  delete UI_SERVERS[conversationId];
106
108
  }
107
109
  });
110
+ router.post('/:handle/ui/iterative', async (req, res) => {
111
+ const handle = req.params.handle;
112
+ try {
113
+ const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
114
+ const aiRequest = JSON.parse(req.stringBody ?? '{}');
115
+ const landingPagesStream = await stormClient_1.stormClient.createUILandingPages(aiRequest.prompt, conversationId);
116
+ onRequestAborted(req, res, () => {
117
+ landingPagesStream.abort();
118
+ });
119
+ res.set('Content-Type', 'application/x-ndjson');
120
+ res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
121
+ res.set(stormClient_1.ConversationIdHeader, landingPagesStream.getConversationId());
122
+ const promises = {};
123
+ const pageEventPromises = [];
124
+ const systemId = landingPagesStream.getConversationId();
125
+ const pageQueue = new PageGenerator_1.PageQueue(systemId, 5);
126
+ landingPagesStream.on('data', async (data) => {
127
+ try {
128
+ sendEvent(res, data);
129
+ if (data.type !== 'LANDING_PAGE') {
130
+ return;
131
+ }
132
+ if (landingPagesStream.isAborted()) {
133
+ return;
134
+ }
135
+ const landingPage = data.payload;
136
+ if (landingPage.name in promises) {
137
+ return;
138
+ }
139
+ // We add the landing pages to the prompt queue.
140
+ // These will then be analysed - creating further pages as needed
141
+ promises[landingPage.name] = pageQueue.addPrompt({
142
+ prompt: landingPage.create_prompt,
143
+ method: 'GET',
144
+ path: landingPage.path,
145
+ description: landingPage.create_prompt,
146
+ name: landingPage.name,
147
+ title: landingPage.title,
148
+ filename: landingPage.filename,
149
+ storage_prefix: systemId + '_',
150
+ });
151
+ }
152
+ catch (e) {
153
+ console.error('Failed to process event', e);
154
+ }
155
+ });
156
+ UI_SERVERS[systemId] = new UIServer_1.UIServer(systemId);
157
+ await UI_SERVERS[systemId].start();
158
+ onRequestAborted(req, res, () => {
159
+ pageQueue.cancel();
160
+ });
161
+ pageQueue.on('page', (screenData) => {
162
+ pageEventPromises.push(sendPageEvent(landingPagesStream.getConversationId(), screenData, res));
163
+ });
164
+ pageQueue.on('event', (screenData) => {
165
+ sendEvent(res, screenData);
166
+ });
167
+ await waitForStormStream(landingPagesStream);
168
+ await pageQueue.wait();
169
+ await Promise.allSettled(pageEventPromises);
170
+ if (landingPagesStream.isAborted()) {
171
+ return;
172
+ }
173
+ sendDone(res);
174
+ }
175
+ catch (err) {
176
+ sendError(err, res);
177
+ if (!res.closed) {
178
+ res.end();
179
+ }
180
+ }
181
+ });
108
182
  router.post('/:handle/ui', async (req, res) => {
109
183
  const handle = req.params.handle;
110
184
  try {
@@ -180,60 +254,34 @@ router.post('/:handle/ui', async (req, res) => {
180
254
  UI_SERVERS[outerConversationId] = new UIServer_1.UIServer(outerConversationId);
181
255
  await UI_SERVERS[outerConversationId].start();
182
256
  // Get the pages (5 at a time)
183
- const queue = new PromiseQueue_1.PromiseQueue(5);
257
+ const queue = new PageGenerator_1.PageQueue(outerConversationId, 5);
258
+ const pagePromises = [];
184
259
  onRequestAborted(req, res, () => {
185
260
  queue.cancel();
186
261
  });
262
+ const pageEventPromises = [];
263
+ queue.on('page', (screenData) => {
264
+ pageEventPromises.push(sendPageEvent(outerConversationId, screenData, res));
265
+ });
266
+ queue.on('event', (screenData) => {
267
+ sendEvent(res, screenData);
268
+ });
187
269
  for (const screen of Object.values(uniqueUserJourneyScreens)) {
188
- void 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
- }
270
+ pagePromises.push(queue.addPrompt({
271
+ prompt: screen.requirements,
272
+ method: screen.method,
273
+ path: screen.path,
274
+ description: screen.requirements,
275
+ name: screen.name,
276
+ title: screen.title,
277
+ filename: screen.filename,
278
+ storage_prefix: outerConversationId + '_',
279
+ shell_page: uiShells.find((shell) => shell.screens.includes(screen.name))?.content,
234
280
  }));
235
281
  }
236
282
  await queue.wait();
283
+ await Promise.allSettled(pagePromises);
284
+ await Promise.allSettled(pageEventPromises);
237
285
  if (userJourneysStream.isAborted()) {
238
286
  return;
239
287
  }
@@ -329,6 +377,7 @@ router.post('/:handle/all', async (req, res) => {
329
377
  try {
330
378
  const result = await eventParser.processEvent(handle, data);
331
379
  switch (data.type) {
380
+ case 'API_STREAM_START':
332
381
  case 'CREATE_API':
333
382
  case 'CREATE_MODEL':
334
383
  case 'CREATE_TYPE':
@@ -23,6 +23,9 @@ export interface UIPagePrompt {
23
23
  storage_prefix: string;
24
24
  shell_page?: string;
25
25
  }
26
+ export interface UIPageSamplePrompt extends UIPagePrompt {
27
+ variantId: string;
28
+ }
26
29
  export interface UIPageEditPrompt {
27
30
  planDescription: string;
28
31
  blockDescription: string;
@@ -44,7 +47,9 @@ declare class StormClient {
44
47
  createUIPages(prompt: string, conversationId?: string): Promise<StormStream>;
45
48
  createUIUserJourneys(prompt: string, conversationId?: string): Promise<StormStream>;
46
49
  createUIShells(prompt: UIShellsPrompt, conversationId?: string): Promise<StormStream>;
47
- createUIPage(prompt: UIPagePrompt, conversationId?: string): Promise<StormStream>;
50
+ createUILandingPages(prompt: string, conversationId?: string): Promise<StormStream>;
51
+ createUIPage(prompt: UIPagePrompt, conversationId?: string, history?: ConversationItem[]): Promise<StormStream>;
52
+ classifyUIReferences(prompt: string, conversationId?: string): Promise<StormStream>;
48
53
  editPages(prompt: UIPageEditPrompt, conversationId?: string): Promise<StormStream>;
49
54
  listScreens(prompt: StormUIListPrompt, conversationId?: string): Promise<StormStream>;
50
55
  createUIImplementation(prompt: StormUIImplementationPrompt, conversationId?: string): Promise<StormStream>;
@@ -53,6 +58,7 @@ declare class StormClient {
53
58
  createCodeFix(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
54
59
  createErrorDetails(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
55
60
  generateCode(prompt: StormFileImplementationPrompt, conversationId?: string): Promise<StormStream>;
61
+ deleteUIPageConversation(conversationId: string): Promise<string>;
56
62
  }
57
63
  export declare const stormClient: StormClient;
58
64
  export {};