diff --git a/.env.example b/.env.example index 0f0730ba2..30f28e508 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,14 @@ -OPENAI_API_KEY="..." \ No newline at end of file +## This file is an example of the .env and .env.docker files you should have before running docker-compose command + +PINO_PRETTIFY="true" +DATABASE_URL=postgresql://postgres:postgres@postgres:5432/pezzo +SUPERTOKENS_CONNECTION_URI="http://supertokens:3567" +CONSOLE_HOST="http://pezzo-console:4200" +KAFKA_BROKERS="kafka:9092" +OPENSEARCH_URL="http://opensearch:9200" +REDIS_URL="redis://redis-stack-server:6379" + +NX_BASE_API_URL="http://localhost:3000" +NX_SUPERTOKENS_API_DOMAIN="http://localhost:3000" +NX_SUPERTOKENS_WEBSITE_DOMAIN="http://localhost:4200" +NX_DEBUG_MODE="true" diff --git a/.eslintignore b/.eslintignore index 4f4aa6926..e24b1c731 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ node_modules -@generated \ No newline at end of file +@generated +.next \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 38d329eed..de671d112 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,7 @@ { "root": true, "ignorePatterns": ["**/*"], - "plugins": ["@nrwl/nx"], + "plugins": ["@nx"], "overrides": [ { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], @@ -13,11 +13,11 @@ "allowTemplateLiterals": true } ], - "@nrwl/nx/enforce-module-boundaries": [ + "@nx/enforce-module-boundaries": [ "error", { + "allow": ["@pezzo/kafka"], "enforceBuildableLibDependency": true, - "allow": ["@pezzo/graphql", "@pezzo/integrations", "@pezzo/client"], "depConstraints": [ { "sourceTag": "*", @@ -30,7 +30,7 @@ }, { "files": ["*.ts", "*.tsx"], - "extends": ["plugin:@nrwl/nx/typescript"], + "extends": ["plugin:@nx/typescript"], "rules": { "@typescript-eslint/quotes": [ "error", @@ -43,7 +43,7 @@ }, { "files": ["*.js", "*.jsx"], - "extends": ["plugin:@nrwl/nx/javascript"], + "extends": ["plugin:@nx/javascript"], "rules": {} } ] diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..90307ecb3 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Global rule: +* @arielweinberger \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 000000000..427019a1c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,56 @@ +name: Report a bug 🐛 +description: Create a report to help us improve +labels: "bug" +body: + - type: textarea + attributes: + label: Report + description: "What issue have you encountered?" + placeholder: "A clear and concise description of what issue bug is." + validations: + required: true + - type: textarea + attributes: + label: Expected behavior + description: What did you expect to happen? + placeholder: What did you expect to happen? + validations: + required: false + - type: textarea + attributes: + label: Steps to reproduce the problem + description: "How can we reproduce this bug? Please walk us through it step by step." + value: | + 1. + 2. + 3. + validations: + required: false + - type: textarea + attributes: + label: Logs (if applicable) + description: "Provide any logs, if applicable. This will help us identify the issue." + value: | + ``` + example + ``` + validations: + required: false + - type: input + attributes: + label: Pezzo version + description: > + What version of Pezzo are you using? + validations: + required: false + - type: dropdown + attributes: + label: How do you use Pezzo? + description: Where + options: + - Local Development Setup + - Docker Compose + - Kubernetes Deployment + - Pezzo Cloud + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 000000000..c9f3f98bc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,27 @@ +name: Feature request 🧭 +description: Suggest an idea for the Pezzo project +labels: "feature-request" +body: + - type: textarea + attributes: + label: Proposal + description: "What would you like to have as a feature" + placeholder: "A clear and concise description of what you want to happen." + validations: + required: true + - type: textarea + attributes: + label: Use-Case + description: "How would this help you?" + placeholder: "Tell us more what you'd like to achieve." + validations: + required: false + - type: dropdown + attributes: + label: Is this a feature you are interested in implementing yourself? + options: + - "No" + - "Maybe" + - "Yes" + validations: + required: true diff --git a/.github/assets/banner-with-play-button.png b/.github/assets/banner-with-play-button.png new file mode 100644 index 000000000..181f3438a Binary files /dev/null and b/.github/assets/banner-with-play-button.png differ diff --git a/.github/assets/banner.png b/.github/assets/banner.png index 5a9e69a92..dafe1209b 100644 Binary files a/.github/assets/banner.png and b/.github/assets/banner.png differ diff --git a/.github/assets/features/features-1.png b/.github/assets/features/features-1.png new file mode 100644 index 000000000..f1ac0855a Binary files /dev/null and b/.github/assets/features/features-1.png differ diff --git a/.github/assets/features/features-2.png b/.github/assets/features/features-2.png new file mode 100644 index 000000000..79ee747ed Binary files /dev/null and b/.github/assets/features/features-2.png differ diff --git a/.github/assets/features/features-3.png b/.github/assets/features/features-3.png new file mode 100644 index 000000000..8a314cde9 Binary files /dev/null and b/.github/assets/features/features-3.png differ diff --git a/.github/assets/logo-dark-mode.svg b/.github/assets/logo-dark-mode.svg new file mode 100644 index 000000000..8bca740ba --- /dev/null +++ b/.github/assets/logo-dark-mode.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/.github/assets/logo-dark.svg b/.github/assets/logo-dark.svg deleted file mode 100644 index 7808c55a3..000000000 --- a/.github/assets/logo-dark.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/.github/assets/logo-light-mode.svg b/.github/assets/logo-light-mode.svg new file mode 100644 index 000000000..0588a1bb4 --- /dev/null +++ b/.github/assets/logo-light-mode.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e044c290e..af9025b1e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -5,24 +5,37 @@ on: pull_request: branches: - "main" + - "release/*" paths-ignore: - "**/*.md" - ".github/workflows/release.yaml" + - "docs" push: branches: - "main" + - "release/*" paths-ignore: - "**/*.md" - ".github/workflows/release.yaml" + - "docs" + +env: + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} + NX_BRANCH: ${{ github.head_ref || github.ref_name }} + BASE: ${{ github.base_ref || github.event.repository.default_branch }} jobs: ci: name: Continuous Integration - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 permissions: packages: write steps: - - uses: actions/checkout@v2 + - name: Set variables + id: variables + run: echo "::set-output name=short_sha::$(echo ${{ github.sha }} | cut -c1-7)" + + - uses: actions/checkout@v3 with: fetch-depth: 0 @@ -53,7 +66,39 @@ jobs: run: npx nx run-many --target=test --all --parallel --maxParallel=3 - name: Build - run: npx nx run-many --target=build --all --parallel --maxParallel=3 + run: | + npx nx graphql:generate --skip-nx-cache + npx nx run-many --target=build --all --parallel --maxParallel=3 + + - name: Upload dist artifact + uses: actions/upload-artifact@v2 + with: + name: dist-artifact + path: ./dist + + dockerize: + name: Dockerize + needs: ci + runs-on: ubuntu-20.04 + strategy: + matrix: + project: ["server", "console", "proxy"] + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Download 'dist' folder + uses: actions/download-artifact@v2 + with: + name: dist-artifact + path: ./dist + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 - name: Login to GitHub Container Registry uses: docker/login-action@v2 @@ -62,14 +107,28 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + - name: Docker metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ghcr.io/pezzolabs/pezzo/${{ matrix.project }} + tags: | + type=raw,value=${{ github.run_id }},prefix=gh- + type=ref,event=branch,prefix=branch- + type=ref,event=pr,prefix=pr- + type=ref,event=tag + type=sha,format=short - - name: Dockerize - run: npx nx run-many --target=docker:build --configuration=ci --all --parallel --maxParallel=4 - env: - INPUT_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - INPUT_TAGS: "type=raw,value=${{ github.run_id }},prefix=gh-\ntype=ref,event=branch,prefix=branch-\ntype=sha,format=short" - INPUT_PLATFORMS: "linux/amd64" + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + file: ./apps/${{ matrix.project }}/Dockerfile + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64 + # only push if the branch is main or a release branch + push: true + # push: ${{ startsWith(github.ref, 'refs/heads/main') || startsWith(github.ref, 'refs/heads/release/') }} + provenance: false diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 222eb6be6..4a8350a84 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -5,8 +5,8 @@ on: types: [published] jobs: - ci: - name: Release + build: + name: Build runs-on: ubuntu-20.04 permissions: packages: write @@ -46,18 +46,112 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build - run: npx nx run-many --target=build --all --parallel --maxParallel=3 + run: | + npx nx graphql:generate --skip-nx-cache + npx nx run-many --target=build --all --parallel --maxParallel=3 - - name: Dockerize - run: npx nx run-many --target=docker:build --configuration=ci --all --parallel --maxParallel=4 - env: - INPUT_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - INPUT_TAGS: "type=semver,pattern={{version}}\ntype=raw,value=latest" - INPUT_PLATFORMS: "linux/amd64,linux/arm64" - - - name: NPM Publish (@pezzo/client) - continue-on-error: true - run: npm publish --access public - working-directory: dist/libs/client + - name: Upload dist artifact + uses: actions/upload-artifact@v2 + with: + name: dist-artifact + path: ./dist + + dockerize: + name: Dockerize + needs: build + runs-on: ubuntu-20.04 + strategy: + matrix: + project: ["server", "console", "proxy"] + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Download 'dist' folder + uses: actions/download-artifact@v2 + with: + name: dist-artifact + path: ./dist + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ghcr.io/pezzolabs/pezzo/${{ matrix.project }} + tags: | + type=semver,pattern={{version}} + type=sha,format=short + type=raw,value=latest,enable=${{ contains(github.ref, 'refs/tags/v') && !contains(github.ref, 'alpha') }} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + file: ./apps/${{ matrix.project }}/Dockerfile + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64 + push: true + provenance: false + + npm_publish: + name: NPM Publish + needs: build + runs-on: ubuntu-20.04 + strategy: + matrix: + library: ["client"] + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Download 'dist' folder + uses: actions/download-artifact@v2 + with: + name: dist-artifact + path: ./dist + + - uses: actions/setup-node@v3 + with: + node-version: 18.x + registry-url: https://registry.npmjs.org/ + + - name: Check package version + id: cpv + uses: PostHog/check-package-version@v2 + with: + path: ./dist/libs/${{ matrix.library }} + + - name: Echo versions + run: | + echo "Committed version: ${{ steps.cpv.outputs.committed-version }}" + echo "Published version: ${{ steps.cpv.outputs.published-version }}" + + - name: NPM Publish (@pezzo/${{ matrix.library }}) + if: steps.cpv.outputs.is-new-version == 'true' + run: | + version=$(node -p "require('./package.json').version") + if [[ $version == *"alpha"* ]]; then + npm publish --access public --tag alpha + else + npm publish --access public + fi + working-directory: dist/libs/${{ matrix.library }} env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index a9443a509..4b585d7a6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ # compiled output dist -tmp /out-tsc # dependencies @@ -44,4 +43,15 @@ Thumbs.db **/@generated .nx-container -.env.local \ No newline at end of file +.env +.env.local +.env.docker +schema.graphql + +# temp +temp +tmp + +kms/data + +volumes \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..8ddbc0c64 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v18.16.0 diff --git a/.nxignore b/.nxignore new file mode 100644 index 000000000..a83931952 --- /dev/null +++ b/.nxignore @@ -0,0 +1,3 @@ +.github +.next +volumes \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index ff8fc2792..264f07c84 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,9 @@ # Add files here to ignore them from prettier formatting /dist /coverage -apps/server/src/schema.graphql -**/@generated \ No newline at end of file +**/schema.graphql +**/@generated +libs/common/src/version.json +**/.next +/docs +volumes \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 1ca87ab7d..6c1a13a1f 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,3 +1,6 @@ { - "singleQuote": false + "singleQuote": false, + "plugins": ["prettier-plugin-tailwindcss"], + "tailwindConfig": "./apps/console/tailwind.config.js", + "tailwindFunctions": ["clsx", "cva"] } diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..75bc45116 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Attach - Examples - Task Generator App", + "port": 9230, + "request": "attach", + "skipFiles": ["/**"], + "type": "node", + "cwd": "${workspaceFolder}/examples/task-generator-app" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..2f1ecf384 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "css.customData": [".vscode/tailwind.json"] +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..403a5a65b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,120 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +hello@pezzo.ai. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0. + +[homepage]: http://contributor-covenant.org diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..c487ae1f1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,175 @@ +# Contributing + +We opened sourced Pezzo because we believe in the power of community. We believe you can help making Pezzo better! +We are excited to see what you will build with Pezzo and we are looking forward to your contributions. We want to make contributing to this project as easy and transparent as possible, whether it's features, bug fixes, documentation updates, guides, examples and more. + +## How can I contribute? + +Ready to contribute but seeking guidance, we have several avenues to assist you. Explore the upcoming segment for clarity on the kind of contributions we appreciate and how to jump in. Reach out directly to the Pezzo team on [Discord](https://discord.gg/h5nBW5ySqQ) for immediate assistance! Alternatively, you're welcome to raise an issue and one of our dedicated maintainers will promptly steer you in the right direction! + +## Found a bug? + +If you find a bug in the source code, you can help us by [submitting an issue](https://github.com/pezzolabs/pezzo/issues/new?assignees=&labels=bug&projects=&template=bug_report.yaml) to our GitHub Repository. Even better, you can submit a Pull Request with a fix. + +## Missing a feature? + +So, you've got an awesome feature in mind? Throw it over to us by [creating an issue](https://github.com/pezzolabs/pezzo/issues/new?assignees=&labels=feature-request&projects=&template=feature_request.yaml) on our GitHub Repo. + +Planning to code a feature yourself? We love the enthusiasm, but hang on, always good to have a little chinwag with us before you burn that midnight oil. Unfortunately, not every feature might fit into our plans. + +- Dreaming big? Kick off by opening an issue and sketch out your cool ideas. Helps us all stay on the same page, avoid doing the same thing twice, and ensures your hard work gels well into the project. +- Cooking up something small? Just craft it and [shoot it straight as a Pull Request](#submit-pr). + +## What do you need to know to help? + +If you want to help out with a code contribution, our project uses the following stack: + +### Server-side + +- [Node.JS](https://nodejs.org/) +- [TypeScript](https://www.typescriptlang.org/docs) +- [NestJS](https://docs.nestjs.com/) +- [Prisma](https://www.prisma.io/docs/) (with [PostgreSQL](https://www.postgresql.org/about/)) +- [GraphQL API](https://docs.nestjs.com/graphql/quick-start) +- [Jest](https://docs.nestjs.com/fundamentals/testing) (for testing) + +### Client-side + +- [React](https://reactjs.org/docs/getting-started.html) +- [TypeScript](https://www.typescriptlang.org/docs) +- [Apollo Client](https://www.apollographql.com/docs/react/) + +If you don't feel ready to make a code contribution yet, no problem! You can also check out the [documentation issues](https://github.com/pezzolabs/pezzo/labels/type%3A%20docs). + +# How do I make a code contribution? + +## Good first issues + +Are you new to open source contribution? Wondering how contributions work in our project? Here's a quick rundown. + +Find an issue that you're interested in addressing, or a feature that you'd like to add. +You can use [this view](https://github.com/pezzolabs/pezzo/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) which helps new contributors find easy gateways into our project. + +## Step 1: Make a fork + +Fork the Pezzo repository to your GitHub organization. This means that you'll have a copy of the repository under _your-GitHub-username/repository-name_. + +## Step 2: Clone the repository to your local machine + +``` +git clone https://github.com/{your-GitHub-username}/pezzo.git + +``` + +## Step 3: Prepare the development environment + +Set up and run the development environment on your local machine: + +**BEFORE** you run the following steps make sure: + +1. You have typescript installed locally on you machine `npm install -g typescript` +2. You are using node version: ^18.16.0 || ^14.0.0" +3. You are using npm version: ^8.1.0 || ^7.3.0" +4. You have `docker` installed and running on your machine + +```shell +cd pezzo +npm install +``` + +## Step 4: Create a branch + +Create a new branch for your changes. +In order to keep branch names uniform and easy-to-understand, please use the following conventions for branch naming. +Generally speaking, it is a good idea to add a group/type prefix to a branch. +Here is a list of good examples: + +- for docs change : docs/{ISSUE_NUMBER}-{CUSTOM_NAME} +- for new features : feat/{ISSUE_NUMBER}-{CUSTOM_NAME} +- for bug fixes : fix/{ISSUE_NUMBER}-{CUSTOM_NAME} + +```jsx +git checkout -b branch-name-here +``` + +## Step 5: Make your changes + +Update the code with your bug fix or new feature. + +## Step 6: Add the changes that are ready to be committed + +Stage the changes that are ready to be committed: + +```jsx +git add . +``` + +## Step 7: Commit the changes (Git) + +Commit the changes with a short message. (See below for more details on how we structure our commit messages) + +```jsx +git commit -m "(): " +``` + +## Step 8: Push the changes to the remote repository + +Push the changes to the remote repository using: + +```jsx +git push origin branch-name-here +``` + +## Step 9: Create Pull Request + +In GitHub, do the following to submit a pull request to the upstream repository: + +1. Give the pull request a title and a short description of the changes made. Include also the issue or bug number associated with your change. Explain the changes that you made, any issues you think exist with the pull request you made, and any questions you have for the maintainer. + +Remember, it's okay if your pull request is not perfect (no pull request ever is). The reviewer will be able to help you fix any problems and improve it! + +2. Wait for the pull request to be reviewed by a maintainer. + +3. Make changes to the pull request if the reviewing maintainer recommends them. + +Celebrate your success after your pull request is merged :-) + +## Git Commit Messages + +We structure our commit messages like this: + +``` +(): +``` + +Example + +``` +fix(server): missing entity on init +``` + +### Types: + +- **feat**: A new feature +- **fix**: A bug fix +- **docs**: Changes to the documentation +- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc.) +- **refactor**: A code change that neither fixes a bug nor adds a feature +- **perf**: A code change that improves performance +- **test**: Adding missing or correcting existing tests +- **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation + +### Packages: + +- **server** +- **console** +- **client** +- **types** + +## Code of conduct + +Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. + +[Code of Conduct](https://github.com/pezzolabs/pezzo/blob/main/CODE_OF_CONDUCT.md) + +Our Code of Conduct means that you are responsible for treating everyone on the project with respect and courtesy. diff --git a/README.md b/README.md index e07ce1103..88824728d 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,124 @@

- -
- -
- Pezzo is an open-source AI development toolkit designed to streamline prompt design, version management, publishing, collaboration, troubleshooting, observability and more. Our mission is to empower individuals and teams to harness the power of AI with maximum productivity and visibility. -
-
+ +

+ + logo + + + + logo + +

-
+

- + + Pezzo is a fully cloud-native and open-source LLMOps platform. Seamlessly observe and monitor your AI operations, troubleshoot issues, save up to 90% on costs and latency, collaborate and manage your prompts in one place, and instantly deliver AI changes. +

+ + +   + + +   + + +  

- -
- Join Pezzo on Discord + + + Contributor Covenant + + + License + + + +

-# Features - -đŸŽ›ī¸ **Centralized Prompt Management**: Manage all AI prompts in one place for maximum visibility and efficiency. - -🚀 **Streamlined Prompt Design, Publishing & Versioning:** Create, edit, test and publish prompts with ease. - -🔍 **Observability**: Access detailed prompt execution history, stats and metrics (duration, prompt cost, completion cost, etc.) for better insights. - -đŸ› ī¸ **Troubleshooting:** Effortlessly resolve issues with your prompts. Time travel to retroactively fine-tune failed prompts and commit the fix instantly. - -💰 **Cost Transparency**: Gain comprehensive cost transparency across all prompts and AI models. - -💡 **Simplified Integration:** Reduce code overhead by 90% by consuming your AI prompts using the Pezzo Client, regardless of the model provider. - -# Roadmap - -Below you can find the roadmap with all upcoming features: - -| **Feature** | **Status** | -| -------------------------- | -------------- | -| Demo app | 🚧 In Progress | -| Documentation Site | 🚧 In Progress | -| Logger | 🚧 In Progress | -| Error Handling | 🚧 In Progress | -| Official Helm Chart | 🔜 Coming Soon | -| Test Coverage | 🔜 Coming Soon | -| Public Sandbox Environment | 🔜 Coming Soon | -| Pezzo Client for Python | 🔜 Coming Soon | -| Pezzo Client for Golang | 🔜 Coming Soon | - -If you are missing features, please create an issue and we'll consider adding them to the roadmap. - -# Getting Started - -Clone the repository: - -``` -git clone git@github.com:pezzolabs/pezzo.git -``` +

+ + + +

-## đŸŗ Option 1: Running Pezzo via Docker Compose +

-This is a straightforward way to run Pezzo and start using it. +

-Simply run the following command: +# ✨ Features -``` -docker-compose up -``` +

+ +

-Pezzo should now be accessible at https://localhost:4200 🚀 +

+ +

-## đŸ•šī¸ Option 2: Running Pezzo in Development Mode +

+ +

-This method is useful for contirbutors and developers. +# Documentation + +[Click here to navigate to the Official Pezzo Documentation](https://docs.pezzo.ai/) + +In the documentation, you can find information on how to use Pezzo, its architecture, including tutorials and recipes for varius use cases and LLM providers. + +# Supported Clients + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Feature + Node.js + â€ĸ + Docs + + Python + â€ĸ + Docs + + LangChain +
Prompt Management✅✅✅
Observability✅✅✅
Caching✅✅✅
+ +Looking for a client that's not listed here? [Open an issue](https://github.com/pezzolabs/pezzo/issues/new/choose) and let us know! + +# Getting Started - Docker Compose + +If you simply want to run the full Pezzo stack locally, check out [Running With Docker Compose](http://docs.pezzo.ai/introduction/docker-compose) in the documentation. + +If you want to run Pezzo in development mode, continue reading. ### Prerequisites @@ -88,22 +134,25 @@ Install NPM dependencies by running: npm install ``` -### Spin up development dependencies via Docker Compose +### Set up the environment files -Pezzo relies on a Postgres database. You can spin it up using Docker Compose: +Pezzo uses a .env file to store environment variables. +When using docker, you should also create a .env.docker file. -``` -docker-compose -f docker-compose.dev.yaml up -``` +See the .env.example file for reference. -### Start Pezzo +### Spin up infrastructure dependencies via Docker Compose -Generate the Prisma client: +Pezzo is entirely cloud-native and relies solely on open-source technologies such as [PostgreSQL](https://www.postgresql.org/), [ClickHouse](https://github.com/ClickHouse/ClickHouse), [Redis](https://github.com/redis/redis) and [Supertokens](https://supertokens.com/). + +You can run these dependencies via Docker Compose: ``` -npx nx prisma:generate server +docker-compose -f docker-compose.infra.yaml up ``` +### Start Pezzo + Deploy Prisma migrations: ``` @@ -116,29 +165,31 @@ Run the server: npx nx serve server ``` -The server is now running. In the background, [graphql-codegen](https://www.npmjs.com/package/@graphql-codegen/cli) has generated GraphQL types based on the actual schema. These can be found at [libs/graphql/src/@generated](libs/graphql/src/@generated). This provides excellent type safety across the monorepo. +The server is now running. You can verify that by navigating to http://localhost:3000/api/healthz. -In development mode, you want to run `graphql-codegen` in watch mode, so whenever you make changes to the schema, types are generated automatically. In a separate Terminal tab, run: +In development mode, you want to run `codegen` in watch mode, so whenever you make changes to the schema, types are generated automatically. After running the server, run the following in a _separate terminal Window_: ``` -npx nx graphql:codegen graphql --watch +npm run graphql:codegen:watch ``` +This will connect [codegen](https://the-guild.dev/graphql/codegen/docs/getting-started) directly to the server and keep your GraphQL schema up-to-date as you make changes. + Finally, you are ready to run the Pezzo Console: ``` npx nx serve console ``` -That's it! Pezzo is now accessible at http://localhost:4200 🚀 +That's it! The Pezzo Console is now accessible at http://localhost:4200 🚀 # Contributing We welcome contributions from the community! Please feel free to submit pull requests or create issues for bugs or feature suggestions. -# Alpha Disclaimer +If you want to contribute but not sure how, join our [Discord](https://pezzo.cc/discord) and we'll be happy to help you out! -Pezzo is currently in early development stages. As we strive to provide a reliable and useful platform, you may encounter bugs, performance issues or other limitations. As a result, Pezzo cannot be held responsible for any errors, data loss, or other negative outcomes that may arise from usage during this stage. +Please check out [CONTRIBUTING.md](CONTRIBUTING.md) before contributing. # License diff --git a/apps/console/.babelrc b/apps/console/.babelrc index 8b8fbf654..88ee27b14 100644 --- a/apps/console/.babelrc +++ b/apps/console/.babelrc @@ -1,12 +1,11 @@ { "presets": [ [ - "@nrwl/react/babel", + "@nx/react/babel", { - "runtime": "automatic", - "importSource": "@emotion/react" + "runtime": "automatic" } ] ], - "plugins": ["@emotion/babel-plugin"] + "plugins": [] } diff --git a/apps/console/.env b/apps/console/.env deleted file mode 100644 index bf9e6281e..000000000 --- a/apps/console/.env +++ /dev/null @@ -1 +0,0 @@ -NX_BASE_API_URL=http://localhost:3000/graphql \ No newline at end of file diff --git a/apps/console/.env.example b/apps/console/.env.example new file mode 100644 index 000000000..985026da7 --- /dev/null +++ b/apps/console/.env.example @@ -0,0 +1,4 @@ +NX_BASE_API_URL="http://localhost:3000" +NX_SUPERTOKENS_API_DOMAIN="http://localhost:3000" +NX_SUPERTOKENS_WEBSITE_DOMAIN="http://localhost:4200" +NX_DEBUG_MODE="true" \ No newline at end of file diff --git a/apps/console/.eslintrc.json b/apps/console/.eslintrc.json index 734ddacee..854638323 100644 --- a/apps/console/.eslintrc.json +++ b/apps/console/.eslintrc.json @@ -1,10 +1,17 @@ { - "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], + "extends": ["plugin:@nx/react", "../../.eslintrc.json"], "ignorePatterns": ["!**/*"], "overrides": [ { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": {} + "rules": { + "@nx/enforce-module-boundaries": [ + "error", + { + "allowCircularSelfDependency": true + } + ] + } }, { "files": ["*.ts", "*.tsx"], diff --git a/apps/console/.storybook/main.ts b/apps/console/.storybook/main.ts new file mode 100644 index 000000000..8dcaaf895 --- /dev/null +++ b/apps/console/.storybook/main.ts @@ -0,0 +1,20 @@ +import type { StorybookConfig } from "@storybook/react-webpack5"; + +const config: StorybookConfig = { + stories: ["../src/**/*.stories.@(js|jsx|ts|tsx|mdx)"], + addons: [ + "@storybook/addon-essentials", + "@storybook/addon-interactions", + "@nx/react/plugins/storybook", + ], + framework: { + name: "@storybook/react-webpack5", + options: {}, + }, +}; + +export default config; + +// To customize your webpack configuration you can use the webpackFinal field. +// Check https://storybook.js.org/docs/react/builders/webpack#extending-storybooks-webpack-config +// and https://nx.dev/packages/storybook/documents/custom-builder-configs diff --git a/.env b/apps/console/.storybook/preview.ts similarity index 100% rename from .env rename to apps/console/.storybook/preview.ts diff --git a/apps/console/jest.config.ts b/apps/console/jest.config.ts index fd8885a3e..188215d96 100644 --- a/apps/console/jest.config.ts +++ b/apps/console/jest.config.ts @@ -3,7 +3,7 @@ export default { displayName: "console", preset: "../../jest.preset.js", transform: { - "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "@nrwl/react/plugins/jest", + "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "@nx/react/plugins/jest", "^.+\\.[tj]sx?$": [ "@swc/jest", { jsc: { transform: { react: { runtime: "automatic" } } } }, diff --git a/apps/console/postcss.config.js b/apps/console/postcss.config.js index 8ac4e9263..a9649690d 100644 --- a/apps/console/postcss.config.js +++ b/apps/console/postcss.config.js @@ -1,4 +1,3 @@ -// apps/site/postcss.config.js const { join } = require("path"); module.exports = { diff --git a/apps/console/project.json b/apps/console/project.json index d26421123..ec38f1e8b 100644 --- a/apps/console/project.json +++ b/apps/console/project.json @@ -3,20 +3,22 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "apps/console/src", "projectType": "application", - "implicitDependencies": ["server"], + "implicitDependencies": ["pezzo"], "targets": { "build": { - "dependsOn": ["^graphql:codegen:offline"], - "executor": "@nrwl/webpack:webpack", + "dependsOn": ["^graphql:generate", "^prebuild"], + "executor": "@nx/webpack:webpack", "outputs": ["{options.outputPath}"], "defaultConfiguration": "production", "options": { - "compiler": "swc", + "compiler": "babel", "outputPath": "dist/apps/console", "index": "apps/console/src/index.html", "baseHref": "/", "main": "apps/console/src/main.tsx", "tsConfig": "apps/console/tsconfig.app.json", + "sourceMap": true, + "postcssConfig": "apps/console/postcss.config.js", "assets": [ "apps/console/src/favicon.ico", "apps/console/src/assets", @@ -33,14 +35,14 @@ ], "styles": [], "scripts": [], - "isolatedConfig": true, - "webpackConfig": "apps/console/webpack.config.js" + "isolatedConfig": false, + "webpackConfig": "apps/console/webpack.config.js", + "buildLibsFromSource": true }, "configurations": { "development": { "extractLicenses": false, "optimization": false, - "sourceMap": true, "vendorChunk": true }, "production": { @@ -52,7 +54,6 @@ ], "optimization": true, "outputHashing": "all", - "sourceMap": false, "namedChunks": false, "extractLicenses": true, "vendorChunk": false @@ -60,8 +61,8 @@ } }, "serve": { - "dependsOn": ["^graphql:codegen"], - "executor": "@nrwl/webpack:dev-server", + "dependsOn": ["^graphql:generate"], + "executor": "@nx/webpack:dev-server", "defaultConfiguration": "development", "options": { "buildTarget": "console:build", @@ -78,20 +79,20 @@ } }, "lint": { - "executor": "@nrwl/linter:eslint", + "executor": "@nx/linter:eslint", "outputs": ["{options.outputFile}"], "options": { "lintFilePatterns": ["apps/console/**/*.{ts,tsx,js,jsx}"] } }, "serve-static": { - "executor": "@nrwl/web:file-server", + "executor": "@nx/web:file-server", "options": { "buildTarget": "console:build" } }, "test": { - "executor": "@nrwl/jest:jest", + "executor": "@nx/jest:jest", "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], "options": { "jestConfig": "apps/console/jest.config.ts", @@ -113,15 +114,39 @@ "local": { "tags": ["ghcr.io/pezzolabs/pezzo/console"], "push": false - }, + } + } + }, + "storybook": { + "executor": "@nx/storybook:storybook", + "options": { + "port": 4400, + "configDir": "apps/console/.storybook" + }, + "configurations": { "ci": { - "push": true, - "metadata": { - "images": ["ghcr.io/pezzolabs/pezzo/console"], - "platforms": ["linux/amd64", "linux/arm64"] - } + "quiet": true + } + } + }, + "build-storybook": { + "executor": "@nx/storybook:build", + "outputs": ["{options.outputDir}"], + "options": { + "outputDir": "dist/storybook/console", + "configDir": "apps/console/.storybook" + }, + "configurations": { + "ci": { + "quiet": true } } + }, + "test-storybook": { + "executor": "nx:run-commands", + "options": { + "command": "test-storybook -c apps/console/.storybook --url=http://localhost:4400" + } } }, "tags": [] diff --git a/apps/console/src/app.tsx b/apps/console/src/app.tsx new file mode 100644 index 000000000..4614cc0bf --- /dev/null +++ b/apps/console/src/app.tsx @@ -0,0 +1,177 @@ +import "./styles.css"; +import { Route, Routes, Navigate, Outlet } from "react-router-dom"; +import { hotjar } from "react-hotjar"; +import { HOTJAR_SITE_ID, HOTJAR_VERSION } from "~/env"; +import { Toaster } from "@pezzo/ui"; + +// Auth +import { QueryClientProvider } from "@tanstack/react-query"; +import { SuperTokensWrapper } from "supertokens-auth-react"; +import { SessionAuth } from "supertokens-auth-react/recipe/session"; +import { initSuperTokens } from "./lib/auth/supertokens"; + +// Pages +import { EnvironmentsPage } from "./pages/environments/EnvironmentsPage"; +import { PromptPage } from "./pages/prompts/PromptPage"; +import { PromptsPage } from "./pages/prompts/PromptsPage"; +import { OnboardingPage } from "./pages/organizations/onboarding"; +import { LogoutPage } from "./pages/auth/LogoutPage"; +import { RequestsPage } from "./pages/requests/RequestsPage"; +import { DashboardPage } from "./pages/projects/overview/DashboardPage"; +import { LoginPage } from "./pages/auth/LoginPage"; +import { AuthCallbackPage } from "./pages/auth/AuthCallbackPage"; +import { queryClient } from "./lib/graphql"; +import { AuthProvider } from "./lib/providers/AuthProvider"; +import { OptionalIntercomProvider } from "./lib/providers/OptionalIntercomProvider"; +import { LayoutWrapper } from "./components/layout/LayoutWrapper"; +import { AcceptInvitationPage } from "./pages/invitations/AcceptInvitationPage"; +import { CurrentPromptProvider } from "./lib/providers/CurrentPromptContext"; +import { RequiredProviderApiKeyModalProvider } from "./lib/providers/RequiredProviderApiKeyModalProvider"; +import { OrgMembersPage } from "./pages/organizations/OrgMembersPage"; +import { OrgSettingsPage } from "./pages/organizations/OrgSettingsPage"; +import { OrgApiKeysPage } from "./pages/organizations/OrgApiKeysPage"; +import { PromptEditView } from "./features/editor/PromptEditView"; +import { EditorProvider } from "./lib/providers/EditorContext"; +import { PromptTesterProvider } from "./lib/providers/PromptTesterContext"; +import { PromptVersionsView } from "./components/prompts/views/PromptVersionsView"; +import { Suspense } from "react"; +import { FullScreenLoader } from "./components/common/FullScreenLoader"; +import { OrgPage } from "./pages/projects/OrgPage"; +import { useCurrentOrganization } from "./lib/hooks/useCurrentOrganization"; +import { WaitlistWrapper } from "~/pages/WaitlistWrapper"; + +initSuperTokens(); + +if (HOTJAR_SITE_ID && HOTJAR_VERSION) { + hotjar.initialize(Number(HOTJAR_SITE_ID), Number(HOTJAR_VERSION)); +} + +const RootHandler = () => { + const { organizationId, isLoading } = useCurrentOrganization(); + + if (isLoading) { + return ; + } + + if (organizationId) { + return ; + } +}; + +export function App() { + return ( +
+ + + + {/* Non-authorized routes */} + + {/* We don't render the LayoutWrapper for non-authorized routes */} + } + /> + } /> + } /> + + {/* Authorized routes */} + + + + + + + + + } + > + } /> + + + + + } + /> + + + + + + + } + /> + + {/* Organizations */} + + }> + + + + + + } + > + } /> + } /> + } /> + } /> + + + {/* In-project routes */} + }> + + + + + + + + + + + } + > + } /> + } /> + } /> + } /> + } /> + }> + } /> + + + + + + } + /> + } /> + + + + + + +
+ ); +} + +export default App; diff --git a/apps/console/src/app/app.tsx b/apps/console/src/app/app.tsx deleted file mode 100644 index 04d5a343d..000000000 --- a/apps/console/src/app/app.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import styled from "@emotion/styled"; -import { Navigate, Route, Routes } from "react-router-dom"; -import "antd/dist/reset.css"; -import "./styles.css"; - -import { ThemeProvider } from "./lib/providers/ThemeProvider"; -import { QueryClientProvider } from "@tanstack/react-query"; -import { queryClient } from "./lib/graphql"; -import { SideNavigation } from "./components/layout/SideNavigation"; -import { Layout } from "antd"; -import { CurrentPromptProvider } from "./lib/providers/CurrentPromptContext"; -import { PromptTesterProvider } from "./lib/providers/PromptTesterContext"; -import { EnvironmentsPage } from "./pages/environments"; -import { PromptsPage } from "./pages/prompts"; -import { PromptPage } from "./pages/prompts/[promptId]"; -import { APIKeysPage } from "./pages/api-keys"; - -const { Content } = Layout; - -const StyledContent = styled(Content)` - padding: 22px; - min-height: 200px; - overflow-y: auto; - - scrollbar-width: none; /* Firefox */ - ::-webkit-scrollbar { - display: none; /* Chrome, Safari, Opera */ - } - ::-ms-scrollbar { - display: none; /* IE */ - } -`; - -export function App() { - return ( - <> -
- - - - - - - - - - } /> - } /> - } - /> - } - /> - } /> - - - - - - - - -
- - // - // - - // {/* START: routes */} - // {/* These routes and navigation have been generated for you */} - // {/* Feel free to move and update them to fit your needs */} - //
- //
- //
- //
- //
    - //
  • - // Home - //
  • - //
  • - // Page 2 - //
  • - //
- //
- - // {/* END: routes */} - //
- ); -} - -export default App; diff --git a/apps/console/src/app/components/api-keys/APIKeyListItem.tsx b/apps/console/src/app/components/api-keys/APIKeyListItem.tsx deleted file mode 100644 index 9cb467c99..000000000 --- a/apps/console/src/app/components/api-keys/APIKeyListItem.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { Avatar, Card, Row, Col, Typography, Button, Input } from "antd"; -import styled from "@emotion/styled"; -import { CloseOutlined, EditOutlined, SaveOutlined } from "@ant-design/icons"; -import { useState } from "react"; -import { useMutation } from "@tanstack/react-query"; -import { UPDATE_PROVIDER_API_KEY } from "../../graphql/mutations/provider-api-keys"; -import { gqlClient, queryClient } from "../../lib/graphql"; -import { CreateProviderApiKeyInput } from "@pezzo/graphql"; -import { useEffect } from "react"; - -const APIKeyContainer = styled.div` - display: flex; - align-items: center; - width: 600px; -`; - -interface Props { - provider: string; - value: string | null; - iconBase64: string; -} - -export const APIKeyListItem = ({ provider, value, iconBase64 }: Props) => { - const updateKeyMutation = useMutation({ - mutationFn: (data: CreateProviderApiKeyInput) => - gqlClient.request(UPDATE_PROVIDER_API_KEY, { - data: { - provider: data.provider, - value: data.value, - }, - }), - onSuccess: (data) => { - queryClient.invalidateQueries({ queryKey: ["providerAPIKeys"] }); - }, - }); - - const [isEditing, setIsEditing] = useState(false); - const [editValue, setEditValue] = useState(""); - - useEffect(() => { - if (!isEditing) { - setEditValue(""); - } - }, [isEditing]); - - const handleEdit = () => { - setIsEditing(true); - }; - - const handleSave = async () => { - await updateKeyMutation.mutateAsync({ provider, value: editValue }); - setIsEditing(false); - }; - - return ( - - - - - - - {provider} - - - - {isEditing ? ( - setEditValue(e.target.value)} - autoComplete="off" - /> - ) : ( - - {value || "No API key provided"} - - )} - - - {isEditing ? ( - <> - - - - - ); -}; diff --git a/apps/console/src/app/components/layout/SideNavigation.tsx b/apps/console/src/app/components/layout/SideNavigation.tsx deleted file mode 100644 index 380432ffd..000000000 --- a/apps/console/src/app/components/layout/SideNavigation.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { BoltIcon, ServerStackIcon, KeyIcon } from "@heroicons/react/24/solid"; -import { Layout, Menu } from "antd"; -import { css } from "@emotion/css"; -import { useState } from "react"; -import LogoSquare from "../../../assets/logo-square.svg"; -import { useNavigate, useLocation } from "react-router-dom"; - -const { Sider } = Layout; - -const menuItems = [ - { - key: "prompts", - label: "Prompts", - icon: , - }, - { - key: "environments", - label: "Environments", - icon: , - }, - { - key: "api-keys", - label: "API Keys", - icon: , - }, -]; - -export const SideNavigation = () => { - const location = useLocation(); - const navigate = useNavigate(); - const [isCollapsed] = useState(true); - - const handleMenuClick = (item) => { - navigate(`/${item.key}`); - }; - - return ( - -
- Logo -
- - - ); -}; diff --git a/apps/console/src/app/components/prompts/CommitPromptModal.tsx b/apps/console/src/app/components/prompts/CommitPromptModal.tsx deleted file mode 100644 index f3e4fb63b..000000000 --- a/apps/console/src/app/components/prompts/CommitPromptModal.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { Modal, Form, Input, Button, Alert, FormInstance } from "antd"; -import { gqlClient, queryClient } from "../../lib/graphql"; -import { css } from "@emotion/css"; -import { useState } from "react"; -import { useCurrentPrompt } from "../../lib/providers/CurrentPromptContext"; -import { CREATE_PROMPT_VERSION } from "../../graphql/mutations/prompts"; -import { PromptEditFormInputs } from "../../lib/hooks/usePromptEdit"; -import { CreatePromptVersionInput } from "@pezzo/graphql"; - -interface Props { - open: boolean; - onClose: () => void; - onCommitted: (id: string) => void; - form: FormInstance; -} - -type Inputs = { - message: string; -}; - -export const CommitPromptModal = ({ - open, - onClose, - onCommitted, - form: editPromptForm, -}: Props) => { - const { prompt } = useCurrentPrompt(); - const [form] = Form.useForm(); - const [error, setError] = useState(null); - - const createPromptVersionMutation = useMutation({ - mutationFn: (data: CreatePromptVersionInput) => { - return gqlClient.request(CREATE_PROMPT_VERSION, { - data: { - message: data.message, - content: data.content, - settings: data.settings, - promptId: data.promptId, - }, - }); - }, - onSuccess: () => { - form.resetFields(); - queryClient.invalidateQueries(["prompt", prompt.id]); - onClose(); - }, - }); - - const handleFormFinish = async (values) => { - setError(null); - const editPromptValues = editPromptForm.getFieldsValue(true); - - createPromptVersionMutation.mutate({ - message: form.getFieldValue("message"), - content: editPromptValues.content, - settings: editPromptValues.settings, - promptId: prompt.id, - }); - - form.resetFields(); - }; - - return ( - - {error && } -
- - - - - - - -
-
- ); -}; diff --git a/apps/console/src/app/components/prompts/ConsumePromptModal.tsx b/apps/console/src/app/components/prompts/ConsumePromptModal.tsx deleted file mode 100644 index 6641c13cb..000000000 --- a/apps/console/src/app/components/prompts/ConsumePromptModal.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { Modal, Typography } from "antd"; -import { useCurrentPrompt } from "../../lib/providers/CurrentPromptContext"; -import { Highlight, themes } from "prism-react-renderer"; -import { getIntegration } from "@pezzo/integrations"; - -interface Props { - open: boolean; - onClose: () => void; - variables: Record; -} - -export const ConsumePromptModal = ({ open, onClose, variables }: Props) => { - const { prompt, integration } = useCurrentPrompt(); - const codeBlock = integration.consumeInstructionsFn(prompt.name, variables); - - return ( - - - Step 1: Install the Pezzo client and Pezzo integrations - -
-        npm install @pezzo/client
-        
- npm install @pezzo/integrations -
- - - Step 2: Consume the client to run your prompt - - - {({ className, style, tokens, getLineProps, getTokenProps }) => ( -
-            {tokens.map((line, i) => (
-              
- {line.map((token, key) => ( - - ))} -
- ))} -
- )} -
-
- ); -}; diff --git a/apps/console/src/app/components/prompts/CreatePromptModal.tsx b/apps/console/src/app/components/prompts/CreatePromptModal.tsx deleted file mode 100644 index 18f80040c..000000000 --- a/apps/console/src/app/components/prompts/CreatePromptModal.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { Modal, Form, Input, Button } from "antd"; -import { CREATE_PROMPT } from "../../graphql/mutations/prompts"; -import { gqlClient, queryClient } from "../../lib/graphql"; -import { css } from "@emotion/css"; -import { PromptIntegrationSelector } from "./PromptIntegrationSelector"; -import { integrations } from "@pezzo/integrations"; - -const integrationsArray = Object.values(integrations); - -interface Props { - open: boolean; - onClose: () => void; - onCreated: (id: string) => void; -} - -type Inputs = { - name: string; - integrationId: string; -}; - -export const CreatePromptModal = ({ open, onClose, onCreated }: Props) => { - const [form] = Form.useForm(); - const createPromptMutation = useMutation({ - mutationFn: (data: Inputs) => - gqlClient.request(CREATE_PROMPT, { - data: { - name: data.name, - integrationId: data.integrationId, - }, - }), - onSuccess: (data) => { - onCreated(data.createPrompt.id); - queryClient.invalidateQueries({ queryKey: ["prompts"] }); - }, - }); - - const handleFormFinish = (data: Inputs) => { - createPromptMutation.mutate(data); - }; - - return ( - -
- - form.setFieldValue("integration", value)} - /> - - - - - - - - - -
-
- ); -}; diff --git a/apps/console/src/app/components/prompts/DeletePromptConfirmationModal.tsx b/apps/console/src/app/components/prompts/DeletePromptConfirmationModal.tsx deleted file mode 100644 index c2ad4f542..000000000 --- a/apps/console/src/app/components/prompts/DeletePromptConfirmationModal.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Button, Modal, Typography } from "antd"; - -interface Props { - open: boolean; - onClose: () => void; - onConfirm: () => void; -} - -export const DeletePromptConfirmationModal = ({ - open, - onClose, - onConfirm, -}: Props) => { - return ( - - Cancel - , - , - ]} - > - - Are you sure you want to delete this prompt? - - - ); -}; diff --git a/apps/console/src/app/components/prompts/PromptEditor.tsx b/apps/console/src/app/components/prompts/PromptEditor.tsx deleted file mode 100644 index cec781fdd..000000000 --- a/apps/console/src/app/components/prompts/PromptEditor.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import CodeMirror from "@uiw/react-codemirror"; -import { materialInit } from "@uiw/codemirror-theme-material"; -import { css } from "@emotion/css"; -import { colors } from "../../lib/theme/colors"; -import { Form } from "antd"; - -interface Props { - value?: string; - onChange?: (value: string) => void; -} - -export const PromptEditor = ({ value, onChange }: Props) => { - return ( - <> - - - - - ); -}; diff --git a/apps/console/src/app/components/prompts/PromptIntegrationSelector.tsx b/apps/console/src/app/components/prompts/PromptIntegrationSelector.tsx deleted file mode 100644 index e107155b1..000000000 --- a/apps/console/src/app/components/prompts/PromptIntegrationSelector.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Select, Typography } from "antd"; -import { integrations } from "@pezzo/integrations"; -import styled from "@emotion/styled"; - -const Icon = styled.img` - border-radius: 2px; - height: 100%; -`; - -const SelectItem = styled.div` - display: flex; - align-items: center; - height: 28px; - position: relative; - padding-top: 4px; - padding-bottom: 4px; -`; - -interface Props { - onChange: (value: string) => void; -} - -const integrationsArray = Object.values(integrations); - -export const PromptIntegrationSelector = ({ onChange }: Props) => { - return ( - - )} - {field.type === "slider" && ( - - )} - - ))} - - ); -}; diff --git a/apps/console/src/app/components/prompts/PromptSettingsSlider.tsx b/apps/console/src/app/components/prompts/PromptSettingsSlider.tsx deleted file mode 100644 index 78686685c..000000000 --- a/apps/console/src/app/components/prompts/PromptSettingsSlider.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Slider, Row, Col, InputNumber } from "antd"; - -interface Props { - min: number; - max: number; - step: number; - value?: number; - onChange?: (value: number) => void; -} - -export const PromptSettingsSlider = ({ - min, - max, - step, - value, - onChange, -}: Props) => { - return ( - - - - - - - - - ); -}; diff --git a/apps/console/src/app/components/prompts/PromptTester/PromptTester.tsx b/apps/console/src/app/components/prompts/PromptTester/PromptTester.tsx deleted file mode 100644 index a4da1a858..000000000 --- a/apps/console/src/app/components/prompts/PromptTester/PromptTester.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { PromptExecutionStatus } from "@prisma/client"; -import { - Button, - Col, - Descriptions, - Drawer, - Form, - Row, - Space, - Tag, - Typography, -} from "antd"; -import styled from "@emotion/styled"; -import { - CheckCircleOutlined, - CloseCircleOutlined, - RedoOutlined, -} from "@ant-design/icons"; -import { PromptVariables } from "../PromptVariables"; -import { PromptEditor } from "../PromptEditor"; -import { useEffect, useState } from "react"; -import { usePromptTester } from "../../../lib/providers/PromptTesterContext"; -import { isJson } from "../../../lib/utils/is-json"; - -const StyledPre = styled.pre` - white-space: pre-wrap; - background: #000; - padding: 20px; - border-radius: 6px; -`; - -const { Item } = Descriptions; - -export const PromptTester = () => { - const { - isTesterOpen, - closeTester, - testResult: result, - isTestInProgress, - runTest, - } = usePromptTester(); - const [variables, setVariables] = useState>({}); - const [form] = Form.useForm(); - - useEffect(() => { - if (result?.variables) { - setVariables(result.variables); - } - }, [result]); - - const handleRerunTest = () => { - const { content, settings } = form.getFieldsValue(true); - runTest({ - content, - settings, - variables, - }); - }; - - return ( - closeTester()} - title={ - - Prompt Test Result - - - - - - - } - width="80%" - > - {result && ( -
-
- Stats - - {result.status && ( - <> - - {result.status === PromptExecutionStatus.Success ? ( - } color="success"> - Success - - ) : ( - } color="error"> - Error - - )} - - - {Math.ceil(result.duration / 1000)} seconds - - ${result.totalCost} - ${result.promptCost} - ${result.completionCost} - {result.totalTokens} - {result.promptTokens} - - {result.completionTokens} - - - )} - - - Variables - - setVariables({ ...variables, [key]: value }) - } - /> - -
- - - Request -
- -
- - - {result.status === PromptExecutionStatus.Success ? ( - <> - Result - - {isJson(result.result) - ? JSON.stringify(JSON.parse(result.result), null, 2) - : result.result} - {" "} - - ) : ( - <> - Error - - {JSON.stringify(JSON.parse(result.error), null, 2)} - - - )} - -
-
-
-
- )} -
- ); -}; diff --git a/apps/console/src/app/components/prompts/PromptVariable.tsx b/apps/console/src/app/components/prompts/PromptVariable.tsx deleted file mode 100644 index 45d0ab47d..000000000 --- a/apps/console/src/app/components/prompts/PromptVariable.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { ExpandAltOutlined } from "@ant-design/icons"; -import { css } from "@emotion/css"; -import { Input, Popover } from "antd"; -import { useRef, useState } from "react"; -import { useOnClickOutside } from "usehooks-ts"; - -interface Props { - name: string; - value: string; - onChange: (value: string) => void; -} - -export const PromptVariable = ({ name, value, onChange }: Props) => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const popoverRef = useRef(); - - useOnClickOutside(popoverRef, (e) => { - const target = e.target as HTMLElement; - const closest = target.closest(".ant-popover"); - if (closest === null) { - setIsPopoverOpen(false); - } - }); - - const handleChange = (e) => { - onChange(e.target.value); - }; - - return ( -
- - -
- } - > - setIsPopoverOpen(true)} /> - } - /> - - - ); -}; diff --git a/apps/console/src/app/components/prompts/PromptVariables.tsx b/apps/console/src/app/components/prompts/PromptVariables.tsx deleted file mode 100644 index 3a10aa4ef..000000000 --- a/apps/console/src/app/components/prompts/PromptVariables.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Space, Typography } from "antd"; -import { PromptVariable } from "./PromptVariable"; -import { isJson } from "../../lib/utils/is-json"; - -interface Props { - variables: Record; - onVariableChange: (name: string, value: string) => void; -} - -export const PromptVariables = ({ variables, onVariableChange }: Props) => { - return ( -
- {Object.keys(variables).length === 0 && ( - No variables found. - )} - - - {Object.keys(variables).length > 0 && - Object.keys(variables).map((variableName) => ( - onVariableChange(variableName, value)} - /> - ))} - -
- ); -}; diff --git a/apps/console/src/app/components/prompts/PromptVersionSelector.tsx b/apps/console/src/app/components/prompts/PromptVersionSelector.tsx deleted file mode 100644 index cd7a733f6..000000000 --- a/apps/console/src/app/components/prompts/PromptVersionSelector.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { CaretDownOutlined } from "@ant-design/icons"; -import { useQuery } from "@tanstack/react-query"; -import { Button, Dropdown } from "antd"; -import { GET_PROMPT_VERSIONS_LIST } from "../../graphql/queries/prompts"; -import { gqlClient } from "../../lib/graphql"; -import { useCurrentPrompt } from "../../lib/providers/CurrentPromptContext"; -import { useState } from "react"; - -export const PromptVersionSelector = () => { - const { prompt, currentPromptVersion, setPromptVersion } = useCurrentPrompt(); - const latestVersion = prompt.latestVersion; - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - - const { data: promptVersionsList } = useQuery({ - queryKey: ["promptVersionsList", prompt.id], - queryFn: () => - gqlClient.request(GET_PROMPT_VERSIONS_LIST, { - data: { id: prompt.id }, - }), - enabled: isDropdownOpen, - }); - - const itemsFromVersionsList = - promptVersionsList?.promptVersions - .filter((version) => version.sha !== latestVersion.sha) - .map((version) => ({ - key: version.sha, - label: version.sha.slice(0, 7), - onClick: () => setPromptVersion(version.sha), - })) || []; - - const menu = { - items: [ - { - key: "latest", - label: `Latest (${latestVersion.sha.slice(0, 7)})`, - onClick: () => setPromptVersion("latest"), - }, - ...itemsFromVersionsList, - ], - }; - - const isLatest = currentPromptVersion.sha === latestVersion.sha; - - const selectedVersionLabel = isLatest - ? `Latest (${latestVersion.sha.slice(0, 7)})` - : `${currentPromptVersion.sha.slice(0, 7)}`; - - return ( - - - - ); -}; diff --git a/apps/console/src/app/components/prompts/PublishPromptModal.tsx b/apps/console/src/app/components/prompts/PublishPromptModal.tsx deleted file mode 100644 index c6da8c490..000000000 --- a/apps/console/src/app/components/prompts/PublishPromptModal.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { Alert, List, Modal, Radio, Typography } from "antd"; -import { useEnvironments } from "../../lib/hooks/useEnvironments"; -import { useCurrentPrompt } from "../../lib/providers/CurrentPromptContext"; -import { useEffect, useState } from "react"; -import { useMutation } from "@tanstack/react-query"; -import { gqlClient, queryClient } from "../../lib/graphql"; -import { PUBLISH_PROMPT } from "../../graphql/mutations/prompt-environments"; -import { PublishPromptInput } from "@pezzo/graphql"; - -interface Props { - open: boolean; - onClose: () => void; -} - -export const PublishPromptModal = ({ open, onClose }: Props) => { - const { currentPromptVersion, prompt } = useCurrentPrompt(); - const { environments } = useEnvironments(); - const [selectedEnvironmentSlug, setSelectedEnvironmentSlug] = - useState(undefined); - const [selectedEnvironmentName, setSelectedEnvironmentName] = - useState(undefined); - - const publishPromptMutation = useMutation({ - mutationFn: (data: PublishPromptInput) => - gqlClient.request(PUBLISH_PROMPT, { - data: { - promptId: data.promptId, - environmentSlug: data.environmentSlug, - promptVersionSha: data.promptVersionSha, - }, - }), - mutationKey: ["publishPrompt", prompt.id, currentPromptVersion.sha], - onSuccess: () => { - queryClient.invalidateQueries(["promptEnvironments"]); - }, - }); - - const handlePublish = async () => { - publishPromptMutation.mutate({ - promptId: prompt.id, - environmentSlug: selectedEnvironmentSlug, - promptVersionSha: currentPromptVersion.sha, - }); - }; - - useEffect(() => { - setSelectedEnvironmentSlug(undefined); - setSelectedEnvironmentName(undefined); - publishPromptMutation.reset(); - }, [open]); - - return ( - environments && ( - - <> -

Select the environment to publish this version to.

- {publishPromptMutation.isSuccess && ( - - )} - - {publishPromptMutation.isError && ( - - )} - - - ( - { - setSelectedEnvironmentSlug(env.slug); - setSelectedEnvironmentName(env.name); - }} - > - {env.name} - - - )} - /> - - -
- ) - ); -}; diff --git a/apps/console/src/app/components/prompts/views/PromptEditView.tsx b/apps/console/src/app/components/prompts/views/PromptEditView.tsx deleted file mode 100644 index a9a24da27..000000000 --- a/apps/console/src/app/components/prompts/views/PromptEditView.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { Form, Space, Button, Row, Col, Card } from "antd"; -import { PromptEditor } from "../PromptEditor"; -import { PromptSettings } from "../PromptSettings"; -import { - CodeOutlined, - ExperimentOutlined, - PlayCircleOutlined, - SendOutlined, -} from "@ant-design/icons"; -import { css } from "@emotion/css"; -import { - getDraftPromptData, - usePromptEdit, -} from "../../../lib/hooks/usePromptEdit"; -import { useCurrentPrompt } from "../../../lib/providers/CurrentPromptContext"; -import { useEffect, useState } from "react"; -import { PromptTester } from "../PromptTester/PromptTester"; -import { PromptVariables } from "../PromptVariables"; -import { usePromptTester } from "../../../lib/providers/PromptTesterContext"; -import { PromptVersionSelector } from "../PromptVersionSelector"; -import { CommitPromptModal } from "../CommitPromptModal"; -import { PublishPromptModal } from "../PublishPromptModal"; -import { ConsumePromptModal } from "../ConsumePromptModal"; - -export const PromptEditView = () => { - const { - form, - handleFormValuesChange, - isChangesToCommit, - variables, - setVariable, - } = usePromptEdit(); - const { prompt, currentPromptVersion, isDraft } = useCurrentPrompt(); - const { openTester, runTest, isTestInProgress } = usePromptTester(); - const [isCommitModalOpen, setIsCommitModalOpen] = useState(false); - const [isPublishModalOpen, setIsPublishModalOpen] = useState(false); - const [isConsumePromptModalOpen, setIsConsumePromptModalOpen] = - useState(false); - - useEffect(() => { - form.resetFields(); - }, [prompt.id, currentPromptVersion]); - - const handleTest = async () => { - await runTest({ - content: form.getFieldValue("content"), - settings: form.getFieldValue("settings"), - variables, - }); - openTester(); - }; - - const initialValues = { - settings: isDraft - ? getDraftPromptData(prompt.integrationId).settings - : currentPromptVersion.settings, - content: isDraft - ? getDraftPromptData(prompt.integrationId).content - : currentPromptVersion.content, - }; - - return ( - <> - setIsCommitModalOpen(false)} - onCommitted={() => { - form.resetFields(); - setIsCommitModalOpen(false); - }} - /> - {currentPromptVersion && ( - setIsPublishModalOpen(false)} - open={isPublishModalOpen} - /> - )} - - - setIsConsumePromptModalOpen(false)} - variables={variables} - /> - - - {!isDraft && } - - - - {currentPromptVersion && ( - - )} - - - - - - - -
- - -
- - - -
- - -
- - - - - - - - - -
- -
-
- - ); -}; diff --git a/apps/console/src/app/components/prompts/views/PromptHistoryView.tsx b/apps/console/src/app/components/prompts/views/PromptHistoryView.tsx deleted file mode 100644 index 29e189fc9..000000000 --- a/apps/console/src/app/components/prompts/views/PromptHistoryView.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { usePromptExecutions } from "../../../lib/hooks/usePromptExecutions"; -import { useCurrentPrompt } from "../../../lib/providers/CurrentPromptContext"; -import { Button, Space, Table, Tag, Tooltip } from "antd"; -import { PromptExecutionStatus } from "@pezzo/graphql"; -import { CheckCircleOutlined, CloseCircleOutlined } from "@ant-design/icons"; -import { InlineCodeSnippet } from "../../common/InlineCodeSnippet"; -import { PromptTester } from "../PromptTester/PromptTester"; -import { usePromptTester } from "../../../lib/providers/PromptTesterContext"; -import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; -import { css } from "@emotion/css"; - -export const PromptHistoryView = () => { - const { prompt } = useCurrentPrompt(); - const { promptExecutions } = usePromptExecutions(prompt.id); - const { openTester, loadTestResult } = usePromptTester(); - - const handleInspect = async (promptExecution) => { - loadTestResult(promptExecution); - openTester(); - }; - - if (!promptExecutions) { - return null; - } - - const columns = [ - { - title: "Timestamp", - dataIndex: "timestamp", - key: "timestamp", - render: (value) => {value}, - }, - { - title: "Status", - dataIndex: "status", - key: "status", - render: (value: PromptExecutionStatus) => - value === PromptExecutionStatus.Success ? ( - } color="success"> - Success - - ) : ( - } color="error"> - Error - - ), - }, - { - title: "Cost", - dataIndex: "totalCost", - key: "totalCost", - render: (value) => ${value}, - }, - { - title: "Duration", - dataIndex: "duration", - key: "duration", - render: (value) => {Math.ceil(value / 1000)}s, - }, - { - title: "Version", - dataIndex: "promptVersionSha", - key: "promptVersionSha", - render: (value) => ( - {value.substring(0, 7)} - ), - }, - { - render: (promptExecution) => ( -
- - -
- ), - }, - ]; - - const data = promptExecutions.map((data) => data); - - return ( - <> - - - - ); -}; diff --git a/apps/console/src/app/graphql/mutations/environments.tsx b/apps/console/src/app/graphql/mutations/environments.tsx deleted file mode 100644 index aa098a8c3..000000000 --- a/apps/console/src/app/graphql/mutations/environments.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { graphql } from "@pezzo/graphql"; - -export const CREATE_ENVIRONMENT = graphql(/* GraphQL */ ` - mutation CreateEnvironment($data: EnvironmentCreateInput!) { - createEnvironment(data: $data) { - slug - name - } - } -`); diff --git a/apps/console/src/app/graphql/mutations/prompts.tsx b/apps/console/src/app/graphql/mutations/prompts.tsx deleted file mode 100644 index c5cb93461..000000000 --- a/apps/console/src/app/graphql/mutations/prompts.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { graphql } from "@pezzo/graphql"; - -export const CREATE_PROMPT = graphql(/* GraphQL */ ` - mutation createPrompt($data: CreatePromptInput!) { - createPrompt(data: $data) { - id - } - } -`); - -export const CREATE_PROMPT_VERSION = graphql(/* GraphQL */ ` - mutation createPromptVersion($data: CreatePromptVersionInput!) { - createPromptVersion(data: $data) { - sha - } - } -`); - -export const UPDATE_PROMPT = graphql(/* GraphQL */ ` - mutation updatePrompt($data: PromptUpdateInput!) { - updatePrompt(data: $data) { - id - } - } -`); - -export const TEST_PROMPT = graphql(/* GraphQL */ ` - mutation testPrompt($data: TestPromptInput!) { - testPrompt(data: $data) { - id - timestamp - status - settings - result - duration - promptTokens - completionTokens - totalTokens - promptCost - completionCost - totalCost - error - content - interpolatedContent - variables - } - } -`); diff --git a/apps/console/src/app/graphql/mutations/provider-api-keys.tsx b/apps/console/src/app/graphql/mutations/provider-api-keys.tsx deleted file mode 100644 index 7b800663c..000000000 --- a/apps/console/src/app/graphql/mutations/provider-api-keys.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { graphql } from "@pezzo/graphql"; - -export const UPDATE_PROVIDER_API_KEY = graphql(/* GraphQL */ ` - mutation UpdateProviderAPIKey($data: CreateProviderAPIKeyInput!) { - updateProviderAPIKey(data: $data) { - provider - value - } - } -`); diff --git a/apps/console/src/app/graphql/queries/environments.tsx b/apps/console/src/app/graphql/queries/environments.tsx deleted file mode 100644 index 9d0936bc1..000000000 --- a/apps/console/src/app/graphql/queries/environments.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { graphql } from "@pezzo/graphql"; - -export const GET_ALL_ENVIRONMENTS = graphql(/* GraphQL */ ` - query Environments { - environments { - slug - name - } - } -`); - -export const GET_ENVIRONMENT = graphql(/* GraphQL */ ` - query Environment($data: EnvironmentWhereUniqueInput!) { - environment(data: $data) { - slug - name - } - } -`); diff --git a/apps/console/src/app/graphql/queries/prompt-executions.tsx b/apps/console/src/app/graphql/queries/prompt-executions.tsx deleted file mode 100644 index bdf5ad804..000000000 --- a/apps/console/src/app/graphql/queries/prompt-executions.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { graphql } from "@pezzo/graphql"; - -export const GET_PROMPT_EXECUTIONS = graphql(/* GraphQL */ ` - query getPromptExecutions($data: PromptExecutionWhereInput!) { - promptExecutions(data: $data) { - id - timestamp - status - settings - result - duration - promptTokens - completionTokens - totalTokens - promptCost - completionCost - totalCost - error - content - interpolatedContent - variables - promptVersionSha - } - } -`); - -export const GET_PROMPT_EXECUTION = graphql(/* GraphQL */ ` - query getPromptExecution($data: PromptExecutionWhereUniqueInput!) { - promptExecution(data: $data) { - id - timestamp - status - promptCost - completionCost - totalCost - promptTokens - completionTokens - totalTokens - duration - settings - variables - interpolatedContent - error - result - content - } - } -`); diff --git a/apps/console/src/app/graphql/queries/prompts.tsx b/apps/console/src/app/graphql/queries/prompts.tsx deleted file mode 100644 index c5e7e8881..000000000 --- a/apps/console/src/app/graphql/queries/prompts.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { graphql } from "@pezzo/graphql"; - -export const GET_ALL_PROMPTS = graphql(/* GraphQL */ ` - query getAllPrompts { - prompts { - id - name - integrationId - } - } -`); - -export const GET_PROMPT = graphql(/* GraphQL */ ` - query getPrompt($data: GetPromptInput!) { - prompt(data: $data) { - id - integrationId - name - latestVersion { - sha - content - settings - } - version(data: $data) { - sha - content - settings - } - } - } -`); - -export const GET_PROMPT_VERSIONS_LIST = graphql(/* GraphQL */ ` - query promptVersions($data: PromptWhereUniqueInput!) { - promptVersions(data: $data) { - sha - message - } - } -`); - -export const GET_PROMPT_VERSION = graphql(/* GraphQL */ ` - query getPromptVersion($data: GetPromptVersionInput!) { - promptVersion(data: $data) { - sha - content - settings - } - } -`); diff --git a/apps/console/src/app/graphql/queries/provider-api-keys.tsx b/apps/console/src/app/graphql/queries/provider-api-keys.tsx deleted file mode 100644 index 30bc674e8..000000000 --- a/apps/console/src/app/graphql/queries/provider-api-keys.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { graphql } from "@pezzo/graphql"; - -export const GET_ALL_PROVIDER_API_KEYS = graphql(/* GraphQL */ ` - query ProviderAPIKeys { - providerAPIKeys { - id - provider - value - } - } -`); diff --git a/apps/console/src/app/lib/graphql.ts b/apps/console/src/app/lib/graphql.ts deleted file mode 100644 index 34c28cb46..000000000 --- a/apps/console/src/app/lib/graphql.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { QueryClient } from "@tanstack/react-query"; -import { GraphQLClient } from "graphql-request"; -import { BASE_API_URL } from "../../env"; - -export const gqlClient = new GraphQLClient(BASE_API_URL, {}); - -export const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 5 * 1000, - }, - }, -}); diff --git a/apps/console/src/app/lib/hooks/useEnvironments.ts b/apps/console/src/app/lib/hooks/useEnvironments.ts deleted file mode 100644 index fa5afdbfe..000000000 --- a/apps/console/src/app/lib/hooks/useEnvironments.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { gqlClient } from "../graphql"; -import { GET_ALL_ENVIRONMENTS } from "../../graphql/queries/environments"; - -export const useEnvironments = () => { - const { data } = useQuery({ - queryKey: ["environments"], - queryFn: () => gqlClient.request(GET_ALL_ENVIRONMENTS), - }); - - return { - environments: data?.environments, - }; -}; diff --git a/apps/console/src/app/lib/hooks/usePromptEdit.ts b/apps/console/src/app/lib/hooks/usePromptEdit.ts deleted file mode 100644 index 96edefa86..000000000 --- a/apps/console/src/app/lib/hooks/usePromptEdit.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { OpenAIChatSettings } from "@pezzo/common"; -import { Form } from "antd"; -import { useEffect, useState } from "react"; -import { useCurrentPrompt } from "../providers/CurrentPromptContext"; -import { getIntegration } from "@pezzo/integrations"; - -export type PromptEditFormInputs = { - content: string; - settings: OpenAIChatSettings; -}; - -function findVariables(text: string): Record { - const regex = /\{\{([\w\s]+)\}\}/g; - const matches = text.match(regex); - const interpolatableValues = matches - ? matches.map((match) => match.replace(/[{}]/g, "")) - : []; - const uniqueValues = Array.from(new Set(interpolatableValues)); - const interpolatableObject: Record = {}; - uniqueValues.forEach((value) => { - interpolatableObject[value] = null; - }); - return interpolatableObject; -} - -export const getDraftPromptData = (integrationId: string) => { - return { - content: "Start typing your prompt here...", - settings: getIntegration(integrationId).defaultSettings, - }; -}; - -export const usePromptEdit = () => { - const { prompt, currentPromptVersion, isDraft } = useCurrentPrompt(); - const [form] = Form.useForm(); - const [isFormTouched, setIsFormTouched] = useState(false); - const [variables, setVariables] = useState<{ [key: string]: string }>({}); - const versionInitialSnapshot = isDraft - ? getDraftPromptData(prompt.integrationId) - : currentPromptVersion; - const [content, setContent] = useState( - versionInitialSnapshot.content - ); - - const handleFormValuesChange = () => { - const { content } = form.getFieldsValue(true); - const touched = form.isFieldsTouched(["content", "settings"]); - setIsFormTouched(touched); - setContent(content); - - const variables = findVariables(content); - setVariables(variables); - }; - - useEffect(() => { - if (content) { - const variables = findVariables(content); - setVariables(variables); - } - }, [content]); - - const setVariable = (key: string, value: string) => { - const newVariables = { ...variables }; - newVariables[key] = value; - setVariables(newVariables); - }; - - const isPromptChanged = () => { - const contentChanged = content !== versionInitialSnapshot.content; - const settingsChanged = - JSON.stringify(form.getFieldValue("settings")) !== - JSON.stringify(versionInitialSnapshot.settings); - return contentChanged || settingsChanged; - }; - - return { - form, - handleFormValuesChange, - isSaveDisabled: !isFormTouched, - isSaving: false, - isChangesToCommit: isPromptChanged(), - variables, - setVariable, - }; -}; diff --git a/apps/console/src/app/lib/hooks/usePromptExecutions.ts b/apps/console/src/app/lib/hooks/usePromptExecutions.ts deleted file mode 100644 index dba46452e..000000000 --- a/apps/console/src/app/lib/hooks/usePromptExecutions.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { gqlClient } from "../graphql"; -import { GET_PROMPT_EXECUTIONS } from "../../graphql/queries/prompt-executions"; - -export const usePromptExecutions = (promptId: string) => { - const { data: promptExecutions, isLoading } = useQuery({ - queryKey: ["promptExecutions", promptId], - queryFn: () => - gqlClient.request(GET_PROMPT_EXECUTIONS, { - data: { - promptId: { equals: promptId }, - }, - }), - enabled: !!promptId, - }); - - return { - isLoading, - promptExecutions: promptExecutions?.promptExecutions, - }; -}; diff --git a/apps/console/src/app/lib/providers/CurrentPromptContext.tsx b/apps/console/src/app/lib/providers/CurrentPromptContext.tsx deleted file mode 100644 index 161d1d4c6..000000000 --- a/apps/console/src/app/lib/providers/CurrentPromptContext.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { createContext, useContext, useState } from "react"; -import { gqlClient } from "../graphql"; -import { GET_PROMPT } from "../../graphql/queries/prompts"; -import { GetPromptQuery, GetPromptVersionQuery } from "@pezzo/graphql"; -import { useQuery } from "@tanstack/react-query"; -import { IntegrationDefinition, getIntegration } from "@pezzo/integrations"; - -interface CurrentPromptContextValue { - isDraft: boolean; - prompt: GetPromptQuery["prompt"]; - integration: IntegrationDefinition; - currentPromptVersion: GetPromptVersionQuery["promptVersion"]; - setCurrentPromptId: (promptId: string, version: string) => void; - isLoading: boolean; - setPromptVersion: (version: string) => void; -} - -const CurrentPromptContext = createContext({ - isDraft: undefined, - prompt: undefined, - integration: undefined, - currentPromptVersion: undefined, - setCurrentPromptId: () => void 0, - isLoading: false, - setPromptVersion: () => void 0, -}); - -export const useCurrentPrompt = () => { - return useContext(CurrentPromptContext); -}; - -export const CurrentPromptProvider = ({ children }) => { - const [promptId, setPromptId] = useState(undefined); - const [promptVersion, setPromptVersion] = useState( - undefined - ); - - const queryKey = ["prompt", promptId, promptVersion]; - - const { data: currentPrompt, isLoading } = useQuery({ - queryKey, - queryFn: () => - gqlClient.request(GET_PROMPT, { - data: { promptId, version: promptVersion }, - }), - enabled: !!promptId && !!promptVersion, - }); - - const value = { - isDraft: currentPrompt?.prompt.version === null, - prompt: currentPrompt?.prompt, - currentPromptVersion: currentPrompt?.prompt?.version, - integration: - currentPrompt?.prompt && - getIntegration(currentPrompt.prompt.integrationId), - setCurrentPromptId: (promptId: string, version: string) => { - setPromptId(promptId); - setPromptVersion(version); - }, - setPromptVersion, - isLoading, - }; - - return ( - - {children} - - ); -}; diff --git a/apps/console/src/app/lib/providers/PromptTesterContext.tsx b/apps/console/src/app/lib/providers/PromptTesterContext.tsx deleted file mode 100644 index 25d8f31e2..000000000 --- a/apps/console/src/app/lib/providers/PromptTesterContext.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { createContext, useContext, useState } from "react"; -import { OpenAIChatSettings } from "@pezzo/common"; -import { TEST_PROMPT } from "../../graphql/mutations/prompts"; -import { gqlClient } from "../graphql"; -import { GetPromptExecutionQuery, PromptExecution } from "@pezzo/graphql"; -import { TestPromptResult } from "@pezzo/client"; -import { useCurrentPrompt } from "./CurrentPromptContext"; - -export interface PromptTestInput { - content: string; - settings: OpenAIChatSettings; - variables: Record; -} - -interface PromptTesterContextValue { - openTester: () => void; - closeTester: () => void; - isTesterOpen: boolean; - runTest: (input: PromptTestInput) => Promise; - testResult: Partial; - isTestInProgress: boolean; - loadTestResult: ( - data: Partial - ) => void; -} - -const CurrentPromptContext = createContext({ - openTester: () => void 0, - closeTester: () => void 0, - isTesterOpen: false, - runTest: () => void 0, - testResult: undefined, - isTestInProgress: false, - loadTestResult: () => void 0, -}); - -export const usePromptTester = () => { - return useContext(CurrentPromptContext); -}; - -export const PromptTesterProvider = ({ children }) => { - const [isTesterOpen, setIsTesterOpen] = useState(false); - const [testResult, setTestResult] = - useState>(undefined); - const [isTestInProgress, setIsTestInProgress] = useState(false); - const { integration } = useCurrentPrompt(); - - const value = { - isTesterOpen, - setIsTesterOpen, - openTester: () => { - setIsTesterOpen(true); - }, - closeTester: () => { - setTestResult(null); - setIsTesterOpen(false); - }, - runTest: async (input: PromptTestInput) => { - setIsTestInProgress(true); - const result = await gqlClient.request(TEST_PROMPT, { - data: { - integrationId: integration.id, - content: input.content, - settings: input.settings, - variables: input.variables, - }, - }); - setTestResult(result.testPrompt); - setIsTestInProgress(false); - }, - testResult, - isTestInProgress: isTestInProgress, - loadTestResult: (data: Partial) => { - setTestResult(data); - }, - }; - - return ( - - {children} - - ); -}; diff --git a/apps/console/src/app/lib/providers/ThemeProvider.tsx b/apps/console/src/app/lib/providers/ThemeProvider.tsx deleted file mode 100644 index 72cafce20..000000000 --- a/apps/console/src/app/lib/providers/ThemeProvider.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { ConfigProvider } from "antd"; -import { antTheme } from "../../lib/theme/ant-theme"; - -interface Props { - children: React.ReactNode; -} - -export const ThemeProvider = ({ children }: Props) => ( - {children} -); diff --git a/apps/console/src/app/lib/theme/ant-theme.ts b/apps/console/src/app/lib/theme/ant-theme.ts deleted file mode 100644 index 738342bad..000000000 --- a/apps/console/src/app/lib/theme/ant-theme.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ThemeConfig, theme } from "antd"; -import { colors } from "./colors"; - -export const antTheme: ThemeConfig = { - algorithm: theme.darkAlgorithm, - token: { - colorPrimary: colors.indigo["400"], - fontFamily: "Inter", - }, -}; diff --git a/apps/console/src/app/pages/api-keys/index.tsx b/apps/console/src/app/pages/api-keys/index.tsx deleted file mode 100644 index 43f958dd4..000000000 --- a/apps/console/src/app/pages/api-keys/index.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Space, Typography } from "antd"; -import { integrations } from "@pezzo/integrations"; -import styled from "@emotion/styled"; -import { useQuery } from "@tanstack/react-query"; -import { gqlClient } from "../../lib/graphql"; -import { GET_ALL_PROVIDER_API_KEYS } from "../../graphql/queries/provider-api-keys"; -import { APIKeyListItem } from "../../components/api-keys/APIKeyListItem"; - -export const APIKeysPage = () => { - const providers = Object.values(integrations).map((integration) => ({ - name: integration.name, - iconBase64: integration.iconBase64, - provider: integration.provider, - })); - - const { data } = useQuery({ - queryKey: ["providerAPIKeys"], - queryFn: () => gqlClient.request(GET_ALL_PROVIDER_API_KEYS), - }); - - const renderAPIKey = (provider) => { - const apiKey = data.providerAPIKeys.find( - (key) => key.provider === provider.provider - ); - - return ( - - ); - }; - - if (!data) { - return null; - } - - return ( - <> - Provider API Keys - - - In order to be able to test your prompts within the Pezzo Console, you - must provide an API key for each provider you wish to test. This is - optional. - - - - {providers.map((item, index) => renderAPIKey(item))} - - - ); -}; diff --git a/apps/console/src/app/pages/environments/index.tsx b/apps/console/src/app/pages/environments/index.tsx deleted file mode 100644 index 22be091d0..000000000 --- a/apps/console/src/app/pages/environments/index.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { PlusOutlined } from "@ant-design/icons"; -import { useQuery } from "@tanstack/react-query"; -import { Button, List, Typography } from "antd"; -import { InlineCodeSnippet } from "../../components/common/InlineCodeSnippet"; -import { CreateEnvironmentModal } from "../../components/environments/CreateEnvironmentModal"; -import { GET_ALL_ENVIRONMENTS } from "../../graphql/queries/environments"; -import { gqlClient } from "../../lib/graphql"; -import { useState } from "react"; - -export const EnvironmentsPage = () => { - const [isCreateEnvironmentModalOpen, setIsCreateEnvironmentModalOpen] = - useState(false); - - const { data } = useQuery({ - queryKey: ["environments"], - queryFn: () => gqlClient.request(GET_ALL_ENVIRONMENTS), - }); - - return ( - <> - setIsCreateEnvironmentModalOpen(false)} - onCreated={() => setIsCreateEnvironmentModalOpen(false)} - /> - Environments -
- -
- - {data?.environments && ( - ( - - - {item.name} {item.slug} - - - )} - /> - )} - - ); -}; diff --git a/apps/console/src/app/pages/prompts/[promptId]/edit.tsx b/apps/console/src/app/pages/prompts/[promptId]/edit.tsx deleted file mode 100644 index b319dc68b..000000000 --- a/apps/console/src/app/pages/prompts/[promptId]/edit.tsx +++ /dev/null @@ -1,5 +0,0 @@ -const EditPromptPage = () => { - return
Edit
; -}; - -export default EditPromptPage; diff --git a/apps/console/src/app/pages/prompts/[promptId]/index.tsx b/apps/console/src/app/pages/prompts/[promptId]/index.tsx deleted file mode 100644 index 469c6e686..000000000 --- a/apps/console/src/app/pages/prompts/[promptId]/index.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { Breadcrumb, Button, Col, Row, Space, Tabs, Typography } from "antd"; -import { - EditOutlined, - HistoryOutlined, - DeleteOutlined, -} from "@ant-design/icons"; -import styled from "@emotion/styled"; -import { useEffect, useState } from "react"; -import { PromptHistoryView } from "../../../components/prompts/views/PromptHistoryView"; -import { PromptEditView } from "../../../components/prompts/views/PromptEditView"; -import { css } from "@emotion/css"; -import { DeletePromptConfirmationModal } from "../../../components/prompts/DeletePromptConfirmationModal"; -import { useCurrentPrompt } from "../../../lib/providers/CurrentPromptContext"; -import { useNavigate, useParams } from "react-router-dom"; -import { IntegrationDefinition } from "@pezzo/integrations"; - -const TabLabel = styled.div` - display: inline-block; - padding-left: 10px; - padding-right: 10px; -`; - -const BreadcrumbTitle = styled.span` - cursor: pointer; -`; - -export const PromptPage = () => { - const navigate = useNavigate(); - const params = useParams(); - const { setCurrentPromptId, prompt, integration } = useCurrentPrompt(); - const [activeView, setActiveView] = useState("edit"); - const [isDeleteConfirmationModalOpen, setIsDeleteConfirmationModalOpen] = - useState(false); - - useEffect(() => { - if (params.promptId) { - setCurrentPromptId(params.promptId as string, "latest"); - } - }, [params.promptId]); - const tabs = [ - { - label: ( - - Edit - - ), - key: "edit", - }, - { - label: ( - - History - - ), - key: "history", - }, - ]; - - return ( - prompt && ( - <> - setIsDeleteConfirmationModalOpen(false)} - onConfirm={() => setIsDeleteConfirmationModalOpen(false)} - /> - -
- Prompts, - onClick: () => navigate("/prompts"), - }, - { - title: ( -
- - - {prompt.name} - -
- ), - key: "prompt", - }, - ]} - /> - -
- - - - - - setActiveView(selectedView)} - > - - {activeView === "history" && } - {activeView === "edit" && } - - ) - ); -}; diff --git a/apps/console/src/app/pages/prompts/index.tsx b/apps/console/src/app/pages/prompts/index.tsx deleted file mode 100644 index bf261cf36..000000000 --- a/apps/console/src/app/pages/prompts/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { PromptListItem } from "../../components/prompts/PromptListItem"; -import { GET_ALL_PROMPTS } from "../../graphql/queries/prompts"; -import { gqlClient } from "../../lib/graphql"; -import { PlusOutlined } from "@ant-design/icons"; -import { CreatePromptModal } from "../../components/prompts/CreatePromptModal"; -import { useState } from "react"; -import { css } from "@emotion/css"; -import { Button, Typography } from "antd"; -import { useNavigate } from "react-router-dom"; - -export const PromptsPage = () => { - const navigate = useNavigate(); - const [isCreatePromptModalOpen, setIsCreatePromptModalOpen] = useState(false); - const { data, isLoading } = useQuery({ - queryKey: ["prompts"], - queryFn: () => gqlClient.request(GET_ALL_PROMPTS), - }); - - return ( - <> - setIsCreatePromptModalOpen(false)} - onCreated={(id) => navigate(`/prompts/${id}`)} - /> - - Prompts - {isLoading &&

Loading...

} - - {data && ( -
-
- -
- {data.prompts.map((prompt) => ( -
- navigate(`/prompts/${prompt.id}`)} - /> -
- ))} -
- )} - - ); -}; diff --git a/apps/console/src/app/styles.css b/apps/console/src/app/styles.css deleted file mode 100644 index 396b3a8ef..000000000 --- a/apps/console/src/app/styles.css +++ /dev/null @@ -1,10 +0,0 @@ -@import url("https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@200;300;400;500;600;700&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700&display=swap"); -@tailwind utilities; - -html, -body { - font-family: "Inter", sans-serif; - font-size: 16px; - color: theme("colors.slate.200"); -} diff --git a/apps/console/src/assets/blurry-blurb-2.svg b/apps/console/src/assets/blurry-blurb-2.svg new file mode 100644 index 000000000..3ee504a4c --- /dev/null +++ b/apps/console/src/assets/blurry-blurb-2.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/console/src/assets/blurry-blurb.svg b/apps/console/src/assets/blurry-blurb.svg new file mode 100644 index 000000000..3c5fe1061 --- /dev/null +++ b/apps/console/src/assets/blurry-blurb.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/apps/console/src/assets/favicon/android-chrome-192x192.png b/apps/console/src/assets/favicon/android-chrome-192x192.png new file mode 100644 index 000000000..bc6c63fad Binary files /dev/null and b/apps/console/src/assets/favicon/android-chrome-192x192.png differ diff --git a/apps/console/src/assets/favicon/android-chrome-384x384.png b/apps/console/src/assets/favicon/android-chrome-384x384.png new file mode 100644 index 000000000..86477cb02 Binary files /dev/null and b/apps/console/src/assets/favicon/android-chrome-384x384.png differ diff --git a/apps/console/src/assets/favicon/android-chrome-96x96.png b/apps/console/src/assets/favicon/android-chrome-96x96.png deleted file mode 100644 index f41888515..000000000 Binary files a/apps/console/src/assets/favicon/android-chrome-96x96.png and /dev/null differ diff --git a/apps/console/src/assets/favicon/apple-touch-icon.png b/apps/console/src/assets/favicon/apple-touch-icon.png index 951d5345b..05d40e54e 100644 Binary files a/apps/console/src/assets/favicon/apple-touch-icon.png and b/apps/console/src/assets/favicon/apple-touch-icon.png differ diff --git a/apps/console/src/assets/favicon/favicon-16x16.png b/apps/console/src/assets/favicon/favicon-16x16.png index e84b9deca..cc27dfc98 100644 Binary files a/apps/console/src/assets/favicon/favicon-16x16.png and b/apps/console/src/assets/favicon/favicon-16x16.png differ diff --git a/apps/console/src/assets/favicon/favicon-32x32.png b/apps/console/src/assets/favicon/favicon-32x32.png index 1b8880f6a..31086f016 100644 Binary files a/apps/console/src/assets/favicon/favicon-32x32.png and b/apps/console/src/assets/favicon/favicon-32x32.png differ diff --git a/apps/console/src/assets/favicon/favicon.ico b/apps/console/src/assets/favicon/favicon.ico index 087ce999c..c3830248f 100644 Binary files a/apps/console/src/assets/favicon/favicon.ico and b/apps/console/src/assets/favicon/favicon.ico differ diff --git a/apps/console/src/assets/favicon/mstile-150x150.png b/apps/console/src/assets/favicon/mstile-150x150.png index 1bde0351c..c82c549d6 100644 Binary files a/apps/console/src/assets/favicon/mstile-150x150.png and b/apps/console/src/assets/favicon/mstile-150x150.png differ diff --git a/apps/console/src/assets/favicon/safari-pinned-tab.svg b/apps/console/src/assets/favicon/safari-pinned-tab.svg index 9f0487aad..ccc24de9f 100644 --- a/apps/console/src/assets/favicon/safari-pinned-tab.svg +++ b/apps/console/src/assets/favicon/safari-pinned-tab.svg @@ -2,16 +2,52 @@ Created by potrace 1.14, written by Peter Selinger 2001-2017 - - + + + diff --git a/apps/console/src/assets/favicon/site.webmanifest b/apps/console/src/assets/favicon/site.webmanifest index 64aeb5a29..161c642e0 100644 --- a/apps/console/src/assets/favicon/site.webmanifest +++ b/apps/console/src/assets/favicon/site.webmanifest @@ -3,8 +3,13 @@ "short_name": "", "icons": [ { - "src": "/android-chrome-96x96.png", - "sizes": "96x96", + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-384x384.png", + "sizes": "384x384", "type": "image/png" } ], diff --git a/apps/console/src/assets/icons/google.svg b/apps/console/src/assets/icons/google.svg new file mode 100644 index 000000000..088288fa3 --- /dev/null +++ b/apps/console/src/assets/icons/google.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/console/src/assets/logo-loader.json b/apps/console/src/assets/logo-loader.json new file mode 100644 index 000000000..2924cf605 --- /dev/null +++ b/apps/console/src/assets/logo-loader.json @@ -0,0 +1,1122 @@ +{ + "assets": [ + { + "id": "emog8Q2RYtAYjLvWYonxL", + "layers": [ + { + "ddd": 0, + "ind": 2, + "ty": 4, + "ln": "layer_2", + "sr": 1, + "ks": { + "a": { "a": 0, "k": [103.88, 138.005] }, + "o": { "a": 0, "k": 100 }, + "p": { "a": 0, "k": [900, 917.75] }, + "r": { "a": 0, "k": 0 }, + "s": { "a": 0, "k": [133.33, 133.33] }, + "sk": { "a": 0, "k": 0 }, + "sa": { "a": 0, "k": 0 } + }, + "ao": 0, + "ip": 0, + "op": 60, + "st": 0, + "bm": 0, + "shapes": [ + { + "ty": "gr", + "nm": "surface271", + "it": [ + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "i": [ + [0, 0], + [0, 0], + [0, 0], + [0, 1.2], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, -2.58], + [0, 0], + [-0.04, -0.19], + [-1.08, -0.54], + [0, 0], + [0, 0], + [0, 0], + [-0.06, -0.03], + [0, 0], + [0, 2.24], + [0, 0], + [0, 0.16], + [0, 0], + [0, 0], + [-1.8, 0], + [-1.12, 0.38] + ], + "o": [ + [0, 0], + [0, 0], + [1.11, -0.47], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [-2.31, -1.15], + [0, 0], + [0, 0.2], + [0.19, 1.17], + [0, 0], + [0, 0], + [0, 0], + [0.06, 0.03], + [0, 0], + [2, 1.01], + [0, 0], + [0.02, -0.15], + [0, 0], + [0, 0], + [0.5, 0.38], + [1.2, 0.3], + [0, 0] + ], + "v": [ + [144.75, 231.75], + [202.32, 203.31], + [205.92, 201.78], + [207.75, 199.02], + [207.75, 35.25], + [140.63, 67.13], + [84.07, 39.83], + [5.02, 0.41], + [0, 3.52], + [0, 232.84], + [0.05, 233.43], + [2.06, 236.16], + [65.63, 268.23], + [65.63, 268.25], + [78.59, 274.78], + [78.78, 274.87], + [79.65, 275.3], + [84, 272.63], + [84, 271.9], + [84.03, 271.45], + [84.34, 205.9], + [136.13, 231.38], + [139.88, 232.5], + [144.75, 231.75] + ] + } + } + }, + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "i": [ + [0, 0], + [0, 0], + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0], + [0, 0], + [0, 0] + ], + "v": [ + [65.63, 50.82], + [18.07, 27.1], + [18.07, 223.91], + [65.63, 247.69] + ] + } + } + }, + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "i": [ + [0, 0], + [0, 0], + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0], + [0, 0], + [0, 0] + ], + "v": [ + [85.04, 60.18], + [84.43, 186.21], + [130.5, 208.78], + [130.5, 82.12] + ] + } + } + }, + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "i": [ + [0, 0], + [0, 0], + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0], + [0, 0], + [0, 0] + ], + "v": [ + [148.51, 83.26], + [148.86, 209.06], + [192.7, 187.98], + [192.41, 62.41] + ] + } + } + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.44, 0.82, 0.6, 1] }, + "r": 2, + "o": { "a": 0, "k": 100 } + }, + { + "ty": "tr", + "nm": "Transform", + "a": { "a": 0, "k": [0, 0] }, + "o": { "a": 0, "k": 100 }, + "p": { "a": 0, "k": [0, 0] }, + "r": { "a": 0, "k": 0 }, + "s": { "a": 0, "k": [100, 100] }, + "sk": { "a": 0, "k": 0 }, + "sa": { "a": 0, "k": 0 } + } + ] + }, + { + "ty": "tr", + "nm": "Transform", + "a": { "a": 0, "k": [0, 0] }, + "o": { "a": 0, "k": 100 }, + "p": { "a": 0, "k": [0, 0] }, + "r": { "a": 0, "k": 0 }, + "s": { "a": 0, "k": [100, 100] }, + "sk": { "a": 0, "k": 0 }, + "sa": { "a": 0, "k": 0 } + } + ] + } + ] + }, + { + "ddd": 0, + "ind": 3, + "ty": 4, + "ln": "layer_3", + "sr": 1, + "ks": { + "a": { "a": 0, "k": [70.125, 37.125] }, + "o": { "a": 0, "k": 100 }, + "p": { "a": 0, "k": [945, 747.75] }, + "r": { "a": 0, "k": 0 }, + "s": { "a": 0, "k": [133.33, 133.33] }, + "sk": { "a": 0, "k": 0 }, + "sa": { "a": 0, "k": 0 } + }, + "ao": 0, + "ip": 0, + "op": 60, + "st": 0, + "bm": 0, + "shapes": [ + { + "ty": "gr", + "nm": "surface276", + "it": [ + { + "ty": "gr", + "it": [ + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "i": [ + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [-2.48, -1.13], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, -2.73], + [0, 0], + [0, 0] + ], + "v": [ + [116.25, 73.88], + [18.38, 28.73], + [18.38, 60.38], + [0, 51.38], + [0, 4.32], + [5.3, 0.91], + [140.25, 62.25] + ] + } + } + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.31, 0.67, 0.47, 1] }, + "r": 1, + "o": { "a": 0, "k": 100 } + }, + { + "ty": "tr", + "nm": "Transform", + "a": { "a": 0, "k": [0, 0] }, + "o": { "a": 0, "k": 100 }, + "p": { "a": 0, "k": [0, 0] }, + "r": { "a": 0, "k": 0 }, + "s": { "a": 0, "k": [100, 100] }, + "sk": { "a": 0, "k": 0 }, + "sa": { "a": 0, "k": 0 } + } + ] + }, + { + "ty": "tr", + "nm": "Transform", + "a": { "a": 0, "k": [0, 0] }, + "o": { "a": 0, "k": 100 }, + "p": { "a": 0, "k": [0, 0] }, + "r": { "a": 0, "k": 0 }, + "s": { "a": 0, "k": [100, 100] }, + "sk": { "a": 0, "k": 0 }, + "sa": { "a": 0, "k": 0 } + } + ] + }, + { + "ty": "tr", + "nm": "Transform", + "a": { "a": 0, "k": [0, 0] }, + "o": { "a": 0, "k": 100 }, + "p": { "a": 0, "k": [0, 0] }, + "r": { "a": 0, "k": 0 }, + "s": { "a": 0, "k": [100, 100] }, + "sk": { "a": 0, "k": 0 }, + "sa": { "a": 0, "k": 0 } + } + ] + } + ] + } + ] + } + ], + "ddd": 0, + "fr": 60, + "h": 450, + "ip": 0, + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 0, + "nm": "", + "ln": "precomp_5MmXcI2RP4xfzjgwDmcd71", + "sr": 1, + "ks": { + "a": { "a": 0, "k": [900, 900] }, + "o": { + "a": 1, + "k": [ + { + "t": 0, + "s": [50], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 1, + "s": [51.67], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 2, + "s": [53.34], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 3, + "s": [55.01], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 4, + "s": [56.68], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 5, + "s": [58.35], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 6, + "s": [60.02], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 7, + "s": [61.69], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 8, + "s": [63.36], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 9, + "s": [65.03], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 10, + "s": [66.7], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 11, + "s": [68.37], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 12, + "s": [70.04], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 13, + "s": [71.71], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 14, + "s": [73.38], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 15, + "s": [75.05], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 16, + "s": [76.72], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 17, + "s": [78.39], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 18, + "s": [80.06], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 19, + "s": [81.73], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 20, + "s": [83.4], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 21, + "s": [85.07], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 22, + "s": [86.74], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 23, + "s": [88.41], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 24, + "s": [90.08], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 25, + "s": [91.75], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 26, + "s": [93.42], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 27, + "s": [95.09], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 28, + "s": [96.76], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 29, + "s": [98.43], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 30, + "s": [99.9], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 31, + "s": [98.23], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 32, + "s": [96.56], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 33, + "s": [94.89], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 34, + "s": [93.22], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 35, + "s": [91.55], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 36, + "s": [89.88], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 37, + "s": [88.21], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 38, + "s": [86.54], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 39, + "s": [84.87], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 40, + "s": [83.2], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 41, + "s": [81.53], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 42, + "s": [79.86], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 43, + "s": [78.19], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 44, + "s": [76.52], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 45, + "s": [74.85], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 46, + "s": [73.18], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 47, + "s": [71.51], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 48, + "s": [69.84], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 49, + "s": [68.17], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 50, + "s": [66.5], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 51, + "s": [64.83], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 52, + "s": [63.16], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 53, + "s": [61.49], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 54, + "s": [59.82], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 55, + "s": [58.15], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 56, + "s": [56.48], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 57, + "s": [54.81], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 58, + "s": [53.14], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 59, + "s": [51.47], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + } + ] + }, + "p": { "a": 0, "k": [225, 225] }, + "r": { "a": 0, "k": 0 }, + "s": { + "a": 1, + "k": [ + { + "t": 0, + "s": [95, 95], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 1, + "s": [95.1, 95.1], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 2, + "s": [95.25, 95.25], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 3, + "s": [95.48, 95.48], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 4, + "s": [95.76, 95.76], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 5, + "s": [96.11, 96.11], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 6, + "s": [96.48, 96.48], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 7, + "s": [96.86, 96.86], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 8, + "s": [97.23, 97.23], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 9, + "s": [97.57, 97.57], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 10, + "s": [97.89, 97.89], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 11, + "s": [98.17, 98.17], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 12, + "s": [98.42, 98.42], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 13, + "s": [98.64, 98.64], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 14, + "s": [98.84, 98.84], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 15, + "s": [99.02, 99.02], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 16, + "s": [99.17, 99.17], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 17, + "s": [99.31, 99.31], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 18, + "s": [99.43, 99.43], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 19, + "s": [99.54, 99.54], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 20, + "s": [99.63, 99.63], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 21, + "s": [99.71, 99.71], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 22, + "s": [99.77, 99.77], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 23, + "s": [99.83, 99.83], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 24, + "s": [99.88, 99.88], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 25, + "s": [99.92, 99.92], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 26, + "s": [99.95, 99.95], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 27, + "s": [99.97, 99.97], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 28, + "s": [99.99, 99.99], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 29, + "s": [100, 100], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 30, + "s": [100, 100], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 31, + "s": [99.9, 99.9], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 32, + "s": [99.74, 99.74], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 33, + "s": [99.51, 99.51], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 34, + "s": [99.22, 99.22], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 35, + "s": [98.87, 98.87], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 36, + "s": [98.5, 98.5], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 37, + "s": [98.12, 98.12], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 38, + "s": [97.75, 97.75], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 39, + "s": [97.41, 97.41], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 40, + "s": [97.1, 97.1], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 41, + "s": [96.82, 96.82], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 42, + "s": [96.57, 96.57], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 43, + "s": [96.35, 96.35], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 44, + "s": [96.15, 96.15], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 45, + "s": [95.97, 95.97], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 46, + "s": [95.82, 95.82], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 47, + "s": [95.68, 95.68], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 48, + "s": [95.56, 95.56], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 49, + "s": [95.46, 95.46], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 50, + "s": [95.37, 95.37], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 51, + "s": [95.29, 95.29], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 52, + "s": [95.22, 95.22], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 53, + "s": [95.16, 95.16], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 54, + "s": [95.12, 95.12], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 55, + "s": [95.08, 95.08], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 56, + "s": [95.05, 95.05], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 57, + "s": [95.03, 95.03], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 58, + "s": [95.01, 95.01], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + }, + { + "t": 59, + "s": [95, 95], + "i": { "x": 0, "y": 0 }, + "o": { "x": 1, "y": 1 } + } + ] + }, + "sk": { "a": 0, "k": 0 }, + "sa": { "a": 0, "k": 0 } + }, + "ao": 0, + "w": 1800, + "h": 1800, + "ip": 0, + "op": 60, + "st": 0, + "bm": 0, + "refId": "emog8Q2RYtAYjLvWYonxL" + } + ], + "meta": { "g": "https://jitter.video" }, + "nm": "New-file-[copy]", + "op": 60, + "v": "5.7.4", + "w": 450 +} diff --git a/apps/console/src/assets/logo-square.svg b/apps/console/src/assets/logo-square.svg index 187e62ab6..000c79c0c 100644 --- a/apps/console/src/assets/logo-square.svg +++ b/apps/console/src/assets/logo-square.svg @@ -1,4 +1,21 @@ - - - + + + + + + + + + + + + + + + + + + + + diff --git a/apps/console/src/assets/logo-vertical.svg b/apps/console/src/assets/logo-vertical.svg new file mode 100644 index 000000000..93bab1759 --- /dev/null +++ b/apps/console/src/assets/logo-vertical.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/console/src/assets/logo.svg b/apps/console/src/assets/logo.svg index ff61e8351..9f02c5b00 100644 --- a/apps/console/src/assets/logo.svg +++ b/apps/console/src/assets/logo.svg @@ -1,9 +1,22 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/console/src/assets/openai-logo-white.svg b/apps/console/src/assets/openai-logo-white.svg deleted file mode 100644 index d264c1100..000000000 --- a/apps/console/src/assets/openai-logo-white.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/apps/console/src/assets/providers/anthropic-logo.png b/apps/console/src/assets/providers/anthropic-logo.png new file mode 100644 index 000000000..60dea32b7 Binary files /dev/null and b/apps/console/src/assets/providers/anthropic-logo.png differ diff --git a/apps/console/src/assets/providers/azure-logo.png b/apps/console/src/assets/providers/azure-logo.png new file mode 100644 index 000000000..66ebdbbdf Binary files /dev/null and b/apps/console/src/assets/providers/azure-logo.png differ diff --git a/apps/console/src/assets/providers/meta-logo.png b/apps/console/src/assets/providers/meta-logo.png new file mode 100644 index 000000000..90bfc0e1a Binary files /dev/null and b/apps/console/src/assets/providers/meta-logo.png differ diff --git a/apps/console/src/assets/providers/mistral-logo.png b/apps/console/src/assets/providers/mistral-logo.png new file mode 100644 index 000000000..ff9cbaaae Binary files /dev/null and b/apps/console/src/assets/providers/mistral-logo.png differ diff --git a/apps/console/src/assets/providers/openai-logo.png b/apps/console/src/assets/providers/openai-logo.png new file mode 100644 index 000000000..c608558c6 Binary files /dev/null and b/apps/console/src/assets/providers/openai-logo.png differ diff --git a/apps/console/src/assets/typescript-logo.svg b/apps/console/src/assets/typescript-logo.svg new file mode 100644 index 000000000..a46d53d49 --- /dev/null +++ b/apps/console/src/assets/typescript-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/console/src/components/api-keys/PezzoApiKeyListItem.tsx b/apps/console/src/components/api-keys/PezzoApiKeyListItem.tsx new file mode 100644 index 000000000..01e9fe777 --- /dev/null +++ b/apps/console/src/components/api-keys/PezzoApiKeyListItem.tsx @@ -0,0 +1,28 @@ +import { useCopyToClipboard } from "usehooks-ts"; +import { trackEvent } from "~/lib/utils/analytics"; +import { Button, Card } from "@pezzo/ui"; +import { CopyIcon } from "lucide-react"; + +interface Props { + value: string; +} + +export const PezzoApiKeyListItem = ({ value }: Props) => { + const [copied, copy] = useCopyToClipboard(); + + const onCopy = () => { + copy(value); + trackEvent("organization_api_key_copied"); + }; + + return ( + +
+
{value}
+ +
+ + ); +}; diff --git a/apps/console/src/components/api-keys/ProviderApiKeyListItem.tsx b/apps/console/src/components/api-keys/ProviderApiKeyListItem.tsx new file mode 100644 index 000000000..18d3739a1 --- /dev/null +++ b/apps/console/src/components/api-keys/ProviderApiKeyListItem.tsx @@ -0,0 +1,199 @@ +import { useState } from "react"; +import { useMutation } from "@tanstack/react-query"; +import { UPDATE_PROVIDER_API_KEY } from "~/graphql/definitions/mutations/api-keys"; +import { gqlClient, queryClient } from "~/lib/graphql"; +import { CreateProviderApiKeyInput } from "~/@generated/graphql/graphql"; +import { useEffect } from "react"; +import { useCurrentOrganization } from "~/lib/hooks/useCurrentOrganization"; +import { trackEvent } from "~/lib/utils/analytics"; +import { providersList } from "./providers-list"; +import { useDeleteProviderApiKeyMutation } from "~/graphql/hooks/mutations"; +import { + Card, + Button, + Form, + FormField, + FormItem, + FormControl, + Input, + useToast, +} from "@pezzo/ui"; +import { PencilIcon, SaveIcon, TrashIcon, XIcon } from "lucide-react"; +import { GenericDestructiveConfirmationModal } from "../common/GenericDestructiveConfirmationModal"; +import z from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; + +interface Props { + provider: string; + value: string | null; + onSave?: () => void; + initialIsEditing?: boolean; + canCancelEdit?: boolean; +} + +const formSchema = z.strictObject({ + apiKey: z.string().min(1, "API Key must be at least 1 character long"), +}); + +export const ProviderApiKeyListItem = ({ + provider, + value, + onSave, + initialIsEditing = false, + canCancelEdit = true, +}: Props) => { + const { toast } = useToast(); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + apiKey: value, + }, + }); + + const { currentOrgId } = useCurrentOrganization(); + const { mutate: deleteProviderApiKey } = useDeleteProviderApiKeyMutation(); + const [deletingProviderApiKey, setDeletingProviderApiKey] = + useState(null); + const updateKeyMutation = useMutation({ + mutationFn: (data: CreateProviderApiKeyInput) => + gqlClient.request(UPDATE_PROVIDER_API_KEY, { + data: { + provider: data.provider, + value: data.value, + organizationId: data.organizationId, + }, + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["providerApiKeys"] }); + onSave && onSave(); + setIsEditing(false); + toast({ + title: "API key saved!", + description: `The provider API key has been saved successfully.`, + }); + form.reset(); + }, + }); + + const handleDeleteProvider = async (provider: string) => { + deleteProviderApiKey( + { provider, organizationId: currentOrgId }, + { + onSuccess: () => { + trackEvent("provider_api_key_deleted", { provider }); + setDeletingProviderApiKey(null); + toast({ + title: "API key deleted!", + description: `The provider API key has been deleted successfully.`, + }); + form.reset(); + }, + } + ); + }; + + const [isEditing, setIsEditing] = useState(initialIsEditing); + + useEffect(() => { + form.reset(); + }, [isEditing, form]); + + const onSubmit = (values: z.infer) => { + updateKeyMutation.mutate( + { + provider, + value: values.apiKey, + organizationId: currentOrgId, + }, + { + onSuccess: () => { + trackEvent("provider_api_key_set", { provider }); + setIsEditing(false); + }, + } + ); + }; + + const iconBase64 = providersList.find( + (item) => item.provider === provider + ).iconBase64; + + return ( + <> + handleDeleteProvider(deletingProviderApiKey)} + onCancel={() => setDeletingProviderApiKey(null)} + /> + +
+ + +
+ +
{provider}
+ {isEditing ? ( +
+ ( + + + + + + )} + /> +
+ ) : ( +
+ {value || "No API key provided"} +
+ )} + {!isEditing && ( + + )} + {isEditing && ( + + )} + {isEditing && canCancelEdit && ( + + )} + {!isEditing && value && ( + + )} +
+
+ + + + ); +}; diff --git a/apps/console/src/components/api-keys/ProviderApiKeysList.tsx b/apps/console/src/components/api-keys/ProviderApiKeysList.tsx new file mode 100644 index 000000000..da456078e --- /dev/null +++ b/apps/console/src/components/api-keys/ProviderApiKeysList.tsx @@ -0,0 +1,33 @@ +import { ProviderApiKeyListItem } from "./ProviderApiKeyListItem"; +import { useProviderApiKeys } from "~/graphql/hooks/queries"; +import { providersList } from "./providers-list"; + +export const ProviderApiKeysList = () => { + const { providerApiKeys } = useProviderApiKeys(); + + const renderProviderApiKey = (provider) => { + const apiKey = providerApiKeys.find( + (key) => key.provider === provider.provider + ); + + const value = apiKey?.censoredValue + ? `**********${apiKey?.censoredValue}` + : null; + + return ( + + ); + }; + + return ( + providerApiKeys && ( +
+ {providersList.map((item) => renderProviderApiKey(item))} +
+ ) + ); +}; diff --git a/apps/console/src/components/api-keys/providers-list.tsx b/apps/console/src/components/api-keys/providers-list.tsx new file mode 100644 index 000000000..b060ccf28 --- /dev/null +++ b/apps/console/src/components/api-keys/providers-list.tsx @@ -0,0 +1,8 @@ +export const providersList = [ + { + name: "OpenAI", + provider: "OpenAI", + iconBase64: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAABe6SURBVHgB7Z1NbFZVGscPooWKjLQOBFoDaCkL2hlhBCQoHRa2C0wAFwI7mJ0uJa5mNq6cpbOThcnIYhLKxpEJLGiTgSIEaFVMWhcWKjXTQiC0EESwLpzzv+0xl/r29px7nvNx731+yRucSWnL+97/eT7P8yx47/jHvwiGYWryhGAYZk5YIAyTAQuEYTJggTBMBiwQhsmABcIwGbBAGCYDFgjDZMACYZgMWCAMkwELhGEyYIEwTAYsEIbJgAXCMBmwQBgmAxYIw2TAAmGYDFggDJMBC4RhMmCBMEwGLBCGyYAFwjAZPCkYMhY/tUg0LWsUzfK1atlzor6uLvlv0PD00se+9tHPU+Lhzz+JiQc/yD+nxPjkHTF+d0KM35sQkw/uCyYOWCCWvLh8lWhvWiOaGhpFi/xvXRY/VZe8lHDwPRSTUjTXbt9IXoNj30sx/SSYMCzgwXHmQBQtK1aKjtb25CF3zeD4qBj4blgMyT8Zv7AFMQDC6GrbZGQpKIB1wQuW5fQ3X4qB68OC8QNbEA1CCWMuWCj+YIFk0LBkqdi/ZUc0wpgNC8U97GLNwY71baJrw5+8xBh5aVjyjBRwRyLg0998xdkvB7BAZhG71ajF5rWtye/72ZWLHMgTw4XCFIg1DnfuLZQ4FLAmh159XXTKWImhgy3IDHCpdr+0TRQduIWNsrbS3d8nGHtYIBJkqDrlg0XF+N074tqtm2Lyx/tJID0mq+MAMQJcOFAvYxu8UHlHsRB1lSZZfacALhe+75Ezp5IqPZOfymexqMRx7dYNMTA6LCvfo0kbSR6QEFgn3bu25jXJQ24LhMoisaPSArEVBx68z78dEn3Dg7lFMRcQS7sUSmcbXKZnRF4gkmP955KesAb5fWDBGmW8gheof2pR8rNg7QB6w5K/NzmR/H9j6A+Tr6q2u1RWIDbicCmMWsCa2ArFFggNQumXNZeR2zdEVaikQNqa1iQZnzyck8JAce5RALcFAXgMWSr82+FKVkEslRMIXIx3du5K3A0TYDWOnu9NOmxDgnTu2zvfCGpN0pS9mr9w+1u73xcV4nDXm8bigB9+5OzJxMUIDU7vL+TDuPx3y8SKpctEaHDnBbHSlrXrp++1RPAeUVKpNC/iDlNxoDJ97HJfEJeqFriU1dm28bH7IzGg2l7gBpbJolRGIHCtTINyfMgxFdy4P8w/lRFI1waz4BaWIxZxoAXmwNYOY+sXkrL0h1VCIMhamRTeUAuAWxWaIjZOplH9YXC5eoa+EkWkEgLZs0m/xwriQEAeMuZAnLFj/YbEnSoD+Hegjea4PHSKVtUvvUBw+pq4Jj3ytJucqSaHwFWcgQcT7TD4t43fuyMeTU2JiR9/+PWBVb1heOHkh1uHajtVfxiSCo0yvV601pfSC8SksIagPFT2xcW1Xghi5PbNXyekZDE563+fGx5K/oRY8DvBTUU61waI7d3ON8WHPZ8WRiSlFgh8eJMHDr6yb1zEGUroFEVNWJyBB9PfT4nFpu1lutBZHEtSaoGYZK5OD33l1bVScQbl6CA8xKcduohpsdj0h8GSHJTBO0QSO6UWSMsKvVMZJ9nA6LfCF3i49mzcRiYMuFI9subgsw1GWam8/WGwRLvle3BCpoFjplQCUQPd8OY3y1NK9wEcGhv1Yj2o4wxk3E4ErjPAYvXLwyVPf9iO1jaZKLifdEbHSuEFAnPd1rzaylVxHXsgzoC7R3EJCvhut58PHC7/OP1pYk1ek1k4E/B3hsa+j7biXliBUJ3GaK5zZT2KGGfkBaL97OuLyZ8mLhfSykhSxBqPFE4g1G5K/3U3sQfSoihQUrWHhIgz8qCssYlI8FnC8sToahVGIK7aLqgfOGoB40RGm8Y56U4VhTwigav1xXfD0aV+CyEQvNEuJqnjw7hBdH8B7lRX20YZeLYLCmKLM0yBSOA+6cYk+FqkjWPLakUtEFiNQ9tfT0bYuABLayigFjDcqe6BvujiDFMQk6wy2JuCrBYq+DEF7NEKBD48Wrxd3n3AIAIbqNvQkbY93t8XfZxhAq4MoL2kXvNz3LFugzjx9SURC1EKhHqQ21ygWS8PcKcObN2RiJiCIsYZusAK9gx9mRQFddj8wvokGRFLLBLdbF5f4gB570/v2fgKmTgwJeWDk92lFIcCbpOuVUzillazWopLorIgPsWRFxQmKQp+iDNOSB/d9ZADVYvZIn9njDhV+w99X4eFhWzZqRmLrG9PrEgMRCMQiimHao/ffun+zN4qS0Vb02phg884o9bdEnTTbl7SmogcmSZkyny4M6rlXidghxVBfBfDzK0oBFKkKYdqZKcpPn9PPFxohpwv+wfxbFmz3tsUEhMr0i4PIhaImM5W5RVHUdKhePgwvMC1MPIUU9Pjej46e8qp22ViRdqb10aRzQoqEHygJvfFFUXJ+vhqD6Ho+YJQ/rpr30yvl7v4ZOSWnkDw++AV+vALKpA8I0DVUIWYrQZ+xx5Pbgv1HXbEJli/gMPHxSSS/tHhpGKuQ8vvVyUXtEISTCB5phwmI0DPnIy+9QJ3rn3EGa5WUyNIdhWf4GDDWgWdJEoSQwUeqRVEIHmmHBZFHMDl75i4pYR1mOyfNR2fwKKcuHKJzO0a/N9oksqdj5YVTSI0QQRiOuWwSOJwhYu7JbpgZA9eVPHJ+D292k/DkvCTJL0LJM+Uw6MXeiotDuq7JXlR40Rt3S7d4qia0xWy7cS7QEyyVnhjYg/IXUIRZ6jCJNi3tcN6r0g6LZx37q6JBVpcVyGBwEwbTTn0PIonFijulqjCZPq+/d9PdpOtc1Nzd/O4Xfjd8NLp8FXtMaHwKhCTC/1Y8VXmBr65oLhbkrUmTg2U61jXbjxgoRYQnGpbGbh+VVsoWApa7zmWyoM3gZhOOUQjX5WguFuiW5jEiYzLTH1XBxNXiaL50lVaGJZuRITDm0BMMld4g6viWlHctc87HwvvcfdM4ySV26Xik39e6BU37trf2Ax9L8SbQHSnHIIQM3J9Qxln2DZAqimJlPHJ4c69JGnhSgjEZAVBFawHRXuIi/lYKj6hcrvS8cnsthUcEEXAi0BM7lCU2XrotqFngRMV7pSrPi/lduFz2P3SNuuVB6BWfKIboD+UwXxI/FgQTffK5ZTDkFDO9FJ9UsBlM+RkUqDtJXW7EJ/ApTw9pH8I3i17Ny8+UN0tRa6mHIbCVXtIOhg+Jk97lxeL0lPcX36h1VoosJ6on+ig6iUhcS4QkxVeZRp3Q73ioBYQCq4MuL7DAdQUd6r4RAequWU2eBCInr9NOeUwNNig5HMzbToYdnnHPB2fTA/0o9lfOBfjd8MLxPnYH93sVQynBRV5xYEH2+ahwOmOIW2uT3gI5cOefydiyTtbTIdrt2+K0Li3IA16p0yZ3Ks8IA2q6hk2gbHPO+a2W6bmA9t4Q+MlSNehqh27tQZPUDx4vu6YAxfxCboDYngm3AukTk8gLk11jOjMx6J48FzfMVeo+AQ/5+CrndbZrhhG/gDnAnE1wK2o1GpDz4LiwXN5x3w2qGVRtNXHMps3msmKuMhfdrLa0OeD4sFT8Qm+R3f/OefxCZajojCYx03EKgRU0V1aPR2iG15dRhBnYAcfWvhtrw7jwcP9fJt1ZciyIT6BWFDldwWsAA6ED05153KhYfVe9Jgur0U0AllcgMszpiDO+OR8rzgis0mUWTp1nwMPno27BEvyzp93kVycykJtwc0jalTdXYp4PpwLRNd1qi9Id6cOavIj5mO53GGu4hObegTcrj0vbZMWZb/T+onagmvqMqktuKGIxoI0Bp7YQQmEkTfWyAOsCOITXFPOi4pPXLtdtVrf50NtwQ2Bc4FMaOayY5iBREWo/P0jgtZwWBHEJ9gI5UooeUSCeCTEHXb3LpZmpiSkn8n8FmSREJ+4crtU35guoTZPOReI7pCwdYGzFcxvUW4X4hMX2STEJCbJiy6Z3vZ9kHqwIPouVpncrDKh2updxCdIMJgUBbEF1yfOBWJyQvgYyJymZflKUTVs08KIT1D4o4oHcIAev9yn/fXYguszFnEuEJwOuqleivvPJsBku05vxgZObGTZbHrfqNvqB2UqPNYtuF7SvBh3rwPSeRSnw8MpfZPtK70ZE6ptxbZ+ouITivcN6xV00VmdQIUXgQyNf6/9tRSnw1COeoCP9GZsqLYVm34nCOXdzr3WtwtxUUzX/VNbcH3gRSD4x+sGYjgdbK0IFtfnPRldpzdjAzGA6pfKG5/g89q98RVhi8nPb7dcx62LF4FM7zDXm1hC4WMmaxPkyUjhPoRulvOFals5er431/sG93iVxbwvoLbg6oAtuD7w1mpi4mZRWBF84BR+tqv0ZqwgYM77vjU/az/EQdc99lUW8CYQk9MB4tDdhDofFH52Or1Zxq7jWqj3zcem3tk/VxcfZQGvzYom/3jEAlTuDYWfDZDe9GXaYyA9/d0XcI91f57rsUPAu0BMzPYB6dpQFoVs/WzGDyO39ATSXDaBgB6DuazwMQ9qjqk0wcbPZtyj279XqhhEoUbs64LsCGoTrn6XEH42k82Y5jwseBeuRRLkwpRpwIx4xMVgMqDcLtv4JAaKsnNjPvCZ6NbNXCdNgggEFsT0frKr6X0KiuurCtRPfDZeTq9X6PDey+YS3ctfjY7HSgUb+4Os0obn1xhdtYVIkLlA96eruUlqqiGs1muyHpN3vE7eFckmuFqvEAO4iaozU62+jBYEqGq36YOOXevoJHVduEO7im184qI9XIEU+OGuvdar3Jhsgg5tmJzZzmqKmjvr0uUC6fgklqnrsKBYr4AKf0OJBl3ESvCpJjih81a58eD5uM9BMe7ftj0c7hQaAtE528LXk70Rxdif/tH8box68HCquna71Hgd2/Zw06mG2Ir7tzf2Wa2MLhr6Sz7djlaKYjYvxdVXNU6zKOP+daauI844sLWjkq6U7uFREYHQuQx48PD9+q4OWs2vnY/0OrK3d75BOnWdcituEcH7Esua6DgEsoL2QVDjNDvWtTsf96/a6immrsOiIBlQxrStCSZNiK73WgYXCE5LVy6EevBwErt2u2zH/QOksNstCoxwN+pLICzdxa/jHpa+Bg/Sm5+1u4Wmg6/75ulx/z7bVvBz4e4NjV0XZUD3msPDKbfuFQhuQWyvaZqA6jhOaB9uFx5Y/Ix9Msh2NZhbbatSyz/LMudL1+X2cU8luEAaNbsx4Zsvrltk/bClt8Aekw+xy114+AAp1pHV/N41ln+WAZPRTz7WREcRg+iA0f49Mo6getjUfXMfaWHV4r9/c4d1QmJM+t3/uXLR6y0/n5ikzUeqYEFM10Srh41q5TC+B15q2riLvDqyMqiC26Rt1VIe1E3KCg5L3c/02i0/B0R4gdSZV0zTNYjdMp1L0ebtYgss2kO62jZaV8Btln8WiS1r12l/7cConyRINFtu56PWyQ6hHL3QS+p2qfjko7OnrNwutIfYdtqWNc6oxbT1WK/99b5czMIIJAt1hwMP5MsvtJIIJW/bCkV7CO5CHPc8TSQ0O1o3aL9n+Fx8HRqlEIiCokcqjU6/lIKiPWR22rYq4L3bYmA9+j3WmAojEFiFEY2vS8cnh7a/bj07aa5+KYW61YevsQHf97MrFyslDEXXBv2BfLCuIx4ta3CBYFVBw9Pzf93iOjNfXt3hoI5P8P26+88lbhdVnIH0dZXcqTRdbZuMrH3PN/pjoygILhCcCDqnfN4HPB2fUNxAVG31KFzaWCf8u3scV/RjB+9fp4HlxXvm+/0KLhDdALipwc5Voo5P8oqjqnHGbBB3HDIcCgjX2TfhBaJ5hZViDquKTxB0H3y101mP1FwMjY8mcUYV0rZZQBymd+phOUYCuKHBBaLbsoxgGY2NFP3/agWZix6pWlQ9zkiTRxyqSzoEEQhEf1oI/H/KCzKqbQUXq15bT78Y0md7CMQe++R51Ij+It0q06QG3sNQVjd8Fmtm3L1O/QAtJZ8P016jxRuPhfa4oksVnwB8qD7iDDx0yATFfj0XGT+0BZmCQyxk/1kUdZDxyTtaH7BqhXbRUJjehWHjdvlqD4GrgvpB7LsUbQqoEzN7XUIShUCwnk13tS/2F8Kfd0Xe0aO+2kOKMm7UtoCaTN48ezJ4QiMKgeCh0r1PDSHBzXI97gWjR7FHZD63S6VtfZx0GIi9Z9O2qMcAUQm4+3IcTZrRtJpgC66OFVFbcF1aEUW6baXWZSdfbehUcYbrIQdd0jXdv7VD2IJOBaTEYyAagZi4Wb6siAJCOXL2VNJugnH7EOlVafVcC4PqPokib7Cre6mNYpkNxDFwXW9luA+iEYjagqtzSqotuHkGX9sAofgw+y7ijNMyq5a3TcPHCmwcdtgdGVutKKpuXqRGW3bquREIonFPfaRkxTfqcaMUyQPXs7YmkotvPV7mXJkSlUBMrAjAFtwPez715mq5hOLeehqqni/Xq5aRFv/kQm+0fWnR3QcxsSLweTF3Cqa5qFDHGYDybkmTo7llRRlCEZ1ATK0IBsGhjd1mJUEoKO6TpHHR8+WiQl+ku/ZR3ig8ceVSsihGF1WMKopIXMQZru6WUA4WL2LTZpQCQQNjz9CXSaZKlyKIhHqtgeu7Jfg9KURc5G7maO+ko5KNgQkmQSJEUi99eogrpsCd6t56Gh93S2z6vPD+I8v4xUzHdFGJViB4g5HdwPJLkzSjGlBtO9eKiiLEGXNh6l4pUSD1jj/LcGMy6qkmOB1hDbC2wAQ11wptIKFcLuo2dN9ZH1gPXfcKLTeYdGizCThWoh/7A1cLblOegQsuxonOh4v1ab7ulqTpMoj/zl0dLO014kLMxcIDDquQxydW43rgekFsroQCi7FlZhA2FaHSoSbWw+eUwxAUZnCcmmiR9wFEsK/m7sJ/75NiuWHpEiD4xsBlJBMoLUbI0aPJRSwD6+FzymEICjV61FYkILFES6ZPepx8Y/fuiBF5UmPvBiaszBXYQwz4u82ystz07HMygF1J3oYRQ3UZtxQbDC6Jla0XbjaFm80LkTyamiIZsoAHHq/ZizPh66fXCzc87b6bNYYVB8i4xTzlMASFHF6NIQs4bSkmJdYCKVlf11kRZ5yQ/57QnazTd9zjnnIYgsJOd8dpCx/d5ZJMl8S04kDNqjI5FKpgPUCh1x/g4Tpy5iTpuB7XxDZ6NO+Uw6rMFC78fhCqcT0+iG2VWtGmHIagNAt0qLdMUfPJ+d5oBhGAPOIAIacchqBUG6aAmuKOCnpMQtmzcZsYvzcRRX8Y3FH8PqaJCAi8zFt2a7Fw+1u73xcl49HMOFNMPkFtA82OjR4GD2SBbb6o5osF2O99U4QAtZxdf9wsdv1hi3hy4UKjv4ukwr8u/bdyKxsWvHf8419EBUC9A9XuzWtaSS8B5QEuyjEZN/ksstl0FSPuwN3/Kq5tqIxAZgOx4L41eqgak4LhUq22erhISdVdPizSGFgVLGHlMI7HlVBgMdqbVyetIzYXn46cOVXZ1Q2VFUgtcLrW1y1KhKLE8jCpqk+7FbXiB7W30IbJmSHN16TrRRGjQPRohaGYqxXbIDfflC5ItwH+NV6TBn+Hqj8MQgO4U3Ht1s3kxJ6Q8ZNOQyWsX8vylUmPWPvzq8laY6ouDsAWhAiqJaG1mPzxfhIkp0msnAz8XfSJxTrlMARsQYhQxTMXIoEIfDRMgolkDnH4tQOxwAIhpOj9YahzHLvcV+ntu7N5QjCkqP6wIvUqwaXCLDJU+1kcj8MWxAFF6g8r0pTDELBAHKK26KLtxVUAnxdeTa0HC8QxqsaB/rAY2vJZGGZwmtczquXFp+uVrNq+Nd2bxsIwgy2IZ2BRBh5Mt+Zj6MN0b5ibARBYr43MFCaPcPCdDxZIQFA1PzFTKU8mpshK+IszPWJNDc8ZjVxFi8rV2zeTyjvut7OloIEFEglq/+Fg6lLV4pk2fSWU9JJMfK3qE4vhjklZYYFEDNyix+bd3haMZ7hQyDAZsEAYJgMWCMNkwAJhmAxYIAyTAQuEYTJggTBMBiwQhsmABcIwGbBAGCYDFgjDZMACYZgMWCAMkwELhGEyYIEwTAYsEIbJgAXCMBmwQBgmAxYIw2TAAmGYDFggDJPB/wFdmpLa3s9ZJQAAAABJRU5ErkJggg==", + }, +]; diff --git a/apps/console/src/components/common/Avatar.tsx b/apps/console/src/components/common/Avatar.tsx new file mode 100644 index 000000000..f4f4e657c --- /dev/null +++ b/apps/console/src/components/common/Avatar.tsx @@ -0,0 +1,27 @@ +import { ExtendedUser } from "~/@generated/graphql/graphql"; +import { useMemo } from "react"; +import { Avatar as ShadcnAvatar, AvatarFallback, AvatarImage } from "@pezzo/ui"; +import { cn } from "@pezzo/ui/utils"; + +interface Props { + user: Partial; + className?: string; +} + +const buildInitials = (name: string) => { + const splitName = name.split(" "); + if (splitName.length === 1) return splitName[0][0]; + const [firstName, lastName] = splitName; + return `${firstName[0]}${lastName[0]}`; +}; + +export const Avatar = ({ user, className = "" }: Props) => { + const photoUrl = useMemo(() => user.photoUrl || undefined, [user.photoUrl]); + + return ( + + + {buildInitials(user.name || "")} + + ); +}; diff --git a/apps/console/src/components/common/BreakpointDebugger.tsx b/apps/console/src/components/common/BreakpointDebugger.tsx new file mode 100644 index 000000000..bcb6be8fb --- /dev/null +++ b/apps/console/src/components/common/BreakpointDebugger.tsx @@ -0,0 +1,16 @@ +import { DEBUG_MODE } from "~/env"; + +export const BreakpointDebugger = () => { + if (!DEBUG_MODE) { + return null; + } + + return ( +
+
sm
+
md
+
lg
+
xl
+
+ ); +}; diff --git a/apps/console/src/components/common/Drawer.tsx b/apps/console/src/components/common/Drawer.tsx new file mode 100644 index 000000000..f57093bc0 --- /dev/null +++ b/apps/console/src/components/common/Drawer.tsx @@ -0,0 +1,55 @@ +import { cn } from "@pezzo/ui/utils"; +import { motion } from "framer-motion"; +import { useState } from "react"; + +type Props = { + children: React.ReactNode; + className?: string; + onClose: () => void; + open: boolean; +}; + +export const Drawer = ({ children, className, onClose, open }: Props) => { + const [shouldHide, setShouldHide] = useState(false); + + return ( +
+
{ + if (e.key === "Enter") { + onClose(); + } + }} + role="button" + aria-label="Close" + >
+ open && setShouldHide(false)} + onAnimationComplete={() => !open && setShouldHide(true)} + > + {children} + +
+ ); +}; diff --git a/apps/console/src/components/common/FullScreenLoader.tsx b/apps/console/src/components/common/FullScreenLoader.tsx new file mode 100644 index 000000000..93147b5ac --- /dev/null +++ b/apps/console/src/components/common/FullScreenLoader.tsx @@ -0,0 +1,9 @@ +import { Loader } from "./Loader"; + +export const FullScreenLoader = () => { + return ( +
+ +
+ ); +}; diff --git a/apps/console/src/components/common/GenericDestructiveConfirmationModal.tsx b/apps/console/src/components/common/GenericDestructiveConfirmationModal.tsx new file mode 100644 index 000000000..bfd4bbbff --- /dev/null +++ b/apps/console/src/components/common/GenericDestructiveConfirmationModal.tsx @@ -0,0 +1,63 @@ +import { GraphQLErrorResponse } from "~/graphql/types"; +import { + Alert, + AlertDialog, + AlertDialogContent, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogCancel, + AlertDialogDescription, + Button, + AlertTitle, + AlertDescription, +} from "@pezzo/ui"; +import { AlertCircle } from "lucide-react"; + +type Props = { + open: boolean; + title?: string; + description?: string | React.ReactNode; + error?: GraphQLErrorResponse; + confirmText?: string; + onConfirm: () => void; + onCancel: () => void; +}; + +export const GenericDestructiveConfirmationModal = ({ + open, + error, + title = "Are you sure?", + description, + confirmText = "Confirm", + onConfirm, + onCancel, +}: Props) => { + return ( + + + + {title} + + + {error && ( + + + Oops! + + {error.response.errors[0].message} + + + )} +

{description}

+
+ + Cancel + + +
+
+ ); +}; diff --git a/apps/console/src/components/common/InlineCodeSnippet.tsx b/apps/console/src/components/common/InlineCodeSnippet.tsx new file mode 100644 index 000000000..39646ba5c --- /dev/null +++ b/apps/console/src/components/common/InlineCodeSnippet.tsx @@ -0,0 +1,7 @@ +export const InlineCodeSnippet = ({ children }) => { + return ( +
+ {children} +
+ ); +}; diff --git a/apps/console/src/components/common/Loader.tsx b/apps/console/src/components/common/Loader.tsx new file mode 100644 index 000000000..9dac88b72 --- /dev/null +++ b/apps/console/src/components/common/Loader.tsx @@ -0,0 +1,8 @@ +import Lottie from "lottie-react"; +import LogoLoader from "~/assets/logo-loader.json"; + +export const Loader = () => { + return ( + + ); +}; diff --git a/apps/console/src/components/common/Pagination.tsx b/apps/console/src/components/common/Pagination.tsx new file mode 100644 index 000000000..92666127a --- /dev/null +++ b/apps/console/src/components/common/Pagination.tsx @@ -0,0 +1,115 @@ +import { Button } from "@pezzo/ui"; +import { cn } from "@pezzo/ui/utils"; +import { ChevronLeft, ChevronRight } from "lucide-react"; + +type Props = { + offset: number; + limit: number; + totalResults: number; + onChange: (page: number, limit: number) => void; +}; + +function generatePagination( + currentPage: number, + totalPages: number +): (number | string)[] { + const pages: (number | string)[] = []; + + // Lower limit for pagination + const lowerLimit = Math.max(1, currentPage - 3); + + // Upper limit for pagination + const upperLimit = Math.min(totalPages, currentPage + 1); + + // If there are more pages before the lower limit, add "..." + if (lowerLimit > 1) { + pages.push(1); + pages.push("..."); + } + + // Add the page numbers + for (let i = lowerLimit; i <= upperLimit; i++) { + pages.push(i); + } + + // If there are more pages after the upper limit, add "..." + if (upperLimit < totalPages) { + pages.push("..."); + pages.push(totalPages); + } + + return pages; +} + +export const Pagination = ({ + offset, + limit, + totalResults, + onChange, +}: Props) => { + // calculate currentPage, bsaed on offset and totalResults and limit (size of page) + const currentPage = Math.ceil((offset + 1) / limit); + const totalPages = Math.ceil(totalResults / limit); + const pagination = generatePagination(currentPage + 1, totalPages); + + const handlePageClick = (_page: string) => { + const page = parseInt(_page); + onChange(page, limit); + }; + + const pagesToRender = pagination.map((page, idx) => { + if (page === "...") { + return ( +
  • + +
  • + ); + } + + const selected = page === currentPage; + + return ( +
  • + +
  • + ); + }); + + return ( +
      +
    • + +
    • + + {pagesToRender.map((Page) => Page)} + +
    • + +
    • +
    + ); +}; diff --git a/apps/console/src/components/common/Tag.tsx b/apps/console/src/components/common/Tag.tsx new file mode 100644 index 000000000..98a658db4 --- /dev/null +++ b/apps/console/src/components/common/Tag.tsx @@ -0,0 +1,31 @@ +import { cn } from "@pezzo/ui/utils"; +import colors from "tailwindcss/colors"; + +type Props = { + children: React.ReactNode; + color?: string; + className?: string; +}; + +export const Tag = ({ children, color = "stone", className = "" }: Props) => { + const baseCn = cn( + "rounded-sm border p-1 text-xs inline-flex gap-1 items-center h-6", + className + ); + + return ( +
    + {children} +
    + ); +}; diff --git a/apps/console/src/components/environments/CreateEnvironmentModal.tsx b/apps/console/src/components/environments/CreateEnvironmentModal.tsx new file mode 100644 index 000000000..37c0f565b --- /dev/null +++ b/apps/console/src/components/environments/CreateEnvironmentModal.tsx @@ -0,0 +1,134 @@ +import { useMutation } from "@tanstack/react-query"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + Button, + Form, + FormItem, + FormControl, + Input, + FormField, + FormMessage, + FormLabel, + Alert, + AlertTitle, + AlertDescription, + useToast, +} from "@pezzo/ui"; +import { gqlClient, queryClient } from "~/lib/graphql"; +import { CREATE_ENVIRONMENT } from "~/graphql/definitions/mutations/environments"; +import { CreateEnvironmentMutation } from "~/@generated/graphql/graphql"; +import { GraphQLErrorResponse } from "~/graphql/types"; +import { useCurrentProject } from "~/lib/hooks/useCurrentProject"; +import { trackEvent } from "~/lib/utils/analytics"; +import z from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { AlertCircle } from "lucide-react"; + +interface Props { + open: boolean; + onClose: () => void; + onCreated: (id: string) => void; +} + +const formSchema = z.object({ + environmentName: z + .string() + .min(1, "Name must be at least 1 character long") + .max(64, "Name can't be longer than 64 characters") + .regex(/^[a-zA-Z]+$/, "Name can only contain letters, e.g. Production"), +}); + +export const CreateEnvironmentModal = ({ open, onClose, onCreated }: Props) => { + const { projectId } = useCurrentProject(); + const { toast } = useToast(); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + environmentName: "", + }, + }); + + const { mutate, error } = useMutation< + CreateEnvironmentMutation, + GraphQLErrorResponse, + z.infer + >({ + mutationFn: (data) => + gqlClient.request(CREATE_ENVIRONMENT, { + data: { + name: data.environmentName, + projectId, + }, + }), + onSuccess: (data) => { + onCreated(data.createEnvironment.name); + queryClient.invalidateQueries({ queryKey: ["environments"] }); + toast({ + title: "Environment created!", + description: `Your new environment was created successfully.`, + }); + }, + }); + + const onSubmit = (values: z.infer) => { + mutate(values); + form.reset(); + trackEvent("environment_form_submitted"); + }; + + const onCancel = () => { + onClose(); + form.reset(); + trackEvent("environment_form_cancelled"); + }; + + return ( + + +
    + + + New Environment + + {error && ( + + + Oops! + + {error.response.errors[0].message} + + + )} + ( + + Environment name + + + + + + )} + /> + + + + + + + +
    +
    + ); +}; diff --git a/apps/console/src/components/getting-started-wizard/HighlightCode.tsx b/apps/console/src/components/getting-started-wizard/HighlightCode.tsx new file mode 100644 index 000000000..12b47ef1d --- /dev/null +++ b/apps/console/src/components/getting-started-wizard/HighlightCode.tsx @@ -0,0 +1,28 @@ +import { Highlight, themes } from "prism-react-renderer"; +import { StyledPre } from "./StyledPre"; + +type Props = { + code: string; + language: string; +}; + +export const HighlightCode = ({ code, language = "ts" }: Props) => ( + + {({ style, tokens, getLineProps, getTokenProps }) => ( + + {tokens.map((line, i) => ( +
    + {line.map((token, key) => ( + + ))} +
    + ))} +
    + )} +
    +); diff --git a/apps/console/src/components/getting-started-wizard/PythonOpenAIIntegrationTutorial.tsx b/apps/console/src/components/getting-started-wizard/PythonOpenAIIntegrationTutorial.tsx new file mode 100644 index 000000000..820dbaa7b --- /dev/null +++ b/apps/console/src/components/getting-started-wizard/PythonOpenAIIntegrationTutorial.tsx @@ -0,0 +1,118 @@ +import { HighlightCode } from "./HighlightCode"; +import { useCurrentProject } from "~/lib/hooks/useCurrentProject"; +import { useCurrentPrompt } from "~/lib/providers/CurrentPromptContext"; +import { useEditorContext } from "~/lib/providers/EditorContext"; +import { Alert, AlertDescription, AlertTitle } from "@pezzo/ui"; +import { Link } from "react-router-dom"; +import { InfoIcon } from "lucide-react"; +import { usePezzoApiKeys } from "~/graphql/hooks/queries"; +import { StyledPre } from "./StyledPre"; + +const getVariablesString = (variables: string[]) => { + if (!variables.length) return ""; + const varStrings = variables.map((v) => ` "${v}": "value"`).join(",\n"); + return ` + "variables": { +${varStrings} + }`; +}; + +export const PythonOpenAIIntegrationTutorial = () => { + const { variables } = useEditorContext(); + const { prompt } = useCurrentPrompt(); + const { pezzoApiKeys } = usePezzoApiKeys(); + const API_KEY = pezzoApiKeys && pezzoApiKeys[0].id; + + const { project } = useCurrentProject(); + const codeSetupClients = `from pezzo.client import pezzo +from pezzo.openai import openai + +/* + * The Pezzo client automatically searches for the following environment variables and uses them to initialize the cilent: + * - PEZZO_API_KEY: Your Pezzo API key + * - PEZZO_PROJECT_ID: The ID of the project you want to use + * - PEZZO_ENVIRONMENT: The environment you want to use. By default, Pezzo creates a "Production" environment for you. + */ +`; + + const variablesString = getVariablesString(variables); + + const codeWithPromptManagement = `${codeSetupClients} +// Fetch the prompt from Pezzo +prompt = pezzo.get_prompt("${prompt.name}") + +// Use the OpenAI API as you normally would +response = await openai.ChatCompletion.create( + pezzo_prompt=prompt, + pezzo_options={${variablesString} + } +) +`; + + return ( +
    + + + Need some more help? + + Check out the{" "} + + Using OpenAI With Pezzo + {" "} + page on our documentation to learn more. + + + +

    Installation

    +

    + Pezzo provides a package for intergration with Python projects. Here's + how to install it: +

    + + + # via pip +
    + pip install pezzo +
    +
    + # via poetry +
    + poetry add pezzo +
    +
    + +
    +

    Usage

    +

    + Managing your prompts with Pezzo is easy. You will fetch the prompt + payload from Pezzo, and then pass it to the OpenAI client as an + argument. As soon as you interact with OpenAI, your requests will be + available in the Requests view. +

    + +
    + +
    +
    + +

    + In addition to vriables, you can also provide{" "} + custom properties and caching when + executing prompts. These properties will be available in the Requests + view.{" "} + + Read more in the Pezzo Documentation + + . +

    +
    + ); +}; diff --git a/apps/console/src/components/getting-started-wizard/StyledPre.tsx b/apps/console/src/components/getting-started-wizard/StyledPre.tsx new file mode 100644 index 000000000..7a4aeef94 --- /dev/null +++ b/apps/console/src/components/getting-started-wizard/StyledPre.tsx @@ -0,0 +1,10 @@ +export const StyledPre = ({ children, style = {} }) => { + return ( +
    +      {children}
    +    
    + ); +}; diff --git a/apps/console/src/components/getting-started-wizard/TypeScriptOpenAIIntegrationTutorial.tsx b/apps/console/src/components/getting-started-wizard/TypeScriptOpenAIIntegrationTutorial.tsx new file mode 100644 index 000000000..e356a61b3 --- /dev/null +++ b/apps/console/src/components/getting-started-wizard/TypeScriptOpenAIIntegrationTutorial.tsx @@ -0,0 +1,107 @@ +import { HighlightCode } from "./HighlightCode"; +import { useCurrentProject } from "~/lib/hooks/useCurrentProject"; +import { useCurrentPrompt } from "~/lib/providers/CurrentPromptContext"; +import { usePezzoApiKeys } from "~/graphql/hooks/queries"; +import { useEditorContext } from "~/lib/providers/EditorContext"; +import { Alert, AlertDescription, AlertTitle } from "@pezzo/ui"; +import { Link } from "react-router-dom"; +import { InfoIcon } from "lucide-react"; +import { StyledPre } from "./StyledPre"; + +const getVariablesString = (variables: string[]) => { + if (!variables.length) return ""; + const varStrings = variables.map((v) => ` ${v}: "value"`).join(",\n"); + return `, { + variables: { +${varStrings} + } +}`; +}; + +export const TypeScriptOpenAIIntegrationTutorial = () => { + const { variables } = useEditorContext(); + const { prompt } = useCurrentPrompt(); + const { pezzoApiKeys } = usePezzoApiKeys(); + const API_KEY = pezzoApiKeys && pezzoApiKeys[0].id; + + const { project } = useCurrentProject(); + const codeSetupClients = `import { Pezzo, PezzoOpenAI } from "@pezzo/client"; + +// Initialize the Pezzo client +export const pezzo = new Pezzo({ + apiKey: "${API_KEY}", + projectId: "${project.id}", + environment: "Production", // Your desired environment +}); + +// Initialize the PezzoOpenAI client +const openai = new PezzoOpenAI(pezzo); +`; + + const variablesString = getVariablesString(variables); + + const codeWithPromptManagement = `${codeSetupClients} +// Fetch the prompt from Pezzo +const prompt = await pezzo.getPrompt("${prompt.name}"); + +// Use the OpenAI API as you normally would +const response = await openai.chat.completions.create(prompt${variablesString}); +`; + + return ( +
    + + + Need some more help? + + Check out the{" "} + + Using OpenAI With Pezzo + {" "} + page on our documentation to learn more. + + + +
    +

    Installation

    +

    + Pezzo provides a fully-typed NPM package for integration with + TypeScript projects. +

    + + {"npm install @pezzo/client openai"} + +
    + +
    +

    Usage

    +

    + Managing your prompts with Pezzo is easy. You will fetch the prompt + payload from Pezzo, and then pass it to the OpenAI client as an + argument. As soon as you interact with OpenAI, your requests will be + available in the Requests view. +

    + +
    + +

    + In addition to vriables, you can also provide{" "} + custom properties and caching when + executing prompts. These properties will be available in the Requests + view.{" "} + + Read more in the Pezzo Documentation + + . +

    +
    + ); +}; diff --git a/apps/console/src/components/layout/Header.tsx b/apps/console/src/components/layout/Header.tsx new file mode 100644 index 000000000..13dedc1aa --- /dev/null +++ b/apps/console/src/components/layout/Header.tsx @@ -0,0 +1,74 @@ +import { cn } from "@pezzo/ui/utils"; +import { BriefcaseIcon, ChevronsUpDown } from "lucide-react"; +import LogoSquare from "~/assets/logo-square.svg"; +import { useCurrentOrganization } from "~/lib/hooks/useCurrentOrganization"; +import { useCurrentProject } from "~/lib/hooks/useCurrentProject"; +import { Popover, PopoverContent, PopoverTrigger } from "@pezzo/ui"; +import { Link } from "react-router-dom"; +import { UserMenu } from "./UserMenu"; +import { useAuthContext } from "~/lib/providers/AuthProvider"; +import { ProjectCopy } from "../projects/ProjectCopy"; +import { OrgSelector } from "./OrgSelector"; + +export const Header = () => { + const { organization } = useCurrentOrganization(); + const { project } = useCurrentProject(); + const { currentUser } = useAuthContext(); + + return ( + + ); +}; diff --git a/apps/console/src/components/layout/LayoutWrapper.tsx b/apps/console/src/components/layout/LayoutWrapper.tsx new file mode 100644 index 000000000..132accc54 --- /dev/null +++ b/apps/console/src/components/layout/LayoutWrapper.tsx @@ -0,0 +1,43 @@ +import { SideNavigation } from "./SideNavigation"; +import { Header } from "./Header"; +import { OrgSubHeader } from "./OrgSubHeader"; +import { cn } from "@pezzo/ui/utils"; + +interface Props { + children: React.ReactNode; + withSideNav?: boolean; + withOrgSubHeader?: boolean; +} + +export const LayoutWrapper = ({ + children, + withSideNav = false, + withOrgSubHeader = false, +}: Props) => { + return ( +
    +
    +
    + {/* Top */} +
    +
    + {withOrgSubHeader && } +
    + + {/* Bottom */} +
    + {withSideNav && ( +
    + +
    + )} + +
    + {children} +
    +
    +
    +
    +
    + ); +}; diff --git a/apps/console/src/components/layout/OrgSelector.tsx b/apps/console/src/components/layout/OrgSelector.tsx new file mode 100644 index 000000000..2e0bab361 --- /dev/null +++ b/apps/console/src/components/layout/OrgSelector.tsx @@ -0,0 +1,57 @@ +import { CheckIcon } from "lucide-react"; +import { useOrganizations } from "~/lib/hooks/useOrganizations"; +import { useGetProjects } from "~/graphql/hooks/queries"; +import { useCurrentOrganization } from "~/lib/hooks/useCurrentOrganization"; +import { useCurrentProject } from "~/lib/hooks/useCurrentProject"; +import { Link } from "react-router-dom"; + +export const OrgSelector = () => { + const { organizations } = useOrganizations(); + const { organization: currentOrganization } = useCurrentOrganization(); + const { projects } = useGetProjects(); + const { project: currentProject } = useCurrentProject(); + + return ( +
    +
    +

    Organizations

    +
      + {organizations && + currentOrganization && + organizations.map((org) => ( +
    • + +
      {org.name}
      + {org.id === currentOrganization.id && ( + + )} + +
    • + ))} +
    +
    +
    +

    Projects

    +
      + {projects && + projects.map((project) => ( +
    • + +
      {project.name}
      + {project.id === currentProject?.id && ( + + )} + +
    • + ))} +
    +
    +
    + ); +}; diff --git a/apps/console/src/components/layout/OrgSubHeader.tsx b/apps/console/src/components/layout/OrgSubHeader.tsx new file mode 100644 index 000000000..2d008fc3f --- /dev/null +++ b/apps/console/src/components/layout/OrgSubHeader.tsx @@ -0,0 +1,58 @@ +import { cn } from "@pezzo/ui/utils"; +import { useNavigate } from "react-router-dom"; +import { useCurrentOrganization } from "~/lib/hooks/useCurrentOrganization"; + +const isActive = (href: string) => { + return window.location.pathname === href; +}; + +export const OrgSubHeader = () => { + const navigate = useNavigate(); + const { organization, waitlisted } = useCurrentOrganization(); + + if (!organization || waitlisted) { + return; + } + + const baseClassName = cn( + "cursor-pointer py-3 px-3 text-sm font-medium border-b-2 border-b-transparent hover:border-b-primary transition-all" + ); + + const getClassName = (href: string) => { + return cn(baseClassName, { + "border-b-2 text-primary border-b-primary": isActive(href), + }); + }; + + const orgNavigation = [ + { name: "Projects", href: `/orgs/${organization.id}` }, + { name: "API Keys", href: `/orgs/${organization.id}/api-keys` }, + { name: "Members", href: `/orgs/${organization.id}/members` }, + { name: "Settings", href: `/orgs/${organization.id}/settings` }, + ]; + + return ( + organization && ( +
    +
      + {orgNavigation.map((nav) => ( +
    • navigate(nav.href)} + className={getClassName(nav.href)} + onKeyDown={(e) => { + if (e.key === "Enter") { + navigate(nav.href); + } + }} + role="button" + aria-label={nav.name} + > + {nav.name} +
    • + ))} +
    +
    + ) + ); +}; diff --git a/apps/console/src/components/layout/SideNavigation.tsx b/apps/console/src/components/layout/SideNavigation.tsx new file mode 100644 index 000000000..f38d2387f --- /dev/null +++ b/apps/console/src/components/layout/SideNavigation.tsx @@ -0,0 +1,140 @@ +import { + BarChart2, + BoxIcon, + GraduationCapIcon, + HardDriveIcon, + KeyRoundIcon, + RadioIcon, +} from "lucide-react"; +import { cn } from "@pezzo/ui/utils"; +import { useCurrentProject } from "~/lib/hooks/useCurrentProject"; +import { Link } from "react-router-dom"; +import { useState } from "react"; +import { motion } from "framer-motion"; +import { useCurrentOrganization } from "~/lib/hooks/useCurrentOrganization"; + +export const SideNavigation = () => { + const [collapsed, setCollapsed] = useState(true); + const { projectId } = useCurrentProject(); + const { organizationId } = useCurrentOrganization(); + + const projectNavigation = [ + { + name: "Dashboard", + href: `/projects/${projectId}`, + icon: BarChart2, + isActive: (href: string) => window.location.pathname === href, + }, + { + name: "Requests", + href: `/projects/${projectId}/requests`, + icon: RadioIcon, + isActive: (href: string) => window.location.pathname.startsWith(href), + }, + { + name: "Prompts", + href: `/projects/${projectId}/prompts`, + icon: BoxIcon, + isActive: (href: string) => window.location.pathname.startsWith(href), + }, + { + name: "Environments", + href: `/projects/${projectId}/environments`, + icon: HardDriveIcon, + isActive: (href: string) => window.location.pathname.startsWith(href), + }, + ]; + + return ( +
    setCollapsed(false)} + onMouseLeave={() => setCollapsed(true)} + onFocus={() => setCollapsed(false)} + className={cn( + "z-50 flex h-full grow flex-col gap-y-4 overflow-y-auto overflow-x-hidden border-r border-border bg-stone-900 px-3 pt-2" + )} + > + +
    + ); +}; diff --git a/apps/console/src/components/layout/UserMenu.tsx b/apps/console/src/components/layout/UserMenu.tsx new file mode 100644 index 000000000..f36034bca --- /dev/null +++ b/apps/console/src/components/layout/UserMenu.tsx @@ -0,0 +1,52 @@ +import { GraduationCap, LogOutIcon } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@pezzo/ui"; +import { useAuthContext } from "~/lib/providers/AuthProvider"; +import { Link } from "react-router-dom"; +import { Avatar } from "../common/Avatar"; + +export const UserMenu = () => { + const { currentUser } = useAuthContext(); + + return ( + + +
  • + + {currentUser?.name} +
  • +
    + + +
    +

    + {currentUser.name} +

    +

    + {currentUser.email} +

    +
    +
    + + + + + Documentation + + + + + + Sign out + + +
    +
    + ); +}; diff --git a/apps/console/src/components/metrics/CustomDateTooltip.tsx b/apps/console/src/components/metrics/CustomDateTooltip.tsx new file mode 100644 index 000000000..3725c0a4e --- /dev/null +++ b/apps/console/src/components/metrics/CustomDateTooltip.tsx @@ -0,0 +1,47 @@ +import { DatePicker } from "antd"; +import { Button } from "@pezzo/ui"; +import dayjs from "dayjs"; +import { useState } from "react"; + +interface Props { + startDate: string; + endDate: string; + onApply: (dates: { startDate: string; endDate: string }) => void; +} + +export const CustomDateTooltip = ({ startDate, endDate, onApply }: Props) => { + const [tempStartDate, setTempStartDate] = useState(startDate); + const [tempEndDate, setTempEndDate] = useState(endDate); + + const handleApply = () => { + onApply({ startDate: tempStartDate, endDate: tempEndDate }); + }; + + return ( +
    +
    +

    Start date

    + setTempStartDate(v?.toISOString())} + placeholder="Start date" + showTime={{ format: "HH:mm" }} + /> +
    +
    +

    End date

    + setTempEndDate(v?.toString())} + placeholder="End date" + showTime={{ format: "HH:mm" }} + /> +
    +
    + +
    +
    + ); +}; diff --git a/apps/console/src/components/metrics/StatisticBox.tsx b/apps/console/src/components/metrics/StatisticBox.tsx new file mode 100644 index 000000000..1a4719935 --- /dev/null +++ b/apps/console/src/components/metrics/StatisticBox.tsx @@ -0,0 +1,116 @@ +import CountUp from "react-countup"; +import { ArrowDownIcon, ArrowUpIcon } from "lucide-react"; +import clsx from "clsx"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@pezzo/ui"; + +interface Props { + title: string; + currentValue: number; + previousValue: number; + suffix?: React.ReactNode; + prefix?: React.ReactNode; + precision?: number; + reverseColors?: boolean; + valueStyle?: React.CSSProperties; + numberPrefix?: string; + numberSuffix?: string; + numberSeparator?: string; + loading: boolean; +} + +const getProperties = ( + currentValue: number, + previousValue: number, + reverseColors: boolean +) => { + const diff = currentValue - previousValue; + // Handle case where previousValue is 0 + const calculatedPreviousValue = previousValue === 0 ? 1 : previousValue; + + const percentage = Math.abs((diff / calculatedPreviousValue) * 100); + + const percentageToRender = + percentage < 1 ? percentage.toFixed(3) : percentage; + + const isIncrease = diff > 0; + const isDecrease = diff < 0; + + const className = clsx("h-7 w-7", { + "text-destructive": isDecrease || (reverseColors && isIncrease), + "text-green-500": isIncrease || (reverseColors && isDecrease), + "text-muted": !isIncrease && !isDecrease, + }); + + if (isIncrease) { + return { + suffix: , + percentage, + tooltipTitle: `${percentageToRender}% increase (previously ${previousValue})`, + }; + } + + if (isDecrease) { + return { + suffix: , + percentage, + tooltipTitle: `${percentageToRender}% decrease (previously ${previousValue})`, + }; + } + + return { suffix: null, percentage: 0, tooltipTitle: "" }; +}; + +export const StatisticBox = ({ + title, + currentValue, + previousValue, + prefix, + precision = 0, + reverseColors = false, + numberPrefix, + numberSuffix, + numberSeparator, + loading = false, +}: Props) => { + const calculatedFormatter = (value: number) => ( + + ); + + const { suffix: calculatedSuffix, tooltipTitle } = getProperties( + currentValue, + previousValue, + reverseColors + ); + + return ( +
    +
    {title}
    +
    + {calculatedFormatter(currentValue)} + {calculatedSuffix && ( + + + + {prefix !== undefined ? prefix : calculatedSuffix} + + {tooltipTitle} + + + )} +
    +
    + ); +}; diff --git a/apps/console/src/components/metrics/TimeframeSelector.tsx b/apps/console/src/components/metrics/TimeframeSelector.tsx new file mode 100644 index 000000000..43803f9d9 --- /dev/null +++ b/apps/console/src/components/metrics/TimeframeSelector.tsx @@ -0,0 +1,98 @@ +import { CustomDateTooltip } from "./CustomDateTooltip"; +import { Button, PopoverContent, PopoverTrigger, Popover } from "@pezzo/ui"; +import clsx from "clsx"; +import { + Timeframe, + useTimeframeSelector, +} from "~/lib/providers/TimeframeSelectorContext"; +import { trackEvent } from "~/lib/utils/analytics"; +import { useCurrentProject } from "~/lib/hooks/useCurrentProject"; +import { CalendarDaysIcon } from "lucide-react"; +import { useState } from "react"; + +export const TimeframeSelector = () => { + const { projectId } = useCurrentProject(); + const { + startDate, + endDate, + setStartDate, + setEndDate, + timeframe, + setTimeframe, + } = useTimeframeSelector(); + const [isCustomTimeframePopoverOpen, setIsCustomTimeframePopoverOpen] = + useState(false); + + const handleCustomDateApply = (dates: { + startDate: string; + endDate: string; + }) => { + setTimeframe(Timeframe.Custom); + setStartDate(dates.startDate); + setEndDate(dates.endDate); + trackEvent("project_dashboard_custom_date_applied", { projectId }); + setIsCustomTimeframePopoverOpen(false); + }; + + const handlePopoverOpenChange = (isOpen: boolean) => { + if (isOpen === true) { + trackEvent("project_dashboard_custom_date_popover_opened", { projectId }); + } + + setIsCustomTimeframePopoverOpen(isOpen); + }; + + const handleSetTimeframe = (tf: Timeframe) => { + setTimeframe(tf); + trackEvent("project_dashboard_timeframe_changed", { + projectId, + timeframe: tf, + }); + }; + + return ( + + + + + + + + + + {Object.values(Timeframe) + .filter((tf) => tf !== Timeframe.Custom) + .map((tf) => ( + + ))} + + ); +}; diff --git a/apps/console/src/components/organizations/InviteOrgMemberModal.tsx b/apps/console/src/components/organizations/InviteOrgMemberModal.tsx new file mode 100644 index 000000000..01d7909fb --- /dev/null +++ b/apps/console/src/components/organizations/InviteOrgMemberModal.tsx @@ -0,0 +1,119 @@ +import { useCreateOrgInvitationMutation } from "~/graphql/hooks/mutations"; +import { useCurrentOrganization } from "~/lib/hooks/useCurrentOrganization"; +import { useEffect } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + Button, + Form, + FormItem, + FormControl, + Input, + FormField, + FormMessage, + FormLabel, + Alert, + AlertTitle, + AlertDescription, + toast, +} from "@pezzo/ui"; +import { AlertCircle } from "lucide-react"; +import z from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; + +interface Props { + open: boolean; + onClose: () => void; +} + +const formSchema = z.object({ + inviteeEmail: z + .string() + .email("Must be a valid email") + .max(100, "Email can't be longer than 100 characters"), +}); + +export const InviteOrgMemberModal = ({ open, onClose }: Props) => { + const { organization } = useCurrentOrganization({ + includeMembers: true, + includeInvitations: false, + }); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + inviteeEmail: "", + }, + }); + const { mutateAsync: createInvitation, error } = + useCreateOrgInvitationMutation(); + + useEffect(() => { + form.reset(); + }, [open, form]); + + const onSubmit = (values: z.infer) => { + const { inviteeEmail } = values; + + createInvitation( + { email: inviteeEmail, organizationId: organization.id }, + { + onSuccess: () => { + onClose(); + toast({ + title: "Invitation sent", + description: `An invitation has been sent to ${inviteeEmail}`, + }); + }, + } + ); + }; + + return ( + + +
    + + + Invite a new member + + {error && ( + + + Oops! + + {error.response.errors[0].message} + + + )} + ( + + Email + + + + + + )} + /> + + + + + + + +
    +
    + ); +}; diff --git a/apps/console/src/components/organizations/OrgInvitationsList.tsx b/apps/console/src/components/organizations/OrgInvitationsList.tsx new file mode 100644 index 000000000..f47d62f92 --- /dev/null +++ b/apps/console/src/components/organizations/OrgInvitationsList.tsx @@ -0,0 +1,113 @@ +import { Button, Card, toast } from "@pezzo/ui"; +import { GetOrgQuery, OrgRole } from "~/@generated/graphql/graphql"; +import { OrgRoleSelector } from "./OrgRoleSelector"; +import { useCopyToClipboard } from "usehooks-ts"; +import { useState } from "react"; +import { + useDeleteOrgInvitationMutation, + useUpdateOrgInvitationMutation, +} from "~/graphql/hooks/mutations"; +import { useCurrentOrgMembership } from "~/lib/hooks/useCurrentOrgMembership"; +import { CheckIcon, CopyIcon, TrashIcon } from "lucide-react"; +import { GenericDestructiveConfirmationModal } from "../common/GenericDestructiveConfirmationModal"; + +type Invitation = GetOrgQuery["organization"]["invitations"][0]; + +interface Props { + invitations: Invitation[]; +} + +const CopyInvitationButton = ({ invitationId }: { invitationId: string }) => { + const [copiedValue, copy] = useCopyToClipboard(); + + const url = new URL(window.location.origin); + url.pathname = `/invitations/${invitationId}/accept`; + + return ( + + ); +}; + +export const OrgInvitationsList = ({ invitations }: Props) => { + const { isOrgAdmin } = useCurrentOrgMembership(); + const { mutate: deleteOrgInvitation, error: updateOrgInvitationError } = + useDeleteOrgInvitationMutation(); + const { mutate: updateOrgInvitation } = useUpdateOrgInvitationMutation(); + const [deletingInvitation, setDeletingInvitation] = + useState(null); + + const handleDeleteInvitation = async (invitation: Invitation) => { + deleteOrgInvitation( + { id: invitation.id }, + { + onSuccess: () => { + setDeletingInvitation(null); + toast({ + title: "Invitation deleted", + description: `The invitation for ${invitation.email} has been deleted.`, + }); + }, + } + ); + }; + + const handleRoleChange = (invitation: Invitation, role: OrgRole) => { + updateOrgInvitation({ invitationId: invitation.id, role }); + }; + + return ( + <> + handleDeleteInvitation(deletingInvitation)} + onCancel={() => setDeletingInvitation(null)} + /> + + {invitations + .sort((a, b) => a.email.localeCompare(b.email)) + .map((invitation) => ( + +
    +
    {invitation.email}
    +
    +
    + + + handleRoleChange(invitation, newRole)} + showArrow={isOrgAdmin} + /> +
    + +
    + ))} + + ); +}; diff --git a/apps/console/src/components/organizations/OrgMembersList.tsx b/apps/console/src/components/organizations/OrgMembersList.tsx new file mode 100644 index 000000000..b99392f90 --- /dev/null +++ b/apps/console/src/components/organizations/OrgMembersList.tsx @@ -0,0 +1,92 @@ +import { GetOrgQuery, OrgRole } from "~/@generated/graphql/graphql"; +import { Avatar } from "../common/Avatar"; +import { OrgRoleSelector } from "./OrgRoleSelector"; +import { useState } from "react"; +import { + useDeleteOrgMemberMutation, + useUpdateOrgMemberRoleMutation, +} from "~/graphql/hooks/mutations"; +import { useAuthContext } from "~/lib/providers/AuthProvider"; +import { useCurrentOrgMembership } from "~/lib/hooks/useCurrentOrgMembership"; +import { Button, Card, toast } from "@pezzo/ui"; +import { TrashIcon } from "lucide-react"; +import { GenericDestructiveConfirmationModal } from "../common/GenericDestructiveConfirmationModal"; + +type Member = GetOrgQuery["organization"]["members"][0]; + +interface Props { + members: Member[]; +} + +export const OrgMembersList = ({ members }: Props) => { + const { isOrgAdmin } = useCurrentOrgMembership(); + const { currentUser } = useAuthContext(); + const { mutate: deleteOrgMember, error: deleteOrgMemberError } = + useDeleteOrgMemberMutation(); + const { mutate: updateOrgMemberRole } = useUpdateOrgMemberRoleMutation(); + const [deletingMember, setDeletingMember] = useState(null); + + const handleDeleteMember = async (member: Member) => { + deleteOrgMember( + { id: member.id }, + { + onSuccess: () => { + setDeletingMember(null); + toast({ + title: "Member removed", + description: `${member.user.name} has been removed from your organization.`, + }); + }, + } + ); + }; + + const handleRoleChange = async (member: Member, role: OrgRole) => { + updateOrgMemberRole({ id: member.id, role: role }); + }; + + return ( + <> + handleDeleteMember(deletingMember)} + onCancel={() => setDeletingMember(null)} + /> + + {members.map((member) => ( + + +
    +
    + {member.user.name} {member.user.id === currentUser.id && " (You)"} +
    +
    {member.user.email}
    +
    +
    + handleRoleChange(member, newRole)} + showArrow={isOrgAdmin} + /> +
    + +
    + ))} + + ); +}; diff --git a/apps/console/src/components/organizations/OrgRoleSelector.tsx b/apps/console/src/components/organizations/OrgRoleSelector.tsx new file mode 100644 index 000000000..a58575e0f --- /dev/null +++ b/apps/console/src/components/organizations/OrgRoleSelector.tsx @@ -0,0 +1,33 @@ +import { OrgRole } from "~/@generated/graphql/graphql"; +import { + Select, + SelectTrigger, + SelectContent, + SelectValue, + SelectItem, +} from "@pezzo/ui"; + +interface Props { + value?: OrgRole; + onChange: (role: OrgRole) => void; + disabled?: boolean; + showArrow?: boolean; +} + +export const OrgRoleSelector = ({ + value = OrgRole.Member, + onChange, + disabled, +}: Props) => { + return ( + + ); +}; diff --git a/apps/console/src/components/projects/CreateNewProjectModal.tsx b/apps/console/src/components/projects/CreateNewProjectModal.tsx new file mode 100644 index 000000000..676a7c07e --- /dev/null +++ b/apps/console/src/components/projects/CreateNewProjectModal.tsx @@ -0,0 +1,126 @@ +import { useCreateProjectMutation } from "~/graphql/hooks/mutations"; +import { useCurrentOrganization } from "~/lib/hooks/useCurrentOrganization"; +import { trackEvent } from "~/lib/utils/analytics"; +import { useNavigate } from "react-router-dom"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + Button, + Form, + FormItem, + FormControl, + Input, + FormField, + FormMessage, + FormLabel, + Alert, + AlertTitle, + AlertDescription, +} from "@pezzo/ui"; +import { AlertCircle } from "lucide-react"; +import z from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; + +const formSchema = z.object({ + projectName: z + .string() + .min(1, "Name must be at least 1 character long") + .max(100, "Name can't be longer than 100 characters") + .regex( + /^[a-zA-Z]+(?:[ ]+[a-zA-Z]+)*$/, + "Name can only contain letters and spaces, e.g. My Project" + ), +}); + +interface Props { + open: boolean; + onClose: () => void; +} + +export const CreateNewProjectModal = ({ open, onClose }: Props) => { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + projectName: "", + }, + }); + const { organizationId } = useCurrentOrganization(); + const navigate = useNavigate(); + const { mutateAsync: createProject, error } = useCreateProjectMutation({ + onSuccess: () => { + form.reset(); + onClose(); + }, + }); + + const onSubmit = (values: z.infer) => { + createProject( + { + name: values.projectName, + organizationId, + }, + { + onSuccess: (data) => { + onClose(); + navigate(`/projects/${data.createProject.id}`); + }, + } + ); + + trackEvent("project_form_submitted"); + }; + + const onCancel = () => { + onClose(); + trackEvent("project_form_cancelled"); + }; + + return ( + + +
    + + + New Project + + {error && ( + + + Oops! + + {error.response.errors[0].message} + + + )} + ( + + Project name + + + + + + )} + /> + + + + + + + +
    +
    + ); +}; diff --git a/apps/console/src/components/projects/ProjectCopy.tsx b/apps/console/src/components/projects/ProjectCopy.tsx new file mode 100644 index 000000000..eaddbfb7c --- /dev/null +++ b/apps/console/src/components/projects/ProjectCopy.tsx @@ -0,0 +1,41 @@ +import { Button } from "@pezzo/ui"; +import { useCurrentProject } from "~/lib/hooks/useCurrentProject"; +import { copyToClipboard } from "~/lib/utils/browser-utils"; +import { useState } from "react"; +import { trackEvent } from "~/lib/utils/analytics"; +import { CheckIcon, CopyIcon } from "lucide-react"; + +export const ProjectCopy = () => { + const { project, isLoading } = useCurrentProject(); + const [clicked, setClicked] = useState(false); + + if (isLoading) return null; + + return ( + + ); +}; diff --git a/apps/console/src/components/projects/RenameProjectModal.tsx b/apps/console/src/components/projects/RenameProjectModal.tsx new file mode 100644 index 000000000..b28ce00ba --- /dev/null +++ b/apps/console/src/components/projects/RenameProjectModal.tsx @@ -0,0 +1,122 @@ +import { GetProjectsQuery } from "~/@generated/graphql/graphql"; +import { useUpdateProjectSettingsMutation } from "~/graphql/hooks/mutations"; +import { trackEvent } from "~/lib/utils/analytics"; +import { useEffect } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + Button, + Form, + FormItem, + FormControl, + Input, + FormField, + FormMessage, + FormLabel, + Alert, + AlertTitle, + AlertDescription, +} from "@pezzo/ui"; +import { AlertCircle } from "lucide-react"; +import z from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; + +const formSchema = z.object({ + projectName: z + .string() + .min(1, "Name must be at least 1 character long") + .max(100, "Name can't be longer than 100 characters") + .regex( + /^[a-zA-Z]+(?:[ ]+[a-zA-Z]+)*$/, + "Name can only contain letters and spaces, e.g. My Project" + ), +}); + +interface Props { + projectToRename: GetProjectsQuery["projects"][0] | null; + onClose: () => void; +} + +export const RenameProjectModal = ({ projectToRename, onClose }: Props) => { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + projectName: "", + }, + }); + const { mutate: updateProjectSettings, error } = + useUpdateProjectSettingsMutation(); + + useEffect(() => { + form.reset(); + }, [projectToRename, form]); + + const onSubmit = (values: z.infer) => { + updateProjectSettings( + { projectId: projectToRename.id, name: values.projectName }, + { + onSuccess: () => { + trackEvent("project_rename_submitted"); + onClose(); + }, + } + ); + }; + + const onCancel = () => { + trackEvent("project_rename_cancelled"); + onClose(); + }; + + return ( + + +
    + + + + Rename project{" "} + {projectToRename?.name} + + + {error && ( + + + Oops! + + {error.response.errors[0].message} + + + )} + ( + + Project name + + + + + + )} + /> + + + + + + + +
    +
    + ); +}; diff --git a/apps/console/src/components/prompts/CommitPromptModal.tsx b/apps/console/src/components/prompts/CommitPromptModal.tsx new file mode 100644 index 000000000..b8bc49cbc --- /dev/null +++ b/apps/console/src/components/prompts/CommitPromptModal.tsx @@ -0,0 +1,136 @@ +import { useCurrentPrompt } from "~/lib/providers/CurrentPromptContext"; +import { useCreatePromptVersion } from "~/graphql/hooks/mutations"; +import { trackEvent } from "~/lib/utils/analytics"; +import { useEditorContext } from "~/lib/providers/EditorContext"; +import { + Alert, + AlertDescription, + AlertTitle, + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + Input, + useToast, +} from "@pezzo/ui"; +import { AlertCircle } from "lucide-react"; +import { z } from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; + +interface Props { + open: boolean; + onClose: () => void; + onCommitted: () => void; +} + +const formSchema = z.object({ + message: z + .string() + .min(1, "Message must be at least 1 character long") + .max(120, "Message can't be longer than 64 characters"), +}); + +export const CommitPromptModal = ({ open, onClose, onCommitted }: Props) => { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + message: "", + }, + }); + const { prompt } = useCurrentPrompt(); + const { getForm: getEditorForm } = useEditorContext(); + const editorForm = getEditorForm(); + const { toast } = useToast(); + + const [settings, content, service, type] = editorForm.watch([ + "settings", + "content", + "service", + "type", + ]); + + const { + mutate: createPromptVersion, + error, + isLoading, + } = useCreatePromptVersion(); + + const handleFormFinish = async (values: z.infer) => { + const data = { + type, + message: values.message, + service: service, + content, + settings: settings || {}, + promptId: prompt.id, + }; + + createPromptVersion(data, { + onSuccess: () => { + form.reset(); + onCommitted(); + trackEvent("prompt_commit_submitted"); + toast({ + title: "Changes committed!", + description: `Your commit has been created successfully.`, + }); + }, + }); + }; + + const handleCancel = () => { + form.reset(); + onClose(); + trackEvent("prompt_commit_cancelled"); + }; + + return ( + + +
    + + + Commit Prompt - {prompt.name} + + {error && ( + + + Oops! + + {error.response.errors[0].message} + + + )} + + ( + + Commit message + + + + + + )} + /> + + + + + +
    +
    + ); +}; diff --git a/apps/console/src/components/prompts/ConsumePromptModal.tsx b/apps/console/src/components/prompts/ConsumePromptModal.tsx new file mode 100644 index 000000000..d6c4cd6eb --- /dev/null +++ b/apps/console/src/components/prompts/ConsumePromptModal.tsx @@ -0,0 +1,59 @@ +import { TypeScriptOpenAIIntegrationTutorial } from "../getting-started-wizard/TypeScriptOpenAIIntegrationTutorial"; +import { useState } from "react"; +import { PythonOpenAIIntegrationTutorial } from "../getting-started-wizard/PythonOpenAIIntegrationTutorial"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@pezzo/ui"; + +interface Props { + open: boolean; + onClose: () => void; +} + +export const ConsumePromptModal = ({ open, onClose }: Props) => { + const [integration, setIntegration] = useState("typescript"); + + const renderInsructions = () => { + switch (integration) { + case "typescript": + return ; + case "python": + return ; + } + }; + + return ( + + + + +
    +
    How to consume
    + +
    + +
    +
    +
    +
    +
    {renderInsructions()}
    +
    +
    + ); +}; diff --git a/apps/console/src/components/prompts/CreatePromptModal.tsx b/apps/console/src/components/prompts/CreatePromptModal.tsx new file mode 100644 index 000000000..0a99ffa42 --- /dev/null +++ b/apps/console/src/components/prompts/CreatePromptModal.tsx @@ -0,0 +1,137 @@ +import { useMutation } from "@tanstack/react-query"; +import { CREATE_PROMPT } from "~/graphql/definitions/mutations/prompts"; +import { gqlClient, queryClient } from "~/lib/graphql"; +import { CreatePromptMutation } from "~/@generated/graphql/graphql"; +import { GraphQLErrorResponse } from "~/graphql/types"; +import { useCurrentProject } from "~/lib/hooks/useCurrentProject"; +import { trackEvent } from "~/lib/utils/analytics"; +import z from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { AlertCircle } from "lucide-react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + Button, + Form, + FormItem, + FormControl, + Input, + FormField, + FormMessage, + FormLabel, + Alert, + AlertTitle, + AlertDescription, +} from "@pezzo/ui"; +import { useNavigate } from "react-router-dom"; + +interface Props { + open: boolean; + onClose: () => void; + onCreated: (id: string) => void; +} + +const formSchema = z.object({ + promptName: z + .string() + .min(1, "Name must be at least 1 character long") + .max(100, "Name can't be longer than 64 characters") + .regex( + /^[a-zA-Z0-9]+$/, + "Name can only contain letters and numbers, e.g. SentimentAnalysis" + ), +}); + +export const CreatePromptModal = ({ open, onClose, onCreated }: Props) => { + const { project } = useCurrentProject(); + const navigate = useNavigate(); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + promptName: "", + }, + }); + + const { mutate, error } = useMutation< + CreatePromptMutation, + GraphQLErrorResponse, + z.infer + >({ + mutationFn: (data) => + gqlClient.request(CREATE_PROMPT, { + data: { + name: data.promptName, + projectId: project.id, + }, + }), + onSuccess: (data) => { + onCreated(data.createPrompt.id); + queryClient.invalidateQueries({ queryKey: ["prompts"] }); + trackEvent("prompt_created", { + promptId: data.createPrompt.id, + }); + navigate(`/projects/${project.id}/prompts/${data.createPrompt.id}`); + }, + }); + + const onSubmit = (data: z.infer) => { + mutate(data); + form.reset(); + trackEvent("prompt_form_submitted"); + }; + + const onCancel = () => { + trackEvent("prompt_form_cancelled"); + onClose(); + }; + + return ( + + +
    + + + New Prompt + + {error && ( + + + Oops! + + {error.response.errors[0].message} + + + )} + ( + + Prompt name + + + + + + )} + /> + + + + + + + +
    +
    + ); +}; diff --git a/apps/console/src/components/prompts/PromptSettingsSlider.tsx b/apps/console/src/components/prompts/PromptSettingsSlider.tsx new file mode 100644 index 000000000..0307c960f --- /dev/null +++ b/apps/console/src/components/prompts/PromptSettingsSlider.tsx @@ -0,0 +1,43 @@ +import { Input, Slider } from "@pezzo/ui"; +import { ControllerRenderProps, FieldValues } from "react-hook-form"; + +interface Props { + min: number; + max: number; + step: number; + value?: number; + onChange?: (value: number) => void; + field: ControllerRenderProps; +} + +export const PromptSettingsSlider = ({ + min, + max, + step, + value, + onChange, + field, +}: Props) => { + return ( +
    + field.onChange(value[0])} + value={[field.value]} + min={min} + max={max} + step={step} + /> + field.onChange(e.target.valueAsNumber)} + size="sm" + className="w-20" + /> +
    + ); +}; diff --git a/apps/console/src/components/prompts/PromptVersionSelector.tsx b/apps/console/src/components/prompts/PromptVersionSelector.tsx new file mode 100644 index 000000000..484637852 --- /dev/null +++ b/apps/console/src/components/prompts/PromptVersionSelector.tsx @@ -0,0 +1,80 @@ +import { + Select, + SelectTrigger, + SelectContent, + SelectItem, + SelectValue, +} from "@pezzo/ui"; +import { useCurrentPrompt } from "~/lib/providers/CurrentPromptContext"; +import { usePromptVersions } from "~/lib/hooks/usePromptVersions"; +import { trackEvent } from "~/lib/utils/analytics"; +import { useEditorContext } from "~/lib/providers/EditorContext"; + +export const PromptVersionSelector = () => { + const { prompt } = useCurrentPrompt(); + const latestVersion = prompt.latestVersion; + const { promptVersions } = usePromptVersions(prompt.id); + const { currentVersionSha, setCurrentVersionSha } = useEditorContext(); + + const itemsFromVersionsList = + (promptVersions && + promptVersions + .filter((version) => version.sha !== latestVersion.sha) + .map((version) => ({ + key: version.sha, + label: `${version.sha.slice(0, 7)} - ${version.message} (by ${ + version.createdBy.name + })`, + onClick: () => { + alert(version.sha); + setCurrentVersionSha(version.sha); + trackEvent("prompt_version_selected"); + }, + }))) || + []; + + const menuItems = [ + { + key: latestVersion.sha, + label: `Latest (${latestVersion.sha.slice(0, 7)}) - ${ + latestVersion.message + } (by ${latestVersion.createdBy.name}))`, + sha: latestVersion.sha, + }, + ...itemsFromVersionsList, + ]; + + const handleVersionChange = (versionSha: string) => { + setCurrentVersionSha(versionSha); + trackEvent("prompt_version_selected", { + version: versionSha, + }); + }; + + const isLatest = currentVersionSha === latestVersion.sha; + + const selectedVersionLabel = isLatest + ? `Latest (${latestVersion.sha.slice(0, 7)})` + : `${currentVersionSha.slice(0, 7)}`; + + return ( +
    + +
    + ); +}; diff --git a/apps/console/src/components/prompts/PublishPromptModal.tsx b/apps/console/src/components/prompts/PublishPromptModal.tsx new file mode 100644 index 000000000..2a6091d61 --- /dev/null +++ b/apps/console/src/components/prompts/PublishPromptModal.tsx @@ -0,0 +1,138 @@ +import { useEnvironments } from "~/lib/hooks/useEnvironments"; +import { useCurrentPrompt } from "~/lib/providers/CurrentPromptContext"; +import { useEffect, useState } from "react"; +import { useMutation } from "@tanstack/react-query"; +import { gqlClient, queryClient } from "~/lib/graphql"; +import { PUBLISH_PROMPT } from "~/graphql/definitions/mutations/prompt-environments"; +import { + PublishPromptInput, + PublishPromptMutation, +} from "~/@generated/graphql/graphql"; +import { trackEvent } from "~/lib/utils/analytics"; +import { useEditorContext } from "~/lib/providers/EditorContext"; +import { + Alert, + AlertDescription, + AlertTitle, + Button, + Card, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + useToast, +} from "@pezzo/ui"; +import { AlertCircle, CheckSquare, Square } from "lucide-react"; +import { GraphQLErrorResponse } from "~/graphql/types"; + +interface Props { + open: boolean; + onClose: () => void; +} + +export const PublishPromptModal = ({ open, onClose }: Props) => { + const { currentVersionSha } = useEditorContext(); + const { prompt } = useCurrentPrompt(); + const { environments } = useEnvironments(); + const [selectedEnvironmentId, setSelectedEnvironmentId] = + useState(undefined); + const { toast } = useToast(); + + const { + mutate: publishPrompt, + error, + isLoading, + } = useMutation< + PublishPromptMutation, + GraphQLErrorResponse, + PublishPromptInput + >({ + mutationFn: (data: PublishPromptInput) => + gqlClient.request(PUBLISH_PROMPT, { data }), + mutationKey: ["publishPrompt", prompt.id, currentVersionSha], + onSuccess: () => { + queryClient.invalidateQueries(["promptEnvironments"]); + }, + }); + + const handlePublish = async () => { + publishPrompt( + { + promptId: prompt.id, + environmentId: selectedEnvironmentId, + promptVersionSha: currentVersionSha, + }, + { + onSuccess: () => { + toast({ + title: "Prompt published!", + description: `Your prompt has been published successfully.`, + }); + onClose(); + }, + } + ); + trackEvent("prompt_publish_clicked"); + }; + + useEffect(() => { + setSelectedEnvironmentId(undefined); + }, [open]); + + const handleEnvironmentClick = (environmentId: string) => { + setSelectedEnvironmentId(environmentId); + }; + + return ( + environments && ( + + onClose()}> + Publish Prompt - {prompt.name} +
    + {error && ( + + + Oops! + + {error.response.errors[0].message} + + + )} + +

    + Select the environment to publish this version to. +

    +
    + +
    + {environments.map((environment) => ( + handleEnvironmentClick(environment.id)} + className="flex cursor-pointer items-center justify-between border border-card p-4 hover:border-primary" + > +
    {environment.name}
    +
    + {selectedEnvironmentId === environment.id && ( + + )} + {selectedEnvironmentId !== environment.id && ( + + )} +
    +
    + ))} +
    + + + +
    +
    + ) + ); +}; diff --git a/apps/console/src/components/prompts/editor/ProviderSelector/ProviderSelector.tsx b/apps/console/src/components/prompts/editor/ProviderSelector/ProviderSelector.tsx new file mode 100644 index 000000000..21246c40c --- /dev/null +++ b/apps/console/src/components/prompts/editor/ProviderSelector/ProviderSelector.tsx @@ -0,0 +1,62 @@ +import { sortRenderedProviders } from "./providers"; +import { ProviderProps } from "./types"; +import { PromptService } from "~/@generated/graphql/graphql"; +import { useEditorContext } from "~/lib/providers/EditorContext"; +import { + FormControl, + SelectContent, + FormField, + FormItem, + Select, + SelectTrigger, + SelectValue, + SelectItem, +} from "@pezzo/ui"; + +export const ProviderSelector = () => { + const { getForm } = useEditorContext(); + const form = getForm(); + + const settings = form.watch("settings"); + const providers = sortRenderedProviders( + Object.keys(settings) as PromptService[] + ); + + const renderProvider = (provider: ProviderProps) => { + const isAvailable = provider.value === PromptService.OpenAiChatCompletion; + + return ( + +
    +
    {provider.image}
    +
    {provider.label}
    +
    +
    + ); + }; + + return ( + ( + + + + + + )} + /> + ); +}; diff --git a/apps/console/src/components/prompts/editor/ProviderSelector/providers.tsx b/apps/console/src/components/prompts/editor/ProviderSelector/providers.tsx new file mode 100644 index 000000000..439564330 --- /dev/null +++ b/apps/console/src/components/prompts/editor/ProviderSelector/providers.tsx @@ -0,0 +1,55 @@ +import { ProviderProps } from "./types"; +import { promptProvidersMapping } from "@pezzo/types"; +import { PromptService } from "~/@generated/graphql/graphql"; + +// Logos +import OpenAILogo from "~/assets/providers/openai-logo.png"; +import AzureOpenAILogo from "~/assets/providers/azure-logo.png"; +import AnthropicLogo from "~/assets/providers/anthropic-logo.png"; + +export const providersList: ProviderProps[] = [ + { + image: OpenAI, + value: PromptService.OpenAiChatCompletion, + label: promptProvidersMapping[PromptService.OpenAiChatCompletion].name, + }, + { + image: ( + Azure OpenAI + ), + value: PromptService.AzureOpenAiChatCompletion, + label: promptProvidersMapping[PromptService.AzureOpenAiChatCompletion].name, + }, + { + image: ( + Anthropic + ), + value: PromptService.AnthropicCompletion, + label: promptProvidersMapping[PromptService.AnthropicCompletion].name, + }, +]; + +/** + * This function sorts the providers list for correct rendering in the UI. + * It divides them into two groups - managed and unmanaged. + * Within those groups, they will be provided in the original order as displayed in the "providersList" array. + * + * @param providersKeys Array of provider keys that are managed + * @returns + */ +export const sortRenderedProviders = (providersKeys: PromptService[]) => { + const managed = providersList.filter((provider) => + providersKeys.includes(provider.value) + ); + const unmanaged = providersList.filter( + (provider) => !providersKeys.includes(provider.value) + ); + const sort = (a, b) => providersList.indexOf(a) - providersList.indexOf(b); + const sortedManaged = managed.sort(sort); + const sortedUnmanaged = unmanaged.sort(sort); + return [...sortedManaged, ...sortedUnmanaged]; +}; diff --git a/apps/console/src/components/prompts/editor/ProviderSelector/types.ts b/apps/console/src/components/prompts/editor/ProviderSelector/types.ts new file mode 100644 index 000000000..cfb14b91c --- /dev/null +++ b/apps/console/src/components/prompts/editor/ProviderSelector/types.ts @@ -0,0 +1,7 @@ +import { PromptService } from "~/@generated/graphql/graphql"; + +export interface ProviderProps { + image: React.ReactNode; + value: PromptService; + label: string; +} diff --git a/apps/console/src/components/prompts/editor/ProviderSettings/ProviderSettingsSchemaRenderer.tsx b/apps/console/src/components/prompts/editor/ProviderSettings/ProviderSettingsSchemaRenderer.tsx new file mode 100644 index 000000000..9c14ce6ab --- /dev/null +++ b/apps/console/src/components/prompts/editor/ProviderSettings/ProviderSettingsSchemaRenderer.tsx @@ -0,0 +1,89 @@ +import { + FormField, + FormControl, + FormItem, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + FormLabel, +} from "@pezzo/ui"; +import { PromptSettingsSlider } from "../../PromptSettingsSlider"; +import { useEditorContext } from "~/lib/providers/EditorContext"; +import { generateFormSchema } from "./providers/openai-chat-completion"; +import { SelectFormField, SliderFormField } from "./types"; +import { ControllerRenderProps, FieldValues } from "react-hook-form"; + +interface Props { + schema: ReturnType; +} + +export const ProviderSettingsSchemaRenderer = ({ schema }: Props) => { + const { getForm } = useEditorContext(); + const form = getForm(); + + const renderField = ( + renderSchema: any, + field: ControllerRenderProps + ) => { + switch (renderSchema.type) { + case "select": + return renderSelectField(renderSchema, field); + case "slider": + return renderSliderField(renderSchema, field); + } + }; + + const renderSelectField = ( + renderSchema: SelectFormField, + field: ControllerRenderProps + ) => { + return ( + + ); + }; + + const renderSliderField = ( + renderSchema: SliderFormField, + field: ControllerRenderProps + ) => { + return ( + + ); + }; + + return ( + <> + {schema.map((renderSchema, index) => ( + ( + + {renderSchema.label} + {renderField(renderSchema, field)} + + )} + /> + ))} + + ); +}; diff --git a/apps/console/src/components/prompts/editor/ProviderSettings/providers/azure-openai-chat-completion.ts b/apps/console/src/components/prompts/editor/ProviderSettings/providers/azure-openai-chat-completion.ts new file mode 100644 index 000000000..3846866ad --- /dev/null +++ b/apps/console/src/components/prompts/editor/ProviderSettings/providers/azure-openai-chat-completion.ts @@ -0,0 +1,85 @@ +import OpenAI from "openai"; +import { FormSchema, ProviderSettingsDefinition } from "../types"; + +type OpenAIProviderSettings = Omit< + OpenAI.Chat.Completions.CompletionCreateParams, + "messages" +>; + +const defaultSettings: OpenAIProviderSettings = { + model: "gpt-35-turbo", + temperature: 0.7, + max_tokens: 256, + top_p: 1, + frequency_penalty: 0, + presence_penalty: 0, +}; + +const generateFormSchema = (settings: OpenAIProviderSettings): FormSchema => { + const getMaxResponseTokensMaxValue = () => { + switch (settings.model) { + case "gpt-35-turbo": + return 8192; + case "gpt-4": + return 4096; + } + }; + + return [ + { + label: "Model", + name: "model", + type: "select", + options: [ + { value: "gpt-35-turbo", label: "gpt-35-turbo" }, + { value: "gpt-4", label: "gpt-4" }, + ], + }, + { + label: "Temperature", + name: "temperature", + type: "slider", + min: 0, + max: 1, + step: 0.1, + }, + { + label: "Max Response Length", + name: "max_tokens", + type: "slider", + min: 1, + max: getMaxResponseTokensMaxValue(), + step: 1, + }, + { + label: "Top P", + name: "top_p", + type: "slider", + min: 0, + max: 1, + step: 0.1, + }, + { + label: "Frequency Penalty", + name: "frequency_penalty", + type: "slider", + min: 0, + max: 1, + step: 0.1, + }, + { + label: "Presence Penalty", + name: "presence_penalty", + type: "slider", + min: 0, + max: 1, + step: 0.1, + }, + ]; +}; + +export const azureOpenAIChatCompletionSettingsDefinition: ProviderSettingsDefinition = + { + defaultSettings, + generateFormSchema, + }; diff --git a/apps/console/src/components/prompts/editor/ProviderSettings/providers/index.ts b/apps/console/src/components/prompts/editor/ProviderSettings/providers/index.ts new file mode 100644 index 000000000..fd763857f --- /dev/null +++ b/apps/console/src/components/prompts/editor/ProviderSettings/providers/index.ts @@ -0,0 +1,14 @@ +import { PromptService } from "~/@generated/graphql/graphql"; +import { azureOpenAIChatCompletionSettingsDefinition } from "./azure-openai-chat-completion"; +import { openAIChatCompletionSettingsDefinition } from "./openai-chat-completion"; + +const defaultSettingsMap = { + [PromptService.OpenAiChatCompletion]: + openAIChatCompletionSettingsDefinition.defaultSettings, + [PromptService.AzureOpenAiChatCompletion]: + azureOpenAIChatCompletionSettingsDefinition.defaultSettings, +}; + +export const getServiceDefaultSettings = (service: PromptService) => { + return defaultSettingsMap[service]; +}; diff --git a/apps/console/src/components/prompts/editor/ProviderSettings/providers/openai-chat-completion.ts b/apps/console/src/components/prompts/editor/ProviderSettings/providers/openai-chat-completion.ts new file mode 100644 index 000000000..ef36eb4a3 --- /dev/null +++ b/apps/console/src/components/prompts/editor/ProviderSettings/providers/openai-chat-completion.ts @@ -0,0 +1,85 @@ +import OpenAI from "openai"; +import { FormSchema, ProviderSettingsDefinition } from "../types"; +import { OpenAIToolkit } from "@pezzo/llm-toolkit"; + +const { gptModels } = OpenAIToolkit; + +type OpenAIProviderSettings = Omit< + OpenAI.Chat.Completions.CompletionCreateParams, + "messages" +>; + +const defaultSettings: OpenAIProviderSettings = { + model: "gpt-3.5-turbo", + temperature: 0.7, + max_tokens: 256, + top_p: 1, + frequency_penalty: 0, + presence_penalty: 0, +}; + +export const generateFormSchema = ( + settings: OpenAIProviderSettings +): FormSchema => { + const options = Object.keys(gptModels).map((model) => ({ + value: model, + label: model, + })); + + const maxResponseTokensValue = gptModels[settings.model].maxTokens; + + return [ + { + label: "Model", + name: "model", + type: "select", + options, + }, + { + label: "Temperature", + name: "temperature", + type: "slider", + min: 0, + max: 1, + step: 0.1, + }, + { + label: "Max Response Length", + name: "max_tokens", + type: "slider", + min: 1, + max: maxResponseTokensValue, + step: 1, + }, + { + label: "Top P", + name: "top_p", + type: "slider", + min: 0, + max: 1, + step: 0.1, + }, + { + label: "Frequency Penalty", + name: "frequency_penalty", + type: "slider", + min: 0, + max: 1, + step: 0.1, + }, + { + label: "Presence Penalty", + name: "presence_penalty", + type: "slider", + min: 0, + max: 1, + step: 0.1, + }, + ]; +}; + +export const openAIChatCompletionSettingsDefinition: ProviderSettingsDefinition = + { + defaultSettings, + generateFormSchema, + }; diff --git a/apps/console/src/components/prompts/editor/ProviderSettings/types.ts b/apps/console/src/components/prompts/editor/ProviderSettings/types.ts new file mode 100644 index 000000000..f7082ca14 --- /dev/null +++ b/apps/console/src/components/prompts/editor/ProviderSettings/types.ts @@ -0,0 +1,28 @@ +interface BaseFormField { + label: string; + name: string; +} + +export interface SelectFormField extends BaseFormField { + type: "select"; + options: { + value: string; + label: string; + }[]; +} + +export interface SliderFormField extends BaseFormField { + type: "slider"; + min: number; + max: number; + step: number; +} + +export type FormField = SelectFormField | SliderFormField; + +export type FormSchema = FormField[]; + +export interface ProviderSettingsDefinition { + defaultSettings: TSettings; + generateFormSchema: (settings: TSettings) => FormSchema; +} diff --git a/apps/console/src/components/prompts/editor/chat/ChatEditMode.tsx b/apps/console/src/components/prompts/editor/chat/ChatEditMode.tsx new file mode 100644 index 000000000..c34f493cc --- /dev/null +++ b/apps/console/src/components/prompts/editor/chat/ChatEditMode.tsx @@ -0,0 +1,82 @@ +import { ChatMessage } from "./ChatMessage"; +import { trackEvent } from "~/lib/utils/analytics"; +import { useCurrentPrompt } from "~/lib/providers/CurrentPromptContext"; +import { useEditorContext } from "~/lib/providers/EditorContext"; +import { Button } from "@pezzo/ui"; +import { PlusIcon } from "lucide-react"; +import { + DragDropContext, + Droppable, + Draggable, + DropResult, +} from "react-beautiful-dnd"; + +export const ChatEditMode = () => { + const { promptId } = useCurrentPrompt(); + const { messagesArray } = useEditorContext(); + const { fields, append, remove, move } = messagesArray; + + const handleAdd = () => { + append({ + role: "user", + content: "", + }); + + trackEvent("prompt_chat_completion_message_created", { + promptId, + }); + }; + + const onDragEnd = (result: DropResult) => { + if (!result.destination) { + return; + } + + move(result.source.index, result.destination.index); + }; + + return ( +
    + + + {(provided) => ( +
    + {fields.map((field, index) => ( + + {(provided) => ( +
    + { + remove(index); + trackEvent("prompt_chat_completion_message_deleted", { + promptId, + }); + }} + /> +
    + )} +
    + ))} + {provided.placeholder} +
    + )} +
    +
    + +
    + +
    +
    + ); +}; diff --git a/apps/console/src/components/prompts/editor/chat/ChatMessage.tsx b/apps/console/src/components/prompts/editor/chat/ChatMessage.tsx new file mode 100644 index 000000000..759630132 --- /dev/null +++ b/apps/console/src/components/prompts/editor/chat/ChatMessage.tsx @@ -0,0 +1,129 @@ +import { useCurrentPrompt } from "~/lib/providers/CurrentPromptContext"; +import { trackEvent } from "~/lib/utils/analytics"; +import { + Card, + CardContent, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + FormControl, + FormField, + FormItem, + FormMessage, + Textarea, +} from "@pezzo/ui"; +import { + ArrowDownUpIcon, + BotIcon, + GripVertical, + LucideIcon, + TrashIcon, + UserIcon, + WrenchIcon, +} from "lucide-react"; +import { useEditorContext } from "~/lib/providers/EditorContext"; +import { useMemo } from "react"; +import { useWatch } from "react-hook-form"; + +interface Props { + index: number; + canDelete?: boolean; + onDelete: () => void; +} + +type role = "system" | "user" | "assistant"; + +const roleOptions: { + label: string; + value: role; + icon: LucideIcon; +}[] = [ + { label: "System", value: "system", icon: WrenchIcon }, + { label: "User", value: "user", icon: UserIcon }, + { label: "Assistant", value: "assistant", icon: BotIcon }, +]; + +export const ChatMessage = ({ index, canDelete = true, onDelete }: Props) => { + const { promptId } = useCurrentPrompt(); + const { getForm } = useEditorContext(); + const form = getForm(); + const message = useWatch({ + control: form.control, + name: `content.messages.${index}`, + }); + + const currentRole = useMemo( + () => roleOptions.find((roleOption) => message?.role === roleOption.value), + [message?.role] + ); + + if (!message) { + return null; + } + + const handleRoleChange = (role: "system" | "user" | "assistant") => { + form.setValue(`content.messages.${index}.role`, role, { + shouldDirty: true, + }); + trackEvent("prompt_chat_completion_message_role_changed", { + promptId, + role, + }); + }; + + return ( + +
    + + + +
    + + {currentRole.label} +
    +
    + + {roleOptions.map((role) => ( + handleRoleChange(role.value)} + > + + {role.label} + + ))} + +
    + + {canDelete && ( + + )} +
    + + ( + + + + + + + + ); +}; diff --git a/apps/console/src/components/prompts/prompt-tester/PromptVariables.tsx b/apps/console/src/components/prompts/prompt-tester/PromptVariables.tsx new file mode 100644 index 000000000..c83f31109 --- /dev/null +++ b/apps/console/src/components/prompts/prompt-tester/PromptVariables.tsx @@ -0,0 +1,49 @@ +import { PromptTesterVariablesInputs } from "~/lib/providers/PromptTesterContext"; +import { PromptVariable } from "./PromptVariable"; +import { isJson } from "~/lib/utils/is-json"; +import { UseFormReturn } from "react-hook-form"; +import { FormField, FormItem, FormMessage } from "@pezzo/ui"; + +interface Props { + variables: Record; + form: UseFormReturn; +} + +export const PromptVariables = ({ variables, form }: Props) => { + if (Object.keys(variables).length === 0) { + return

    No variables found

    ; + } + + return ( + <> +

    Provide values for your variables below.

    +
    + {Object.keys(variables).map((variableName) => ( + ( + + + + + )} + /> + ))} +
    + + ); +}; diff --git a/apps/console/src/components/prompts/prompt-tester/VariablesStep.tsx b/apps/console/src/components/prompts/prompt-tester/VariablesStep.tsx new file mode 100644 index 000000000..9380e3b38 --- /dev/null +++ b/apps/console/src/components/prompts/prompt-tester/VariablesStep.tsx @@ -0,0 +1,37 @@ +import { + PromptTesterVariablesInputs, + usePromptTester, +} from "~/lib/providers/PromptTesterContext"; +import { PromptVariables } from "./PromptVariables"; +import { Button, DialogFooter, Form } from "@pezzo/ui"; +import { useWatch } from "react-hook-form"; + +interface Props { + onSubmit: () => void; +} + +export const VariablesStep = ({ onSubmit }: Props) => { + const { testVariablesForm: form, isTestLoading } = usePromptTester(); + const testVariablesFormValues = useWatch({ + control: form.control, + }); + + const handleSubmit = (values: PromptTesterVariablesInputs) => { + onSubmit(); + }; + + return ( +
    +
    + + + + + + + +
    + ); +}; diff --git a/apps/console/src/components/prompts/views/PromptVersionsView.tsx b/apps/console/src/components/prompts/views/PromptVersionsView.tsx new file mode 100644 index 000000000..fcac76eeb --- /dev/null +++ b/apps/console/src/components/prompts/views/PromptVersionsView.tsx @@ -0,0 +1,163 @@ +import { usePromptVersions } from "~/lib/hooks/usePromptVersions"; +import { useCurrentPrompt } from "~/lib/providers/CurrentPromptContext"; +import { trackEvent } from "~/lib/utils/analytics"; +import React, { useMemo } from "react"; +import { + ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@pezzo/ui"; +import { InlineCodeSnippet } from "~/components/common/InlineCodeSnippet"; + +type PromptVersion = ReturnType["promptVersions"][0]; + +const getTableColumns = (): ColumnDef[] => { + return [ + { + accessorKey: "sha", + id: "sha", + header: "SHA", + cell: ({ row }) => ( +
    + {row.original.sha.slice(0, 7)} +
    + ), + }, + { + accessorKey: "author", + id: "author", + header: "Author", + cell: ({ row }) =>
    {row.original.createdBy.email}
    , + }, + { + accessorKey: "message", + id: "message", + header: "Message", + cell: ({ row }) => { + const msg = row.original.message; + + if (!msg) { + return
    No message
    ; + } else { + return
    {row.original.message}
    ; + } + }, + }, + { + accessorKey: "time", + id: "time", + header: "Time", + cell: ({ row }) => ( +
    {new Date(row.original.createdAt).toLocaleString()}
    + ), + }, + ]; +}; + +export const PromptVersionsView = () => { + const { prompt } = useCurrentPrompt(); + const { promptVersions } = usePromptVersions(prompt.id); + + React.useEffect(() => { + trackEvent("prompt_versions_viewed"); + }, [prompt.id]); + + const data = useMemo(() => promptVersions, [promptVersions]); + const columns: ColumnDef[] = useMemo( + () => getTableColumns(), + [] + ); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + defaultColumn: { + minSize: 0, + size: Number.MAX_SAFE_INTEGER, + maxSize: Number.MAX_SAFE_INTEGER, + }, + }); + + return ( + promptVersions && ( +
    +
    +
    + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
    + + + ) + ); +}; diff --git a/apps/console/src/components/requests/RequestDetails.tsx b/apps/console/src/components/requests/RequestDetails.tsx new file mode 100644 index 000000000..6707ebb6c --- /dev/null +++ b/apps/console/src/components/requests/RequestDetails.tsx @@ -0,0 +1,281 @@ +import { + ObservabilityReportMetadata, + ObservabilityRequest, + ObservabilityResponse, + Provider, + ObservabilityReportProperties, +} from "@pezzo/types"; +import { + Alert, + AlertDescription, + Tooltip, + TooltipProvider, + TooltipTrigger, + TooltipContent, + Tabs, + TabsList, + TabsTrigger, + Button, +} from "@pezzo/ui"; +import ms from "ms"; +import { useState } from "react"; +import { RequestResponseViewJsonView } from "./RequestResponseViewJsonView"; +import { trackEvent } from "~/lib/utils/analytics"; +import { + BracesIcon, + CheckIcon, + CircleSlash, + CoinsIcon, + InfoIcon, + Link, + MessageSquare, +} from "lucide-react"; +import { Tag } from "../common/Tag"; +import { cn } from "@pezzo/ui/utils"; +import { normalizeOpenAIChatResponse } from "~/features/chat/normalizers/openai-normalizer"; +import { ChatView } from "~/features/chat/ChatView"; +import { useCopyToClipboard } from "usehooks-ts"; +import { ModelDetails } from "~/pages/requests/ModelDetails"; +import { useReport } from "~/graphql/hooks/queries"; +import { useCurrentProject } from "~/lib/hooks/useCurrentProject"; +import OpenAI from "openai"; + +type Mode = "chat" | "json"; + +interface Props { + disableCopy?: boolean; + id: string; +} + +const getClientDisplayName = (client: string) => { + switch (client) { + case "pezzo-ts": + return "TypeScript (Official)"; + case "pezzo-python": + return "Python (Official)"; + default: + return client; + } +}; + +export const RequestDetails = (props: Props) => { + const { projectId } = useCurrentProject(); + const disableCopy = props.disableCopy || false; + const { report } = useReport( + { projectId, reportId: props.id }, + { enabled: !!projectId && !!props.id } + ); + + const isSuccess = report.isError === false; + const isError = report.isError === true; + + const [selectedMode, setSelectedMode] = useState( + isSuccess ? "chat" : "json" + ); + + const [copied, copy] = useCopyToClipboard(); + + const handleDisplayModeChange = (mode: Mode) => { + setSelectedMode(mode); + trackEvent("prompt_test_display_mode_changed", { mode }); + }; + + const clientString = + report.client && report.clientVersion + ? `${getClientDisplayName(report.client)} - v${report.clientVersion}` + : "Unknown"; + + const listData = [ + { + title: "Request ID", + description: report.id, + }, + { + title: "Cache", + description: ( +
    + {report.cacheEnabled ? "enabled" : "disabled"} + + {report.cacheEnabled && {report.cacheHit ? "hit" : "miss"}} +
    + ), + }, + { + title: "Client", + description: clientString, + }, + { + title: "Provider", + description:
    {report.provider}
    , + }, + { + title: "Model", + description: ( + + ), + }, + { + title: "Tokens", + description: report.isError ? ( + "0" + ) : ( +
    + {report.totalTokens} + + + + + + +
    +
    + Prompt tokens: + {report.promptTokens} +
    +
    + Completion tokens:{" "} + {report.completionTokens} +
    +
    +
    +
    +
    +
    + ), + }, + { + title: "Cost", + description: `$${report?.totalCost?.toFixed(5) ?? 0}`, + }, + { + title: "Status", + description: ( +
    + {isError ? ( + <> + + {report.responseStatusCode} Error + + ) : ( + <> + + Success + + )} +
    + ), + }, + { + title: "Environment", + description: report.environment, + }, + { + title: "Duration", + description: ms(report.duration), + }, + ]; + + const renderResponse = () => { + if (selectedMode === "json") { + return ( + + ); + } + + const chat = normalizeOpenAIChatResponse( + report.requestBody as OpenAI.ChatCompletionCreateParams, + report.responseBody as OpenAI.ChatCompletion + ); + return ; + }; + + return ( +
    +
    +

    Request Details

    + + {!disableCopy && ( +
    + +
    + )} +
    + +
    + {report.environment === "PLAYGROUND" && ( + + + + This is a test request from the Pezzo Console + + + )} + +
    + {listData.map((item) => ( +
    +
    {item.title}
    +
    {item.description}
    +
    + ))} + +
    +
    + handleDisplayModeChange(value)} + > + + + Chat + + + JSON + + + +
    + + {renderResponse()} +
    +
    +
    +
    + ); +}; diff --git a/apps/console/src/components/requests/RequestFilters.tsx b/apps/console/src/components/requests/RequestFilters.tsx new file mode 100644 index 000000000..08b8a1629 --- /dev/null +++ b/apps/console/src/components/requests/RequestFilters.tsx @@ -0,0 +1,34 @@ +import { AddFilterItem, FilterItem } from "./filters/FilterItem"; +import { useFiltersAndSortParams } from "~/lib/hooks/useFiltersAndSortParams"; +import { + NUMBER_FILTER_OPERATORS, + STRING_FILTER_OPERATORS, +} from "~/lib/constants/filters"; + +export const RequestFilters = () => { + const { filters, removeFilter, addFilter } = useFiltersAndSortParams(); + + return ( +
    + + + {filters.length > 0 && ( +
    + {filters.map((filter) => ( + op.value === filter.operator + )?.label + } + value={filter.value} + onRemoveFilter={() => removeFilter(filter)} + /> + ))} +
    + )} +
    + ); +}; diff --git a/apps/console/src/components/requests/RequestResponseViewJsonView.tsx b/apps/console/src/components/requests/RequestResponseViewJsonView.tsx new file mode 100644 index 000000000..43bbef7bb --- /dev/null +++ b/apps/console/src/components/requests/RequestResponseViewJsonView.tsx @@ -0,0 +1,33 @@ +import { Card } from "@pezzo/ui"; +import OpenAI from "openai"; + +interface Props { + requestBody: OpenAI.ChatCompletionCreateParams; + responseBody: OpenAI.ChatCompletion; +} + +export const RequestResponseViewJsonView = ({ + requestBody, + responseBody, +}: Props) => { + return ( +
    +
    +

    Request

    + +
    +            {JSON.stringify(requestBody, null, 2)}
    +          
    +
    +
    +
    +

    Response

    + +
    +            {JSON.stringify(responseBody, null, 2)}
    +          
    +
    +
    +
    + ); +}; diff --git a/apps/console/src/components/requests/filters/FilterItem.tsx b/apps/console/src/components/requests/filters/FilterItem.tsx new file mode 100644 index 000000000..f0fddbe71 --- /dev/null +++ b/apps/console/src/components/requests/filters/FilterItem.tsx @@ -0,0 +1,232 @@ +import { + Button, + Form, + FormControl, + FormField, + FormItem, + Select, + SelectTrigger, + SelectContent, + SelectValue, + SelectItem, + Input, +} from "@pezzo/ui"; +import { useCallback, useMemo, useState } from "react"; +import { FilterInput, FilterOperator } from "~/@generated/graphql/graphql"; +import { + FILTER_FIELDS_LIST, + NUMBER_FILTER_OPERATORS, + STRING_FILTER_OPERATORS, +} from "~/lib/constants/filters"; +import { useForm } from "react-hook-form"; +import z from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { CheckIcon, PlusIcon, XIcon } from "lucide-react"; + +interface Props { + onRemoveFilter: () => void; + field: string; + operator: string; + value: string; + property?: string; +} + +export const FilterItem = ({ + field, + operator, + value, + onRemoveFilter, +}: Props) => { + const translatedField = useMemo(() => { + const found = FILTER_FIELDS_LIST.find((f) => f.value === field); + return found?.label.toLocaleLowerCase(); + }, [field]); + + return ( +
    +
    {translatedField}
    +
    {operator}
    +
    {value}
    + +
    + ); +}; + +const formSchema = z.object({ + field: z.string().min(1).max(100), + operator: z.nativeEnum(FilterOperator), + value: z.string().min(1).max(100), + // property: z.string().min(1).max(100).optional(), +}); + +export const AddFilterForm = ({ + onAdd, + onCancel, +}: { + onAdd: (input: FilterInput) => void; + onCancel: () => void; +}) => { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + field: "", + operator: FilterOperator.Eq, + value: "", + // property: undefined, + }, + }); + + const onSubmit = (values: z.infer) => { + const filterValue: FilterInput = { + field: values.field, + operator: values.operator, + value: values.value, + }; + + onAdd(filterValue); + form.reset(); + }; + + const formValues = form.watch(); + const { field } = formValues; + const selectedFilterField = FILTER_FIELDS_LIST.find( + (fieldInList) => fieldInList.value === field + ); + + const isFormValid = form.formState.isValid; + + return ( +
    + +
    Add Filter
    +
    + ( + + + + + + )} + /> + {/* {form.watch("field") === "property" && ( + ( + + + + + + )} + /> + )} */} + ( + + + + + + )} + /> + ( + + + + + + )} + /> + + +
    +
    + + ); +}; + +export const AddFilterItem = ({ + onAdd, +}: { + onAdd: (input: FilterInput) => void; +}) => { + const [addFormOpen, setAddFormOpen] = useState(false); + + const handleAdd = useCallback( + (filter: FilterInput) => { + onAdd(filter); + setAddFormOpen(false); + }, + [onAdd] + ); + + if (addFormOpen) { + return ( + setAddFormOpen(false)} /> + ); + } + return ( + + ); +}; diff --git a/apps/console/src/components/requests/index.ts b/apps/console/src/components/requests/index.ts new file mode 100644 index 000000000..c2151bd20 --- /dev/null +++ b/apps/console/src/components/requests/index.ts @@ -0,0 +1 @@ +export * from "./RequestDetails"; diff --git a/apps/console/src/env.ts b/apps/console/src/env.ts index 4dba6f0a4..779930805 100644 --- a/apps/console/src/env.ts +++ b/apps/console/src/env.ts @@ -1,5 +1,20 @@ -function get(name: string): string | undefined { +export function getEnvVariable(name: string): string | undefined { return window[name] || process.env[name]; } -export const BASE_API_URL = get("NX_BASE_API_URL"); +export const BASE_API_URL = getEnvVariable("NX_BASE_API_URL"); +export const AUTH_GITHUB_ENABLED = getEnvVariable("NX_AUTH_GITHUB_ENABLED"); +export const AUTH_GOOGLE_ENABLED = getEnvVariable("NX_AUTH_GOOGLE_ENABLED"); +export const INTERCOM_APP_ID = getEnvVariable("NX_INTERCOM_APP_ID"); +export const HOTJAR_SITE_ID = getEnvVariable("NX_HOTJAR_SITE_ID"); +export const HOTJAR_VERSION = getEnvVariable("NX_HOTJAR_VERSION"); +export const SUPERTOKENS_API_DOMAIN = getEnvVariable( + "NX_SUPERTOKENS_API_DOMAIN" +); +export const SUPERTOKENS_WEBSITE_DOMAIN = getEnvVariable( + "NX_SUPERTOKENS_WEBSITE_DOMAIN" +); +export const SENTRY_DSN_URL = getEnvVariable("NX_SENTRY_DSN_URL"); +export const SEGMENT_WRITE_KEY = getEnvVariable("NX_SEGMENT_WRITE_KEY"); +export const GTM_TAG_ID = getEnvVariable("NX_GTM_TAG_ID"); +export const DEBUG_MODE = getEnvVariable("NX_DEBUG_MODE"); diff --git a/apps/console/src/environments/environment.prod.ts b/apps/console/src/environments/environment.prod.ts deleted file mode 100644 index c9669790b..000000000 --- a/apps/console/src/environments/environment.prod.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const environment = { - production: true, -}; diff --git a/apps/console/src/environments/environment.ts b/apps/console/src/environments/environment.ts deleted file mode 100644 index 7ed83767f..000000000 --- a/apps/console/src/environments/environment.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file can be replaced during build by using the `fileReplacements` array. -// When building for production, this file is replaced with `environment.prod.ts`. - -export const environment = { - production: false, -}; diff --git a/apps/console/src/features/chat/ChatView.tsx b/apps/console/src/features/chat/ChatView.tsx new file mode 100644 index 000000000..5c2f2a0e1 --- /dev/null +++ b/apps/console/src/features/chat/ChatView.tsx @@ -0,0 +1,66 @@ +import { Chat, SubChatMessage } from "./types"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@pezzo/ui"; + +type Props = { + chat: Chat; +}; + +export const ChatView = ({ chat }: Props) => { + const renderSubMessages = (subMessages: SubChatMessage[]) => { + return subMessages.map((subMessage, index) => { + return ( +
    +
    + + + {subMessage.icon} + + {subMessage.label} + + + +
    +
    {subMessage.content}
    +
    + ); + }); + }; + + return ( +
    +
    + {chat.messages.map((message, index) => ( +
    +
    + + + {message.icon} + + {message.role} + + + +
    +
    + {message.subMessages ? ( +
    + {renderSubMessages(message.subMessages)} +
    + ) : ( + message.content + )} +
    +
    + ))} +
    +
    + ); +}; diff --git a/apps/console/src/features/chat/normalizers/openai-normalizer.tsx b/apps/console/src/features/chat/normalizers/openai-normalizer.tsx new file mode 100644 index 000000000..813e8d3e8 --- /dev/null +++ b/apps/console/src/features/chat/normalizers/openai-normalizer.tsx @@ -0,0 +1,141 @@ +import OpenAI from "openai"; +import { Chat, ChatMessage, SubChatMessage } from "../types"; +import { Provider } from "@pezzo/types"; +import { cn } from "@pezzo/ui/utils"; +import { + BotIcon, + ImageIcon, + TypeIcon, + UserIcon, + WrenchIcon, +} from "lucide-react"; +import { InlineCodeSnippet } from "~/components/common/InlineCodeSnippet"; + +const baseIconCn = cn( + "w-10 h-10 flex items-center justify-center rounded-sm border" +); + +export const getIcon = (role: OpenAI.ChatCompletionRole) => { + switch (role) { + case "user": + return ( +
    + +
    + ); + case "assistant": + return ( +
    + +
    + ); + case "system": + return ( +
    + +
    + ); + } +}; + +const renderFunctionCall = (message: OpenAI.Chat.ChatCompletionMessage) => { + const function_call = { + ...message.function_call, + arguments: JSON.parse(message.function_call.arguments), + }; + + return ( +
    +

    Function call:

    +
    +        {JSON.stringify(function_call, null, 2)}
    +      
    +
    + ); +}; + +export const normalizeOpenAIChatResponse = ( + request: OpenAI.Chat.CompletionCreateParams, + response: OpenAI.Chat.ChatCompletion +): Chat => { + const messages: ChatMessage[] = []; + + // First, populate messages from the request + request.messages.forEach((message) => { + if (Array.isArray(message.content)) { + const subMessages: SubChatMessage[] = []; + const contentParts = message.content; + + contentParts.forEach((contentPart) => { + const { type } = contentPart; + + if (type === "text") { + subMessages.push({ + icon: ( +
    + +
    + ), + label: "text", + content: contentPart.text, + }); + } else if (type === "image_url") { + subMessages.push({ + icon: ( +
    + +
    + ), + label: "image", + content: ( +
    +
    + Detail: + + {contentPart.image_url.detail} + +
    + Report +
    + ), + }); + } + }); + + messages.push({ + icon: getIcon(message.role), + role: message.role, + subMessages, + content: "", + }); + } else { + messages.push({ + icon: getIcon(message.role), + role: message.role, + content: message.content as string, // TODO: support vision model + }); + } + }); + + // Then, populate response messages + response.choices.forEach((choice) => { + if (choice.message.function_call) { + messages.push({ + icon: getIcon(choice.message.role), + role: choice.message.role, + content: renderFunctionCall(choice.message), + }); + } else { + messages.push({ + icon: getIcon(choice.message.role), + role: choice.message.role, + content: choice.message.content, + }); + } + }); + + return { + provider: Provider.OpenAI, + messages, + }; +}; diff --git a/apps/console/src/features/chat/types.ts b/apps/console/src/features/chat/types.ts new file mode 100644 index 000000000..b6c39ccfe --- /dev/null +++ b/apps/console/src/features/chat/types.ts @@ -0,0 +1,20 @@ +import { Provider } from "@pezzo/types"; +import OpenAI from "openai"; + +export type ChatMessage = { + icon: string | React.ReactNode; + role: OpenAI.ChatCompletionRole; + content: string | React.ReactNode; + subMessages?: SubChatMessage[]; +}; + +export type SubChatMessage = { + icon: string | React.ReactNode; + label: "text" | "image"; + content: string | React.ReactNode; +}; + +export type Chat = { + provider: Provider; + messages: ChatMessage[]; +}; diff --git a/apps/console/src/features/editor/CommitButton.tsx b/apps/console/src/features/editor/CommitButton.tsx new file mode 100644 index 000000000..401fd5f63 --- /dev/null +++ b/apps/console/src/features/editor/CommitButton.tsx @@ -0,0 +1,24 @@ +import { Button } from "@pezzo/ui"; +import { SaveIcon } from "lucide-react"; +import { useEditorContext } from "~/lib/providers/EditorContext"; + +interface Props { + onClick: () => void; +} + +export const CommitButton = ({ onClick }: Props) => { + const { hasChangesToCommit, getForm } = useEditorContext(); + const form = getForm(); + const isValid = form.formState.isValid; + + return ( + + ); +}; diff --git a/apps/console/src/features/editor/PromptEditMode.tsx b/apps/console/src/features/editor/PromptEditMode.tsx new file mode 100644 index 000000000..eac0bc3e8 --- /dev/null +++ b/apps/console/src/features/editor/PromptEditMode.tsx @@ -0,0 +1,38 @@ +import { + Card, + FormControl, + FormField, + FormItem, + FormMessage, + Textarea, +} from "@pezzo/ui"; +import { useEditorContext } from "~/lib/providers/EditorContext"; + +export const PromptEditMode = () => { + const { getForm } = useEditorContext(); + const form = getForm(); + + return ( + + ( + + +