diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 00000000..e63dce0a --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "supabase": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-postgres", + "postgresql://postgres:postgres@127.0.0.1:54322/postgres" + ] + } + } +} diff --git a/.cursor/rules/psql.mdc b/.cursor/rules/psql.mdc new file mode 100644 index 00000000..97d4195f --- /dev/null +++ b/.cursor/rules/psql.mdc @@ -0,0 +1,8 @@ +--- +description: +globs: *.sql, *.psql +alwaysApply: false +--- +1. All psql should be written in lowercase syntax +2. When creating psql indices we should be as concise as we can, omitting defaults like the index name and the type. So we rather do `create index on table(column)` instead of `create idx_table_column on table using btree (column)` +3. We should add indices to columns that reference other tables and columns that are queried frequently. e.g. if the column is `slug` it will most likely be queried on that instead of `id` so we should create an index on it. \ No newline at end of file diff --git a/.cursor/rules/typescript.mdc b/.cursor/rules/typescript.mdc new file mode 100644 index 00000000..0345a313 --- /dev/null +++ b/.cursor/rules/typescript.mdc @@ -0,0 +1,25 @@ +--- +description: These rules are helpful for contrubuting to the front-end Nextjs code and the typescript code. +globs: *.tsx, *.ts +alwaysApply: false +--- + +# Agentsmith tsx contributing rules for cursor AI + +1. Only use named exports (`export const thing`, not `export default`)¹ +2. All pages should have a single top level page component in page-components +3. All components should be in components folder either as a single file named after the component or in a subfolder named after the component¹ +4. All functions should be declared in const rocket format: `const rocket = async (payload: any) => { ... }`¹ +5. All props should be explicitly typed as `type ComponentProps = { ... }` and not typed inline ex: `props: { foo: any }` is bad `props: ComponentProps` is good. DO NOT deconstruct props, if a component has props, it should be named as `props` and deconstructed in the body of the component. Write `const foo = (props: FooProps) => { const { arg, arg2 } = props; }` rather than `const foo = ({ arg, arg2 }: FooProps) => {}` +6. Only use icons from `lucide-react` +7. Only use tailwind css +8. Each component file should have at most 2 react components, any more than that and it should be split up into multiple component files/folders. +9. Any links to pages that exist under the web-studio should be prefixed with /app +10. All functions that take more than 1 argument, should use an `options` syntax instead of adding many arguments. i.e. `const foo = (options: FooOptions) => {}` rather than `const foo = (bar: string, baz: string, quux: string) => {}` +11. If the response for a function is an object, then it should be typed separately rather than inline. i.e. `const foo = (): FooResult => {}` instead of `const foo = (): { foo: string; bar: string; baz: string } => {}` +12. DO NOT edit any generated files within the `__generated__` folder. +13. DO NOT create new types if they can be inferred from the supabase generated types OR can be inferred from the return of a supabase.from call using `Awaited<>` + + +¹ Exception for the Nextjs page.tsx files. They should only have one default function export. + diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..3d43a38d --- /dev/null +++ b/.env.example @@ -0,0 +1,36 @@ +# FREE_MODELS_ONLY=true + +NEXT_PUBLIC_SITE_URL="http://localhost:3000" + +NEXT_PUBLIC_SUPABASE_URL= +NEXT_PUBLIC_SUPABASE_ANON_KEY= + +NEXT_PUBLIC_POSTHOG_KEY= +NEXT_PUBLIC_POSTHOG_HOST= + +SUPABASE_AUTH_GITHUB_CLIENT_ID= +SUPABASE_AUTH_GITHUB_SECRET= + +SUPABASE_JWT_SECRET= + +# used for testing local dev +SMEE_WEBHOOK_PROXY_URL= + +GITHUB_APP_NAME= +GITHUB_APP_ID= +GITHUB_APP_PRIVATE_KEY= +GITHUB_APP_WEBHOOK_SECRET= +GITHUB_WEBHOOK_SERVICE_USER_ID= + + +# TESTING TURNSTILE LOCALLY +# NEXT_PUBLIC_CF_TURNSTILE_SITE_KEY=1x00000000000000000000AA # Always passes visible +# NEXT_PUBLIC_CF_TURNSTILE_SITE_KEY=2x00000000000000000000AB # Always blocks visible +NEXT_PUBLIC_CF_TURNSTILE_SITE_KEY=1x00000000000000000000BB # Always passes invisible +# NEXT_PUBLIC_CF_TURNSTILE_SITE_KEY=2x00000000000000000000BB # Always blocks invisible +# NEXT_PUBLIC_CF_TURNSTILE_SITE_KEY=3x00000000000000000000FF # Forces an interactive challenge visible + +# unused right now, but good to have in case we validate on our end +CF_TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA # Always passes +# CF_TURNSTILE_SECRET_KEY=2x0000000000000000000000000000000AA # Always fails +# CF_TURNSTILE_SECRET_KEY=3x0000000000000000000000000000000AA # Yields a "token already spent" error \ No newline at end of file diff --git a/.github/workflows/supabase_migrations_ci.yml b/.github/workflows/supabase_migrations_ci.yml new file mode 100644 index 00000000..1bbcbcd0 --- /dev/null +++ b/.github/workflows/supabase_migrations_ci.yml @@ -0,0 +1,36 @@ +name: Supabase Tests + +on: + pull_request: + workflow_dispatch: + +jobs: + supabase_tests: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: supabase/setup-cli@v1 + with: + version: latest + + - name: Overwrite Supabase db version + run: | + mkdir -p supabase/.temp + echo "15.8.1.038" > supabase/.temp/postgres-version + + - name: Start Supabase local development setup + run: supabase db start + + - name: Run Tests + run: supabase test db + + - name: Verify generated types are checked in + run: | + supabase gen types typescript --local > types.gen.ts + if ! git diff --ignore-space-at-eol --exit-code --quiet types.gen.ts; then + echo "Detected uncommitted changes after build. See status below:" + git diff + exit 1 + fi diff --git a/.github/workflows/supabase_migrations_prod.yml b/.github/workflows/supabase_migrations_prod.yml new file mode 100644 index 00000000..4d2e25e2 --- /dev/null +++ b/.github/workflows/supabase_migrations_prod.yml @@ -0,0 +1,27 @@ +name: Push Supabase Migrations to Production + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + push_supabase_migrations_production: + environment: Production + runs-on: ubuntu-latest + + env: + SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} + SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }} + SUPABASE_PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_ID }} + + steps: + - uses: actions/checkout@v3 + + - uses: supabase/setup-cli@v1 + with: + version: latest + + - run: supabase link --project-ref $SUPABASE_PROJECT_ID + - run: supabase db push diff --git a/.github/workflows/supabase_migrations_staging.yml b/.github/workflows/supabase_migrations_staging.yml new file mode 100644 index 00000000..e407647f --- /dev/null +++ b/.github/workflows/supabase_migrations_staging.yml @@ -0,0 +1,27 @@ +name: Push Supabase Migrations to Staging + +on: + push: + branches: + - staging + workflow_dispatch: + +jobs: + push_supabase_migrations_staging: + environment: Staging + runs-on: ubuntu-latest + + env: + SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} + SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }} + SUPABASE_PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_ID }} + + steps: + - uses: actions/checkout@v3 + + - uses: supabase/setup-cli@v1 + with: + version: latest + + - run: supabase link --project-ref $SUPABASE_PROJECT_ID + - run: supabase db push diff --git a/.nvmrc b/.nvmrc index 9446372c..adb55585 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.13.1 \ No newline at end of file +22.14.0 \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..46f36250 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "semi": true +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..d9a398dc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,28 @@ +# CLAUDE.md - AgentSmith Development Guidelines + +## Build and Test Commands + +- **Development**: `npm run dev` - Start Next.js dev server +- **Build**: `npm run build` - Build the Next.js application +- **Start**: `npm run start` - Start production server +- **Test**: `npm run test` - Run all tests with Jest +- **Test (Watch)**: `npm run test:watch` - Run tests in watch mode +- **Test Single File**: `npm run test -- -t "test name"` or `npm run test -- path/to/file.test.ts` +- **Type Generation**: `npm run typegen` - Generate Supabase TypeScript types + +## Code Style Guidelines + +- **Components**: Use named exports only; default exports only for Next.js page files +- **Functions**: Declare with const arrow format: `const myFunc = (params: Type) => {}` +- **Types**: Always explicitly type props as separate types, not inline +- **File Structure**: + - Components in `/components` folder with max 2 components per file + - Page components in `/page-components` +- **Imports**: Use path aliases: `@/` for src, `&/` for lib, `~/` for root +- **Naming**: PascalCase for components, camelCase for functions, kebab-case for files +- **Icons**: Only use icons from `lucide-react` +- **Styling**: Only use Tailwind CSS, no custom CSS +- **SQL**: Lowercase syntax for queries, concise index declarations with `create index concurrently` +- **Error Handling**: Use try/catch with appropriate error logging + +Always follow existing patterns in the codebase. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..841ef247 --- /dev/null +++ b/LICENSE @@ -0,0 +1,192 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +Copyright 2025 Chad Syntax LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/__tests__/lib/template-utils.test.ts b/__tests__/lib/template-utils.test.ts new file mode 100644 index 00000000..8731d735 --- /dev/null +++ b/__tests__/lib/template-utils.test.ts @@ -0,0 +1,827 @@ +import { extractTemplateVariables, validateVariables } from '../../src/utils/template-utils'; +import { Database } from '@/app/__generated__/supabase.types'; + +type PromptVariable = Database['public']['Tables']['prompt_variables']['Row']; + +describe('extractTemplateVariables', () => { + it('should extract simple variables', () => { + const template = ` + Hello {{ name }}! + The weather is {{ weather }}. + `; + + const { variables, error } = extractTemplateVariables(template); + expect(error).toBeUndefined(); + expect(variables).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'name', + type: 'STRING', + required: true, + default_value: null, + }), + expect.objectContaining({ + name: 'weather', + type: 'STRING', + required: true, + default_value: null, + }), + ]), + ); + }); + + it('should extract variables from conditional statements', () => { + const template = ` + {% if hungry %} + I am hungry + {% elif tired %} + I am tired + {% else %} + I am good! + {% endif %} + `; + + const { variables, error } = extractTemplateVariables(template); + expect(error).toBeUndefined(); + expect(variables).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'hungry', + type: 'STRING', + required: true, + default_value: null, + }), + expect.objectContaining({ + name: 'tired', + type: 'STRING', + required: true, + default_value: null, + }), + ]), + ); + }); + + it('should mark variables with dot notation as JSON type and extract children', () => { + const template = ` + {{ user.name }} + {{ settings.theme.color }} + `; + + const { variables, error } = extractTemplateVariables(template); + expect(error).toBeUndefined(); + expect(variables).toHaveLength(2); + expect(variables).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'user', + type: 'JSON', + required: true, + default_value: null, + children: [expect.objectContaining({ name: 'name', type: 'STRING' })], + }), + expect.objectContaining({ + name: 'settings', + type: 'JSON', + required: true, + default_value: null, + children: [ + expect.objectContaining({ + name: 'theme', + type: 'JSON', + children: [expect.objectContaining({ name: 'color', type: 'STRING' })], + }), + ], + }), + ]), + ); + }); + + it('should mark collection variables as JSON type and not extract loop var properties unless explicitly used', () => { + const template = ` + {% for item in items %} +
  • {{ item.title }}
  • + {% else %} +
  • This would display if the 'items' collection were empty
  • + {% endfor %} + + {% for user_loop_var in users_collection %} + {{ user_loop_var.name }} + {% endfor %} + {{ other_var }} + `; + + const { variables, error } = extractTemplateVariables(template); + expect(error).toBeUndefined(); + + const itemsVar = variables.find((v) => v.name === 'items'); + expect(itemsVar).toEqual( + expect.objectContaining({ + name: 'items', + type: 'JSON', + required: true, + default_value: null, + children: expect.arrayContaining([ + expect.objectContaining({ + name: 'title', + type: 'STRING', + required: true, + default_value: null, + }), + ]), + }), + ); + + const usersCollectionVar = variables.find((v) => v.name === 'users_collection'); + expect(usersCollectionVar).toEqual( + expect.objectContaining({ + name: 'users_collection', + type: 'JSON', + required: true, + default_value: null, + children: expect.arrayContaining([ + expect.objectContaining({ + name: 'name', + type: 'STRING', + required: true, + default_value: null, + }), + ]), + }), + ); + + const otherVar = variables.find((v) => v.name === 'other_var'); + expect(otherVar).toEqual( + expect.objectContaining({ + name: 'other_var', + type: 'STRING', + required: true, + default_value: null, + }), + ); + + expect(variables.find((v) => v.name === 'item')).toBeUndefined(); + expect(variables.find((v) => v.name === 'user_loop_var')).toBeUndefined(); + expect(variables.filter((v) => v.name === 'title' && v.type === 'JSON').length).toBe(0); // title should be a child, not a top-level JSON var + expect(variables.filter((v) => v.name === 'name' && v.type === 'JSON').length).toBe(0); // name should be a child, not a top-level JSON var + }); + + it('should handle complex nested structures with children', () => { + const template = ` + {% if user.isAdmin %} + {% for role in user.roles %} + {{ role.name }} + {% endfor %} + + Settings: {{ settings.theme }} + {% endif %} + `; + + const { variables, error } = extractTemplateVariables(template); + expect(error).toBeUndefined(); + expect(variables).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'user', + type: 'JSON', + children: expect.arrayContaining([ + expect.objectContaining({ + name: 'isAdmin', + type: 'STRING', + required: true, + default_value: null, + }), + expect.objectContaining({ + name: 'roles', + type: 'JSON', + required: true, + default_value: null, + children: expect.arrayContaining([ + expect.objectContaining({ + name: 'name', + type: 'STRING', + required: true, + default_value: null, + }), + ]), + }), + ]), + }), + expect.objectContaining({ + name: 'settings', + type: 'JSON', + children: [ + expect.objectContaining({ + name: 'theme', + type: 'STRING', + required: true, + default_value: null, + }), + ], + }), + ]), + ); + // Ensure 'name' (from role.name) is not a top-level variable + const topLevelNameVar = variables.find( + (v) => v.name === 'name' && v.type === 'STRING' && !v.children, + ); + expect(topLevelNameVar).toBeUndefined(); + }); + + it('should handle empty templates', () => { + const { variables, error } = extractTemplateVariables(''); + expect(error).toBeUndefined(); + expect(variables).toEqual([]); + }); + + it('should handle invalid templates', () => { + const template = ` + {% if unclosed + {{ invalid }} + `; + + const { variables, error } = extractTemplateVariables(template); + expect(error).toBeDefined(); + expect(variables).toEqual([]); + }); + + it('should handle variables used in both simple and complex contexts with children', () => { + const template = ` + {{ user }} + {{ user.name }} + {% for item_loop in user.items %} + {{ item_loop.title }} + {% endfor %} + `; + + const { variables, error } = extractTemplateVariables(template); + expect(error).toBeUndefined(); + expect(variables).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'user', + type: 'JSON', + children: expect.arrayContaining([ + expect.objectContaining({ + name: 'name', + type: 'STRING', + required: true, + default_value: null, + }), + expect.objectContaining({ + name: 'items', + type: 'JSON', + required: true, + default_value: null, + children: expect.arrayContaining([ + expect.objectContaining({ + name: 'title', + type: 'STRING', + required: true, + default_value: null, + }), + ]), + }), + ]), + }), + ]), + ); + const topLevelTitleVar = variables.find( + (v) => v.name === 'title' && v.type === 'STRING' && !v.children, + ); + expect(topLevelTitleVar).toBeUndefined(); + }); + + it('should handle whitespace in variable names and their properties', () => { + const template = ` + {{ spacedName }} + {{ spaced. name }} + `; + // Nunjucks appears to trim whitespace from symbol names during parsing, + // so `spaced. name ` becomes `spaced.name`. + const { variables, error } = extractTemplateVariables(template); + expect(error).toBeUndefined(); + expect(variables).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'spacedName', type: 'STRING' }), + expect.objectContaining({ + name: 'spaced', + type: 'JSON', + children: [expect.objectContaining({ name: 'name', type: 'STRING' })], + }), + ]), + ); + }); + + it('should handle variables in complex expressions', () => { + const template = ` + {{ "true" if foo else "false" }} + {{ bar + 1 }} + {{ baz | upper }} + `; + + const { variables, error } = extractTemplateVariables(template); + expect(error).toBeUndefined(); + expect(variables).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'foo', type: 'STRING' }), + expect.objectContaining({ name: 'bar', type: 'STRING' }), + expect.objectContaining({ name: 'baz', type: 'STRING' }), + ]), + ); + }); + + // New tests moved from PromptService.test.ts and updated + it('should correctly extract simple variables (new)', () => { + const content = 'Hello {{ name }} and {{ age }}'; + const { variables, error } = extractTemplateVariables(content); + expect(error).toBeUndefined(); + expect(variables).toHaveLength(2); + expect(variables).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'name', + type: 'STRING', + required: true, + default_value: null, + }), + expect.objectContaining({ + name: 'age', + type: 'STRING', + required: true, + default_value: null, + }), + ]), + ); + }); + + it('should handle nested variables like user.first_name (new)', () => { + const content = 'Hello {{ user.first_name }}!'; + const { variables, error } = extractTemplateVariables(content); + expect(error).toBeUndefined(); + expect(variables).toHaveLength(1); + const userVar = variables.find((v) => v.name === 'user'); + expect(userVar).toEqual({ + name: 'user', + type: 'JSON', + required: true, + default_value: null, + children: [ + { + name: 'first_name', + type: 'STRING', + required: true, + default_value: null, + }, + ], + }); + }); + + it('should handle deeper nested variables like account.settings.theme (new)', () => { + const content = 'Theme: {{ account.settings.theme }}'; + const { variables, error } = extractTemplateVariables(content); + expect(error).toBeUndefined(); + expect(variables).toHaveLength(1); + const accountVar = variables.find((v) => v.name === 'account'); + expect(accountVar).toEqual({ + name: 'account', + type: 'JSON', + required: true, + default_value: null, + children: [ + { + name: 'settings', + type: 'JSON', + required: true, + default_value: null, + children: [ + { + name: 'theme', + type: 'STRING', + required: true, + default_value: null, + }, + ], + }, + ], + }); + }); + + it('should handle mixed simple and nested variables (new)', () => { + const content = '{{ greeting }} {{ user.name }} from {{ user.location.city }}'; + const { variables, error } = extractTemplateVariables(content); + expect(error).toBeUndefined(); + expect(variables).toHaveLength(2); + + const greetingVar = variables.find((v) => v.name === 'greeting'); + expect(greetingVar).toEqual({ + name: 'greeting', + type: 'STRING', + required: true, + default_value: null, + }); + + const userVar = variables.find((v) => v.name === 'user'); + expect(userVar).toEqual({ + name: 'user', + type: 'JSON', + required: true, + default_value: null, + children: [ + { name: 'name', type: 'STRING', required: true, default_value: null }, + { + name: 'location', + type: 'JSON', + required: true, + default_value: null, + children: [{ name: 'city', type: 'STRING', required: true, default_value: null }], + }, + ], + }); + }); + + it('should correctly identify array variable in for loop as JSON (new)', () => { + const content = + '{% for item_loop_var in items_collection_new %}{{ item_loop_var.name }}{% endfor %}'; + const { variables, error } = extractTemplateVariables(content); + expect(error).toBeUndefined(); + + const itemsVar = variables.find((v) => v.name === 'items_collection_new'); + expect(itemsVar).toBeDefined(); + expect(itemsVar).toEqual( + expect.objectContaining({ + name: 'items_collection_new', + type: 'JSON', + required: true, + default_value: null, + children: expect.arrayContaining([ + expect.objectContaining({ + name: 'name', + type: 'STRING', + required: true, + default_value: null, + }), + ]), + }), + ); + expect(variables.find((v) => v.name === 'item_loop_var')).toBeUndefined(); + // Ensure 'name' (from item_loop_var.name) is not a top-level variable + const topLevelNameVar = variables.find( + (v) => + v.name === 'name' && v.type === 'STRING' && !v.children && v !== itemsVar?.children?.[0], + ); + expect(topLevelNameVar).toBeUndefined(); + }); + + it('should parse user.first_name and user.last_name into a single user object with two children (new)', () => { + const content = 'hello {{ user.first_name }} {{ user.last_name }}'; + const { variables, error } = extractTemplateVariables(content); + + expect(error).toBeUndefined(); + expect(variables).toHaveLength(1); + const userVar = variables[0]; + expect(userVar.name).toBe('user'); + expect(userVar.type).toBe('JSON'); + expect(userVar.children).toBeDefined(); + expect(userVar.children).toHaveLength(2); + expect(userVar.children).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'first_name', type: 'STRING' }), + expect.objectContaining({ name: 'last_name', type: 'STRING' }), + ]), + ); + }); + + it('should handle a variable used as both an object and a simple variable (should be JSON) (new)', () => { + const content = '{{ user.name }} and {{ user }}'; // user is used as object and then directly + const { variables, error } = extractTemplateVariables(content); + expect(error).toBeUndefined(); + expect(variables).toHaveLength(1); + const userVar = variables.find((v) => v.name === 'user'); + expect(userVar).toBeDefined(); + expect(userVar?.type).toBe('JSON'); // Should be JSON because of user.name + expect(userVar?.children).toEqual( + expect.arrayContaining([expect.objectContaining({ name: 'name', type: 'STRING' })]), + ); + }); +}); + +describe('validateVariables', () => { + it('should return empty missing and include all provided variables when all required are given', () => { + const variables: PromptVariable[] = [ + { + id: 1, + name: 'var1', + type: 'STRING', + required: true, + default_value: null, + created_at: '', + prompt_version_id: 1, + uuid: '', + updated_at: '', + }, + { + id: 2, + name: 'var2', + type: 'NUMBER', + required: true, + default_value: null, + created_at: '', + prompt_version_id: 1, + uuid: '', + updated_at: '', + }, + ]; + const variablesToCheck = { var1: 'hello', var2: 123 }; + const { missingRequiredVariables, variablesWithDefaults } = validateVariables( + variables, + variablesToCheck, + ); + + expect(missingRequiredVariables).toEqual([]); + expect(variablesWithDefaults).toEqual({ var1: 'hello', var2: 123 }); + }); + + it('should identify missing required variables', () => { + const variables: PromptVariable[] = [ + { + id: 1, + name: 'var1', + type: 'STRING', + required: true, + default_value: null, + created_at: '', + prompt_version_id: 1, + uuid: '', + updated_at: '', + }, + { + id: 2, + name: 'var2', + type: 'NUMBER', + required: true, + default_value: null, + created_at: '', + prompt_version_id: 1, + uuid: '', + updated_at: '', + }, + { + id: 3, + name: 'var3', + type: 'BOOLEAN', + required: false, + default_value: null, + created_at: '', + prompt_version_id: 1, + uuid: '', + updated_at: '', + }, + ]; + const variablesToCheck = { var1: 'hello' }; // Missing var2 + const { missingRequiredVariables, variablesWithDefaults } = validateVariables( + variables, + variablesToCheck, + ); + + expect(missingRequiredVariables).toHaveLength(1); + expect(missingRequiredVariables[0].name).toBe('var2'); + expect(variablesWithDefaults).toEqual({ var1: 'hello' }); + }); + + it('should apply default values for missing optional variables', () => { + const variables: PromptVariable[] = [ + { + id: 1, + name: 'var1', + type: 'STRING', + required: true, + default_value: null, + created_at: '', + prompt_version_id: 1, + uuid: '', + updated_at: '', + }, + { + id: 2, + name: 'var2', + type: 'NUMBER', + required: false, + default_value: '42', + created_at: '', + prompt_version_id: 1, + uuid: '', + updated_at: '', + }, + { + id: 3, + name: 'var3', + type: 'BOOLEAN', + required: false, + default_value: 'true', + created_at: '', + prompt_version_id: 1, + uuid: '', + updated_at: '', + }, + ]; + const variablesToCheck = { var1: 'test' }; // Missing var2 and var3 + const { missingRequiredVariables, variablesWithDefaults } = validateVariables( + variables, + variablesToCheck, + ); + + expect(missingRequiredVariables).toEqual([]); + expect(variablesWithDefaults).toEqual({ var1: 'test', var2: '42', var3: 'true' }); + }); + + it('should override default values if variables are provided', () => { + const variables: PromptVariable[] = [ + { + id: 1, + name: 'var1', + type: 'STRING', + required: false, + default_value: 'default1', + created_at: '', + prompt_version_id: 1, + uuid: '', + updated_at: '', + }, + { + id: 2, + name: 'var2', + type: 'NUMBER', + required: false, + default_value: '100', + created_at: '', + prompt_version_id: 1, + uuid: '', + updated_at: '', + }, + ]; + const variablesToCheck = { var1: 'override1', var2: 200 }; + const { missingRequiredVariables, variablesWithDefaults } = validateVariables( + variables, + variablesToCheck, + ); + + expect(missingRequiredVariables).toEqual([]); + expect(variablesWithDefaults).toEqual({ var1: 'override1', var2: 200 }); + }); + + it('should handle a mix of missing required, provided, and default values', () => { + const variables: PromptVariable[] = [ + { + id: 1, + name: 'req1', + type: 'STRING', + required: true, + default_value: null, + created_at: '', + prompt_version_id: 1, + uuid: '', + updated_at: '', + }, // Missing + { + id: 2, + name: 'req2', + type: 'NUMBER', + required: true, + default_value: null, + created_at: '', + prompt_version_id: 1, + uuid: '', + updated_at: '', + }, // Provided + { + id: 3, + name: 'opt1', + type: 'BOOLEAN', + required: false, + default_value: 'false', + created_at: '', + prompt_version_id: 1, + uuid: '', + updated_at: '', + }, // Default used + { + id: 4, + name: 'opt2', + type: 'STRING', + required: false, + default_value: 'defaultOpt2', + created_at: '', + prompt_version_id: 1, + uuid: '', + updated_at: '', + }, // Overridden + { + id: 5, + name: 'opt3', + type: 'NUMBER', + required: false, + default_value: null, + created_at: '', + prompt_version_id: 1, + uuid: '', + updated_at: '', + }, // No default, not provided + ]; + const variablesToCheck = { req2: 999, opt2: 'overriddenOpt2' }; + const { missingRequiredVariables, variablesWithDefaults } = validateVariables( + variables, + variablesToCheck, + ); + + expect(missingRequiredVariables).toHaveLength(1); + expect(missingRequiredVariables[0].name).toBe('req1'); + expect(variablesWithDefaults).toEqual({ + req2: 999, + opt1: 'false', // Default applied + opt2: 'overriddenOpt2', // Override applied + // opt3 is missing as it wasn\'t provided and had no default + }); + }); + + it('should return empty results for empty inputs', () => { + const variables: PromptVariable[] = []; + const variablesToCheck = {}; + const { missingRequiredVariables, variablesWithDefaults } = validateVariables( + variables, + variablesToCheck, + ); + + expect(missingRequiredVariables).toEqual([]); + expect(variablesWithDefaults).toEqual({}); + }); + + it('should handle only optional variables with some provided', () => { + const variables: PromptVariable[] = [ + { + id: 1, + name: 'opt1', + type: 'STRING', + required: false, + default_value: 'a', + created_at: '', + prompt_version_id: 1, + uuid: '', + updated_at: '', + }, + { + id: 2, + name: 'opt2', + type: 'NUMBER', + required: false, + default_value: '1', + created_at: '', + prompt_version_id: 1, + uuid: '', + updated_at: '', + }, + ]; + const variablesToCheck = { opt1: 'b' }; + const { missingRequiredVariables, variablesWithDefaults } = validateVariables( + variables, + variablesToCheck, + ); + + expect(missingRequiredVariables).toEqual([]); + expect(variablesWithDefaults).toEqual({ opt1: 'b', opt2: '1' }); + }); + + it('should handle only optional variables with none provided', () => { + const variables: PromptVariable[] = [ + { + id: 1, + name: 'opt1', + type: 'STRING', + required: false, + default_value: 'a', + created_at: '', + prompt_version_id: 1, + uuid: '', + updated_at: '', + }, + { + id: 2, + name: 'opt2', + type: 'NUMBER', + required: false, + default_value: '1', + created_at: '', + prompt_version_id: 1, + uuid: '', + updated_at: '', + }, + ]; + const variablesToCheck = {}; + const { missingRequiredVariables, variablesWithDefaults } = validateVariables( + variables, + variablesToCheck, + ); + + expect(missingRequiredVariables).toEqual([]); + expect(variablesWithDefaults).toEqual({ opt1: 'a', opt2: '1' }); + }); +}); diff --git a/app/(auth-pages)/forgot-password/page.tsx b/app/(auth-pages)/forgot-password/page.tsx deleted file mode 100644 index bcf97255..00000000 --- a/app/(auth-pages)/forgot-password/page.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { forgotPasswordAction } from "@/app/actions"; -import { FormMessage, Message } from "@/components/form-message"; -import { SubmitButton } from "@/components/submit-button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import Link from "next/link"; -import { SmtpMessage } from "../smtp-message"; - -export default async function ForgotPassword(props: { - searchParams: Promise; -}) { - const searchParams = await props.searchParams; - return ( - <> -
    -
    -

    Reset Password

    -

    - Already have an account?{" "} - - Sign in - -

    -
    -
    - - - - Reset Password - - -
    -
    - - - ); -} diff --git a/app/(auth-pages)/layout.tsx b/app/(auth-pages)/layout.tsx deleted file mode 100644 index e038de15..00000000 --- a/app/(auth-pages)/layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export default async function Layout({ - children, -}: { - children: React.ReactNode; -}) { - return ( -
    {children}
    - ); -} diff --git a/app/(auth-pages)/sign-in/page.tsx b/app/(auth-pages)/sign-in/page.tsx deleted file mode 100644 index 7628cc7a..00000000 --- a/app/(auth-pages)/sign-in/page.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { signInAction } from "@/app/actions"; -import { FormMessage, Message } from "@/components/form-message"; -import { SubmitButton } from "@/components/submit-button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import Link from "next/link"; - -export default async function Login(props: { searchParams: Promise }) { - const searchParams = await props.searchParams; - return ( -
    -

    Sign in

    -

    - Don't have an account?{" "} - - Sign up - -

    -
    - - -
    - - - Forgot Password? - -
    - - - Sign in - - -
    -
    - ); -} diff --git a/app/(auth-pages)/sign-up/page.tsx b/app/(auth-pages)/sign-up/page.tsx deleted file mode 100644 index 2f994837..00000000 --- a/app/(auth-pages)/sign-up/page.tsx +++ /dev/null @@ -1,57 +0,0 @@ -// import { signUpAction } from "@/app/actions"; -// import { FormMessage, Message } from "@/components/form-message"; -// import { SubmitButton } from "@/components/submit-button"; -// import { Input } from "@/components/ui/input"; -// import { Label } from "@/components/ui/label"; -// import Link from "next/link"; -// import { SmtpMessage } from "../smtp-message"; -import { redirect } from 'next/navigation'; - -export default async function Signup() { - // redirecting to home page for now, no sign up functionality yet - redirect('/'); -} - -// export default async function Signup(props: { -// searchParams: Promise; -// }) { -// const searchParams = await props.searchParams; -// if ("message" in searchParams) { -// return ( -//
    -// -//
    -// ); -// } - -// return ( -// <> -//
    -//

    Sign up

    -//

    -// Already have an account?{" "} -// -// Sign in -// -//

    -//
    -// -// -// -// -// -// Sign up -// -// -//
    -//
    -// -// -// ); -// } diff --git a/app/actions.ts b/app/actions.ts deleted file mode 100644 index dbf8a26a..00000000 --- a/app/actions.ts +++ /dev/null @@ -1,134 +0,0 @@ -"use server"; - -import { encodedRedirect } from "@/utils/utils"; -import { createClient } from "@/utils/supabase/server"; -import { headers } from "next/headers"; -import { redirect } from "next/navigation"; - -export const signUpAction = async (formData: FormData) => { - const email = formData.get("email")?.toString(); - const password = formData.get("password")?.toString(); - const supabase = await createClient(); - const origin = (await headers()).get("origin"); - - if (!email || !password) { - return encodedRedirect( - "error", - "/sign-up", - "Email and password are required", - ); - } - - const { error } = await supabase.auth.signUp({ - email, - password, - options: { - emailRedirectTo: `${origin}/auth/callback`, - }, - }); - - if (error) { - console.error(error.code + " " + error.message); - return encodedRedirect("error", "/sign-up", error.message); - } else { - return encodedRedirect( - "success", - "/sign-up", - "Thanks for signing up! Please check your email for a verification link.", - ); - } -}; - -export const signInAction = async (formData: FormData) => { - const email = formData.get("email") as string; - const password = formData.get("password") as string; - const supabase = await createClient(); - - const { error } = await supabase.auth.signInWithPassword({ - email, - password, - }); - - if (error) { - return encodedRedirect("error", "/sign-in", error.message); - } - - return redirect("/protected"); -}; - -export const forgotPasswordAction = async (formData: FormData) => { - const email = formData.get("email")?.toString(); - const supabase = await createClient(); - const origin = (await headers()).get("origin"); - const callbackUrl = formData.get("callbackUrl")?.toString(); - - if (!email) { - return encodedRedirect("error", "/forgot-password", "Email is required"); - } - - const { error } = await supabase.auth.resetPasswordForEmail(email, { - redirectTo: `${origin}/auth/callback?redirect_to=/protected/reset-password`, - }); - - if (error) { - console.error(error.message); - return encodedRedirect( - "error", - "/forgot-password", - "Could not reset password", - ); - } - - if (callbackUrl) { - return redirect(callbackUrl); - } - - return encodedRedirect( - "success", - "/forgot-password", - "Check your email for a link to reset your password.", - ); -}; - -export const resetPasswordAction = async (formData: FormData) => { - const supabase = await createClient(); - - const password = formData.get("password") as string; - const confirmPassword = formData.get("confirmPassword") as string; - - if (!password || !confirmPassword) { - encodedRedirect( - "error", - "/protected/reset-password", - "Password and confirm password are required", - ); - } - - if (password !== confirmPassword) { - encodedRedirect( - "error", - "/protected/reset-password", - "Passwords do not match", - ); - } - - const { error } = await supabase.auth.updateUser({ - password: password, - }); - - if (error) { - encodedRedirect( - "error", - "/protected/reset-password", - "Password update failed", - ); - } - - encodedRedirect("success", "/protected/reset-password", "Password updated"); -}; - -export const signOutAction = async () => { - const supabase = await createClient(); - await supabase.auth.signOut(); - return redirect("/sign-in"); -}; diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts deleted file mode 100644 index dd415a48..00000000 --- a/app/auth/callback/route.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createClient } from "@/utils/supabase/server"; -import { NextResponse } from "next/server"; - -export async function GET(request: Request) { - // The `/auth/callback` route is required for the server-side auth flow implemented - // by the SSR package. It exchanges an auth code for the user's session. - // https://supabase.com/docs/guides/auth/server-side/nextjs - const requestUrl = new URL(request.url); - const code = requestUrl.searchParams.get("code"); - const origin = requestUrl.origin; - const redirectTo = requestUrl.searchParams.get("redirect_to")?.toString(); - - if (code) { - const supabase = await createClient(); - await supabase.auth.exchangeCodeForSession(code); - } - - if (redirectTo) { - return NextResponse.redirect(`${origin}${redirectTo}`); - } - - // URL to redirect to after sign up process completes - return NextResponse.redirect(`${origin}/protected`); -} diff --git a/app/constants.ts b/app/constants.ts deleted file mode 100644 index d35e0b7b..00000000 --- a/app/constants.ts +++ /dev/null @@ -1,37 +0,0 @@ -export const tsCodeSnippet = `import { createClient } from '@chad-syntax/agentsmith' -import Agency from '__generated__/agentsmith.types.ts'; - -const agentsmithClient = createClient({ apiKey: '***' }); - -const greeterAgent = await agentsmithClient.getAgent('greeter_agent@1.2.3'); -const agentResponse = await greeterAgent.action('greet', { - first_name: 'Susan', // types enforced! - last_name: 'Storm' -}, { stream: true }); // or could be false - -for await (const chunk of agentResponse) { - console.log(chunk); // { "choices": [{ "index": 0, "delta": { "role": "assistant", "content": "Hi" }]} -} -`; - -export const pyCodeSnippet = `from agentsmith import create_client -from agentsmith.types import Agency - -# Create a typed client -agentsmith_client = create_client[Agency](api_key="***") - -# Get agent with version -greeter_agent = agentsmith_client.get_agent("greeter_agent@0.0.1") - -# Types are enforced in the completion parameters -agent_response = greeter_agent.action('greet', - parameters={ - "first_name": "Susan", # type checked! - "last_name": "Storm" - }, - stream=True # or False -) - -async for chunk in agent_response: - print(chunk) # {"choices": [{"index": 0, "delta": {"role": "assistant", "content": "Hi"}}]} -`; diff --git a/app/globals.css b/app/globals.css deleted file mode 100644 index ce06c128..00000000 --- a/app/globals.css +++ /dev/null @@ -1,79 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 0 0% 3.9%; - --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; - --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - --success: 142 72% 29%; - --success-foreground: 0 0% 98%; - --warning: 38 92% 50%; - --warning-foreground: 0 0% 9%; - --info: 214 80% 56%; - --info-foreground: 0 0% 98%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; - --radius: 0.5rem; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - } - - .dark { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; - --card: 0 0% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - } -} - -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } -} - -html { - scroll-behavior: smooth; -} diff --git a/app/layout.tsx b/app/layout.tsx deleted file mode 100644 index e4feb2df..00000000 --- a/app/layout.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import './globals.css'; -import { Roboto_Mono } from 'next/font/google'; -import { ThemeProvider } from 'next-themes'; -import { GoogleAnalytics } from '@next/third-parties/google'; -import { Footer } from '@/components/Footer'; -import { PostHogProvider } from './providers/posthog'; - -const defaultUrl = - process.env.VERCEL_ENV === 'production' - ? 'https://agentsmith.app' - : process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : 'http://localhost:3000'; - -export const metadata = { - metadataBase: new URL(defaultUrl), - title: 'Agentsmith - AI Agent Development Platform', - description: - 'Agentsmith is the fastest way to build and iterate on LLM-powered apps', -}; - -const robotoMono = Roboto_Mono({ - display: 'swap', - subsets: ['latin'], -}); - -type RootLayoutProps = Readonly<{ - children: React.ReactNode; -}>; - -export default function RootLayout(props: RootLayoutProps) { - const { children } = props; - - return ( - - - - - -
    -
    -
    {children}
    -
    -
    -
    -
    - -
    - - ); -} diff --git a/app/protected/page.tsx b/app/protected/page.tsx deleted file mode 100644 index 5508abab..00000000 --- a/app/protected/page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import FetchDataSteps from "@/components/tutorial/fetch-data-steps"; -import { createClient } from "@/utils/supabase/server"; -import { InfoIcon } from "lucide-react"; -import { redirect } from "next/navigation"; - -export default async function ProtectedPage() { - const supabase = await createClient(); - - const { - data: { user }, - } = await supabase.auth.getUser(); - - if (!user) { - return redirect("/sign-in"); - } - - return ( -
    -
    -
    - - This is a protected page that you can only see as an authenticated - user -
    -
    -
    -

    Your user details

    -
    -          {JSON.stringify(user, null, 2)}
    -        
    -
    -
    -

    Next steps

    - -
    -
    - ); -} diff --git a/app/protected/reset-password/page.tsx b/app/protected/reset-password/page.tsx deleted file mode 100644 index 9cd70846..00000000 --- a/app/protected/reset-password/page.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { resetPasswordAction } from "@/app/actions"; -import { FormMessage, Message } from "@/components/form-message"; -import { SubmitButton } from "@/components/submit-button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; - -export default async function ResetPassword(props: { - searchParams: Promise; -}) { - const searchParams = await props.searchParams; - return ( -
    -

    Reset password

    -

    - Please enter your new password below. -

    - - - - - - Reset password - - - - ); -} diff --git a/app/twitter-image.png b/app/twitter-image.png deleted file mode 100644 index 57595e66..00000000 Binary files a/app/twitter-image.png and /dev/null differ diff --git a/components.json b/components.json index ec9676bf..c919e853 100644 --- a/components.json +++ b/components.json @@ -12,6 +12,6 @@ }, "aliases": { "components": "@/components", - "utils": "@/lib/utils" + "utils": "@/utils/shadcn" } } diff --git a/components/BrevoEmailSubscribe/BrevoEmailSubscribe.tsx b/components/BrevoEmailSubscribe/BrevoEmailSubscribe.tsx deleted file mode 100644 index 4740c6ad..00000000 --- a/components/BrevoEmailSubscribe/BrevoEmailSubscribe.tsx +++ /dev/null @@ -1,410 +0,0 @@ -'use client'; - -import { useEffect } from 'react'; -import Script from 'next/script'; -import './BrevoEmailSubscribe.css'; -import { usePostHog } from 'posthog-js/react'; - -declare global { - interface Window { - REQUIRED_CODE_ERROR_MESSAGE: string; - LOCALE: string; - EMAIL_INVALID_MESSAGE: string; - SMS_INVALID_MESSAGE: string; - REQUIRED_ERROR_MESSAGE: string; - GENERIC_INVALID_MESSAGE: string; - AUTOHIDE: boolean; - translation: { - common: { - selectedList: string; - selectedLists: string; - }; - }; - } -} - -const AGENTSMITH_INITIAL_LANDING_FORM_URL = - 'https://f2fdd414.sibforms.com/serve/MUIFAEfXwaofbSDfuJbYKpt255dwGkOyoJGRbydfUap-LjkURZrTEFj8mNzSST9zNHWZ88Nu0zoPrfakDdSFw-eEEFT0mtuJc0fSReYBQjbDy3Fa4XfsioEtitRVYh-ArolbK7lWiWjd7tbIo9dFvUsX9B7A2QlbRi2CWkDR_rpGSI4n2gL0hmj2B4GEti6Bg0rPGtkk5coyLux8'; - -export const BrevoForms = { - agentsmithInitialLanding: AGENTSMITH_INITIAL_LANDING_FORM_URL, -} as const; - -type BrevoForm = keyof typeof BrevoForms; - -type BrevoEmailSubscribeProps = { - form: BrevoForm; -}; - -export const BrevoEmailSubscribe = (props: BrevoEmailSubscribeProps) => { - const posthog = usePostHog(); - - const { form } = props; - const formSubmitUrl = BrevoForms[form]; - - useEffect(() => { - window.REQUIRED_CODE_ERROR_MESSAGE = 'Please choose a country code'; - window.LOCALE = 'en'; - window.EMAIL_INVALID_MESSAGE = window.SMS_INVALID_MESSAGE = - 'The information provided is invalid. Please review the field format and try again.'; - window.REQUIRED_ERROR_MESSAGE = 'This field cannot be left blank. '; - window.GENERIC_INVALID_MESSAGE = - 'The information provided is invalid. Please review the field format and try again.'; - window.translation = { - common: { - selectedList: '{quantity} list selected', - selectedLists: '{quantity} lists selected', - }, - }; - window.AUTOHIDE = Boolean(0); - }, []); - - const handleSubmit = (e: React.FormEvent) => { - posthog.capture('brevo_email_subscribe_submitted'); - - // Google Search-1 campaign conversion tracking - window.gtag('event', 'conversion', { - send_to: 'AW-16839610676/zqbPCJr38pUaELSi4N0-', - value: 1.0, - currency: 'USD', - }); - }; - - return ( -
    -
    -
    -
    -
    - - - - - Your subscription could not be saved. Please try again. - -
    -
    -
    -
    - - - - - Your subscription has been successful. - -
    -
    -
    -
    -
    -
    -

    Be the first in the door

    -
    -
    -
    -
    -
    -

    Early subscribers will get access first!

    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    - - -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    - - -
    -
    -
    -
    -
    -
    - - - - - - - - - - -
    -
    -

    - We use Brevo as our marketing platform. By submitting this - form you agree that the personal data you provided will be - transferred to Brevo for processing in accordance with{' '} - - Brevo's Privacy Policy. - -

    -
    -
    -
    -
    -
    -
    -
    -