@kapeta/local-cluster-service 0.61.2 → 0.62.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 +7 -0
- package/dist/cjs/src/storm/PageGenerator.d.ts +39 -0
- package/dist/cjs/src/storm/PageGenerator.js +143 -0
- package/dist/cjs/src/storm/events.d.ts +30 -1
- package/dist/cjs/src/storm/page-utils.d.ts +10 -0
- package/dist/cjs/src/storm/page-utils.js +65 -6
- package/dist/cjs/src/storm/routes.js +104 -56
- package/dist/cjs/src/storm/stormClient.d.ts +7 -1
- package/dist/cjs/src/storm/stormClient.js +24 -1
- package/dist/cjs/src/storm/stream.d.ts +1 -0
- package/dist/cjs/src/storm/stream.js +1 -0
- package/dist/esm/src/storm/PageGenerator.d.ts +39 -0
- package/dist/esm/src/storm/PageGenerator.js +143 -0
- package/dist/esm/src/storm/events.d.ts +30 -1
- package/dist/esm/src/storm/page-utils.d.ts +10 -0
- package/dist/esm/src/storm/page-utils.js +65 -6
- package/dist/esm/src/storm/routes.js +104 -56
- package/dist/esm/src/storm/stormClient.d.ts +7 -1
- package/dist/esm/src/storm/stormClient.js +24 -1
- package/dist/esm/src/storm/stream.d.ts +1 -0
- package/dist/esm/src/storm/stream.js +1 -0
- package/package.json +1 -1
- package/src/storm/PageGenerator.ts +181 -0
- package/src/storm/events.ts +37 -1
- package/src/storm/page-utils.ts +88 -12
- package/src/storm/routes.ts +146 -70
- package/src/storm/stormClient.ts +33 -1
- package/src/storm/stream.ts +2 -0
package/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
# [0.62.0](https://github.com/kapetacom/local-cluster-service/compare/v0.61.2...v0.62.0) (2024-08-13)
|
2
|
+
|
3
|
+
|
4
|
+
### Features
|
5
|
+
|
6
|
+
* Add iterative reference resolving ([#215](https://github.com/kapetacom/local-cluster-service/issues/215)) ([8bf5e23](https://github.com/kapetacom/local-cluster-service/commit/8bf5e23ce36031ca10df6c407a8a3e73a3e1ac46))
|
7
|
+
|
1
8
|
## [0.61.2](https://github.com/kapetacom/local-cluster-service/compare/v0.61.1...v0.61.2) (2024-08-13)
|
2
9
|
|
3
10
|
|
@@ -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;
|
@@ -342,5 +342,34 @@ export interface StormEventPromptImprove {
|
|
342
342
|
prompt: string;
|
343
343
|
};
|
344
344
|
}
|
345
|
-
export
|
345
|
+
export interface LandingPage {
|
346
|
+
name: string;
|
347
|
+
title: string;
|
348
|
+
filename: string;
|
349
|
+
create_prompt: string;
|
350
|
+
path: string;
|
351
|
+
archetype: string;
|
352
|
+
requires_authentication: boolean;
|
353
|
+
}
|
354
|
+
export interface StormEventLandingPage {
|
355
|
+
type: 'LANDING_PAGE';
|
356
|
+
reason: string;
|
357
|
+
created: number;
|
358
|
+
payload: LandingPage;
|
359
|
+
}
|
360
|
+
export interface ReferenceClassification {
|
361
|
+
name: string;
|
362
|
+
title: string;
|
363
|
+
url: string;
|
364
|
+
description: string;
|
365
|
+
type: 'image' | 'css' | 'javascript' | 'html';
|
366
|
+
source: 'local' | 'cdn' | 'example';
|
367
|
+
}
|
368
|
+
export interface StormEventReferenceClassification {
|
369
|
+
type: 'REF_CLASSIFICATION';
|
370
|
+
reason: string;
|
371
|
+
created: number;
|
372
|
+
payload: ReferenceClassification;
|
373
|
+
}
|
374
|
+
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;
|
346
375
|
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
|
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 =
|
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 =
|
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,
|
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
|
-
|
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
|
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
|
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
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
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
|
}
|
@@ -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
|
-
|
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 {};
|
@@ -95,13 +95,25 @@ class StormClient {
|
|
95
95
|
createUIShells(prompt, conversationId) {
|
96
96
|
return this.send('/v2/ui/shells', {
|
97
97
|
prompt: JSON.stringify(prompt),
|
98
|
+
});
|
99
|
+
}
|
100
|
+
createUILandingPages(prompt, conversationId) {
|
101
|
+
return this.send('/v2/ui/landing-pages', {
|
102
|
+
prompt: prompt,
|
98
103
|
conversationId,
|
99
104
|
});
|
100
105
|
}
|
101
|
-
createUIPage(prompt, conversationId) {
|
106
|
+
createUIPage(prompt, conversationId, history) {
|
102
107
|
return this.send('/v2/ui/page', {
|
103
108
|
prompt: prompt,
|
104
109
|
conversationId,
|
110
|
+
history,
|
111
|
+
});
|
112
|
+
}
|
113
|
+
classifyUIReferences(prompt, conversationId) {
|
114
|
+
return this.send('/v2/ui/references', {
|
115
|
+
prompt: prompt,
|
116
|
+
conversationId,
|
105
117
|
});
|
106
118
|
}
|
107
119
|
editPages(prompt, conversationId) {
|
@@ -152,5 +164,16 @@ class StormClient {
|
|
152
164
|
conversationId: conversationId,
|
153
165
|
});
|
154
166
|
}
|
167
|
+
async deleteUIPageConversation(conversationId) {
|
168
|
+
const options = await this.createOptions('/v2/ui/page', 'DELETE', {
|
169
|
+
prompt: '',
|
170
|
+
conversationId: conversationId,
|
171
|
+
});
|
172
|
+
const response = await fetch(options.url, {
|
173
|
+
method: options.method,
|
174
|
+
headers: options.headers,
|
175
|
+
});
|
176
|
+
return response.text();
|
177
|
+
}
|
155
178
|
}
|
156
179
|
exports.stormClient = new StormClient();
|