@kapeta/local-cluster-service 0.65.0 → 0.67.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 +16 -1
- package/dist/cjs/src/storm/PageGenerator.js +36 -7
- package/dist/cjs/src/storm/PromiseQueue.d.ts +7 -0
- package/dist/cjs/src/storm/PromiseQueue.js +27 -1
- package/dist/cjs/src/storm/UIServer.js +3 -0
- package/dist/cjs/src/storm/events.d.ts +9 -1
- package/dist/cjs/src/storm/page-utils.d.ts +9 -1
- package/dist/cjs/src/storm/page-utils.js +45 -7
- package/dist/cjs/src/storm/routes.js +97 -5
- package/dist/cjs/src/storm/stormClient.d.ts +4 -0
- package/dist/cjs/src/storm/stormClient.js +12 -0
- package/dist/esm/src/storm/PageGenerator.d.ts +16 -1
- package/dist/esm/src/storm/PageGenerator.js +36 -7
- package/dist/esm/src/storm/PromiseQueue.d.ts +7 -0
- package/dist/esm/src/storm/PromiseQueue.js +27 -1
- package/dist/esm/src/storm/UIServer.js +3 -0
- package/dist/esm/src/storm/events.d.ts +9 -1
- package/dist/esm/src/storm/page-utils.d.ts +9 -1
- package/dist/esm/src/storm/page-utils.js +45 -7
- package/dist/esm/src/storm/routes.js +97 -5
- package/dist/esm/src/storm/stormClient.d.ts +4 -0
- package/dist/esm/src/storm/stormClient.js +12 -0
- package/package.json +1 -1
- package/src/storm/PageGenerator.ts +58 -15
- package/src/storm/PromiseQueue.ts +34 -0
- package/src/storm/UIServer.ts +5 -1
- package/src/storm/events.ts +11 -1
- package/src/storm/page-utils.ts +53 -8
- package/src/storm/routes.ts +110 -8
- package/src/storm/stormClient.ts +17 -0
@@ -5,22 +5,27 @@
|
|
5
5
|
|
6
6
|
import uuid from 'node-uuid';
|
7
7
|
import { stormClient, UIPagePrompt } from './stormClient';
|
8
|
-
import {
|
9
|
-
ReferenceClassification,
|
10
|
-
StormEvent,
|
11
|
-
StormEventPage,
|
12
|
-
StormEventReferenceClassification,
|
13
|
-
UIShell,
|
14
|
-
} from './events';
|
8
|
+
import { ReferenceClassification, StormEvent, StormEventPage, StormImage, UIShell } from './events';
|
15
9
|
import { EventEmitter } from 'node:events';
|
16
|
-
import { PromiseQueue } from './PromiseQueue';
|
10
|
+
import { createFuture, Future, FuturePromise, PromiseQueue } from './PromiseQueue';
|
17
11
|
import { hasPageOnDisk } from './page-utils';
|
18
12
|
|
13
|
+
export interface ImagePrompt {
|
14
|
+
name: string;
|
15
|
+
description: string;
|
16
|
+
source: 'local' | 'cdn' | 'example';
|
17
|
+
title: string;
|
18
|
+
type: 'image' | 'css' | 'javascript' | 'html';
|
19
|
+
url: string;
|
20
|
+
content: string;
|
21
|
+
}
|
22
|
+
|
19
23
|
export class PageQueue extends EventEmitter {
|
20
24
|
private readonly queue: PromiseQueue;
|
21
25
|
private readonly systemId: string;
|
22
26
|
private readonly references: Map<string, PageGenerator> = new Map();
|
23
27
|
private uiShells: UIShell[] = [];
|
28
|
+
private theme = '';
|
24
29
|
|
25
30
|
constructor(systemId: string, concurrency: number = 5) {
|
26
31
|
super();
|
@@ -30,6 +35,7 @@ export class PageQueue extends EventEmitter {
|
|
30
35
|
|
31
36
|
on(event: 'event', listener: (data: StormEvent) => void): this;
|
32
37
|
on(event: 'page', listener: (data: StormEventPage) => void): this;
|
38
|
+
on(event: 'image', listener: (data: StormImage, source: ImagePrompt, future: FuturePromise<void>) => void): this;
|
33
39
|
|
34
40
|
on(event: string, listener: (...args: any[]) => void): this {
|
35
41
|
return super.on(event, listener);
|
@@ -37,6 +43,7 @@ export class PageQueue extends EventEmitter {
|
|
37
43
|
|
38
44
|
emit(type: 'event', event: StormEvent): boolean;
|
39
45
|
emit(type: 'page', event: StormEventPage): boolean;
|
46
|
+
emit(type: 'image', event: StormImage, source: ImagePrompt, future: FuturePromise<void>): boolean;
|
40
47
|
emit(eventName: string | symbol, ...args: any[]): boolean {
|
41
48
|
return super.emit(eventName, ...args);
|
42
49
|
}
|
@@ -45,24 +52,29 @@ export class PageQueue extends EventEmitter {
|
|
45
52
|
this.uiShells.push(uiShell);
|
46
53
|
}
|
47
54
|
|
55
|
+
public setUiTheme(theme: string) {
|
56
|
+
this.theme = theme;
|
57
|
+
}
|
58
|
+
|
48
59
|
public addPrompt(
|
49
60
|
initialPrompt: Omit<UIPagePrompt, 'shell_page'>,
|
50
61
|
conversationId: string = uuid.v4(),
|
51
62
|
overwrite: boolean = false
|
52
63
|
) {
|
53
64
|
if (!overwrite && this.references.has(initialPrompt.path)) {
|
54
|
-
console.log('Ignoring duplicate prompt', initialPrompt.path);
|
65
|
+
//console.log('Ignoring duplicate prompt', initialPrompt.path);
|
55
66
|
return Promise.resolve();
|
56
67
|
}
|
57
68
|
|
58
69
|
if (!overwrite && hasPageOnDisk(this.systemId, initialPrompt.method, initialPrompt.path)) {
|
59
|
-
console.log('Ignoring prompt with existing page', initialPrompt.path);
|
70
|
+
//console.log('Ignoring prompt with existing page', initialPrompt.path);
|
60
71
|
return Promise.resolve();
|
61
72
|
}
|
62
73
|
|
63
74
|
const prompt: UIPagePrompt = {
|
64
75
|
...initialPrompt,
|
65
76
|
shell_page: this.uiShells.find((shell) => shell.screens.includes(initialPrompt.name))?.content,
|
77
|
+
theme: this.theme,
|
66
78
|
};
|
67
79
|
|
68
80
|
const generator = new PageGenerator(prompt, conversationId);
|
@@ -73,9 +85,8 @@ export class PageQueue extends EventEmitter {
|
|
73
85
|
|
74
86
|
private async addPageGenerator(generator: PageGenerator) {
|
75
87
|
generator.on('event', (event: StormEvent) => this.emit('event', event));
|
76
|
-
generator.on('page_refs', ({ event, references }) => {
|
77
|
-
|
78
|
-
references.forEach((reference) => {
|
88
|
+
generator.on('page_refs', async ({ event, references }) => {
|
89
|
+
const promises = references.map(async (reference) => {
|
79
90
|
if (
|
80
91
|
reference.url.startsWith('#') ||
|
81
92
|
reference.url.startsWith('javascript:') ||
|
@@ -87,14 +98,17 @@ export class PageQueue extends EventEmitter {
|
|
87
98
|
|
88
99
|
switch (reference.type) {
|
89
100
|
case 'image':
|
90
|
-
|
101
|
+
await this.addImagePrompt({
|
102
|
+
...reference,
|
103
|
+
content: event.payload.content,
|
104
|
+
});
|
91
105
|
break;
|
92
106
|
case 'css':
|
93
107
|
case 'javascript':
|
94
108
|
//console.log('Ignoring reference', reference);
|
95
109
|
break;
|
96
110
|
case 'html':
|
97
|
-
console.log('Adding page generator for', reference);
|
111
|
+
//console.log('Adding page generator for', reference);
|
98
112
|
const paths = Array.from(this.references.keys());
|
99
113
|
this.addPrompt({
|
100
114
|
name: reference.name,
|
@@ -110,10 +124,14 @@ export class PageQueue extends EventEmitter {
|
|
110
124
|
: ''),
|
111
125
|
description: reference.description,
|
112
126
|
filename: '',
|
127
|
+
theme: this.theme,
|
113
128
|
});
|
114
129
|
break;
|
115
130
|
}
|
116
131
|
});
|
132
|
+
|
133
|
+
await Promise.allSettled(promises);
|
134
|
+
this.emit('page', event);
|
117
135
|
});
|
118
136
|
return this.queue.add(() => generator.generate());
|
119
137
|
}
|
@@ -125,6 +143,31 @@ export class PageQueue extends EventEmitter {
|
|
125
143
|
public wait() {
|
126
144
|
return this.queue.wait();
|
127
145
|
}
|
146
|
+
|
147
|
+
private async addImagePrompt(prompt: ImagePrompt) {
|
148
|
+
const result = await stormClient.createImage(
|
149
|
+
`Create an image for the url "${prompt.url}" with this description: ${prompt.description}`.trim()
|
150
|
+
);
|
151
|
+
|
152
|
+
const futures: FuturePromise<void>[] = [];
|
153
|
+
|
154
|
+
result.on('data', async (event: StormEvent) => {
|
155
|
+
if (event.type === 'IMAGE') {
|
156
|
+
const future = createFuture();
|
157
|
+
futures.push(future);
|
158
|
+
this.emit('image', event, prompt, future);
|
159
|
+
setTimeout(() => {
|
160
|
+
if (!future.isResolved()) {
|
161
|
+
console.log('Image prompt timed out', prompt);
|
162
|
+
future.reject(new Error('Image prompt timed out'));
|
163
|
+
}
|
164
|
+
}, 30000);
|
165
|
+
}
|
166
|
+
});
|
167
|
+
|
168
|
+
await result.waitForDone();
|
169
|
+
await Promise.allSettled(futures.map((f) => f.promise));
|
170
|
+
}
|
128
171
|
}
|
129
172
|
|
130
173
|
export class PageGenerator extends EventEmitter {
|
@@ -4,6 +4,40 @@
|
|
4
4
|
*/
|
5
5
|
export type Future<T> = () => Promise<T>;
|
6
6
|
|
7
|
+
export type FuturePromise<T> = {
|
8
|
+
promise: Promise<T>;
|
9
|
+
resolve: (value: T) => void;
|
10
|
+
reject: (reason: any) => void;
|
11
|
+
isResolved: () => boolean;
|
12
|
+
};
|
13
|
+
|
14
|
+
export function createFuture<T = void>(): FuturePromise<T> {
|
15
|
+
let resolved = false;
|
16
|
+
let resolve: (value: T) => void = () => {
|
17
|
+
resolved = true;
|
18
|
+
};
|
19
|
+
let reject: (reason: any) => void = () => {
|
20
|
+
resolved = true;
|
21
|
+
};
|
22
|
+
|
23
|
+
const promise = new Promise<T>((res, rej) => {
|
24
|
+
resolve = (value: T) => {
|
25
|
+
resolved = true;
|
26
|
+
res(value);
|
27
|
+
};
|
28
|
+
reject = (reason: any) => {
|
29
|
+
resolved = true;
|
30
|
+
rej(reason);
|
31
|
+
};
|
32
|
+
});
|
33
|
+
return {
|
34
|
+
promise,
|
35
|
+
resolve,
|
36
|
+
reject,
|
37
|
+
isResolved: () => resolved,
|
38
|
+
};
|
39
|
+
}
|
40
|
+
|
7
41
|
type InternalFuture<T> = {
|
8
42
|
execute: Future<T>;
|
9
43
|
promise: Promise<T>;
|
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/events.ts
CHANGED
@@ -311,6 +311,15 @@ export interface StormEventDone {
|
|
311
311
|
created: number;
|
312
312
|
}
|
313
313
|
|
314
|
+
export interface StormImage {
|
315
|
+
type: 'IMAGE';
|
316
|
+
reason: string;
|
317
|
+
created: number;
|
318
|
+
payload: {
|
319
|
+
href: string;
|
320
|
+
};
|
321
|
+
}
|
322
|
+
|
314
323
|
export interface StormEventDefinitionChange {
|
315
324
|
type: 'DEFINITION_CHANGE';
|
316
325
|
reason: string;
|
@@ -494,4 +503,5 @@ export type StormEvent =
|
|
494
503
|
| StormEventLandingPage
|
495
504
|
| StormEventReferenceClassification
|
496
505
|
| StormEventApiBase
|
497
|
-
| StormEventUIStarted
|
506
|
+
| StormEventUIStarted
|
507
|
+
| StormImage;
|
package/src/storm/page-utils.ts
CHANGED
@@ -2,7 +2,8 @@
|
|
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, StormImage } from './events';
|
6
|
+
|
6
7
|
import { Response } from 'express';
|
7
8
|
import os from 'node:os';
|
8
9
|
import Path from 'path';
|
@@ -10,6 +11,7 @@ import FS from 'fs-extra';
|
|
10
11
|
import FSExtra from 'fs-extra';
|
11
12
|
import { ConversationItem } from './stream';
|
12
13
|
import exp from 'node:constants';
|
14
|
+
import { ImagePrompt } from './PageGenerator';
|
13
15
|
|
14
16
|
export const SystemIdHeader = 'System-Id';
|
15
17
|
|
@@ -21,7 +23,7 @@ function normalizePath(path: string) {
|
|
21
23
|
}
|
22
24
|
|
23
25
|
export async function writePageToDisk(systemId: string, event: StormEventPage) {
|
24
|
-
const baseDir =
|
26
|
+
const baseDir = getSystemBaseDir(systemId);
|
25
27
|
const filePath = getFilePath(event.payload.method);
|
26
28
|
const path = Path.join(baseDir, normalizePath(event.payload.path), filePath);
|
27
29
|
await FS.ensureDir(Path.dirname(path));
|
@@ -34,14 +36,46 @@ export async function writePageToDisk(systemId: string, event: StormEventPage) {
|
|
34
36
|
};
|
35
37
|
}
|
36
38
|
|
39
|
+
export async function writeAssetToDisk(systemId: string, event: StormEventFileDone) {
|
40
|
+
const baseDir = getSystemBaseDir(systemId);
|
41
|
+
const path = Path.join(baseDir, 'public', event.payload.filename);
|
42
|
+
await FS.ensureDir(Path.dirname(path));
|
43
|
+
await FS.writeFile(path, event.payload.content);
|
44
|
+
|
45
|
+
return {
|
46
|
+
path,
|
47
|
+
};
|
48
|
+
}
|
49
|
+
|
50
|
+
export async function writeImageToDisk(systemId: string, event: StormImage, prompt: ImagePrompt) {
|
51
|
+
const baseDir = getSystemBaseDir(systemId);
|
52
|
+
const path = Path.join(baseDir, normalizePath(prompt.url));
|
53
|
+
|
54
|
+
const response = await fetch(event.payload.href);
|
55
|
+
if (!response.ok || !response.body) {
|
56
|
+
throw new Error(`Failed to fetch image: ${event.payload.href}`);
|
57
|
+
}
|
58
|
+
|
59
|
+
await FS.ensureDir(Path.dirname(path));
|
60
|
+
|
61
|
+
const buffer = await response.arrayBuffer();
|
62
|
+
await FS.writeFile(path, Buffer.from(buffer));
|
63
|
+
|
64
|
+
console.log(`Image written to disk: ${event.payload.href} > ${path}`);
|
65
|
+
|
66
|
+
return {
|
67
|
+
path,
|
68
|
+
};
|
69
|
+
}
|
70
|
+
|
37
71
|
export function hasPageOnDisk(systemId: string, method: string, path: string) {
|
38
|
-
const baseDir =
|
72
|
+
const baseDir = getSystemBaseDir(systemId);
|
39
73
|
const filePath = getFilePath(method);
|
40
74
|
const fullPath = Path.join(baseDir, normalizePath(path), filePath);
|
41
75
|
return FS.existsSync(fullPath);
|
42
76
|
}
|
43
77
|
|
44
|
-
function
|
78
|
+
export function getSystemBaseDir(systemId: string) {
|
45
79
|
return Path.join(os.tmpdir(), 'ai-systems', systemId);
|
46
80
|
}
|
47
81
|
|
@@ -50,13 +84,24 @@ function getFilePath(method: string) {
|
|
50
84
|
}
|
51
85
|
|
52
86
|
export function resolveReadPath(systemId: string, path: string, method: string) {
|
53
|
-
const baseDir =
|
87
|
+
const baseDir = getSystemBaseDir(systemId);
|
54
88
|
|
55
89
|
path = normalizePath(path);
|
56
90
|
|
57
|
-
|
91
|
+
let fullPath = Path.join(baseDir, path);
|
92
|
+
|
93
|
+
//First check if there is a file at the exact path
|
94
|
+
try {
|
95
|
+
const stat = FS.statSync(fullPath);
|
96
|
+
if (stat && stat.isFile()) {
|
97
|
+
return fullPath;
|
98
|
+
}
|
99
|
+
} catch (e) {
|
100
|
+
// Ignore
|
101
|
+
}
|
58
102
|
|
59
|
-
const
|
103
|
+
const filePath = getFilePath(method);
|
104
|
+
fullPath = Path.join(baseDir, path, filePath);
|
60
105
|
|
61
106
|
if (FS.existsSync(fullPath)) {
|
62
107
|
return fullPath;
|
@@ -108,7 +153,7 @@ export function readPageFromDisk(systemId: string, path: string, method: string,
|
|
108
153
|
|
109
154
|
res.type(filePath.split('.').pop() as string);
|
110
155
|
|
111
|
-
const content = FS.readFileSync(filePath
|
156
|
+
const content = FS.readFileSync(filePath);
|
112
157
|
res.write(content);
|
113
158
|
res.end();
|
114
159
|
}
|
package/src/storm/routes.ts
CHANGED
@@ -23,7 +23,7 @@ import {
|
|
23
23
|
UIPageVoteRequest,
|
24
24
|
UIPageGetVoteRequest,
|
25
25
|
} from './stormClient';
|
26
|
-
import { Page, StormEvent, StormEventPage, StormEventPhaseType, UserJourneyScreen } from './events';
|
26
|
+
import { Page, StormEvent, StormEventPage, StormEventPhaseType, StormImage, UserJourneyScreen } from './events';
|
27
27
|
|
28
28
|
import {
|
29
29
|
createPhaseEndEvent,
|
@@ -35,9 +35,17 @@ 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
|
+
writeImageToDisk,
|
44
|
+
writePageToDisk,
|
45
|
+
} from './page-utils';
|
39
46
|
import { UIServer } from './UIServer';
|
40
|
-
import {
|
47
|
+
import { randomUUID } from 'crypto';
|
48
|
+
import { ImagePrompt, PageQueue } from './PageGenerator';
|
41
49
|
|
42
50
|
const UI_SERVERS: { [key: string]: UIServer } = {};
|
43
51
|
const router = Router();
|
@@ -107,6 +115,21 @@ router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
|
|
107
115
|
}
|
108
116
|
});
|
109
117
|
|
118
|
+
queue.on('image', async (screenData, prompt, future) => {
|
119
|
+
if (!systemId) {
|
120
|
+
return;
|
121
|
+
}
|
122
|
+
try {
|
123
|
+
const promise = handleImageEvent(systemId, screenData, prompt);
|
124
|
+
promises.push(promise);
|
125
|
+
await promise;
|
126
|
+
future.resolve();
|
127
|
+
} catch (e) {
|
128
|
+
console.error('Failed to handle image event', e);
|
129
|
+
future.reject(e);
|
130
|
+
}
|
131
|
+
});
|
132
|
+
|
110
133
|
await queue.addPrompt(aiRequest, conversationId, true);
|
111
134
|
|
112
135
|
await queue.wait();
|
@@ -185,6 +208,8 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
|
|
185
208
|
title: landingPage.title,
|
186
209
|
filename: landingPage.filename,
|
187
210
|
storage_prefix: systemId + '_',
|
211
|
+
// TODO: Add themes to this request type
|
212
|
+
theme: '',
|
188
213
|
});
|
189
214
|
} catch (e) {
|
190
215
|
console.error('Failed to process event', e);
|
@@ -202,6 +227,18 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
|
|
202
227
|
pageEventPromises.push(sendPageEvent(landingPagesStream.getConversationId(), screenData, res));
|
203
228
|
});
|
204
229
|
|
230
|
+
pageQueue.on('image', async (screenData, prompt, future) => {
|
231
|
+
try {
|
232
|
+
const promise = handleImageEvent(landingPagesStream.getConversationId(), screenData, prompt);
|
233
|
+
pageEventPromises.push(promise);
|
234
|
+
await promise;
|
235
|
+
future.resolve();
|
236
|
+
} catch (e) {
|
237
|
+
console.error('Failed to handle image event', e);
|
238
|
+
future.reject(e);
|
239
|
+
}
|
240
|
+
});
|
241
|
+
|
205
242
|
pageQueue.on('event', (screenData: StormEvent) => {
|
206
243
|
sendEvent(res, screenData);
|
207
244
|
});
|
@@ -226,13 +263,13 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
|
|
226
263
|
router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
227
264
|
const handle = req.params.handle as string;
|
228
265
|
try {
|
229
|
-
const
|
266
|
+
const outerConversationId =
|
267
|
+
(req.headers[ConversationIdHeader.toLowerCase()] as string | undefined) || randomUUID();
|
230
268
|
|
231
269
|
const aiRequest: BasePromptRequest = JSON.parse(req.stringBody ?? '{}');
|
232
270
|
|
233
271
|
// Get user journeys
|
234
|
-
const userJourneysStream = await stormClient.createUIUserJourneys(aiRequest,
|
235
|
-
const outerConversationId = userJourneysStream.getConversationId();
|
272
|
+
const userJourneysStream = await stormClient.createUIUserJourneys(aiRequest, outerConversationId);
|
236
273
|
|
237
274
|
onRequestAborted(req, res, () => {
|
238
275
|
userJourneysStream.abort();
|
@@ -271,11 +308,53 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
271
308
|
sendError(error, res);
|
272
309
|
});
|
273
310
|
|
311
|
+
let theme = '';
|
312
|
+
try {
|
313
|
+
const themeStream = await stormClient.createTheme(aiRequest, outerConversationId);
|
314
|
+
onRequestAborted(req, res, () => {
|
315
|
+
themeStream.abort();
|
316
|
+
});
|
317
|
+
|
318
|
+
themeStream.on('data', (evt) => {
|
319
|
+
sendEvent(res, evt);
|
320
|
+
if (evt.type === 'FILE_DONE') {
|
321
|
+
theme = evt.payload.content;
|
322
|
+
writeAssetToDisk(outerConversationId, evt).catch((err) => {
|
323
|
+
sendEvent(res, {
|
324
|
+
type: 'ERROR_INTERNAL',
|
325
|
+
created: new Date().getTime(),
|
326
|
+
payload: { error: err.message },
|
327
|
+
reason: 'Failed to save theme',
|
328
|
+
});
|
329
|
+
});
|
330
|
+
}
|
331
|
+
});
|
332
|
+
themeStream.on('error', (error) => {
|
333
|
+
console.error(error);
|
334
|
+
sendEvent(res, {
|
335
|
+
type: 'ERROR_INTERNAL',
|
336
|
+
created: new Date().getTime(),
|
337
|
+
payload: { error: error.message },
|
338
|
+
reason: 'Failed to create theme',
|
339
|
+
});
|
340
|
+
});
|
341
|
+
await waitForStormStream(themeStream);
|
342
|
+
} catch (e: any) {
|
343
|
+
console.error('Failed to generate theme', e);
|
344
|
+
sendEvent(res, {
|
345
|
+
type: 'ERROR_INTERNAL',
|
346
|
+
created: new Date().getTime(),
|
347
|
+
payload: { error: e.message },
|
348
|
+
reason: 'Failed to create theme',
|
349
|
+
});
|
350
|
+
}
|
351
|
+
|
274
352
|
await waitForStormStream(userJourneysStream);
|
275
353
|
|
276
354
|
// Get the UI shells
|
277
355
|
const shellsStream = await stormClient.createUIShells(
|
278
356
|
{
|
357
|
+
theme: theme ? `// filename: theme.css\n${theme}` : undefined,
|
279
358
|
pages: Object.values(uniqueUserJourneyScreens).map((screen) => ({
|
280
359
|
name: screen.name,
|
281
360
|
title: screen.title,
|
@@ -285,7 +364,7 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
285
364
|
requirements: screen.requirements,
|
286
365
|
})),
|
287
366
|
},
|
288
|
-
|
367
|
+
outerConversationId
|
289
368
|
);
|
290
369
|
|
291
370
|
onRequestAborted(req, res, () => {
|
@@ -293,8 +372,10 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
293
372
|
});
|
294
373
|
|
295
374
|
const queue = new PageQueue(outerConversationId, 5);
|
375
|
+
queue.setUiTheme(theme);
|
376
|
+
|
296
377
|
shellsStream.on('data', (data: StormEvent) => {
|
297
|
-
console.log('Processing shell event', data);
|
378
|
+
//console.log('Processing shell event', data);
|
298
379
|
sendEvent(res, data);
|
299
380
|
|
300
381
|
if (data.type !== 'UI_SHELL') {
|
@@ -341,6 +422,18 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
341
422
|
pageEventPromises.push(sendPageEvent(outerConversationId, screenData, res));
|
342
423
|
});
|
343
424
|
|
425
|
+
queue.on('image', async (screenData, prompt, future) => {
|
426
|
+
try {
|
427
|
+
const promise = handleImageEvent(outerConversationId, screenData, prompt);
|
428
|
+
pageEventPromises.push(promise);
|
429
|
+
await promise;
|
430
|
+
future.resolve();
|
431
|
+
} catch (e) {
|
432
|
+
console.error('Failed to handle image event', e);
|
433
|
+
future.reject(e);
|
434
|
+
}
|
435
|
+
});
|
436
|
+
|
344
437
|
queue.on('event', (screenData: StormEvent) => {
|
345
438
|
sendEvent(res, screenData);
|
346
439
|
});
|
@@ -356,6 +449,7 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
356
449
|
title: screen.title,
|
357
450
|
filename: screen.filename,
|
358
451
|
storage_prefix: outerConversationId + '_',
|
452
|
+
theme,
|
359
453
|
})
|
360
454
|
);
|
361
455
|
}
|
@@ -712,4 +806,12 @@ async function sendPageEvent(mainConversationId: string, data: StormEventPage, r
|
|
712
806
|
sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
|
713
807
|
}
|
714
808
|
|
809
|
+
async function handleImageEvent(mainConversationId: string, data: StormImage, prompt: ImagePrompt) {
|
810
|
+
try {
|
811
|
+
await writeImageToDisk(mainConversationId, data, prompt);
|
812
|
+
} catch (err) {
|
813
|
+
console.error('Failed to write image to disk', err);
|
814
|
+
}
|
815
|
+
}
|
816
|
+
|
715
817
|
export default router;
|
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),
|
@@ -253,6 +263,13 @@ class StormClient {
|
|
253
263
|
});
|
254
264
|
}
|
255
265
|
|
266
|
+
public createImage(prompt: string, conversationId?: string) {
|
267
|
+
return this.send('/v2/ui/image', {
|
268
|
+
prompt,
|
269
|
+
conversationId,
|
270
|
+
});
|
271
|
+
}
|
272
|
+
|
256
273
|
public createUIImplementation(prompt: StormUIImplementationPrompt, conversationId?: string) {
|
257
274
|
return this.send('/v2/ui/merge', {
|
258
275
|
prompt,
|