@kapeta/local-cluster-service 0.65.0 → 0.66.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 +2 -0
- package/dist/cjs/src/storm/PageGenerator.js +6 -0
- package/dist/cjs/src/storm/UIServer.js +3 -0
- package/dist/cjs/src/storm/page-utils.d.ts +5 -1
- package/dist/cjs/src/storm/page-utils.js +16 -5
- package/dist/cjs/src/storm/routes.js +49 -4
- package/dist/cjs/src/storm/stormClient.d.ts +3 -0
- package/dist/cjs/src/storm/stormClient.js +6 -0
- package/dist/esm/src/storm/PageGenerator.d.ts +2 -0
- package/dist/esm/src/storm/PageGenerator.js +6 -0
- package/dist/esm/src/storm/UIServer.js +3 -0
- package/dist/esm/src/storm/page-utils.d.ts +5 -1
- package/dist/esm/src/storm/page-utils.js +16 -5
- package/dist/esm/src/storm/routes.js +49 -4
- package/dist/esm/src/storm/stormClient.d.ts +3 -0
- package/dist/esm/src/storm/stormClient.js +6 -0
- package/package.json +1 -1
- package/src/storm/PageGenerator.ts +7 -0
- package/src/storm/UIServer.ts +5 -1
- package/src/storm/page-utils.ts +16 -5
- package/src/storm/routes.ts +59 -5
- package/src/storm/stormClient.ts +10 -0
package/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
# [0.66.0](https://github.com/kapetacom/local-cluster-service/compare/v0.65.0...v0.66.0) (2024-08-27)
|
2
|
+
|
3
|
+
|
4
|
+
### Features
|
5
|
+
|
6
|
+
* use new UI theme functionality [CORE-3289] ([#225](https://github.com/kapetacom/local-cluster-service/issues/225)) ([95d8078](https://github.com/kapetacom/local-cluster-service/commit/95d807877d585d4d62af3dd228ea9138ab989d55))
|
7
|
+
|
1
8
|
# [0.65.0](https://github.com/kapetacom/local-cluster-service/compare/v0.64.3...v0.65.0) (2024-08-26)
|
2
9
|
|
3
10
|
|
@@ -11,12 +11,14 @@ export declare class PageQueue extends EventEmitter {
|
|
11
11
|
private readonly systemId;
|
12
12
|
private readonly references;
|
13
13
|
private uiShells;
|
14
|
+
private theme;
|
14
15
|
constructor(systemId: string, concurrency?: number);
|
15
16
|
on(event: 'event', listener: (data: StormEvent) => void): this;
|
16
17
|
on(event: 'page', listener: (data: StormEventPage) => void): this;
|
17
18
|
emit(type: 'event', event: StormEvent): boolean;
|
18
19
|
emit(type: 'page', event: StormEventPage): boolean;
|
19
20
|
addUiShell(uiShell: UIShell): void;
|
21
|
+
setUiTheme(theme: string): void;
|
20
22
|
addPrompt(initialPrompt: Omit<UIPagePrompt, 'shell_page'>, conversationId?: string, overwrite?: boolean): Promise<void>;
|
21
23
|
private addPageGenerator;
|
22
24
|
cancel(): void;
|
@@ -18,6 +18,7 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
18
18
|
systemId;
|
19
19
|
references = new Map();
|
20
20
|
uiShells = [];
|
21
|
+
theme = '';
|
21
22
|
constructor(systemId, concurrency = 5) {
|
22
23
|
super();
|
23
24
|
this.systemId = systemId;
|
@@ -32,6 +33,9 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
32
33
|
addUiShell(uiShell) {
|
33
34
|
this.uiShells.push(uiShell);
|
34
35
|
}
|
36
|
+
setUiTheme(theme) {
|
37
|
+
this.theme = theme;
|
38
|
+
}
|
35
39
|
addPrompt(initialPrompt, conversationId = node_uuid_1.default.v4(), overwrite = false) {
|
36
40
|
if (!overwrite && this.references.has(initialPrompt.path)) {
|
37
41
|
console.log('Ignoring duplicate prompt', initialPrompt.path);
|
@@ -44,6 +48,7 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
44
48
|
const prompt = {
|
45
49
|
...initialPrompt,
|
46
50
|
shell_page: this.uiShells.find((shell) => shell.screens.includes(initialPrompt.name))?.content,
|
51
|
+
theme: this.theme,
|
47
52
|
};
|
48
53
|
const generator = new PageGenerator(prompt, conversationId);
|
49
54
|
this.references.set(prompt.path, generator);
|
@@ -84,6 +89,7 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
84
89
|
: ''),
|
85
90
|
description: reference.description,
|
86
91
|
filename: '',
|
92
|
+
theme: this.theme,
|
87
93
|
});
|
88
94
|
break;
|
89
95
|
}
|
@@ -12,6 +12,7 @@ const express_1 = __importDefault(require("express"));
|
|
12
12
|
const page_utils_1 = require("./page-utils");
|
13
13
|
const clusterService_1 = require("../clusterService");
|
14
14
|
const http_1 = require("http");
|
15
|
+
const path_1 = require("path");
|
15
16
|
class UIServer {
|
16
17
|
systemId;
|
17
18
|
port = 50000;
|
@@ -28,6 +29,8 @@ class UIServer {
|
|
28
29
|
window.sessionStorage.clear();
|
29
30
|
</script>`);
|
30
31
|
});
|
32
|
+
// Make it possible to serve static assets
|
33
|
+
app.use(express_1.default.static((0, path_1.join)((0, page_utils_1.getSystemBaseDir)(this.systemId), 'public'), { fallthrough: true }));
|
31
34
|
app.all('/*', (req, res) => {
|
32
35
|
(0, page_utils_1.readPageFromDisk)(this.systemId, req.params[0], req.method, res);
|
33
36
|
});
|
@@ -2,14 +2,18 @@
|
|
2
2
|
* Copyright 2023 Kapeta Inc.
|
3
3
|
* SPDX-License-Identifier: BUSL-1.1
|
4
4
|
*/
|
5
|
-
import { StormEventPage } from './events';
|
5
|
+
import { StormEventFileDone, StormEventPage } from './events';
|
6
6
|
import { Response } from 'express';
|
7
7
|
import { ConversationItem } from './stream';
|
8
8
|
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 writeAssetToDisk(systemId: string, event: StormEventFileDone): Promise<{
|
13
|
+
path: string;
|
14
|
+
}>;
|
12
15
|
export declare function hasPageOnDisk(systemId: string, method: string, path: string): boolean;
|
16
|
+
export declare function getSystemBaseDir(systemId: string): string;
|
13
17
|
export declare function resolveReadPath(systemId: string, path: string, method: string): string | null;
|
14
18
|
export declare function readPageFromDiskAsString(systemId: string, path: string, method: string): string | null;
|
15
19
|
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.hasPageOnDisk = exports.writePageToDisk = exports.SystemIdHeader = void 0;
|
6
|
+
exports.writeConversationToFile = exports.readConversationFromFile = exports.readPageFromDisk = exports.readPageFromDiskAsString = exports.resolveReadPath = exports.getSystemBaseDir = exports.hasPageOnDisk = exports.writeAssetToDisk = 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"));
|
@@ -15,7 +15,7 @@ function normalizePath(path) {
|
|
15
15
|
.replace(/\{[a-z]+}/gi, '*');
|
16
16
|
}
|
17
17
|
async function writePageToDisk(systemId, event) {
|
18
|
-
const baseDir =
|
18
|
+
const baseDir = getSystemBaseDir(systemId);
|
19
19
|
const filePath = getFilePath(event.payload.method);
|
20
20
|
const path = path_1.default.join(baseDir, normalizePath(event.payload.path), filePath);
|
21
21
|
await fs_extra_1.default.ensureDir(path_1.default.dirname(path));
|
@@ -26,21 +26,32 @@ async function writePageToDisk(systemId, event) {
|
|
26
26
|
};
|
27
27
|
}
|
28
28
|
exports.writePageToDisk = writePageToDisk;
|
29
|
+
async function writeAssetToDisk(systemId, event) {
|
30
|
+
const baseDir = getSystemBaseDir(systemId);
|
31
|
+
const path = path_1.default.join(baseDir, 'public', event.payload.filename);
|
32
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(path));
|
33
|
+
await fs_extra_1.default.writeFile(path, event.payload.content);
|
34
|
+
return {
|
35
|
+
path,
|
36
|
+
};
|
37
|
+
}
|
38
|
+
exports.writeAssetToDisk = writeAssetToDisk;
|
29
39
|
function hasPageOnDisk(systemId, method, path) {
|
30
|
-
const baseDir =
|
40
|
+
const baseDir = getSystemBaseDir(systemId);
|
31
41
|
const filePath = getFilePath(method);
|
32
42
|
const fullPath = path_1.default.join(baseDir, normalizePath(path), filePath);
|
33
43
|
return fs_extra_1.default.existsSync(fullPath);
|
34
44
|
}
|
35
45
|
exports.hasPageOnDisk = hasPageOnDisk;
|
36
|
-
function
|
46
|
+
function getSystemBaseDir(systemId) {
|
37
47
|
return path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId);
|
38
48
|
}
|
49
|
+
exports.getSystemBaseDir = getSystemBaseDir;
|
39
50
|
function getFilePath(method) {
|
40
51
|
return path_1.default.join(method.toLowerCase(), 'index.html');
|
41
52
|
}
|
42
53
|
function resolveReadPath(systemId, path, method) {
|
43
|
-
const baseDir =
|
54
|
+
const baseDir = getSystemBaseDir(systemId);
|
44
55
|
path = normalizePath(path);
|
45
56
|
const filePath = getFilePath(method);
|
46
57
|
const fullPath = path_1.default.join(baseDir, path, filePath);
|
@@ -22,6 +22,7 @@ const node_uuid_1 = __importDefault(require("node-uuid"));
|
|
22
22
|
const page_utils_1 = require("./page-utils");
|
23
23
|
const UIServer_1 = require("./UIServer");
|
24
24
|
const PageGenerator_1 = require("./PageGenerator");
|
25
|
+
const crypto_1 = require("crypto");
|
25
26
|
const UI_SERVERS = {};
|
26
27
|
const router = (0, express_promise_router_1.default)();
|
27
28
|
router.use('/', cors_1.corsHandler);
|
@@ -142,6 +143,8 @@ router.post('/:handle/ui/iterative', async (req, res) => {
|
|
142
143
|
title: landingPage.title,
|
143
144
|
filename: landingPage.filename,
|
144
145
|
storage_prefix: systemId + '_',
|
146
|
+
// TODO: Add themes to this request type
|
147
|
+
theme: '',
|
145
148
|
});
|
146
149
|
}
|
147
150
|
catch (e) {
|
@@ -177,11 +180,10 @@ router.post('/:handle/ui/iterative', async (req, res) => {
|
|
177
180
|
router.post('/:handle/ui', async (req, res) => {
|
178
181
|
const handle = req.params.handle;
|
179
182
|
try {
|
180
|
-
const
|
183
|
+
const outerConversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()] || (0, crypto_1.randomUUID)();
|
181
184
|
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
182
185
|
// Get user journeys
|
183
|
-
const userJourneysStream = await stormClient_1.stormClient.createUIUserJourneys(aiRequest,
|
184
|
-
const outerConversationId = userJourneysStream.getConversationId();
|
186
|
+
const userJourneysStream = await stormClient_1.stormClient.createUIUserJourneys(aiRequest, outerConversationId);
|
185
187
|
onRequestAborted(req, res, () => {
|
186
188
|
userJourneysStream.abort();
|
187
189
|
});
|
@@ -213,9 +215,50 @@ router.post('/:handle/ui', async (req, res) => {
|
|
213
215
|
userJourneysStream.abort();
|
214
216
|
sendError(error, res);
|
215
217
|
});
|
218
|
+
let theme = '';
|
219
|
+
try {
|
220
|
+
const themeStream = await stormClient_1.stormClient.createTheme(aiRequest, outerConversationId);
|
221
|
+
onRequestAborted(req, res, () => {
|
222
|
+
themeStream.abort();
|
223
|
+
});
|
224
|
+
themeStream.on('data', (evt) => {
|
225
|
+
sendEvent(res, evt);
|
226
|
+
if (evt.type === 'FILE_DONE') {
|
227
|
+
theme = evt.payload.content;
|
228
|
+
(0, page_utils_1.writeAssetToDisk)(outerConversationId, evt).catch((err) => {
|
229
|
+
sendEvent(res, {
|
230
|
+
type: 'ERROR_INTERNAL',
|
231
|
+
created: new Date().getTime(),
|
232
|
+
payload: { error: err.message },
|
233
|
+
reason: 'Failed to save theme',
|
234
|
+
});
|
235
|
+
});
|
236
|
+
}
|
237
|
+
});
|
238
|
+
themeStream.on('error', (error) => {
|
239
|
+
console.error(error);
|
240
|
+
sendEvent(res, {
|
241
|
+
type: 'ERROR_INTERNAL',
|
242
|
+
created: new Date().getTime(),
|
243
|
+
payload: { error: error.message },
|
244
|
+
reason: 'Failed to create theme',
|
245
|
+
});
|
246
|
+
});
|
247
|
+
await waitForStormStream(themeStream);
|
248
|
+
}
|
249
|
+
catch (e) {
|
250
|
+
console.error('Failed to generate theme', e);
|
251
|
+
sendEvent(res, {
|
252
|
+
type: 'ERROR_INTERNAL',
|
253
|
+
created: new Date().getTime(),
|
254
|
+
payload: { error: e.message },
|
255
|
+
reason: 'Failed to create theme',
|
256
|
+
});
|
257
|
+
}
|
216
258
|
await waitForStormStream(userJourneysStream);
|
217
259
|
// Get the UI shells
|
218
260
|
const shellsStream = await stormClient_1.stormClient.createUIShells({
|
261
|
+
theme: theme ? `// filename: theme.css\n${theme}` : undefined,
|
219
262
|
pages: Object.values(uniqueUserJourneyScreens).map((screen) => ({
|
220
263
|
name: screen.name,
|
221
264
|
title: screen.title,
|
@@ -224,11 +267,12 @@ router.post('/:handle/ui', async (req, res) => {
|
|
224
267
|
method: screen.method,
|
225
268
|
requirements: screen.requirements,
|
226
269
|
})),
|
227
|
-
},
|
270
|
+
}, outerConversationId);
|
228
271
|
onRequestAborted(req, res, () => {
|
229
272
|
shellsStream.abort();
|
230
273
|
});
|
231
274
|
const queue = new PageGenerator_1.PageQueue(outerConversationId, 5);
|
275
|
+
queue.setUiTheme(theme);
|
232
276
|
shellsStream.on('data', (data) => {
|
233
277
|
console.log('Processing shell event', data);
|
234
278
|
sendEvent(res, data);
|
@@ -279,6 +323,7 @@ router.post('/:handle/ui', async (req, res) => {
|
|
279
323
|
title: screen.title,
|
280
324
|
filename: screen.filename,
|
281
325
|
storage_prefix: outerConversationId + '_',
|
326
|
+
theme,
|
282
327
|
}));
|
283
328
|
}
|
284
329
|
await queue.wait();
|
@@ -4,6 +4,7 @@ import { Page, StormEventPageUrl } from './events';
|
|
4
4
|
export declare const STORM_ID = "storm";
|
5
5
|
export declare const ConversationIdHeader = "Conversation-Id";
|
6
6
|
export interface UIShellsPrompt {
|
7
|
+
theme?: string;
|
7
8
|
pages: {
|
8
9
|
name: string;
|
9
10
|
title: string;
|
@@ -23,6 +24,7 @@ export interface UIPagePrompt {
|
|
23
24
|
description: string;
|
24
25
|
storage_prefix: string;
|
25
26
|
shell_page?: string;
|
27
|
+
theme: string;
|
26
28
|
}
|
27
29
|
export interface UIPageSamplePrompt extends UIPagePrompt {
|
28
30
|
variantId: string;
|
@@ -60,6 +62,7 @@ declare class StormClient {
|
|
60
62
|
createMetadata(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
|
61
63
|
createUIPages(prompt: string, conversationId?: string): Promise<StormStream>;
|
62
64
|
createUIUserJourneys(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
|
65
|
+
createTheme(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
|
63
66
|
createUIShells(prompt: UIShellsPrompt, conversationId?: string): Promise<StormStream>;
|
64
67
|
createUILandingPages(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
|
65
68
|
createUIPage(prompt: UIPagePrompt, conversationId?: string, history?: ConversationItem[]): Promise<StormStream>;
|
@@ -92,6 +92,12 @@ class StormClient {
|
|
92
92
|
conversationId,
|
93
93
|
});
|
94
94
|
}
|
95
|
+
createTheme(prompt, conversationId) {
|
96
|
+
return this.send('/v2/ui/theme', {
|
97
|
+
prompt: prompt,
|
98
|
+
conversationId,
|
99
|
+
});
|
100
|
+
}
|
95
101
|
createUIShells(prompt, conversationId) {
|
96
102
|
return this.send('/v2/ui/shells', {
|
97
103
|
prompt: JSON.stringify(prompt),
|
@@ -11,12 +11,14 @@ export declare class PageQueue extends EventEmitter {
|
|
11
11
|
private readonly systemId;
|
12
12
|
private readonly references;
|
13
13
|
private uiShells;
|
14
|
+
private theme;
|
14
15
|
constructor(systemId: string, concurrency?: number);
|
15
16
|
on(event: 'event', listener: (data: StormEvent) => void): this;
|
16
17
|
on(event: 'page', listener: (data: StormEventPage) => void): this;
|
17
18
|
emit(type: 'event', event: StormEvent): boolean;
|
18
19
|
emit(type: 'page', event: StormEventPage): boolean;
|
19
20
|
addUiShell(uiShell: UIShell): void;
|
21
|
+
setUiTheme(theme: string): void;
|
20
22
|
addPrompt(initialPrompt: Omit<UIPagePrompt, 'shell_page'>, conversationId?: string, overwrite?: boolean): Promise<void>;
|
21
23
|
private addPageGenerator;
|
22
24
|
cancel(): void;
|
@@ -18,6 +18,7 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
18
18
|
systemId;
|
19
19
|
references = new Map();
|
20
20
|
uiShells = [];
|
21
|
+
theme = '';
|
21
22
|
constructor(systemId, concurrency = 5) {
|
22
23
|
super();
|
23
24
|
this.systemId = systemId;
|
@@ -32,6 +33,9 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
32
33
|
addUiShell(uiShell) {
|
33
34
|
this.uiShells.push(uiShell);
|
34
35
|
}
|
36
|
+
setUiTheme(theme) {
|
37
|
+
this.theme = theme;
|
38
|
+
}
|
35
39
|
addPrompt(initialPrompt, conversationId = node_uuid_1.default.v4(), overwrite = false) {
|
36
40
|
if (!overwrite && this.references.has(initialPrompt.path)) {
|
37
41
|
console.log('Ignoring duplicate prompt', initialPrompt.path);
|
@@ -44,6 +48,7 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
44
48
|
const prompt = {
|
45
49
|
...initialPrompt,
|
46
50
|
shell_page: this.uiShells.find((shell) => shell.screens.includes(initialPrompt.name))?.content,
|
51
|
+
theme: this.theme,
|
47
52
|
};
|
48
53
|
const generator = new PageGenerator(prompt, conversationId);
|
49
54
|
this.references.set(prompt.path, generator);
|
@@ -84,6 +89,7 @@ class PageQueue extends node_events_1.EventEmitter {
|
|
84
89
|
: ''),
|
85
90
|
description: reference.description,
|
86
91
|
filename: '',
|
92
|
+
theme: this.theme,
|
87
93
|
});
|
88
94
|
break;
|
89
95
|
}
|
@@ -12,6 +12,7 @@ const express_1 = __importDefault(require("express"));
|
|
12
12
|
const page_utils_1 = require("./page-utils");
|
13
13
|
const clusterService_1 = require("../clusterService");
|
14
14
|
const http_1 = require("http");
|
15
|
+
const path_1 = require("path");
|
15
16
|
class UIServer {
|
16
17
|
systemId;
|
17
18
|
port = 50000;
|
@@ -28,6 +29,8 @@ class UIServer {
|
|
28
29
|
window.sessionStorage.clear();
|
29
30
|
</script>`);
|
30
31
|
});
|
32
|
+
// Make it possible to serve static assets
|
33
|
+
app.use(express_1.default.static((0, path_1.join)((0, page_utils_1.getSystemBaseDir)(this.systemId), 'public'), { fallthrough: true }));
|
31
34
|
app.all('/*', (req, res) => {
|
32
35
|
(0, page_utils_1.readPageFromDisk)(this.systemId, req.params[0], req.method, res);
|
33
36
|
});
|
@@ -2,14 +2,18 @@
|
|
2
2
|
* Copyright 2023 Kapeta Inc.
|
3
3
|
* SPDX-License-Identifier: BUSL-1.1
|
4
4
|
*/
|
5
|
-
import { StormEventPage } from './events';
|
5
|
+
import { StormEventFileDone, StormEventPage } from './events';
|
6
6
|
import { Response } from 'express';
|
7
7
|
import { ConversationItem } from './stream';
|
8
8
|
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 writeAssetToDisk(systemId: string, event: StormEventFileDone): Promise<{
|
13
|
+
path: string;
|
14
|
+
}>;
|
12
15
|
export declare function hasPageOnDisk(systemId: string, method: string, path: string): boolean;
|
16
|
+
export declare function getSystemBaseDir(systemId: string): string;
|
13
17
|
export declare function resolveReadPath(systemId: string, path: string, method: string): string | null;
|
14
18
|
export declare function readPageFromDiskAsString(systemId: string, path: string, method: string): string | null;
|
15
19
|
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.hasPageOnDisk = exports.writePageToDisk = exports.SystemIdHeader = void 0;
|
6
|
+
exports.writeConversationToFile = exports.readConversationFromFile = exports.readPageFromDisk = exports.readPageFromDiskAsString = exports.resolveReadPath = exports.getSystemBaseDir = exports.hasPageOnDisk = exports.writeAssetToDisk = 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"));
|
@@ -15,7 +15,7 @@ function normalizePath(path) {
|
|
15
15
|
.replace(/\{[a-z]+}/gi, '*');
|
16
16
|
}
|
17
17
|
async function writePageToDisk(systemId, event) {
|
18
|
-
const baseDir =
|
18
|
+
const baseDir = getSystemBaseDir(systemId);
|
19
19
|
const filePath = getFilePath(event.payload.method);
|
20
20
|
const path = path_1.default.join(baseDir, normalizePath(event.payload.path), filePath);
|
21
21
|
await fs_extra_1.default.ensureDir(path_1.default.dirname(path));
|
@@ -26,21 +26,32 @@ async function writePageToDisk(systemId, event) {
|
|
26
26
|
};
|
27
27
|
}
|
28
28
|
exports.writePageToDisk = writePageToDisk;
|
29
|
+
async function writeAssetToDisk(systemId, event) {
|
30
|
+
const baseDir = getSystemBaseDir(systemId);
|
31
|
+
const path = path_1.default.join(baseDir, 'public', event.payload.filename);
|
32
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(path));
|
33
|
+
await fs_extra_1.default.writeFile(path, event.payload.content);
|
34
|
+
return {
|
35
|
+
path,
|
36
|
+
};
|
37
|
+
}
|
38
|
+
exports.writeAssetToDisk = writeAssetToDisk;
|
29
39
|
function hasPageOnDisk(systemId, method, path) {
|
30
|
-
const baseDir =
|
40
|
+
const baseDir = getSystemBaseDir(systemId);
|
31
41
|
const filePath = getFilePath(method);
|
32
42
|
const fullPath = path_1.default.join(baseDir, normalizePath(path), filePath);
|
33
43
|
return fs_extra_1.default.existsSync(fullPath);
|
34
44
|
}
|
35
45
|
exports.hasPageOnDisk = hasPageOnDisk;
|
36
|
-
function
|
46
|
+
function getSystemBaseDir(systemId) {
|
37
47
|
return path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId);
|
38
48
|
}
|
49
|
+
exports.getSystemBaseDir = getSystemBaseDir;
|
39
50
|
function getFilePath(method) {
|
40
51
|
return path_1.default.join(method.toLowerCase(), 'index.html');
|
41
52
|
}
|
42
53
|
function resolveReadPath(systemId, path, method) {
|
43
|
-
const baseDir =
|
54
|
+
const baseDir = getSystemBaseDir(systemId);
|
44
55
|
path = normalizePath(path);
|
45
56
|
const filePath = getFilePath(method);
|
46
57
|
const fullPath = path_1.default.join(baseDir, path, filePath);
|
@@ -22,6 +22,7 @@ const node_uuid_1 = __importDefault(require("node-uuid"));
|
|
22
22
|
const page_utils_1 = require("./page-utils");
|
23
23
|
const UIServer_1 = require("./UIServer");
|
24
24
|
const PageGenerator_1 = require("./PageGenerator");
|
25
|
+
const crypto_1 = require("crypto");
|
25
26
|
const UI_SERVERS = {};
|
26
27
|
const router = (0, express_promise_router_1.default)();
|
27
28
|
router.use('/', cors_1.corsHandler);
|
@@ -142,6 +143,8 @@ router.post('/:handle/ui/iterative', async (req, res) => {
|
|
142
143
|
title: landingPage.title,
|
143
144
|
filename: landingPage.filename,
|
144
145
|
storage_prefix: systemId + '_',
|
146
|
+
// TODO: Add themes to this request type
|
147
|
+
theme: '',
|
145
148
|
});
|
146
149
|
}
|
147
150
|
catch (e) {
|
@@ -177,11 +180,10 @@ router.post('/:handle/ui/iterative', async (req, res) => {
|
|
177
180
|
router.post('/:handle/ui', async (req, res) => {
|
178
181
|
const handle = req.params.handle;
|
179
182
|
try {
|
180
|
-
const
|
183
|
+
const outerConversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()] || (0, crypto_1.randomUUID)();
|
181
184
|
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
182
185
|
// Get user journeys
|
183
|
-
const userJourneysStream = await stormClient_1.stormClient.createUIUserJourneys(aiRequest,
|
184
|
-
const outerConversationId = userJourneysStream.getConversationId();
|
186
|
+
const userJourneysStream = await stormClient_1.stormClient.createUIUserJourneys(aiRequest, outerConversationId);
|
185
187
|
onRequestAborted(req, res, () => {
|
186
188
|
userJourneysStream.abort();
|
187
189
|
});
|
@@ -213,9 +215,50 @@ router.post('/:handle/ui', async (req, res) => {
|
|
213
215
|
userJourneysStream.abort();
|
214
216
|
sendError(error, res);
|
215
217
|
});
|
218
|
+
let theme = '';
|
219
|
+
try {
|
220
|
+
const themeStream = await stormClient_1.stormClient.createTheme(aiRequest, outerConversationId);
|
221
|
+
onRequestAborted(req, res, () => {
|
222
|
+
themeStream.abort();
|
223
|
+
});
|
224
|
+
themeStream.on('data', (evt) => {
|
225
|
+
sendEvent(res, evt);
|
226
|
+
if (evt.type === 'FILE_DONE') {
|
227
|
+
theme = evt.payload.content;
|
228
|
+
(0, page_utils_1.writeAssetToDisk)(outerConversationId, evt).catch((err) => {
|
229
|
+
sendEvent(res, {
|
230
|
+
type: 'ERROR_INTERNAL',
|
231
|
+
created: new Date().getTime(),
|
232
|
+
payload: { error: err.message },
|
233
|
+
reason: 'Failed to save theme',
|
234
|
+
});
|
235
|
+
});
|
236
|
+
}
|
237
|
+
});
|
238
|
+
themeStream.on('error', (error) => {
|
239
|
+
console.error(error);
|
240
|
+
sendEvent(res, {
|
241
|
+
type: 'ERROR_INTERNAL',
|
242
|
+
created: new Date().getTime(),
|
243
|
+
payload: { error: error.message },
|
244
|
+
reason: 'Failed to create theme',
|
245
|
+
});
|
246
|
+
});
|
247
|
+
await waitForStormStream(themeStream);
|
248
|
+
}
|
249
|
+
catch (e) {
|
250
|
+
console.error('Failed to generate theme', e);
|
251
|
+
sendEvent(res, {
|
252
|
+
type: 'ERROR_INTERNAL',
|
253
|
+
created: new Date().getTime(),
|
254
|
+
payload: { error: e.message },
|
255
|
+
reason: 'Failed to create theme',
|
256
|
+
});
|
257
|
+
}
|
216
258
|
await waitForStormStream(userJourneysStream);
|
217
259
|
// Get the UI shells
|
218
260
|
const shellsStream = await stormClient_1.stormClient.createUIShells({
|
261
|
+
theme: theme ? `// filename: theme.css\n${theme}` : undefined,
|
219
262
|
pages: Object.values(uniqueUserJourneyScreens).map((screen) => ({
|
220
263
|
name: screen.name,
|
221
264
|
title: screen.title,
|
@@ -224,11 +267,12 @@ router.post('/:handle/ui', async (req, res) => {
|
|
224
267
|
method: screen.method,
|
225
268
|
requirements: screen.requirements,
|
226
269
|
})),
|
227
|
-
},
|
270
|
+
}, outerConversationId);
|
228
271
|
onRequestAborted(req, res, () => {
|
229
272
|
shellsStream.abort();
|
230
273
|
});
|
231
274
|
const queue = new PageGenerator_1.PageQueue(outerConversationId, 5);
|
275
|
+
queue.setUiTheme(theme);
|
232
276
|
shellsStream.on('data', (data) => {
|
233
277
|
console.log('Processing shell event', data);
|
234
278
|
sendEvent(res, data);
|
@@ -279,6 +323,7 @@ router.post('/:handle/ui', async (req, res) => {
|
|
279
323
|
title: screen.title,
|
280
324
|
filename: screen.filename,
|
281
325
|
storage_prefix: outerConversationId + '_',
|
326
|
+
theme,
|
282
327
|
}));
|
283
328
|
}
|
284
329
|
await queue.wait();
|
@@ -4,6 +4,7 @@ import { Page, StormEventPageUrl } from './events';
|
|
4
4
|
export declare const STORM_ID = "storm";
|
5
5
|
export declare const ConversationIdHeader = "Conversation-Id";
|
6
6
|
export interface UIShellsPrompt {
|
7
|
+
theme?: string;
|
7
8
|
pages: {
|
8
9
|
name: string;
|
9
10
|
title: string;
|
@@ -23,6 +24,7 @@ export interface UIPagePrompt {
|
|
23
24
|
description: string;
|
24
25
|
storage_prefix: string;
|
25
26
|
shell_page?: string;
|
27
|
+
theme: string;
|
26
28
|
}
|
27
29
|
export interface UIPageSamplePrompt extends UIPagePrompt {
|
28
30
|
variantId: string;
|
@@ -60,6 +62,7 @@ declare class StormClient {
|
|
60
62
|
createMetadata(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
|
61
63
|
createUIPages(prompt: string, conversationId?: string): Promise<StormStream>;
|
62
64
|
createUIUserJourneys(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
|
65
|
+
createTheme(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
|
63
66
|
createUIShells(prompt: UIShellsPrompt, conversationId?: string): Promise<StormStream>;
|
64
67
|
createUILandingPages(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
|
65
68
|
createUIPage(prompt: UIPagePrompt, conversationId?: string, history?: ConversationItem[]): Promise<StormStream>;
|
@@ -92,6 +92,12 @@ class StormClient {
|
|
92
92
|
conversationId,
|
93
93
|
});
|
94
94
|
}
|
95
|
+
createTheme(prompt, conversationId) {
|
96
|
+
return this.send('/v2/ui/theme', {
|
97
|
+
prompt: prompt,
|
98
|
+
conversationId,
|
99
|
+
});
|
100
|
+
}
|
95
101
|
createUIShells(prompt, conversationId) {
|
96
102
|
return this.send('/v2/ui/shells', {
|
97
103
|
prompt: JSON.stringify(prompt),
|
package/package.json
CHANGED
@@ -21,6 +21,7 @@ export class PageQueue extends EventEmitter {
|
|
21
21
|
private readonly systemId: string;
|
22
22
|
private readonly references: Map<string, PageGenerator> = new Map();
|
23
23
|
private uiShells: UIShell[] = [];
|
24
|
+
private theme = '';
|
24
25
|
|
25
26
|
constructor(systemId: string, concurrency: number = 5) {
|
26
27
|
super();
|
@@ -45,6 +46,10 @@ export class PageQueue extends EventEmitter {
|
|
45
46
|
this.uiShells.push(uiShell);
|
46
47
|
}
|
47
48
|
|
49
|
+
public setUiTheme(theme: string) {
|
50
|
+
this.theme = theme;
|
51
|
+
}
|
52
|
+
|
48
53
|
public addPrompt(
|
49
54
|
initialPrompt: Omit<UIPagePrompt, 'shell_page'>,
|
50
55
|
conversationId: string = uuid.v4(),
|
@@ -63,6 +68,7 @@ export class PageQueue extends EventEmitter {
|
|
63
68
|
const prompt: UIPagePrompt = {
|
64
69
|
...initialPrompt,
|
65
70
|
shell_page: this.uiShells.find((shell) => shell.screens.includes(initialPrompt.name))?.content,
|
71
|
+
theme: this.theme,
|
66
72
|
};
|
67
73
|
|
68
74
|
const generator = new PageGenerator(prompt, conversationId);
|
@@ -110,6 +116,7 @@ export class PageQueue extends EventEmitter {
|
|
110
116
|
: ''),
|
111
117
|
description: reference.description,
|
112
118
|
filename: '',
|
119
|
+
theme: this.theme,
|
113
120
|
});
|
114
121
|
break;
|
115
122
|
}
|
package/src/storm/UIServer.ts
CHANGED
@@ -3,10 +3,11 @@
|
|
3
3
|
* SPDX-License-Identifier: BUSL-1.1
|
4
4
|
*/
|
5
5
|
import express, { Express, Request, Response } from 'express';
|
6
|
-
import { readPageFromDisk } from './page-utils';
|
6
|
+
import { getSystemBaseDir, readPageFromDisk } from './page-utils';
|
7
7
|
import { clusterService } from '../clusterService';
|
8
8
|
import { createServer, Server } from 'http';
|
9
9
|
import { StormEventPage } from './events';
|
10
|
+
import { join } from 'path';
|
10
11
|
|
11
12
|
export class UIServer {
|
12
13
|
private readonly systemId: string;
|
@@ -30,6 +31,9 @@ export class UIServer {
|
|
30
31
|
);
|
31
32
|
});
|
32
33
|
|
34
|
+
// Make it possible to serve static assets
|
35
|
+
app.use(express.static(join(getSystemBaseDir(this.systemId), 'public'), { fallthrough: true }));
|
36
|
+
|
33
37
|
app.all('/*', (req: Request, res: Response) => {
|
34
38
|
readPageFromDisk(this.systemId, req.params[0], req.method, res);
|
35
39
|
});
|
package/src/storm/page-utils.ts
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
* Copyright 2023 Kapeta Inc.
|
3
3
|
* SPDX-License-Identifier: BUSL-1.1
|
4
4
|
*/
|
5
|
-
import { StormEventPage } from './events';
|
5
|
+
import { StormEventFileDone, StormEventPage } from './events';
|
6
6
|
import { Response } from 'express';
|
7
7
|
import os from 'node:os';
|
8
8
|
import Path from 'path';
|
@@ -21,7 +21,7 @@ function normalizePath(path: string) {
|
|
21
21
|
}
|
22
22
|
|
23
23
|
export async function writePageToDisk(systemId: string, event: StormEventPage) {
|
24
|
-
const baseDir =
|
24
|
+
const baseDir = getSystemBaseDir(systemId);
|
25
25
|
const filePath = getFilePath(event.payload.method);
|
26
26
|
const path = Path.join(baseDir, normalizePath(event.payload.path), filePath);
|
27
27
|
await FS.ensureDir(Path.dirname(path));
|
@@ -34,14 +34,25 @@ export async function writePageToDisk(systemId: string, event: StormEventPage) {
|
|
34
34
|
};
|
35
35
|
}
|
36
36
|
|
37
|
+
export async function writeAssetToDisk(systemId: string, event: StormEventFileDone) {
|
38
|
+
const baseDir = getSystemBaseDir(systemId);
|
39
|
+
const path = Path.join(baseDir, 'public', event.payload.filename);
|
40
|
+
await FS.ensureDir(Path.dirname(path));
|
41
|
+
await FS.writeFile(path, event.payload.content);
|
42
|
+
|
43
|
+
return {
|
44
|
+
path,
|
45
|
+
};
|
46
|
+
}
|
47
|
+
|
37
48
|
export function hasPageOnDisk(systemId: string, method: string, path: string) {
|
38
|
-
const baseDir =
|
49
|
+
const baseDir = getSystemBaseDir(systemId);
|
39
50
|
const filePath = getFilePath(method);
|
40
51
|
const fullPath = Path.join(baseDir, normalizePath(path), filePath);
|
41
52
|
return FS.existsSync(fullPath);
|
42
53
|
}
|
43
54
|
|
44
|
-
function
|
55
|
+
export function getSystemBaseDir(systemId: string) {
|
45
56
|
return Path.join(os.tmpdir(), 'ai-systems', systemId);
|
46
57
|
}
|
47
58
|
|
@@ -50,7 +61,7 @@ function getFilePath(method: string) {
|
|
50
61
|
}
|
51
62
|
|
52
63
|
export function resolveReadPath(systemId: string, path: string, method: string) {
|
53
|
-
const baseDir =
|
64
|
+
const baseDir = getSystemBaseDir(systemId);
|
54
65
|
|
55
66
|
path = normalizePath(path);
|
56
67
|
|
package/src/storm/routes.ts
CHANGED
@@ -35,9 +35,16 @@ import {
|
|
35
35
|
import { StormCodegen } from './codegen';
|
36
36
|
import { assetManager } from '../assetManager';
|
37
37
|
import uuid from 'node-uuid';
|
38
|
-
import {
|
38
|
+
import {
|
39
|
+
readPageFromDisk,
|
40
|
+
readPageFromDiskAsString,
|
41
|
+
SystemIdHeader,
|
42
|
+
writeAssetToDisk,
|
43
|
+
writePageToDisk,
|
44
|
+
} from './page-utils';
|
39
45
|
import { UIServer } from './UIServer';
|
40
46
|
import { PageQueue } from './PageGenerator';
|
47
|
+
import { randomUUID } from 'crypto';
|
41
48
|
|
42
49
|
const UI_SERVERS: { [key: string]: UIServer } = {};
|
43
50
|
const router = Router();
|
@@ -185,6 +192,8 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
|
|
185
192
|
title: landingPage.title,
|
186
193
|
filename: landingPage.filename,
|
187
194
|
storage_prefix: systemId + '_',
|
195
|
+
// TODO: Add themes to this request type
|
196
|
+
theme: '',
|
188
197
|
});
|
189
198
|
} catch (e) {
|
190
199
|
console.error('Failed to process event', e);
|
@@ -226,13 +235,13 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
|
|
226
235
|
router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
227
236
|
const handle = req.params.handle as string;
|
228
237
|
try {
|
229
|
-
const
|
238
|
+
const outerConversationId =
|
239
|
+
(req.headers[ConversationIdHeader.toLowerCase()] as string | undefined) || randomUUID();
|
230
240
|
|
231
241
|
const aiRequest: BasePromptRequest = JSON.parse(req.stringBody ?? '{}');
|
232
242
|
|
233
243
|
// Get user journeys
|
234
|
-
const userJourneysStream = await stormClient.createUIUserJourneys(aiRequest,
|
235
|
-
const outerConversationId = userJourneysStream.getConversationId();
|
244
|
+
const userJourneysStream = await stormClient.createUIUserJourneys(aiRequest, outerConversationId);
|
236
245
|
|
237
246
|
onRequestAborted(req, res, () => {
|
238
247
|
userJourneysStream.abort();
|
@@ -271,11 +280,53 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
271
280
|
sendError(error, res);
|
272
281
|
});
|
273
282
|
|
283
|
+
let theme = '';
|
284
|
+
try {
|
285
|
+
const themeStream = await stormClient.createTheme(aiRequest, outerConversationId);
|
286
|
+
onRequestAborted(req, res, () => {
|
287
|
+
themeStream.abort();
|
288
|
+
});
|
289
|
+
|
290
|
+
themeStream.on('data', (evt) => {
|
291
|
+
sendEvent(res, evt);
|
292
|
+
if (evt.type === 'FILE_DONE') {
|
293
|
+
theme = evt.payload.content;
|
294
|
+
writeAssetToDisk(outerConversationId, evt).catch((err) => {
|
295
|
+
sendEvent(res, {
|
296
|
+
type: 'ERROR_INTERNAL',
|
297
|
+
created: new Date().getTime(),
|
298
|
+
payload: { error: err.message },
|
299
|
+
reason: 'Failed to save theme',
|
300
|
+
});
|
301
|
+
});
|
302
|
+
}
|
303
|
+
});
|
304
|
+
themeStream.on('error', (error) => {
|
305
|
+
console.error(error);
|
306
|
+
sendEvent(res, {
|
307
|
+
type: 'ERROR_INTERNAL',
|
308
|
+
created: new Date().getTime(),
|
309
|
+
payload: { error: error.message },
|
310
|
+
reason: 'Failed to create theme',
|
311
|
+
});
|
312
|
+
});
|
313
|
+
await waitForStormStream(themeStream);
|
314
|
+
} catch (e: any) {
|
315
|
+
console.error('Failed to generate theme', e);
|
316
|
+
sendEvent(res, {
|
317
|
+
type: 'ERROR_INTERNAL',
|
318
|
+
created: new Date().getTime(),
|
319
|
+
payload: { error: e.message },
|
320
|
+
reason: 'Failed to create theme',
|
321
|
+
});
|
322
|
+
}
|
323
|
+
|
274
324
|
await waitForStormStream(userJourneysStream);
|
275
325
|
|
276
326
|
// Get the UI shells
|
277
327
|
const shellsStream = await stormClient.createUIShells(
|
278
328
|
{
|
329
|
+
theme: theme ? `// filename: theme.css\n${theme}` : undefined,
|
279
330
|
pages: Object.values(uniqueUserJourneyScreens).map((screen) => ({
|
280
331
|
name: screen.name,
|
281
332
|
title: screen.title,
|
@@ -285,7 +336,7 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
285
336
|
requirements: screen.requirements,
|
286
337
|
})),
|
287
338
|
},
|
288
|
-
|
339
|
+
outerConversationId
|
289
340
|
);
|
290
341
|
|
291
342
|
onRequestAborted(req, res, () => {
|
@@ -293,6 +344,8 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
293
344
|
});
|
294
345
|
|
295
346
|
const queue = new PageQueue(outerConversationId, 5);
|
347
|
+
queue.setUiTheme(theme);
|
348
|
+
|
296
349
|
shellsStream.on('data', (data: StormEvent) => {
|
297
350
|
console.log('Processing shell event', data);
|
298
351
|
sendEvent(res, data);
|
@@ -356,6 +409,7 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
356
409
|
title: screen.title,
|
357
410
|
filename: screen.filename,
|
358
411
|
storage_prefix: outerConversationId + '_',
|
412
|
+
theme,
|
359
413
|
})
|
360
414
|
);
|
361
415
|
}
|
package/src/storm/stormClient.ts
CHANGED
@@ -22,6 +22,7 @@ export const STORM_ID = 'storm';
|
|
22
22
|
export const ConversationIdHeader = 'Conversation-Id';
|
23
23
|
|
24
24
|
export interface UIShellsPrompt {
|
25
|
+
theme?: string;
|
25
26
|
pages: {
|
26
27
|
name: string;
|
27
28
|
title: string;
|
@@ -42,6 +43,8 @@ export interface UIPagePrompt {
|
|
42
43
|
description: string;
|
43
44
|
storage_prefix: string;
|
44
45
|
shell_page?: string;
|
46
|
+
// contents of theme.css
|
47
|
+
theme: string;
|
45
48
|
}
|
46
49
|
|
47
50
|
export interface UIPageSamplePrompt extends UIPagePrompt {
|
@@ -185,6 +188,13 @@ class StormClient {
|
|
185
188
|
});
|
186
189
|
}
|
187
190
|
|
191
|
+
public createTheme(prompt: BasePromptRequest, conversationId?: string) {
|
192
|
+
return this.send('/v2/ui/theme', {
|
193
|
+
prompt: prompt,
|
194
|
+
conversationId,
|
195
|
+
});
|
196
|
+
}
|
197
|
+
|
188
198
|
public createUIShells(prompt: UIShellsPrompt, conversationId?: string) {
|
189
199
|
return this.send('/v2/ui/shells', {
|
190
200
|
prompt: JSON.stringify(prompt),
|