@kapeta/local-cluster-service 0.64.3 → 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 +14 -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 +61 -4
- package/dist/cjs/src/storm/stormClient.d.ts +17 -0
- package/dist/cjs/src/storm/stormClient.js +27 -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 +61 -4
- package/dist/esm/src/storm/stormClient.d.ts +17 -0
- package/dist/esm/src/storm/stormClient.js +27 -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 +75 -5
- package/src/storm/stormClient.ts +47 -0
package/CHANGELOG.md
CHANGED
@@ -1,3 +1,17 @@
|
|
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
|
+
|
8
|
+
# [0.65.0](https://github.com/kapetacom/local-cluster-service/compare/v0.64.3...v0.65.0) (2024-08-26)
|
9
|
+
|
10
|
+
|
11
|
+
### Features
|
12
|
+
|
13
|
+
* Add votes endpoints ([82c1ee7](https://github.com/kapetacom/local-cluster-service/commit/82c1ee7a959f61475a7403845f8037b11fdd5084))
|
14
|
+
|
1
15
|
## [0.64.3](https://github.com/kapetacom/local-cluster-service/compare/v0.64.2...v0.64.3) (2024-08-23)
|
2
16
|
|
3
17
|
|
@@ -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();
|
@@ -360,6 +405,18 @@ router.post('/ui/edit', async (req, res) => {
|
|
360
405
|
}
|
361
406
|
}
|
362
407
|
});
|
408
|
+
router.post('/ui/vote', async (req, res) => {
|
409
|
+
const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()] || '';
|
410
|
+
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
411
|
+
const { topic, vote, mainConversationId } = aiRequest;
|
412
|
+
return stormClient_1.stormClient.voteUIPage(topic, conversationId, vote, mainConversationId);
|
413
|
+
});
|
414
|
+
router.post('/ui/get-vote', async (req, res) => {
|
415
|
+
const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()] || '';
|
416
|
+
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
417
|
+
const { topic, mainConversationId } = aiRequest;
|
418
|
+
return stormClient_1.stormClient.getVoteUIPage(topic, conversationId, mainConversationId);
|
419
|
+
});
|
363
420
|
router.post('/:handle/all', async (req, res) => {
|
364
421
|
const handle = req.params.handle;
|
365
422
|
try {
|
@@ -1,8 +1,10 @@
|
|
1
|
+
/// <reference types="node" />
|
1
2
|
import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
|
2
3
|
import { Page, StormEventPageUrl } from './events';
|
3
4
|
export declare const STORM_ID = "storm";
|
4
5
|
export declare const ConversationIdHeader = "Conversation-Id";
|
5
6
|
export interface UIShellsPrompt {
|
7
|
+
theme?: string;
|
6
8
|
pages: {
|
7
9
|
name: string;
|
8
10
|
title: string;
|
@@ -22,6 +24,7 @@ export interface UIPagePrompt {
|
|
22
24
|
description: string;
|
23
25
|
storage_prefix: string;
|
24
26
|
shell_page?: string;
|
27
|
+
theme: string;
|
25
28
|
}
|
26
29
|
export interface UIPageSamplePrompt extends UIPagePrompt {
|
27
30
|
variantId: string;
|
@@ -38,6 +41,15 @@ export interface UIPageEditRequest {
|
|
38
41
|
pages: StormEventPageUrl['payload'][];
|
39
42
|
prompt: BasePromptRequest;
|
40
43
|
}
|
44
|
+
export interface UIPageVoteRequest {
|
45
|
+
topic: string;
|
46
|
+
vote: -1 | 0 | 1;
|
47
|
+
mainConversationId: string;
|
48
|
+
}
|
49
|
+
export interface UIPageGetVoteRequest {
|
50
|
+
topic: string;
|
51
|
+
mainConversationId: string;
|
52
|
+
}
|
41
53
|
export interface BasePromptRequest {
|
42
54
|
prompt: string;
|
43
55
|
skipImprovement?: boolean;
|
@@ -50,9 +62,14 @@ declare class StormClient {
|
|
50
62
|
createMetadata(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
|
51
63
|
createUIPages(prompt: string, conversationId?: string): Promise<StormStream>;
|
52
64
|
createUIUserJourneys(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
|
65
|
+
createTheme(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
|
53
66
|
createUIShells(prompt: UIShellsPrompt, conversationId?: string): Promise<StormStream>;
|
54
67
|
createUILandingPages(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
|
55
68
|
createUIPage(prompt: UIPagePrompt, conversationId?: string, history?: ConversationItem[]): Promise<StormStream>;
|
69
|
+
voteUIPage(topic: string, conversationId: string, vote: -1 | 0 | 1, mainConversationId?: string): Promise<Response>;
|
70
|
+
getVoteUIPage(topic: string, conversationId: string, mainConversationId?: string): Promise<{
|
71
|
+
vote: -1 | 0 | 1;
|
72
|
+
}>;
|
56
73
|
classifyUIReferences(prompt: string, conversationId?: string): Promise<StormStream>;
|
57
74
|
editPages(prompt: UIPageEditPrompt, conversationId?: string): Promise<StormStream>;
|
58
75
|
listScreens(prompt: StormUIListPrompt, conversationId?: string): 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),
|
@@ -110,6 +116,27 @@ class StormClient {
|
|
110
116
|
history,
|
111
117
|
});
|
112
118
|
}
|
119
|
+
async voteUIPage(topic, conversationId, vote, mainConversationId) {
|
120
|
+
const options = await this.createOptions('/v2/ui/vote', 'POST', {
|
121
|
+
prompt: JSON.stringify({ topic, vote, mainConversationId }),
|
122
|
+
conversationId,
|
123
|
+
});
|
124
|
+
return fetch(options.url, {
|
125
|
+
method: options.method,
|
126
|
+
headers: options.headers,
|
127
|
+
});
|
128
|
+
}
|
129
|
+
async getVoteUIPage(topic, conversationId, mainConversationId) {
|
130
|
+
const options = await this.createOptions('/v2/ui/get-vote', 'POST', {
|
131
|
+
prompt: JSON.stringify({ topic, mainConversationId }),
|
132
|
+
conversationId,
|
133
|
+
});
|
134
|
+
const response = await fetch(options.url, {
|
135
|
+
method: options.method,
|
136
|
+
headers: options.headers,
|
137
|
+
});
|
138
|
+
return response.json();
|
139
|
+
}
|
113
140
|
classifyUIReferences(prompt, conversationId) {
|
114
141
|
return this.send('/v2/ui/references', {
|
115
142
|
prompt: 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();
|
@@ -360,6 +405,18 @@ router.post('/ui/edit', async (req, res) => {
|
|
360
405
|
}
|
361
406
|
}
|
362
407
|
});
|
408
|
+
router.post('/ui/vote', async (req, res) => {
|
409
|
+
const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()] || '';
|
410
|
+
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
411
|
+
const { topic, vote, mainConversationId } = aiRequest;
|
412
|
+
return stormClient_1.stormClient.voteUIPage(topic, conversationId, vote, mainConversationId);
|
413
|
+
});
|
414
|
+
router.post('/ui/get-vote', async (req, res) => {
|
415
|
+
const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()] || '';
|
416
|
+
const aiRequest = JSON.parse(req.stringBody ?? '{}');
|
417
|
+
const { topic, mainConversationId } = aiRequest;
|
418
|
+
return stormClient_1.stormClient.getVoteUIPage(topic, conversationId, mainConversationId);
|
419
|
+
});
|
363
420
|
router.post('/:handle/all', async (req, res) => {
|
364
421
|
const handle = req.params.handle;
|
365
422
|
try {
|
@@ -1,8 +1,10 @@
|
|
1
|
+
/// <reference types="node" />
|
1
2
|
import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
|
2
3
|
import { Page, StormEventPageUrl } from './events';
|
3
4
|
export declare const STORM_ID = "storm";
|
4
5
|
export declare const ConversationIdHeader = "Conversation-Id";
|
5
6
|
export interface UIShellsPrompt {
|
7
|
+
theme?: string;
|
6
8
|
pages: {
|
7
9
|
name: string;
|
8
10
|
title: string;
|
@@ -22,6 +24,7 @@ export interface UIPagePrompt {
|
|
22
24
|
description: string;
|
23
25
|
storage_prefix: string;
|
24
26
|
shell_page?: string;
|
27
|
+
theme: string;
|
25
28
|
}
|
26
29
|
export interface UIPageSamplePrompt extends UIPagePrompt {
|
27
30
|
variantId: string;
|
@@ -38,6 +41,15 @@ export interface UIPageEditRequest {
|
|
38
41
|
pages: StormEventPageUrl['payload'][];
|
39
42
|
prompt: BasePromptRequest;
|
40
43
|
}
|
44
|
+
export interface UIPageVoteRequest {
|
45
|
+
topic: string;
|
46
|
+
vote: -1 | 0 | 1;
|
47
|
+
mainConversationId: string;
|
48
|
+
}
|
49
|
+
export interface UIPageGetVoteRequest {
|
50
|
+
topic: string;
|
51
|
+
mainConversationId: string;
|
52
|
+
}
|
41
53
|
export interface BasePromptRequest {
|
42
54
|
prompt: string;
|
43
55
|
skipImprovement?: boolean;
|
@@ -50,9 +62,14 @@ declare class StormClient {
|
|
50
62
|
createMetadata(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
|
51
63
|
createUIPages(prompt: string, conversationId?: string): Promise<StormStream>;
|
52
64
|
createUIUserJourneys(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
|
65
|
+
createTheme(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
|
53
66
|
createUIShells(prompt: UIShellsPrompt, conversationId?: string): Promise<StormStream>;
|
54
67
|
createUILandingPages(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
|
55
68
|
createUIPage(prompt: UIPagePrompt, conversationId?: string, history?: ConversationItem[]): Promise<StormStream>;
|
69
|
+
voteUIPage(topic: string, conversationId: string, vote: -1 | 0 | 1, mainConversationId?: string): Promise<Response>;
|
70
|
+
getVoteUIPage(topic: string, conversationId: string, mainConversationId?: string): Promise<{
|
71
|
+
vote: -1 | 0 | 1;
|
72
|
+
}>;
|
56
73
|
classifyUIReferences(prompt: string, conversationId?: string): Promise<StormStream>;
|
57
74
|
editPages(prompt: UIPageEditPrompt, conversationId?: string): Promise<StormStream>;
|
58
75
|
listScreens(prompt: StormUIListPrompt, conversationId?: string): 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),
|
@@ -110,6 +116,27 @@ class StormClient {
|
|
110
116
|
history,
|
111
117
|
});
|
112
118
|
}
|
119
|
+
async voteUIPage(topic, conversationId, vote, mainConversationId) {
|
120
|
+
const options = await this.createOptions('/v2/ui/vote', 'POST', {
|
121
|
+
prompt: JSON.stringify({ topic, vote, mainConversationId }),
|
122
|
+
conversationId,
|
123
|
+
});
|
124
|
+
return fetch(options.url, {
|
125
|
+
method: options.method,
|
126
|
+
headers: options.headers,
|
127
|
+
});
|
128
|
+
}
|
129
|
+
async getVoteUIPage(topic, conversationId, mainConversationId) {
|
130
|
+
const options = await this.createOptions('/v2/ui/get-vote', 'POST', {
|
131
|
+
prompt: JSON.stringify({ topic, mainConversationId }),
|
132
|
+
conversationId,
|
133
|
+
});
|
134
|
+
const response = await fetch(options.url, {
|
135
|
+
method: options.method,
|
136
|
+
headers: options.headers,
|
137
|
+
});
|
138
|
+
return response.json();
|
139
|
+
}
|
113
140
|
classifyUIReferences(prompt, conversationId) {
|
114
141
|
return this.send('/v2/ui/references', {
|
115
142
|
prompt: 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
@@ -20,6 +20,8 @@ import {
|
|
20
20
|
UIPageEditPrompt,
|
21
21
|
UIPageEditRequest,
|
22
22
|
BasePromptRequest,
|
23
|
+
UIPageVoteRequest,
|
24
|
+
UIPageGetVoteRequest,
|
23
25
|
} from './stormClient';
|
24
26
|
import { Page, StormEvent, StormEventPage, StormEventPhaseType, UserJourneyScreen } from './events';
|
25
27
|
|
@@ -33,9 +35,16 @@ import {
|
|
33
35
|
import { StormCodegen } from './codegen';
|
34
36
|
import { assetManager } from '../assetManager';
|
35
37
|
import uuid from 'node-uuid';
|
36
|
-
import {
|
38
|
+
import {
|
39
|
+
readPageFromDisk,
|
40
|
+
readPageFromDiskAsString,
|
41
|
+
SystemIdHeader,
|
42
|
+
writeAssetToDisk,
|
43
|
+
writePageToDisk,
|
44
|
+
} from './page-utils';
|
37
45
|
import { UIServer } from './UIServer';
|
38
46
|
import { PageQueue } from './PageGenerator';
|
47
|
+
import { randomUUID } from 'crypto';
|
39
48
|
|
40
49
|
const UI_SERVERS: { [key: string]: UIServer } = {};
|
41
50
|
const router = Router();
|
@@ -183,6 +192,8 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
|
|
183
192
|
title: landingPage.title,
|
184
193
|
filename: landingPage.filename,
|
185
194
|
storage_prefix: systemId + '_',
|
195
|
+
// TODO: Add themes to this request type
|
196
|
+
theme: '',
|
186
197
|
});
|
187
198
|
} catch (e) {
|
188
199
|
console.error('Failed to process event', e);
|
@@ -224,13 +235,13 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
|
|
224
235
|
router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
225
236
|
const handle = req.params.handle as string;
|
226
237
|
try {
|
227
|
-
const
|
238
|
+
const outerConversationId =
|
239
|
+
(req.headers[ConversationIdHeader.toLowerCase()] as string | undefined) || randomUUID();
|
228
240
|
|
229
241
|
const aiRequest: BasePromptRequest = JSON.parse(req.stringBody ?? '{}');
|
230
242
|
|
231
243
|
// Get user journeys
|
232
|
-
const userJourneysStream = await stormClient.createUIUserJourneys(aiRequest,
|
233
|
-
const outerConversationId = userJourneysStream.getConversationId();
|
244
|
+
const userJourneysStream = await stormClient.createUIUserJourneys(aiRequest, outerConversationId);
|
234
245
|
|
235
246
|
onRequestAborted(req, res, () => {
|
236
247
|
userJourneysStream.abort();
|
@@ -269,11 +280,53 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
269
280
|
sendError(error, res);
|
270
281
|
});
|
271
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
|
+
|
272
324
|
await waitForStormStream(userJourneysStream);
|
273
325
|
|
274
326
|
// Get the UI shells
|
275
327
|
const shellsStream = await stormClient.createUIShells(
|
276
328
|
{
|
329
|
+
theme: theme ? `// filename: theme.css\n${theme}` : undefined,
|
277
330
|
pages: Object.values(uniqueUserJourneyScreens).map((screen) => ({
|
278
331
|
name: screen.name,
|
279
332
|
title: screen.title,
|
@@ -283,7 +336,7 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
283
336
|
requirements: screen.requirements,
|
284
337
|
})),
|
285
338
|
},
|
286
|
-
|
339
|
+
outerConversationId
|
287
340
|
);
|
288
341
|
|
289
342
|
onRequestAborted(req, res, () => {
|
@@ -291,6 +344,8 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
291
344
|
});
|
292
345
|
|
293
346
|
const queue = new PageQueue(outerConversationId, 5);
|
347
|
+
queue.setUiTheme(theme);
|
348
|
+
|
294
349
|
shellsStream.on('data', (data: StormEvent) => {
|
295
350
|
console.log('Processing shell event', data);
|
296
351
|
sendEvent(res, data);
|
@@ -354,6 +409,7 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
354
409
|
title: screen.title,
|
355
410
|
filename: screen.filename,
|
356
411
|
storage_prefix: outerConversationId + '_',
|
412
|
+
theme,
|
357
413
|
})
|
358
414
|
);
|
359
415
|
}
|
@@ -449,6 +505,20 @@ router.post('/ui/edit', async (req: KapetaBodyRequest, res: Response) => {
|
|
449
505
|
}
|
450
506
|
});
|
451
507
|
|
508
|
+
router.post('/ui/vote', async (req: KapetaBodyRequest, res: Response) => {
|
509
|
+
const conversationId = (req.headers[ConversationIdHeader.toLowerCase()] as string | undefined) || '';
|
510
|
+
const aiRequest: UIPageVoteRequest = JSON.parse(req.stringBody ?? '{}');
|
511
|
+
const { topic, vote, mainConversationId } = aiRequest;
|
512
|
+
return stormClient.voteUIPage(topic, conversationId, vote, mainConversationId);
|
513
|
+
});
|
514
|
+
|
515
|
+
router.post('/ui/get-vote', async (req: KapetaBodyRequest, res: Response) => {
|
516
|
+
const conversationId = (req.headers[ConversationIdHeader.toLowerCase()] as string | undefined) || '';
|
517
|
+
const aiRequest: UIPageGetVoteRequest = JSON.parse(req.stringBody ?? '{}');
|
518
|
+
const { topic, mainConversationId } = aiRequest;
|
519
|
+
return stormClient.getVoteUIPage(topic, conversationId, mainConversationId);
|
520
|
+
});
|
521
|
+
|
452
522
|
router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
|
453
523
|
const handle = req.params.handle as string;
|
454
524
|
|
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 {
|
@@ -62,6 +65,17 @@ export interface UIPageEditRequest {
|
|
62
65
|
prompt: BasePromptRequest;
|
63
66
|
}
|
64
67
|
|
68
|
+
export interface UIPageVoteRequest {
|
69
|
+
topic: string;
|
70
|
+
vote: -1 | 0 | 1;
|
71
|
+
mainConversationId: string;
|
72
|
+
}
|
73
|
+
|
74
|
+
export interface UIPageGetVoteRequest {
|
75
|
+
topic: string;
|
76
|
+
mainConversationId: string;
|
77
|
+
}
|
78
|
+
|
65
79
|
export interface BasePromptRequest {
|
66
80
|
prompt: string;
|
67
81
|
skipImprovement?: boolean;
|
@@ -174,6 +188,13 @@ class StormClient {
|
|
174
188
|
});
|
175
189
|
}
|
176
190
|
|
191
|
+
public createTheme(prompt: BasePromptRequest, conversationId?: string) {
|
192
|
+
return this.send('/v2/ui/theme', {
|
193
|
+
prompt: prompt,
|
194
|
+
conversationId,
|
195
|
+
});
|
196
|
+
}
|
197
|
+
|
177
198
|
public createUIShells(prompt: UIShellsPrompt, conversationId?: string) {
|
178
199
|
return this.send('/v2/ui/shells', {
|
179
200
|
prompt: JSON.stringify(prompt),
|
@@ -195,6 +216,32 @@ class StormClient {
|
|
195
216
|
});
|
196
217
|
}
|
197
218
|
|
219
|
+
public async voteUIPage(topic: string, conversationId: string, vote: -1 | 0 | 1, mainConversationId?: string) {
|
220
|
+
const options = await this.createOptions('/v2/ui/vote', 'POST', {
|
221
|
+
prompt: JSON.stringify({ topic, vote, mainConversationId }),
|
222
|
+
conversationId,
|
223
|
+
});
|
224
|
+
|
225
|
+
return fetch(options.url, {
|
226
|
+
method: options.method,
|
227
|
+
headers: options.headers,
|
228
|
+
});
|
229
|
+
}
|
230
|
+
|
231
|
+
public async getVoteUIPage(topic: string, conversationId: string, mainConversationId?: string) {
|
232
|
+
const options = await this.createOptions('/v2/ui/get-vote', 'POST', {
|
233
|
+
prompt: JSON.stringify({ topic, mainConversationId }),
|
234
|
+
conversationId,
|
235
|
+
});
|
236
|
+
|
237
|
+
const response = await fetch(options.url, {
|
238
|
+
method: options.method,
|
239
|
+
headers: options.headers,
|
240
|
+
});
|
241
|
+
|
242
|
+
return response.json() as Promise<{ vote: -1 | 0 | 1 }>;
|
243
|
+
}
|
244
|
+
|
198
245
|
public classifyUIReferences(prompt: string, conversationId?: string) {
|
199
246
|
return this.send('/v2/ui/references', {
|
200
247
|
prompt: prompt,
|