@positronic/template-new-project 0.0.52 → 0.0.53

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/index.js CHANGED
@@ -53,9 +53,10 @@ module.exports = {
53
53
  ],
54
54
  setup: async ctx => {
55
55
  const devRootPath = process.env.POSITRONIC_LOCAL_PATH;
56
- let coreVersion = '^0.0.52';
57
- let cloudflareVersion = '^0.0.52';
58
- let clientVercelVersion = '^0.0.52';
56
+ let coreVersion = '^0.0.53';
57
+ let cloudflareVersion = '^0.0.53';
58
+ let clientVercelVersion = '^0.0.53';
59
+ let genUIComponentsVersion = '^0.0.53';
59
60
 
60
61
  // Map backend selection to package names
61
62
  const backendPackageMap = {
@@ -98,6 +99,12 @@ module.exports = {
98
99
  if (existsSync(clientVercelPath)) {
99
100
  clientVercelVersion = `file:${clientVercelPath}`;
100
101
  }
102
+
103
+ const genUIComponentsPath = path.resolve(devRootPath, 'packages', 'gen-ui-components');
104
+ if (existsSync(genUIComponentsPath)) {
105
+ genUIComponentsVersion = `file:${genUIComponentsPath}`;
106
+ console.log(` - Mapping @positronic/gen-ui-components to ${genUIComponentsVersion}`);
107
+ }
101
108
  }
102
109
 
103
110
  ctx.answers.positronicCoreVersion = coreVersion;
@@ -106,6 +113,7 @@ module.exports = {
106
113
  }
107
114
 
108
115
  ctx.answers.positronicClientVercelVersion = clientVercelVersion;
116
+ ctx.answers.positronicGenUIComponentsVersion = genUIComponentsVersion;
109
117
 
110
118
  if (ctx.answers.install) {
111
119
  const pm = ctx.answers.pm;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@positronic/template-new-project",
3
- "version": "0.0.52",
3
+ "version": "0.0.53",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/template/brain.ts CHANGED
@@ -1,72 +1,63 @@
1
- import { brain as coreBrain, type BrainFactory } from '@positronic/core';
1
+ import { createBrain } from '@positronic/core';
2
+ import { components } from './components/index.js';
2
3
 
3
4
  /**
4
- * Base brain factory for this project.
5
- *
6
- * This wrapper allows you to configure services once and have them available
7
- * in all brains throughout your project.
8
- *
9
- * To add services:
10
- * 1. Define your service interfaces
11
- * 2. Create service instances
12
- * 3. Call .withServices() on the brain before returning it
13
- *
14
- * Example with services:
5
+ * Project-level brain function with pre-configured components.
6
+ *
7
+ * All brains in your project should import from this file:
8
+ *
15
9
  * ```typescript
16
- * interface ProjectServices {
17
- * logger: {
18
- * info: (message: string) => void;
19
- * error: (message: string) => void;
20
- * };
21
- * api: {
22
- * fetch: (endpoint: string) => Promise<any>;
23
- * };
24
- * }
25
- *
26
- * export const brain: BrainFactory = (brainConfig) => {
27
- * return coreBrain(brainConfig)
28
- * .withServices({
29
- * logger: {
30
- * info: (msg) => console.log(`[INFO] <%= '${msg}' %>`),
31
- * error: (msg) => console.error(`[ERROR] <%= '${msg}' %>`)
32
- * },
33
- * api: {
34
- * fetch: async (endpoint) => {
35
- * const response = await fetch(`https://api.example.com<%= '${endpoint}' %>`);
36
- * return response.json();
37
- * }
38
- * }
39
- * });
40
- * }
10
+ * import { brain } from '../brain.js';
11
+ *
12
+ * export default brain('my-brain')
13
+ * .step('Do something', ({ state }) => ({ ...state, done: true }));
41
14
  * ```
42
- *
43
- * Then in your brain files (in the brains/ directory):
15
+ *
16
+ * To add services (e.g., Slack, Gmail, database clients):
17
+ *
44
18
  * ```typescript
45
- * import { brain } from '../brain.js';
46
- * import { z } from 'zod';
47
- *
48
- * const optionsSchema = z.object({
49
- * environment: z.string().default('prod'),
50
- * verbose: z.string().default('false')
19
+ * import { createBrain } from '@positronic/core';
20
+ * import { components } from './components/index.js';
21
+ * import slack from './services/slack.js';
22
+ * import gmail from './services/gmail.js';
23
+ *
24
+ * export const brain = createBrain({
25
+ * services: { slack, gmail },
26
+ * components,
51
27
  * });
52
- *
53
- * export default brain('My Brain')
54
- * .withOptionsSchema(optionsSchema)
55
- * .step('Use Services', async ({ state, options, logger, api }) => {
56
- * if (options.verbose === 'true') {
57
- * logger.info('Fetching data...');
58
- * }
59
- * const endpoint = options.environment === 'dev' ? '/users/test' : '/users';
60
- * const data = await api.fetch(endpoint);
61
- * return { users: data };
28
+ * ```
29
+ *
30
+ * Then services are available in all brain steps:
31
+ *
32
+ * ```typescript
33
+ * export default brain('notify')
34
+ * .step('Send alert', ({ slack }) => {
35
+ * slack.postMessage('#alerts', 'Something happened!');
36
+ * return { notified: true };
62
37
  * });
63
38
  * ```
64
- *
65
- * Run with custom options from CLI:
66
- * px brain run my-brain -o environment=dev -o verbose=true
39
+ *
40
+ * You can also create agents directly:
41
+ *
42
+ * ```typescript
43
+ * export default brain('my-agent', ({ slack, env }) => ({
44
+ * system: 'You are a helpful assistant',
45
+ * prompt: 'Help the user with their request',
46
+ * tools: {
47
+ * notify: {
48
+ * description: 'Send a Slack notification',
49
+ * inputSchema: z.object({ message: z.string() }),
50
+ * execute: ({ message }) => slack.postMessage('#general', message),
51
+ * },
52
+ * done: {
53
+ * description: 'Complete the task',
54
+ * inputSchema: z.object({ result: z.string() }),
55
+ * terminal: true,
56
+ * },
57
+ * },
58
+ * }));
59
+ * ```
67
60
  */
68
- export const brain: BrainFactory = (brainConfig) => {
69
- // For now, just return the core brain without any services.
70
- // Update this function to add your project-wide services.
71
- return coreBrain(brainConfig);
72
- };
61
+ export const brain = createBrain({
62
+ components,
63
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Bundle entry point for client-side rendering.
3
+ *
4
+ * This file is bundled by esbuild into dist/components.js which exposes
5
+ * React components to window.PositronicComponents for use by generated pages.
6
+ *
7
+ * When you add custom components to ./index.ts, they will automatically
8
+ * be included in the bundle.
9
+ */
10
+ import { components } from './index.js';
11
+
12
+ // Extract the React component from each UIComponent and expose to window
13
+ const PositronicComponents: Record<string, React.ComponentType<any>> = {};
14
+
15
+ for (const [name, uiComponent] of Object.entries(components)) {
16
+ PositronicComponents[name] = uiComponent.component;
17
+ }
18
+
19
+ // Expose to window for client-side rendering
20
+ (window as unknown as { PositronicComponents: typeof PositronicComponents }).PositronicComponents =
21
+ PositronicComponents;
22
+
23
+ export { PositronicComponents };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * UI Components for this project.
3
+ *
4
+ * This file re-exports the default Positronic components and is the place
5
+ * to add your own custom components.
6
+ *
7
+ * To add a custom component:
8
+ * 1. Create a component file (e.g., CustomButton.ts) with UIComponent structure
9
+ * 2. Import and add it to the components object below
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { CustomButton } from './CustomButton.js';
14
+ *
15
+ * export const components = {
16
+ * ...defaultComponents,
17
+ * CustomButton,
18
+ * };
19
+ * ```
20
+ */
21
+ import { components as defaultComponents } from '@positronic/gen-ui-components';
22
+
23
+ // Re-export default components - add your custom components here
24
+ export const components = {
25
+ ...defaultComponents,
26
+ };
@@ -678,6 +678,163 @@ Extract prompts to separate files when:
678
678
  - The prompt might be reused in other brains
679
679
  - You want to test the prompt logic separately
680
680
 
681
+ ## UI Steps
682
+
683
+ UI steps allow brains to generate dynamic user interfaces using AI. The `.ui()` step generates a page and provides a `page` object to the next step. You then notify users and use `waitFor` to pause until the form is submitted.
684
+
685
+ ### Basic UI Step
686
+
687
+ ```typescript
688
+ import { z } from 'zod';
689
+
690
+ brain('Feedback Collector')
691
+ .step('Initialize', ({ state }) => ({
692
+ ...state,
693
+ userName: 'John Doe',
694
+ }))
695
+ // Generate the form
696
+ .ui('Collect Feedback', {
697
+ template: (state) => `
698
+ Create a feedback form for <%= '${state.userName}' %>.
699
+ Include fields for rating (1-5) and comments.
700
+ `,
701
+ responseSchema: z.object({
702
+ rating: z.number().min(1).max(5),
703
+ comments: z.string(),
704
+ }),
705
+ })
706
+ // Notify user and wait for submission
707
+ .step('Notify and Wait', async ({ state, page, slack }) => {
708
+ await slack.post('#feedback', `Please fill out: <%= '${page.url}' %>`);
709
+ return {
710
+ state,
711
+ waitFor: [page.webhook],
712
+ };
713
+ })
714
+ // Process the form data (comes through response, not page)
715
+ .step('Process Feedback', ({ state, response }) => ({
716
+ ...state,
717
+ feedbackReceived: true,
718
+ rating: response.rating, // typed from responseSchema
719
+ comments: response.comments,
720
+ }));
721
+ ```
722
+
723
+ ### How UI Steps Work
724
+
725
+ 1. **Template**: The `template` function generates a prompt describing the desired UI
726
+ 2. **AI Generation**: The AI creates a component tree based on the prompt
727
+ 3. **Page Object**: Next step receives `page` with `url` and `webhook`
728
+ 4. **Notification**: You notify users however you want (Slack, email, etc.)
729
+ 5. **Wait**: Use `waitFor: [page.webhook]` to pause until form submission
730
+ 6. **Form Data**: Step after `waitFor` receives form data via `response`
731
+
732
+ ### The `page` Object
733
+
734
+ After a `.ui()` step, the next step receives:
735
+ - `page.url` - URL where users can access the form
736
+ - `page.webhook` - Pre-configured webhook for form submissions
737
+
738
+ ### Template Best Practices
739
+
740
+ Be specific about layout and content:
741
+
742
+ ```typescript
743
+ .ui('Contact Form', {
744
+ template: (state) => `
745
+ Create a contact form with:
746
+ - Header: "Get in Touch"
747
+ - Name field (required)
748
+ - Email field (required, pre-filled with "<%= '${state.email}' %>")
749
+ - Message textarea (required)
750
+ - Submit button labeled "Send Message"
751
+
752
+ Use a clean, centered single-column layout.
753
+ `,
754
+ responseSchema: z.object({
755
+ name: z.string(),
756
+ email: z.string().email(),
757
+ message: z.string(),
758
+ }),
759
+ })
760
+ ```
761
+
762
+ ### Data Bindings
763
+
764
+ Use `{{path}}` syntax to bind props to runtime data:
765
+
766
+ ```typescript
767
+ .ui('Order Summary', {
768
+ template: (state) => `
769
+ Create an order summary showing:
770
+ - List of items from {{cart.items}}
771
+ - Total: {{cart.total}}
772
+ - Shipping address input
773
+ - Confirm button
774
+ `,
775
+ responseSchema: z.object({
776
+ shippingAddress: z.string(),
777
+ }),
778
+ })
779
+ ```
780
+
781
+ ### Multi-Step Forms
782
+
783
+ Chain UI steps for multi-page workflows:
784
+
785
+ ```typescript
786
+ brain('User Onboarding')
787
+ .step('Start', () => ({ userData: {} }))
788
+
789
+ // Step 1: Personal info
790
+ .ui('Personal Info', {
791
+ template: () => `
792
+ Create a form for personal information:
793
+ - First name, Last name
794
+ - Date of birth
795
+ - Next button
796
+ `,
797
+ responseSchema: z.object({
798
+ firstName: z.string(),
799
+ lastName: z.string(),
800
+ dob: z.string(),
801
+ }),
802
+ })
803
+ .step('Wait for Personal', async ({ state, page, notify }) => {
804
+ await notify(`Step 1: <%= '${page.url}' %>`);
805
+ return { state, waitFor: [page.webhook] };
806
+ })
807
+ .step('Save Personal', ({ state, response }) => ({
808
+ ...state,
809
+ userData: { ...state.userData, ...response },
810
+ }))
811
+
812
+ // Step 2: Preferences
813
+ .ui('Preferences', {
814
+ template: (state) => `
815
+ Create preferences form for <%= '${state.userData.firstName}' %>:
816
+ - Newsletter subscription checkbox
817
+ - Contact preference (email/phone/sms)
818
+ - Complete button
819
+ `,
820
+ responseSchema: z.object({
821
+ newsletter: z.boolean(),
822
+ contactMethod: z.enum(['email', 'phone', 'sms']),
823
+ }),
824
+ })
825
+ .step('Wait for Preferences', async ({ state, page, notify }) => {
826
+ await notify(`Step 2: <%= '${page.url}' %>`);
827
+ return { state, waitFor: [page.webhook] };
828
+ })
829
+ .step('Complete', ({ state, response }) => ({
830
+ ...state,
831
+ userData: { ...state.userData, preferences: response },
832
+ onboardingComplete: true,
833
+ }));
834
+ ```
835
+
836
+ For more details on UI steps, see the full UI Step Guide in the main Positronic documentation.
837
+
681
838
  ## Complete Example
682
839
 
683
840
  ```typescript
@@ -722,14 +879,9 @@ const completeBrain = brain({
722
879
  }),
723
880
  name: 'plan' as const,
724
881
  },
725
- },
726
- // Services available in reduce function too
727
- ({ state, response, logger }) => {
728
- logger.log(`Plan generated with <%= '${response.tasks.length}' %> tasks`);
729
- return { ...state, plan: response };
730
882
  })
731
883
  .step('Process Plan', ({ state, logger, analytics }) => {
732
- logger.log(`Processing <%= '${state.plan.tasks.length}' %> tasks`);
884
+ logger.log(`Plan generated with <%= '${state.plan.tasks.length}' %> tasks`);
733
885
  analytics.track('plan_processed', {
734
886
  task_count: state.plan.tasks.length,
735
887
  duration: state.plan.duration
@@ -239,5 +239,5 @@ const api = {
239
239
 
240
240
  - **Documentation**: https://positronic.dev
241
241
  - **CLI Help**: `px --help`
242
- - **Brain DSL Guide**: `/docs/brain-dsl-guide.md`
242
+ - **Brain DSL Guide**: `/docs/brain-dsl-guide.md` (includes UI steps for generating forms)
243
243
  - **Testing Guide**: `/docs/brain-testing-guide.md`
@@ -182,6 +182,56 @@ brain('validation-example')
182
182
 
183
183
  Most generated brains should not have try-catch blocks. Only use them when the error state is meaningful to subsequent steps in the workflow.
184
184
 
185
+ ## UI Steps for Form Generation
186
+
187
+ When you need to collect user input, use the `.ui()` method. The pattern is:
188
+ 1. `.ui()` generates the page
189
+ 2. Next step gets `page.url` and `page.webhook`
190
+ 3. Notify users and use `waitFor: [page.webhook]`
191
+ 4. Step after `waitFor` gets form data in `response`
192
+
193
+ ```typescript
194
+ import { z } from 'zod';
195
+
196
+ brain('feedback-collector')
197
+ .step('Initialize', ({ state }) => ({
198
+ ...state,
199
+ userName: 'John',
200
+ }))
201
+ // Generate the form
202
+ .ui('Collect Feedback', {
203
+ template: (state) => <%= '\`' %>
204
+ Create a feedback form for <%= '${state.userName}' %>:
205
+ - Rating (1-5)
206
+ - Comments textarea
207
+ - Submit button
208
+ <%= '\`' %>,
209
+ responseSchema: z.object({
210
+ rating: z.number().min(1).max(5),
211
+ comments: z.string(),
212
+ }),
213
+ })
214
+ // Notify and wait for submission
215
+ .step('Notify', async ({ state, page, slack }) => {
216
+ await slack.post('#feedback', `Fill out: <%= '${page.url}' %>`);
217
+ return { state, waitFor: [page.webhook] };
218
+ })
219
+ // Form data comes through response (not page)
220
+ .step('Process', ({ state, response }) => ({
221
+ ...state,
222
+ rating: response.rating, // Typed from responseSchema
223
+ comments: response.comments,
224
+ }));
225
+ ```
226
+
227
+ Key points:
228
+ - `page.url` - where to send users
229
+ - `page.webhook` - use with `waitFor` to pause for submission
230
+ - `response` - form data arrives here (in step after `waitFor`)
231
+ - You control how users are notified (Slack, email, etc.)
232
+
233
+ See `/docs/brain-dsl-guide.md` for more UI step examples.
234
+
185
235
  ## Service Organization
186
236
 
187
237
  When implementing services for the project brain, consider creating a `services/` directory at the root of your project to keep service implementations organized and reusable:
@@ -0,0 +1,26 @@
1
+ /**
2
+ * esbuild configuration for bundling UI components.
3
+ *
4
+ * This bundles the components from ./components/bundle.ts into a single
5
+ * JavaScript file that can be served to the browser.
6
+ *
7
+ * Run: npm run build:components
8
+ * Or let the dev server build it automatically.
9
+ */
10
+ import * as esbuild from 'esbuild';
11
+
12
+ await esbuild.build({
13
+ entryPoints: ['components/bundle.ts'],
14
+ bundle: true,
15
+ external: ['react', 'react-dom'],
16
+ format: 'iife',
17
+ outfile: 'dist/components.js',
18
+ jsx: 'transform',
19
+ jsxFactory: 'React.createElement',
20
+ jsxFragment: 'React.Fragment',
21
+ tsconfigRaw: {
22
+ compilerOptions: {
23
+ jsx: 'react',
24
+ },
25
+ },
26
+ });
@@ -6,7 +6,8 @@
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "test": "NODE_OPTIONS='--experimental-vm-modules' jest",
9
- "typecheck": "tsc --noEmit"
9
+ "typecheck": "tsc --noEmit",
10
+ "build:components": "node esbuild.config.mjs"
10
11
  },
11
12
  "keywords": [],
12
13
  "author": "",
@@ -15,15 +16,18 @@
15
16
  "zod": "^3.24.1",
16
17
  "@positronic/client-vercel": "<%= positronicClientVercelVersion %>",
17
18
  "@ai-sdk/openai": "^1.3.22",
18
- "@positronic/core": "<%= positronicCoreVersion %>"<% if (backend === 'cloudflare') { %>,
19
+ "@positronic/core": "<%= positronicCoreVersion %>",
20
+ "@positronic/gen-ui-components": "<%= positronicGenUIComponentsVersion %>"<% if (backend === 'cloudflare') { %>,
19
21
  "@positronic/cloudflare": "<%= positronicCloudflareVersion %>"<% } %>
20
22
  },
21
23
  "devDependencies": {<% if (backend === 'cloudflare') { %>
22
- "wrangler": "^4.0.0",<% } %>
24
+ "wrangler": "^4.37.0",<% } %>
23
25
  "typescript": "^5.0.0",
24
26
  "jest": "^30.0.4",
25
27
  "@jest/globals": "^30.0.4",
26
28
  "ts-jest": "^29.2.6",
27
- "@types/jest": "^30.0.0"
29
+ "@types/jest": "^30.0.0",
30
+ "esbuild": "^0.24.0",
31
+ "@types/react": "^18.2.0"
28
32
  }
29
33
  }
@@ -19,8 +19,13 @@ export function createMockClient(): MockClient {
19
19
  return responses[responseIndex++];
20
20
  });
21
21
 
22
+ const streamText = jest.fn(async () => {
23
+ throw new Error('streamText not implemented in mock');
24
+ });
25
+
22
26
  return {
23
27
  generateObject,
28
+ streamText,
24
29
  mockResponses: (...newResponses: any[]) => {
25
30
  responses.push(...newResponses);
26
31
  },
@@ -28,6 +33,7 @@ export function createMockClient(): MockClient {
28
33
  responses.length = 0;
29
34
  responseIndex = 0;
30
35
  generateObject.mockClear();
36
+ streamText.mockClear();
31
37
  },
32
38
  };
33
39
  }