@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 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 = 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();
@@ -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 = 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();
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.64.3",
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
 
@@ -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 { readPageFromDisk, readPageFromDiskAsString, SystemIdHeader, writePageToDisk } from './page-utils';
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 conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
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, conversationId);
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
- conversationId
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
 
@@ -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,