@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 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 = getBaseDir(systemId);
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 = getBaseDir(systemId);
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 getBaseDir(systemId) {
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 = getBaseDir(systemId);
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 conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
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, conversationId);
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
- }, conversationId);
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 = getBaseDir(systemId);
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 = getBaseDir(systemId);
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 getBaseDir(systemId) {
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 = getBaseDir(systemId);
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 conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
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, conversationId);
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
- }, conversationId);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.65.0",
3
+ "version": "0.66.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -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
  }
@@ -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
  });
@@ -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 = getBaseDir(systemId);
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 = getBaseDir(systemId);
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 getBaseDir(systemId: string) {
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 = getBaseDir(systemId);
64
+ const baseDir = getSystemBaseDir(systemId);
54
65
 
55
66
  path = normalizePath(path);
56
67
 
@@ -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 { readPageFromDisk, readPageFromDiskAsString, SystemIdHeader, writePageToDisk } from './page-utils';
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 conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
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, conversationId);
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
- conversationId
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
  }
@@ -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),