diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..837bdb720
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,38 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: 'bug: [DESCRIPTION]'
+labels: 'type: bug'
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Smartphone (please complete the following information):**
+ - Device: [e.g. iPhone6]
+ - OS: [e.g. iOS8.1]
+ - Browser [e.g. stock browser, safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/discussion-thread.md b/.github/ISSUE_TEMPLATE/discussion-thread.md
new file mode 100644
index 000000000..09e52ae68
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/discussion-thread.md
@@ -0,0 +1,14 @@
+---
+name: Discussion thread
+about: Start an open ended discussion
+title: 'Discussion: [TOPIC HERE]'
+labels: ''
+assignees: ''
+
+---
+
+**Motivation**
+
+**Discussion**
+
+**Resources**
diff --git a/.github/ISSUE_TEMPLATE/epic-request.md b/.github/ISSUE_TEMPLATE/epic-request.md
new file mode 100644
index 000000000..bfad8e5f8
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/epic-request.md
@@ -0,0 +1,20 @@
+---
+name: Epic request
+about: Suggest an idea for this project
+title: 'epic: [DESCRIPTION]'
+labels: 'type: epic'
+assignees: ''
+
+---
+
+**Problem**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Success Criteria**
+A clear and concise description of what you want to happen.
+
+**Sub Issues**
+-
+
+**Additional context**
+Add any other context or screenshots about the epic request here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 000000000..26f586bd0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,17 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: 'feat: [DESCRIPTION]'
+labels: 'type: feature request'
+assignees: ''
+
+---
+
+**Problem**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Success Criteria**
+A clear and concise description of what you want to happen.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/workflows/ci-production.yml b/.github/workflows/ci-production.yml
deleted file mode 100644
index 0d0a0a152..000000000
--- a/.github/workflows/ci-production.yml
+++ /dev/null
@@ -1,53 +0,0 @@
-name: Jan CI Production
-
-on:
- push:
- tags: ['v*.*.*']
-
-env:
- REGISTRY: ghcr.io
- HASURA_WORKER_IMAGE_NAME: ${{ github.repository }}/worker
- WEB_CLIENT_IMAGE_NAME: ${{ github.repository }}/web-client
-
-jobs:
- build-docker-image:
- runs-on: ubuntu-latest
- environment: production
- permissions:
- contents: read
- packages: write
- steps:
- - name: Getting the repo
- uses: actions/checkout@v3
-
- - name: Log in to the Container registry
- uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
- with:
- registry: ${{ env.REGISTRY }}
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Get tag
- id: tag
- uses: dawidd6/action-get-tag@v1
-
- # Build and docker image for app-backend worker
- - name: Build docker image for app-backend worker
- run: |
- cd ./app-backend/worker
- docker build -t ${{ env.REGISTRY }}/${{ env.HASURA_WORKER_IMAGE_NAME }}:${{ steps.tag.outputs.tag }} .
- docker push ${{ env.REGISTRY }}/${{ env.HASURA_WORKER_IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
-
- # Get .env for FE
- - name: Get .env file for build time
- run: cd ./web-client && base64 -d <<< "$ENV_FILE_BASE64" > .env
- shell: bash
- env:
- ENV_FILE_BASE64: ${{ secrets.ENV_FILE_BASE64 }}
-
- # Build and push docker for web client
- - name: Build docker image for web-client
- run: |
- cd ./web-client
- docker build -t ${{ env.REGISTRY }}/${{ env.WEB_CLIENT_IMAGE_NAME }}:${{ steps.tag.outputs.tag }} .
- docker push ${{ env.REGISTRY }}/${{ env.WEB_CLIENT_IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
diff --git a/.github/workflows/ci-staging.yml b/.github/workflows/ci-staging.yml
deleted file mode 100644
index 5f42cef4b..000000000
--- a/.github/workflows/ci-staging.yml
+++ /dev/null
@@ -1,58 +0,0 @@
-name: Jan CI Staging
-on:
- push:
- branches:
- - stag
- paths:
- - 'app-backend/worker/**' # hasura worker source code
- - 'web-client/**' # web client source code
-
-env:
- REGISTRY: ghcr.io
- HASURA_WORKER_IMAGE_NAME: ${{ github.repository }}/worker
- WEB_CLIENT_IMAGE_NAME: ${{ github.repository }}/web-client
-
-jobs:
- build-docker-image:
- runs-on: ubuntu-latest
- environment: staging
- permissions:
- contents: read
- packages: write
- steps:
- - name: Getting the repo
- uses: actions/checkout@v3
-
- - name: Log in to the Container registry
- uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
- with:
- registry: ${{ env.REGISTRY }}
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Get current date
- id: date
- run: echo "::set-output name=date::$(date +'%Y.%m.%d')"
-
- # Build docker image for app-backend worker
- - name: Build docker image for app-backend worker
- if: ${{ contains(github.event.head_commit.added, 'app-backend/worker/') }}
- run: |
- cd ./app-backend/worker
- docker build -t ${{ env.REGISTRY }}/${{ env.HASURA_WORKER_IMAGE_NAME }}:staging-${{ steps.date.outputs.date }}.${{github.run_number}} .
- docker push ${{ env.REGISTRY }}/${{ env.HASURA_WORKER_IMAGE_NAME }}:staging-${{ steps.date.outputs.date }}.${{github.run_number}}
-
- # Get .env for FE
- - name: Get .env file for build time
- run: cd ./web-client && base64 -d <<< "$ENV_FILE_BASE64" > .env
- shell: bash
- env:
- ENV_FILE_BASE64: ${{ secrets.ENV_FILE_BASE64 }}
-
- # Build and push docker for web client
- - name: Build docker image for web-client
- if: ${{ contains(github.event.head_commit.added, 'web-client/') }}
- run: |
- cd ./web-client
- docker build -t ${{ env.REGISTRY }}/${{ env.WEB_CLIENT_IMAGE_NAME }}:staging-${{ steps.date.outputs.date }}.${{github.run_number}} .
- docker push ${{ env.REGISTRY }}/${{ env.WEB_CLIENT_IMAGE_NAME }}:staging-${{ steps.date.outputs.date }}.${{github.run_number}}
diff --git a/.github/workflows/deploy-jan-docs.yml b/.github/workflows/deploy-jan-docs.yml
index a61abf271..e35117a39 100644
--- a/.github/workflows/deploy-jan-docs.yml
+++ b/.github/workflows/deploy-jan-docs.yml
@@ -19,8 +19,8 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 18
- cache: 'npm'
- cache-dependency-path: './docs/package-lock.json'
+ cache: 'yarn'
+ cache-dependency-path: './docs/yarn.lock'
- name: Install dependencies
run: yarn install
@@ -30,7 +30,7 @@ jobs:
working-directory: docs
- name: Add Custome Domain file
- run: echo "docs.jan.ai" > ./docs/build/CNAME
+ run: echo "${{ vars.DOCUSAURUS_DOMAIN }}" > ./docs/build/CNAME
# Popular action to deploy to GitHub Pages:
# Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus
diff --git a/.github/workflows/macos-build-app.yml b/.github/workflows/macos-build-app.yml
new file mode 100644
index 000000000..0977d1a6f
--- /dev/null
+++ b/.github/workflows/macos-build-app.yml
@@ -0,0 +1,50 @@
+name: Jan Build MacOS App
+
+on:
+ push:
+ tags: ['v*.*.*']
+
+jobs:
+ build-macos-app:
+ runs-on: macos-latest
+
+ permissions:
+ contents: write
+ steps:
+ - name: Getting the repo
+ uses: actions/checkout@v3
+
+ - name: Installing node
+ uses: actions/setup-node@v1
+ with:
+ node-version: 20
+
+ - name: Install jq
+ uses: dcarbone/install-jq-action@v2.0.1
+
+ - name: Get tag
+ id: tag
+ uses: dawidd6/action-get-tag@v1
+
+ - name: Update app version base on tag
+ run: |
+ if [[ ! "${VERSION_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ echo "Error: Tag is not valid!"
+ exit 1
+ fi
+ jq --arg version "${VERSION_TAG#v}" '.version = $version' electron/package.json > /tmp/package.json
+ mv /tmp/package.json electron/package.json
+ env:
+ VERSION_TAG: ${{ steps.tag.outputs.tag }}
+
+ - name: Install yarn dependencies
+ run: |
+ yarn install
+ yarn build:plugins
+
+ - name: Build and publish app
+ run: |
+ yarn build:publish
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
diff --git a/.gitignore b/.gitignore
index 2f01a3534..2fd305c0d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,11 @@
# Jan inference
models/**
error.log
+node_modules
+package-lock.json
+*.tgz
+yarn.lock
+dist
+build
+.DS_Store
+electron/renderer
diff --git a/KC.md b/KC.md
deleted file mode 100644
index 3773e241b..000000000
--- a/KC.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# Configuring Keycloak theme
-Jan comes with a default theme for Keycloak. Extended from [Keywind](https://github.com/lukin/keywind)
-
-## Select keywind as theme
-1. Navigate to http://localhost:8088/admin
-2. Sign in with below credentials
-```
-username: admin
-password: admin
-```
-3. Select `hasura` from the top left dropdown box
-
-
-4. Select `Realm settings` on left navigation bar and open tab `Themes`
-
-
-5. On `Login theme` open the drop down box and select `keywind`
-
-
-6. Save
-
-**That's it!**
-
-Open your web browser and navigate to `http://localhost:3000` to access Jan web application. Proceed to `Login` on the top right.
-
-You should expect the theme as below. If it's does not, try to clear the cache from your browser.
-
-
diff --git a/README.md b/README.md
index 9c65ea486..ef4efa032 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Jan - Self-Hosted AI Platform
+# Jan - Run your own AI
@@ -20,13 +20,14 @@
> ⚠️ **Jan is currently in Development**: Expect breaking changes and bugs!
-Jan helps you run Local AI on your computer, with 1-click installs for the latest models. Easy-to-use yet powerful, with helpful tools to monitor and manage software-hardware performance.
+Jan lets you run AI on your own hardware, with helpful tools to manage models and monitor your hardware performance.
-Jan runs on a wide variety of hardware. We run on consumer-grade GPUs and Mac Minis, as well as datacenter-grade DGX H100 clusters.
+In the background, Jan runs [Nitro](https://nitro.jan.ai), a C++ inference engine. It runs various model formats (GGUF/TensorRT) on various hardware (Mac M1/M2/Intel, Windows, Linux, and datacenter-grade Nvidia GPUs) with optional GPU acceleration.
-Jan can be run as a server or cloud-native application for enterprise. We offer enterprise plugins for LDAP integration and Audit Logs. Contact us at [hello@jan.ai](mailto:hello@jan.ai) for more details.
+> See the Nitro codebase at https://nitro.jan.ai.
-Jan is free, [open core](https://en.wikipedia.org/wiki/Open-core_model), and licensed under a Sustainable Use License.
+
+
## Demo
@@ -34,208 +35,107 @@ Jan is free, [open core](https://en.wikipedia.org/wiki/Open-core_model), and lic
-## Features
+## Quicklinks
-**Self-Hosted AI**
-- [x] Self-hosted Llama2 and LLMs
-- [ ] Self-hosted StableDiffusion and Controlnet
-- [ ] 1-click installs for Models (coming soon)
+- Developer documentation: https://jan.ai/docs (Work in Progress)
+- Desktop app: Download at https://jan.ai/
+- Mobile app shell: Download via [App Store](https://apps.apple.com/us/app/jan-on-device-ai-cloud-ais/id6449664703) | [Android](https://play.google.com/store/apps/details?id=com.jan.ai)
+- Nitro (C++ AI Engine): https://nitro.jan.ai
-**3rd-party AIs**
-- [ ] Connect to ChatGPT, Claude via API Key (coming soon)
-- [ ] Security policy engine for 3rd-party AIs (coming soon)
-- [ ] Pre-flight PII and Sensitive Data checks (coming soon)
+## Plugins
-**Multi-Device**
-- [x] Web App
-- [ ] Jan Mobile support for custom Jan server (in progress)
-- [ ] Cloud deployments (coming soon)
+Jan supports core & 3rd party extensions:
-**Organization Tools**
-- [x] Multi-user support
-- [ ] Audit and Usage logs (coming soon)
-- [ ] Compliance and Audit policy (coming soon)
+- [x] **LLM chat**: Self-hosted Llama2 and LLMs
+- [x] **Model Manager**: 1-click to install, swap, and delete models
+- [x] **Storage**: Optionally store your conversation history and other data in SQLite/your storage of choice
+- [ ] **3rd-party AIs**: Connect to ChatGPT, Claude via API Key (in progress)
+- [ ] **Cross device support**: Mobile & Web support for custom shared servers (in progress)
+- [ ] **File retrieval**: User can upload private and run a vectorDB (planned)
+- [ ] **Multi-user support**: Share a single server across a team/friends (planned)
+- [ ] **Compliance**: Auditing and flagging features (planned)
-**Hardware Support**
+## Hardware Support
-- [x] Nvidia GPUs
-- [x] Apple Silicon (in progress)
-- [x] CPU support via llama.cpp
-- [ ] Nvidia GPUs using TensorRT (in progress)
+Nitro provides both CPU and GPU support, via [llama.cpp](https://github.com/ggerganov/llama.cpp) and [TensorRT](https://github.com/NVIDIA/TensorRT), respectively.
-## Documentation
+- [x] Nvidia GPUs (accelerated)
+- [x] Apple M-series (accelerated)
+- [x] Linux DEB
+- [x] Windows x64
-👋 https://docs.jan.ai (Work in Progress)
+Not supported yet: Apple Intel, Linux RPM, Windows x86|ARM64, AMD ROCm
-## Installation
+> See [developer docs](https://docs.jan.ai/docs/) for detailed installation instructions.
-> ⚠️ **Jan is currently in Development**: Expect breaking changes and bugs!
+## Contributing
-### Step 1: Install Docker
+Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) file
-Jan is currently packaged as a Docker Compose application.
+### Pre-requisites
+- node >= 20.0.0
+- yarn >= 1.22.0
-- Docker ([Installation Instructions](https://docs.docker.com/get-docker/))
-- Docker Compose ([Installation Instructions](https://docs.docker.com/compose/install/))
+### Use as complete suite (in progress)
+### For interactive development
-### Step 2: Clone Repo
+Note: This instruction is tested on MacOS only.
-```bash
-git clone https://github.com/janhq/jan.git
-cd jan
-```
+1. **Clone the Repository:**
-### Step 3: Configure `.env`
+ ```
+ git clone https://github.com/janhq/jan
+ git checkout feature/hackathon-refactor-jan-into-electron-app
+ cd jan
+ ```
-We provide a sample `.env` file that you can use to get started.
+2. **Install dependencies:**
-```shell
-cp sample.env .env
-```
+ ```
+ yarn install
-You will need to set the following `.env` variables
+ # Packing base plugins
+ yarn build:plugins
+ ```
-```shell
-# TODO: Document .env variables
-```
+4. **Run development and Using Jan Desktop**
-### Step 4: Install Models
+ ```
+ yarn dev
+ ```
+ This will start the development server and open the desktop app.
+ In this step, there are a few notification about installing base plugin, just click `OK` and `Next` to continue.
-> Note: These step will change soon as we will be switching to [Nitro](https://github.com/janhq/nitro), an Accelerated Inference Server written in C++
+### For production build
-#### Step 4.1: Install Mamba
+ ```bash
+ # Do step 1 and 2 in previous section
+ git clone https://github.com/janhq/jan
+ cd jan
+ yarn install
+ yarn build:plugins
-> For complete Mambaforge installation instructions, see [miniforge repo](https://github.com/conda-forge/miniforge)
+ # Build the app
+ yarn build
+ ```
-Install Mamba to handle native python binding (which can yield better performance on Mac M/ NVIDIA)
+ This will build the app MacOS m1/m2 for production (with code signing already done) and put the result in `dist` folder.
-```bash
-curl -L -O "https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-$(uname)-$(uname -m).sh"
-bash Mambaforge-$(uname)-$(uname -m).sh
-rm Mambaforge-$(uname)-$(uname -m).sh
+## License
-# Create environment
-conda create -n jan python=3.9.16
-conda activate jan
-```
+Jan is free, [open core](https://en.wikipedia.org/wiki/Open-core_model), and Sustainable Use Licensed.
-Uninstall any previous versions of `llama-cpp-python`
-```bash
-pip uninstall llama-cpp-python -y
-```
+## Acknowledgements
-#### Step 4.2: Install `llama-cpp-python`
-
-> Note: This step will change soon once [Nitro](https://github.com/janhq/nitro) (our accelerated inference server written in C++) is released
-
-- On Mac
-
-```bash
-# See https://github.com/abetlen/llama-cpp-python/blob/main/docs/install/macos.md
-CMAKE_ARGS="-DLLAMA_METAL=on" FORCE_CMAKE=1 pip install -U llama-cpp-python --no-cache-dir
-pip install 'llama-cpp-python[server]'
-```
-
-- On Linux with NVIDIA GPU Hardware Acceleration
-
-```bash
-# See https://github.com/abetlen/llama-cpp-python#installation-with-hardware-acceleration
-CMAKE_ARGS="-DLLAMA_CUBLAS=on" FORCE_CMAKE=1 pip install llama-cpp-python
-pip install 'llama-cpp-python[server]'
-```
-
-- On Linux with Intel/ AMD CPU (support for AVX-2/ AVX-512)
-
-```bash
-CMAKE_ARGS="-DLLAMA_BLAS=ON -DLLAMA_BLAS_VENDOR=OpenBLAS" FORCE_CMAKE=1 pip install llama-cpp-python
-pip install 'llama-cpp-python[server]'
-```
-
-We recommend that Llama2-7B (4-bit quantized) as a basic model to get started.
-
-You will need to download the models to the `models` folder at root level.
-
-```shell
-# Downloads model (~4gb)
-# Download time depends on your internet connection and HuggingFace's bandwidth
-# In this part, please head over to any source contains `.gguf` format model - https://huggingface.co/models?search=gguf
-wget https://huggingface.co/TheBloke/Llama-2-7B-GGUF/resolve/main/llama-2-7b.Q4_0.gguf -P models
-```
-
-- Run the model in host machine
-```bash
-# Please change the value of --model key as your corresponding model path
-# The --n_gpu_layers 1 means using acclerator (can be Metal on Mac, NVIDIA GPU on on linux with NVIDIA GPU)
-# This service will run at `http://localhost:8000` in host level
-# The backend service inside docker compose will connect to this service by using `http://host.docker.internal:8000`
-python3 -m llama_cpp.server --model models/llama-2-7b.Q4_0.gguf --n_gpu_layers 1
-```
-
-### Step 5: `docker compose up`
-
-Jan utilizes Docker Compose to run all services:
-
-```shell
-docker compose up -d # Detached mode
-```
-
-The table below summarizes the services and their respective URLs and credentials.
-
-| Service | Container Name | URL and Port | Credentials |
-| ------------------------------------------------ | -------------------- | --------------------- | ---------------------------------------------------------------------------------- |
-| Jan Web | jan-web-* | http://localhost:3000 | Set in `conf/keycloak_conf/example-realm.json` - Default Username / Password |
-| [Hasura](https://hasura.io) (Backend) | jan-graphql-engine-* | http://localhost:8080 | Set in `conf/sample.env_app-backend` - `HASURA_GRAPHQL_ADMIN_SECRET` |
-| [Keycloak](https://www.keycloak.org/) (Identity) | jan-keycloak-* | http://localhost:8088 | Set in `.env` - `KEYCLOAK_ADMIN` - `KEYCLOAK_ADMIN_PASSWORD` | |
-| PostgresDB | jan-postgres-* | http://localhost:5432 | Set in `.env` |
-
-### Step 6: Configure Keycloak
-
-- [ ] Refactor [Keycloak Instructions](KC.md) into main README.md
-- [ ] Changing login theme
-
-### Step 7: Use Jan
-
-- Launch the web application via `http://localhost:3000`.
-- Login with default user (username: `username`, password: `password`)
-
-### Step 8: Deploying to Production
-
-- [ ] TODO
-
-## About Jan
-
-Jan is a commercial company with a [Fair Code](https://faircode.io/) business model. This means that while we are open-source and can used for free, we require commercial licenses for specific use cases (e.g. hosting Jan as a service).
-
-We are a team of engineers passionate about AI, productivity and the future of work. We are funded through consulting contracts and enterprise licenses. Feel free to reach out to us!
-
-### Repo Structure
-
-Jan comprises of several repositories:
-
-| Repo | Purpose |
-| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| [Jan](https://github.com/janhq/jan) | AI Platform to run AI in the enterprise. Easy-to-use for users, and packed with useful organizational and compliance features. |
-| [Jan Mobile](https://github.com/janhq/jan-react-native) | Mobile App that can be pointed to a custom Jan server. |
-| [Nitro](https://github.com/janhq/nitro) | Inference Engine that runs AI on different types of hardware. Offers popular API formats (e.g. OpenAI, Clipdrop). Written in C++ for blazing fast performance |
-
-### Architecture
-
-Jan builds on top of several open-source projects:
+Jan builds on top of other open-source projects:
+- [llama.cpp](https://github.com/ggerganov/llama.cpp)
+- [TensorRT](https://github.com/NVIDIA/TensorRT)
- [Keycloak Community](https://github.com/keycloak/keycloak) (Apache-2.0)
-- [Hasura Community Edition](https://github.com/hasura/graphql-engine) (Apache-2.0)
-We may re-evaluate this in the future, given different customer requirements.
+## Contact
-
-### Contributing
-
-Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines on how to contribute to this project.
-
-Please note that Jan intends to build a sustainable business that can provide high quality jobs to its contributors. If you are excited about our mission and vision, please contact us to explore opportunities.
-
-### Contact
-
-- For support: please file a Github ticket
-- For questions: join our Discord [here](https://discord.gg/FTk2MvZwJH)
-- For long form inquiries: please email hello@jan.ai
+- Bugs & requests: file a Github ticket
+- For discussion: join our Discord [here](https://discord.gg/FTk2MvZwJH)
+- For business inquiries: email hello@jan.ai
+- For jobs: please email hr@jan.ai
diff --git a/app-backend/.gitignore b/app-backend/.gitignore
deleted file mode 100644
index 4caef3e61..000000000
--- a/app-backend/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-.DS_Store
-.env
-.env_postgresql
-worker/node_modules/.mf
diff --git a/app-backend/README.md b/app-backend/README.md
deleted file mode 100644
index 0c693253b..000000000
--- a/app-backend/README.md
+++ /dev/null
@@ -1,59 +0,0 @@
-## Jan Backend
-
-A Hasura Data API Platform designed to provide APIs for client interaction with the Language Model (LLM) through chat or the generation of art using Stable Diffusion. It is encapsulated within a Docker container for easy local deployment
-
-## Quickstart
-1. Run docker up
-
-```bash
-docker compose up
-```
-
-2. Install [HasuraCLI](https://hasura.io/docs/latest/hasura-cli/overview/)
-
-3. Open Hasura Console
-
-```bash
-cd hasura && hasura console
-```
-
-4. Apply Migration
-
-```bash
-hasura migrate apply
-```
-
-5. Apply Metadata
-
-```bash
-hasura metadata apply
-```
-
-6. Apply seeds
-
-```bash
-hasura seed apply
-```
-
-## Hasura One Click Deploy
-Use this URL to deploy this app to Hasura Cloud
-
-[](https://cloud.hasura.io/deploy?github_repo=https://github.com/janhq/app-backend/&hasura_dir=/hasura)
-
-[One-click deploy docs](https://hasura.io/docs/latest/getting-started/getting-started-cloud/)
-
-## Modify schema & model
-[Hasura Tutorials](https://hasura.io/docs/latest/resources/tutorials/index/)
-
-## Events & Workers
-
-Serverless function (Cloudflare worker) to stream llm message & update
-
-Readmore about Hasura Events here:
-> https://hasura.io/docs/latest/event-triggers/serverless/
-
-## Deploy Worker
-```bash
-npx wrangler deploy
-```
-[Cloudflare Worker Guide](https://developers.cloudflare.com/workers/get-started/guide/)
\ No newline at end of file
diff --git a/app-backend/docker-compose.yml b/app-backend/docker-compose.yml
deleted file mode 100644
index 5f60468e0..000000000
--- a/app-backend/docker-compose.yml
+++ /dev/null
@@ -1,52 +0,0 @@
-version: "3.6"
-services:
- postgres:
- image: postgres:13
- restart: always
- volumes:
- - db_data:/var/lib/postgresql/data
- env_file:
- - .env_postgresql
- graphql-engine:
- image: hasura/graphql-engine:v2.31.0.cli-migrations-v3
- ports:
- - "8080:8080"
- restart: always
- env_file:
- - .env
- volumes:
- - ./hasura/migrations:/migrations
- - ./hasura/metadata:/metadata
- depends_on:
- data-connector-agent:
- condition: service_healthy
-
- data-connector-agent:
- image: hasura/graphql-data-connector:v2.31.0
- restart: always
- ports:
- - 8081:8081
- environment:
- QUARKUS_LOG_LEVEL: ERROR # FATAL, ERROR, WARN, INFO, DEBUG, TRACE
- ## https://quarkus.io/guides/opentelemetry#configuration-reference
- QUARKUS_OPENTELEMETRY_ENABLED: "false"
- ## QUARKUS_OPENTELEMETRY_TRACER_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317
- healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8081/api/v1/athena/health"]
- interval: 5s
- timeout: 10s
- retries: 5
- start_period: 5s
- worker:
- build:
- context: ./worker
- dockerfile: ./Dockerfile
- restart: always
- environment:
- - "NODE_ENV=development"
- volumes:
- - ./worker:/worker
- ports:
- - "8787:8787"
-volumes:
- db_data:
diff --git a/app-backend/hasura/config.yaml b/app-backend/hasura/config.yaml
deleted file mode 100644
index d13faa054..000000000
--- a/app-backend/hasura/config.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
-version: 3
-endpoint: http://localhost:8080
-admin_secret: myadminsecretkey
-metadata_directory: metadata
-actions:
- kind: synchronous
- handler_webhook_baseurl: http://localhost:3000
diff --git a/app-backend/hasura/metadata/actions.graphql b/app-backend/hasura/metadata/actions.graphql
deleted file mode 100644
index 4b8328f43..000000000
--- a/app-backend/hasura/metadata/actions.graphql
+++ /dev/null
@@ -1,20 +0,0 @@
-type Mutation {
- imageGeneration(
- input: ImageGenerationInput!
- ): ImageGenerationOutput
-}
-
-input ImageGenerationInput {
- prompt: String!
- neg_prompt: String!
- model: String!
- seed: Int!
- steps: Int!
- width: Int!
- height: Int!
-}
-
-type ImageGenerationOutput {
- url: String!
-}
-
diff --git a/app-backend/hasura/metadata/actions.yaml b/app-backend/hasura/metadata/actions.yaml
deleted file mode 100644
index 2b1580711..000000000
--- a/app-backend/hasura/metadata/actions.yaml
+++ /dev/null
@@ -1,33 +0,0 @@
-actions:
- - name: imageGeneration
- definition:
- kind: synchronous
- handler: '{{HASURA_ACTION_STABLE_DIFFUSION_URL}}'
- timeout: 1800
- request_transform:
- body:
- action: transform
- template: |-
- {
- "prompt": {{$body.input.input.prompt}},
- "neg_prompt": {{$body.input.input.neg_prompt}},
- "unet_model": {{$body.input.input.model}},
- "seed": {{$body.input.input.seed}},
- "steps": {{$body.input.input.steps}},
- "width": {{$body.input.input.width}},
- "height": {{$body.input.input.height}}
- }
- method: POST
- query_params: {}
- template_engine: Kriti
- url: '{{$base_url}}/inferences/txt2img'
- version: 2
- permissions:
- - role: user
-custom_types:
- enums: []
- input_objects:
- - name: ImageGenerationInput
- objects:
- - name: ImageGenerationOutput
- scalars: []
diff --git a/app-backend/hasura/metadata/allow_list.yaml b/app-backend/hasura/metadata/allow_list.yaml
deleted file mode 100644
index fe51488c7..000000000
--- a/app-backend/hasura/metadata/allow_list.yaml
+++ /dev/null
@@ -1 +0,0 @@
-[]
diff --git a/app-backend/hasura/metadata/api_limits.yaml b/app-backend/hasura/metadata/api_limits.yaml
deleted file mode 100644
index 0967ef424..000000000
--- a/app-backend/hasura/metadata/api_limits.yaml
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/app-backend/hasura/metadata/backend_configs.yaml b/app-backend/hasura/metadata/backend_configs.yaml
deleted file mode 100644
index 1266c8d5c..000000000
--- a/app-backend/hasura/metadata/backend_configs.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-dataconnector:
- athena:
- uri: http://data-connector-agent:8081/api/v1/athena
- mariadb:
- uri: http://data-connector-agent:8081/api/v1/mariadb
- mysql8:
- uri: http://data-connector-agent:8081/api/v1/mysql
- oracle:
- uri: http://data-connector-agent:8081/api/v1/oracle
- snowflake:
- uri: http://data-connector-agent:8081/api/v1/snowflake
diff --git a/app-backend/hasura/metadata/cron_triggers.yaml b/app-backend/hasura/metadata/cron_triggers.yaml
deleted file mode 100644
index fe51488c7..000000000
--- a/app-backend/hasura/metadata/cron_triggers.yaml
+++ /dev/null
@@ -1 +0,0 @@
-[]
diff --git a/app-backend/hasura/metadata/databases/databases.yaml b/app-backend/hasura/metadata/databases/databases.yaml
deleted file mode 100644
index b420475e7..000000000
--- a/app-backend/hasura/metadata/databases/databases.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-- name: jandb
- kind: postgres
- configuration:
- connection_info:
- database_url:
- from_env: PG_DATABASE_URL
- isolation_level: read-committed
- use_prepared_statements: false
- tables: "!include jandb/tables/tables.yaml"
diff --git a/app-backend/hasura/metadata/databases/jandb/tables/public_collection_products.yaml b/app-backend/hasura/metadata/databases/jandb/tables/public_collection_products.yaml
deleted file mode 100644
index 955361aed..000000000
--- a/app-backend/hasura/metadata/databases/jandb/tables/public_collection_products.yaml
+++ /dev/null
@@ -1,43 +0,0 @@
-table:
- name: collection_products
- schema: public
-array_relationships:
- - name: collections
- using:
- manual_configuration:
- column_mapping:
- collection_id: id
- insertion_order: null
- remote_table:
- name: collections
- schema: public
- - name: products
- using:
- manual_configuration:
- column_mapping:
- product_id: id
- insertion_order: null
- remote_table:
- name: products
- schema: public
-select_permissions:
- - role: public
- permission:
- columns:
- - created_at
- - updated_at
- - collection_id
- - id
- - product_id
- filter: {}
- comment: ""
- - role: user
- permission:
- columns:
- - created_at
- - updated_at
- - collection_id
- - id
- - product_id
- filter: {}
- comment: ""
diff --git a/app-backend/hasura/metadata/databases/jandb/tables/public_collections.yaml b/app-backend/hasura/metadata/databases/jandb/tables/public_collections.yaml
deleted file mode 100644
index c84847f1c..000000000
--- a/app-backend/hasura/metadata/databases/jandb/tables/public_collections.yaml
+++ /dev/null
@@ -1,36 +0,0 @@
-table:
- name: collections
- schema: public
-array_relationships:
- - name: collection_products
- using:
- manual_configuration:
- column_mapping:
- id: collection_id
- insertion_order: null
- remote_table:
- name: collection_products
- schema: public
-select_permissions:
- - role: public
- permission:
- columns:
- - slug
- - description
- - name
- - created_at
- - updated_at
- - id
- filter: {}
- comment: ""
- - role: user
- permission:
- columns:
- - slug
- - description
- - name
- - created_at
- - updated_at
- - id
- filter: {}
- comment: ""
diff --git a/app-backend/hasura/metadata/databases/jandb/tables/public_conversations.yaml b/app-backend/hasura/metadata/databases/jandb/tables/public_conversations.yaml
deleted file mode 100644
index 41637a39b..000000000
--- a/app-backend/hasura/metadata/databases/jandb/tables/public_conversations.yaml
+++ /dev/null
@@ -1,68 +0,0 @@
-table:
- name: conversations
- schema: public
-object_relationships:
- - name: conversation_product
- using:
- manual_configuration:
- column_mapping:
- product_id: id
- insertion_order: null
- remote_table:
- name: products
- schema: public
-array_relationships:
- - name: conversation_messages
- using:
- manual_configuration:
- column_mapping:
- id: conversation_id
- insertion_order: null
- remote_table:
- name: messages
- schema: public
-insert_permissions:
- - role: user
- permission:
- check:
- user_id:
- _eq: X-Hasura-User-Id
- columns:
- - last_image_url
- - last_text_message
- - product_id
- - user_id
- comment: ""
-select_permissions:
- - role: user
- permission:
- columns:
- - last_image_url
- - last_text_message
- - user_id
- - created_at
- - updated_at
- - id
- - product_id
- filter:
- user_id:
- _eq: X-Hasura-User-Id
- comment: ""
-update_permissions:
- - role: user
- permission:
- columns:
- - last_image_url
- - last_text_message
- filter:
- user_id:
- _eq: X-Hasura-User-Id
- check: null
- comment: ""
-delete_permissions:
- - role: user
- permission:
- filter:
- user_id:
- _eq: X-Hasura-User-Id
- comment: ""
diff --git a/app-backend/hasura/metadata/databases/jandb/tables/public_message_medias.yaml b/app-backend/hasura/metadata/databases/jandb/tables/public_message_medias.yaml
deleted file mode 100644
index 1f223b01a..000000000
--- a/app-backend/hasura/metadata/databases/jandb/tables/public_message_medias.yaml
+++ /dev/null
@@ -1,68 +0,0 @@
-table:
- name: message_medias
- schema: public
-object_relationships:
- - name: media_message
- using:
- manual_configuration:
- column_mapping:
- message_id: id
- insertion_order: null
- remote_table:
- name: messages
- schema: public
-insert_permissions:
- - role: user
- permission:
- check:
- media_message:
- message_conversation:
- user_id:
- _eq: X-Hasura-User-Id
- columns:
- - media_url
- - message_id
- - mime_type
- comment: ""
-select_permissions:
- - role: user
- permission:
- columns:
- - mime_type
- - media_url
- - created_at
- - updated_at
- - id
- - message_id
- filter:
- media_message:
- message_conversation:
- user_id:
- _eq: X-Hasura-User-Id
- comment: ""
-update_permissions:
- - role: user
- permission:
- columns:
- - mime_type
- - media_url
- - created_at
- - updated_at
- - id
- - message_id
- filter:
- media_message:
- message_conversation:
- user_id:
- _eq: X-Hasura-User-Id
- check: null
- comment: ""
-delete_permissions:
- - role: user
- permission:
- filter:
- media_message:
- message_conversation:
- user_id:
- _eq: X-Hasura-User-Id
- comment: ""
diff --git a/app-backend/hasura/metadata/databases/jandb/tables/public_messages.yaml b/app-backend/hasura/metadata/databases/jandb/tables/public_messages.yaml
deleted file mode 100644
index eb0d06b79..000000000
--- a/app-backend/hasura/metadata/databases/jandb/tables/public_messages.yaml
+++ /dev/null
@@ -1,86 +0,0 @@
-table:
- name: messages
- schema: public
-object_relationships:
- - name: message_conversation
- using:
- manual_configuration:
- column_mapping:
- conversation_id: id
- insertion_order: null
- remote_table:
- name: conversations
- schema: public
-array_relationships:
- - name: message_medias
- using:
- manual_configuration:
- column_mapping:
- id: message_id
- insertion_order: null
- remote_table:
- name: message_medias
- schema: public
-insert_permissions:
- - role: user
- permission:
- check:
- message_conversation:
- user_id:
- _eq: X-Hasura-User-Id
- columns:
- - content
- - conversation_id
- - message_sender_type
- - message_type
- - prompt_cache
- - sender
- - sender_avatar_url
- - sender_name
- - status
- comment: ""
-select_permissions:
- - role: user
- permission:
- columns:
- - content
- - conversation_id
- - created_at
- - id
- - message_sender_type
- - message_type
- - sender
- - sender_avatar_url
- - sender_name
- - status
- - updated_at
- filter:
- message_conversation:
- user_id:
- _eq: X-Hasura-User-Id
- comment: ""
-update_permissions:
- - role: user
- permission:
- columns:
- - content
- - message_sender_type
- - message_type
- - sender
- - sender_avatar_url
- - sender_name
- - status
- filter:
- message_conversation:
- user_id:
- _eq: X-Hasura-User-Id
- check: null
- comment: ""
-delete_permissions:
- - role: user
- permission:
- filter:
- message_conversation:
- user_id:
- _eq: X-Hasura-User-Id
- comment: ""
diff --git a/app-backend/hasura/metadata/databases/jandb/tables/public_product_prompts.yaml b/app-backend/hasura/metadata/databases/jandb/tables/public_product_prompts.yaml
deleted file mode 100644
index 8a0f9e90b..000000000
--- a/app-backend/hasura/metadata/databases/jandb/tables/public_product_prompts.yaml
+++ /dev/null
@@ -1,43 +0,0 @@
-table:
- name: product_prompts
- schema: public
-array_relationships:
- - name: products
- using:
- manual_configuration:
- column_mapping:
- product_id: id
- insertion_order: null
- remote_table:
- name: products
- schema: public
- - name: prompts
- using:
- manual_configuration:
- column_mapping:
- prompt_id: id
- insertion_order: null
- remote_table:
- name: prompts
- schema: public
-select_permissions:
- - role: public
- permission:
- columns:
- - created_at
- - updated_at
- - id
- - product_id
- - prompt_id
- filter: {}
- comment: ""
- - role: user
- permission:
- columns:
- - created_at
- - updated_at
- - id
- - product_id
- - prompt_id
- filter: {}
- comment: ""
diff --git a/app-backend/hasura/metadata/databases/jandb/tables/public_products.yaml b/app-backend/hasura/metadata/databases/jandb/tables/public_products.yaml
deleted file mode 100644
index f38873115..000000000
--- a/app-backend/hasura/metadata/databases/jandb/tables/public_products.yaml
+++ /dev/null
@@ -1,65 +0,0 @@
-table:
- name: products
- schema: public
-array_relationships:
- - name: product_collections
- using:
- manual_configuration:
- column_mapping:
- id: product_id
- insertion_order: null
- remote_table:
- name: collection_products
- schema: public
- - name: product_prompts
- using:
- manual_configuration:
- column_mapping:
- id: product_id
- insertion_order: null
- remote_table:
- name: product_prompts
- schema: public
-select_permissions:
- - role: public
- permission:
- columns:
- - nsfw
- - slug
- - inputs
- - outputs
- - author
- - description
- - greeting
- - image_url
- - long_description
- - name
- - source_url
- - technical_description
- - version
- - created_at
- - updated_at
- - id
- filter: {}
- comment: ""
- - role: user
- permission:
- columns:
- - nsfw
- - slug
- - inputs
- - outputs
- - author
- - description
- - greeting
- - image_url
- - long_description
- - name
- - source_url
- - technical_description
- - version
- - created_at
- - updated_at
- - id
- filter: {}
- comment: ""
diff --git a/app-backend/hasura/metadata/databases/jandb/tables/public_prompts.yaml b/app-backend/hasura/metadata/databases/jandb/tables/public_prompts.yaml
deleted file mode 100644
index 81cb8aadb..000000000
--- a/app-backend/hasura/metadata/databases/jandb/tables/public_prompts.yaml
+++ /dev/null
@@ -1,36 +0,0 @@
-table:
- name: prompts
- schema: public
-array_relationships:
- - name: prompt_products
- using:
- manual_configuration:
- column_mapping:
- id: prompt_id
- insertion_order: null
- remote_table:
- name: product_prompts
- schema: public
-select_permissions:
- - role: public
- permission:
- columns:
- - slug
- - content
- - image_url
- - created_at
- - updated_at
- - id
- filter: {}
- comment: ""
- - role: user
- permission:
- columns:
- - slug
- - content
- - image_url
- - created_at
- - updated_at
- - id
- filter: {}
- comment: ""
diff --git a/app-backend/hasura/metadata/databases/jandb/tables/tables.yaml b/app-backend/hasura/metadata/databases/jandb/tables/tables.yaml
deleted file mode 100644
index f541617f9..000000000
--- a/app-backend/hasura/metadata/databases/jandb/tables/tables.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-- "!include public_collection_products.yaml"
-- "!include public_collections.yaml"
-- "!include public_conversations.yaml"
-- "!include public_message_medias.yaml"
-- "!include public_messages.yaml"
-- "!include public_product_prompts.yaml"
-- "!include public_products.yaml"
-- "!include public_prompts.yaml"
diff --git a/app-backend/hasura/metadata/graphql_schema_introspection.yaml b/app-backend/hasura/metadata/graphql_schema_introspection.yaml
deleted file mode 100644
index 61a4dcac2..000000000
--- a/app-backend/hasura/metadata/graphql_schema_introspection.yaml
+++ /dev/null
@@ -1 +0,0 @@
-disabled_for_roles: []
diff --git a/app-backend/hasura/metadata/inherited_roles.yaml b/app-backend/hasura/metadata/inherited_roles.yaml
deleted file mode 100644
index fe51488c7..000000000
--- a/app-backend/hasura/metadata/inherited_roles.yaml
+++ /dev/null
@@ -1 +0,0 @@
-[]
diff --git a/app-backend/hasura/metadata/metrics_config.yaml b/app-backend/hasura/metadata/metrics_config.yaml
deleted file mode 100644
index 0967ef424..000000000
--- a/app-backend/hasura/metadata/metrics_config.yaml
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/app-backend/hasura/metadata/network.yaml b/app-backend/hasura/metadata/network.yaml
deleted file mode 100644
index 0967ef424..000000000
--- a/app-backend/hasura/metadata/network.yaml
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/app-backend/hasura/metadata/opentelemetry.yaml b/app-backend/hasura/metadata/opentelemetry.yaml
deleted file mode 100644
index 0967ef424..000000000
--- a/app-backend/hasura/metadata/opentelemetry.yaml
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/app-backend/hasura/metadata/query_collections.yaml b/app-backend/hasura/metadata/query_collections.yaml
deleted file mode 100644
index fe51488c7..000000000
--- a/app-backend/hasura/metadata/query_collections.yaml
+++ /dev/null
@@ -1 +0,0 @@
-[]
diff --git a/app-backend/hasura/metadata/remote_schemas.yaml b/app-backend/hasura/metadata/remote_schemas.yaml
deleted file mode 100644
index fe51488c7..000000000
--- a/app-backend/hasura/metadata/remote_schemas.yaml
+++ /dev/null
@@ -1 +0,0 @@
-[]
diff --git a/app-backend/hasura/metadata/rest_endpoints.yaml b/app-backend/hasura/metadata/rest_endpoints.yaml
deleted file mode 100644
index fe51488c7..000000000
--- a/app-backend/hasura/metadata/rest_endpoints.yaml
+++ /dev/null
@@ -1 +0,0 @@
-[]
diff --git a/app-backend/hasura/metadata/version.yaml b/app-backend/hasura/metadata/version.yaml
deleted file mode 100644
index 0a70affa4..000000000
--- a/app-backend/hasura/metadata/version.yaml
+++ /dev/null
@@ -1 +0,0 @@
-version: 3
diff --git a/app-backend/hasura/migrations/jandb/1692603657609_create_table_public_collections/down.sql b/app-backend/hasura/migrations/jandb/1692603657609_create_table_public_collections/down.sql
deleted file mode 100644
index df0f0d344..000000000
--- a/app-backend/hasura/migrations/jandb/1692603657609_create_table_public_collections/down.sql
+++ /dev/null
@@ -1 +0,0 @@
-DROP TABLE "public"."collections";
diff --git a/app-backend/hasura/migrations/jandb/1692603657609_create_table_public_collections/up.sql b/app-backend/hasura/migrations/jandb/1692603657609_create_table_public_collections/up.sql
deleted file mode 100644
index fec2cb55d..000000000
--- a/app-backend/hasura/migrations/jandb/1692603657609_create_table_public_collections/up.sql
+++ /dev/null
@@ -1,18 +0,0 @@
-CREATE TABLE "public"."collections" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "slug" varchar NOT NULL, "name" text NOT NULL, "description" text NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , UNIQUE ("slug"));
-CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
-RETURNS TRIGGER AS $$
-DECLARE
- _new record;
-BEGIN
- _new := NEW;
- _new."updated_at" = NOW();
- RETURN _new;
-END;
-$$ LANGUAGE plpgsql;
-CREATE TRIGGER "set_public_collections_updated_at"
-BEFORE UPDATE ON "public"."collections"
-FOR EACH ROW
-EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
-COMMENT ON TRIGGER "set_public_collections_updated_at" ON "public"."collections"
-IS 'trigger to set value of column "updated_at" to current timestamp on row update';
-CREATE EXTENSION IF NOT EXISTS pgcrypto;
diff --git a/app-backend/hasura/migrations/jandb/1692603899157_create_table_public_products/down.sql b/app-backend/hasura/migrations/jandb/1692603899157_create_table_public_products/down.sql
deleted file mode 100644
index 3b41d9305..000000000
--- a/app-backend/hasura/migrations/jandb/1692603899157_create_table_public_products/down.sql
+++ /dev/null
@@ -1 +0,0 @@
-DROP TABLE "public"."products";
diff --git a/app-backend/hasura/migrations/jandb/1692603899157_create_table_public_products/up.sql b/app-backend/hasura/migrations/jandb/1692603899157_create_table_public_products/up.sql
deleted file mode 100644
index 038926bb7..000000000
--- a/app-backend/hasura/migrations/jandb/1692603899157_create_table_public_products/up.sql
+++ /dev/null
@@ -1,18 +0,0 @@
-CREATE TABLE "public"."products" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "slug" varchar NOT NULL, "name" text NOT NULL, "description" text, "image_url" text, "long_description" text, "technical_description" text, "author" text, "version" text, "source_url" text, "nsfw" boolean NOT NULL DEFAULT true, "greeting" text, "inputs" jsonb, "outputs" jsonb, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , UNIQUE ("slug"));
-CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
-RETURNS TRIGGER AS $$
-DECLARE
- _new record;
-BEGIN
- _new := NEW;
- _new."updated_at" = NOW();
- RETURN _new;
-END;
-$$ LANGUAGE plpgsql;
-CREATE TRIGGER "set_public_products_updated_at"
-BEFORE UPDATE ON "public"."products"
-FOR EACH ROW
-EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
-COMMENT ON TRIGGER "set_public_products_updated_at" ON "public"."products"
-IS 'trigger to set value of column "updated_at" to current timestamp on row update';
-CREATE EXTENSION IF NOT EXISTS pgcrypto;
diff --git a/app-backend/hasura/migrations/jandb/1692603954223_create_table_public_prompts/down.sql b/app-backend/hasura/migrations/jandb/1692603954223_create_table_public_prompts/down.sql
deleted file mode 100644
index 74f77626e..000000000
--- a/app-backend/hasura/migrations/jandb/1692603954223_create_table_public_prompts/down.sql
+++ /dev/null
@@ -1 +0,0 @@
-DROP TABLE "public"."prompts";
diff --git a/app-backend/hasura/migrations/jandb/1692603954223_create_table_public_prompts/up.sql b/app-backend/hasura/migrations/jandb/1692603954223_create_table_public_prompts/up.sql
deleted file mode 100644
index 7f444c43b..000000000
--- a/app-backend/hasura/migrations/jandb/1692603954223_create_table_public_prompts/up.sql
+++ /dev/null
@@ -1,18 +0,0 @@
-CREATE TABLE "public"."prompts" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "slug" varchar NOT NULL, "content" text, "image_url" text, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , UNIQUE ("slug"));
-CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
-RETURNS TRIGGER AS $$
-DECLARE
- _new record;
-BEGIN
- _new := NEW;
- _new."updated_at" = NOW();
- RETURN _new;
-END;
-$$ LANGUAGE plpgsql;
-CREATE TRIGGER "set_public_prompts_updated_at"
-BEFORE UPDATE ON "public"."prompts"
-FOR EACH ROW
-EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
-COMMENT ON TRIGGER "set_public_prompts_updated_at" ON "public"."prompts"
-IS 'trigger to set value of column "updated_at" to current timestamp on row update';
-CREATE EXTENSION IF NOT EXISTS pgcrypto;
diff --git a/app-backend/hasura/migrations/jandb/1692604164229_create_table_public_conversations/down.sql b/app-backend/hasura/migrations/jandb/1692604164229_create_table_public_conversations/down.sql
deleted file mode 100644
index 53839d4c2..000000000
--- a/app-backend/hasura/migrations/jandb/1692604164229_create_table_public_conversations/down.sql
+++ /dev/null
@@ -1 +0,0 @@
-DROP TABLE "public"."conversations";
diff --git a/app-backend/hasura/migrations/jandb/1692604164229_create_table_public_conversations/up.sql b/app-backend/hasura/migrations/jandb/1692604164229_create_table_public_conversations/up.sql
deleted file mode 100644
index 5df679bf7..000000000
--- a/app-backend/hasura/migrations/jandb/1692604164229_create_table_public_conversations/up.sql
+++ /dev/null
@@ -1,18 +0,0 @@
-CREATE TABLE "public"."conversations" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "product_id" uuid NOT NULL, "user_id" Text NOT NULL, "last_image_url" text, "last_text_message" text, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") );
-CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
-RETURNS TRIGGER AS $$
-DECLARE
- _new record;
-BEGIN
- _new := NEW;
- _new."updated_at" = NOW();
- RETURN _new;
-END;
-$$ LANGUAGE plpgsql;
-CREATE TRIGGER "set_public_conversations_updated_at"
-BEFORE UPDATE ON "public"."conversations"
-FOR EACH ROW
-EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
-COMMENT ON TRIGGER "set_public_conversations_updated_at" ON "public"."conversations"
-IS 'trigger to set value of column "updated_at" to current timestamp on row update';
-CREATE EXTENSION IF NOT EXISTS pgcrypto;
diff --git a/app-backend/hasura/migrations/jandb/1692604250751_create_table_public_messages/down.sql b/app-backend/hasura/migrations/jandb/1692604250751_create_table_public_messages/down.sql
deleted file mode 100644
index eae375db7..000000000
--- a/app-backend/hasura/migrations/jandb/1692604250751_create_table_public_messages/down.sql
+++ /dev/null
@@ -1 +0,0 @@
-DROP TABLE "public"."messages";
diff --git a/app-backend/hasura/migrations/jandb/1692604250751_create_table_public_messages/up.sql b/app-backend/hasura/migrations/jandb/1692604250751_create_table_public_messages/up.sql
deleted file mode 100644
index e9645e75d..000000000
--- a/app-backend/hasura/migrations/jandb/1692604250751_create_table_public_messages/up.sql
+++ /dev/null
@@ -1,18 +0,0 @@
-CREATE TABLE "public"."messages" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "conversation_id" uuid NOT NULL, "message_type" varchar, "message_sender_type" varchar, "sender" text NOT NULL, "sender_name" text, "sender_avatar_url" text, "content" text, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") );
-CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
-RETURNS TRIGGER AS $$
-DECLARE
- _new record;
-BEGIN
- _new := NEW;
- _new."updated_at" = NOW();
- RETURN _new;
-END;
-$$ LANGUAGE plpgsql;
-CREATE TRIGGER "set_public_messages_updated_at"
-BEFORE UPDATE ON "public"."messages"
-FOR EACH ROW
-EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
-COMMENT ON TRIGGER "set_public_messages_updated_at" ON "public"."messages"
-IS 'trigger to set value of column "updated_at" to current timestamp on row update';
-CREATE EXTENSION IF NOT EXISTS pgcrypto;
diff --git a/app-backend/hasura/migrations/jandb/1692604354631_create_table_public_message_medias/down.sql b/app-backend/hasura/migrations/jandb/1692604354631_create_table_public_message_medias/down.sql
deleted file mode 100644
index dfec07a82..000000000
--- a/app-backend/hasura/migrations/jandb/1692604354631_create_table_public_message_medias/down.sql
+++ /dev/null
@@ -1 +0,0 @@
-DROP TABLE "public"."message_medias";
diff --git a/app-backend/hasura/migrations/jandb/1692604354631_create_table_public_message_medias/up.sql b/app-backend/hasura/migrations/jandb/1692604354631_create_table_public_message_medias/up.sql
deleted file mode 100644
index 8b82699b9..000000000
--- a/app-backend/hasura/migrations/jandb/1692604354631_create_table_public_message_medias/up.sql
+++ /dev/null
@@ -1,18 +0,0 @@
-CREATE TABLE "public"."message_medias" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "message_id" uuid NOT NULL, "media_url" text, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "mime_type" varchar, PRIMARY KEY ("id") );
-CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
-RETURNS TRIGGER AS $$
-DECLARE
- _new record;
-BEGIN
- _new := NEW;
- _new."updated_at" = NOW();
- RETURN _new;
-END;
-$$ LANGUAGE plpgsql;
-CREATE TRIGGER "set_public_message_medias_updated_at"
-BEFORE UPDATE ON "public"."message_medias"
-FOR EACH ROW
-EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
-COMMENT ON TRIGGER "set_public_message_medias_updated_at" ON "public"."message_medias"
-IS 'trigger to set value of column "updated_at" to current timestamp on row update';
-CREATE EXTENSION IF NOT EXISTS pgcrypto;
diff --git a/app-backend/hasura/migrations/jandb/1692604743434_create_table_public_collection_products/down.sql b/app-backend/hasura/migrations/jandb/1692604743434_create_table_public_collection_products/down.sql
deleted file mode 100644
index f2b37e379..000000000
--- a/app-backend/hasura/migrations/jandb/1692604743434_create_table_public_collection_products/down.sql
+++ /dev/null
@@ -1 +0,0 @@
-DROP TABLE "public"."collection_products";
diff --git a/app-backend/hasura/migrations/jandb/1692604743434_create_table_public_collection_products/up.sql b/app-backend/hasura/migrations/jandb/1692604743434_create_table_public_collection_products/up.sql
deleted file mode 100644
index 16ebe6b43..000000000
--- a/app-backend/hasura/migrations/jandb/1692604743434_create_table_public_collection_products/up.sql
+++ /dev/null
@@ -1,18 +0,0 @@
-CREATE TABLE "public"."collection_products" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "collection_id" uuid NOT NULL, "product_id" uuid NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , UNIQUE ("collection_id", "product_id"));
-CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
-RETURNS TRIGGER AS $$
-DECLARE
- _new record;
-BEGIN
- _new := NEW;
- _new."updated_at" = NOW();
- RETURN _new;
-END;
-$$ LANGUAGE plpgsql;
-CREATE TRIGGER "set_public_collection_products_updated_at"
-BEFORE UPDATE ON "public"."collection_products"
-FOR EACH ROW
-EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
-COMMENT ON TRIGGER "set_public_collection_products_updated_at" ON "public"."collection_products"
-IS 'trigger to set value of column "updated_at" to current timestamp on row update';
-CREATE EXTENSION IF NOT EXISTS pgcrypto;
diff --git a/app-backend/hasura/migrations/jandb/1692604794719_create_table_public_product_prompts/down.sql b/app-backend/hasura/migrations/jandb/1692604794719_create_table_public_product_prompts/down.sql
deleted file mode 100644
index 34881bd4d..000000000
--- a/app-backend/hasura/migrations/jandb/1692604794719_create_table_public_product_prompts/down.sql
+++ /dev/null
@@ -1 +0,0 @@
-DROP TABLE "public"."product_prompts";
diff --git a/app-backend/hasura/migrations/jandb/1692604794719_create_table_public_product_prompts/up.sql b/app-backend/hasura/migrations/jandb/1692604794719_create_table_public_product_prompts/up.sql
deleted file mode 100644
index b8bc3a57d..000000000
--- a/app-backend/hasura/migrations/jandb/1692604794719_create_table_public_product_prompts/up.sql
+++ /dev/null
@@ -1,18 +0,0 @@
-CREATE TABLE "public"."product_prompts" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "product_id" uuid NOT NULL, "prompt_id" uuid NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , UNIQUE ("product_id", "prompt_id"));
-CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
-RETURNS TRIGGER AS $$
-DECLARE
- _new record;
-BEGIN
- _new := NEW;
- _new."updated_at" = NOW();
- RETURN _new;
-END;
-$$ LANGUAGE plpgsql;
-CREATE TRIGGER "set_public_product_prompts_updated_at"
-BEFORE UPDATE ON "public"."product_prompts"
-FOR EACH ROW
-EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
-COMMENT ON TRIGGER "set_public_product_prompts_updated_at" ON "public"."product_prompts"
-IS 'trigger to set value of column "updated_at" to current timestamp on row update';
-CREATE EXTENSION IF NOT EXISTS pgcrypto;
diff --git a/app-backend/hasura/migrations/jandb/1692605865437_set_fk_public_collection_products_collection_id/down.sql b/app-backend/hasura/migrations/jandb/1692605865437_set_fk_public_collection_products_collection_id/down.sql
deleted file mode 100644
index 49b450db6..000000000
--- a/app-backend/hasura/migrations/jandb/1692605865437_set_fk_public_collection_products_collection_id/down.sql
+++ /dev/null
@@ -1 +0,0 @@
-alter table "public"."collection_products" drop constraint "collection_products_collection_id_fkey";
diff --git a/app-backend/hasura/migrations/jandb/1692605865437_set_fk_public_collection_products_collection_id/up.sql b/app-backend/hasura/migrations/jandb/1692605865437_set_fk_public_collection_products_collection_id/up.sql
deleted file mode 100644
index 29ee8b76b..000000000
--- a/app-backend/hasura/migrations/jandb/1692605865437_set_fk_public_collection_products_collection_id/up.sql
+++ /dev/null
@@ -1,5 +0,0 @@
-alter table "public"."collection_products"
- add constraint "collection_products_collection_id_fkey"
- foreign key ("collection_id")
- references "public"."collections"
- ("id") on update cascade on delete cascade;
diff --git a/app-backend/hasura/migrations/jandb/1692605887650_set_fk_public_collection_products_product_id/down.sql b/app-backend/hasura/migrations/jandb/1692605887650_set_fk_public_collection_products_product_id/down.sql
deleted file mode 100644
index 1ca809d97..000000000
--- a/app-backend/hasura/migrations/jandb/1692605887650_set_fk_public_collection_products_product_id/down.sql
+++ /dev/null
@@ -1 +0,0 @@
-alter table "public"."collection_products" drop constraint "collection_products_product_id_fkey";
diff --git a/app-backend/hasura/migrations/jandb/1692605887650_set_fk_public_collection_products_product_id/up.sql b/app-backend/hasura/migrations/jandb/1692605887650_set_fk_public_collection_products_product_id/up.sql
deleted file mode 100644
index ef8457ce6..000000000
--- a/app-backend/hasura/migrations/jandb/1692605887650_set_fk_public_collection_products_product_id/up.sql
+++ /dev/null
@@ -1,5 +0,0 @@
-alter table "public"."collection_products"
- add constraint "collection_products_product_id_fkey"
- foreign key ("product_id")
- references "public"."products"
- ("id") on update cascade on delete cascade;
diff --git a/app-backend/hasura/migrations/jandb/1692885821205_alter_table_public_messages_add_column_status/down.sql b/app-backend/hasura/migrations/jandb/1692885821205_alter_table_public_messages_add_column_status/down.sql
deleted file mode 100644
index 7d02924bb..000000000
--- a/app-backend/hasura/migrations/jandb/1692885821205_alter_table_public_messages_add_column_status/down.sql
+++ /dev/null
@@ -1 +0,0 @@
-alter table "public"."messages" drop column "status";
diff --git a/app-backend/hasura/migrations/jandb/1692885821205_alter_table_public_messages_add_column_status/up.sql b/app-backend/hasura/migrations/jandb/1692885821205_alter_table_public_messages_add_column_status/up.sql
deleted file mode 100644
index 428bf5b74..000000000
--- a/app-backend/hasura/migrations/jandb/1692885821205_alter_table_public_messages_add_column_status/up.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-alter table "public"."messages" add column "status" varchar
- null default 'ready';
diff --git a/app-backend/hasura/migrations/jandb/1692891470212_alter_table_public_messages_add_column_prompt_cache/down.sql b/app-backend/hasura/migrations/jandb/1692891470212_alter_table_public_messages_add_column_prompt_cache/down.sql
deleted file mode 100644
index 29b407049..000000000
--- a/app-backend/hasura/migrations/jandb/1692891470212_alter_table_public_messages_add_column_prompt_cache/down.sql
+++ /dev/null
@@ -1 +0,0 @@
-alter table "public"."messages" drop column "prompt_cache";
\ No newline at end of file
diff --git a/app-backend/hasura/migrations/jandb/1692891470212_alter_table_public_messages_add_column_prompt_cache/up.sql b/app-backend/hasura/migrations/jandb/1692891470212_alter_table_public_messages_add_column_prompt_cache/up.sql
deleted file mode 100644
index ea23f42ed..000000000
--- a/app-backend/hasura/migrations/jandb/1692891470212_alter_table_public_messages_add_column_prompt_cache/up.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-alter table "public"."messages" add column "prompt_cache" jsonb
- null;
diff --git a/app-backend/hasura/migrations/jandb/1694417935572_insert_products_migration/down.sql b/app-backend/hasura/migrations/jandb/1694417935572_insert_products_migration/down.sql
deleted file mode 100644
index b72b858da..000000000
--- a/app-backend/hasura/migrations/jandb/1694417935572_insert_products_migration/down.sql
+++ /dev/null
@@ -1 +0,0 @@
--- DO NOTHING WITH DATA MIGRATION DOWN
diff --git a/app-backend/hasura/migrations/jandb/1694417935572_insert_products_migration/up.sql b/app-backend/hasura/migrations/jandb/1694417935572_insert_products_migration/up.sql
deleted file mode 100644
index 7804caef7..000000000
--- a/app-backend/hasura/migrations/jandb/1694417935572_insert_products_migration/up.sql
+++ /dev/null
@@ -1,3 +0,0 @@
-INSERT INTO public.products ("slug", "name", "nsfw", "image_url", "description", "long_description", "technical_description", "author", "version", "source_url", "inputs", "outputs", "greeting") VALUES
-('llama2', 'Llama-2-7B-Chat', 't', 'https://static-assets.jan.ai/llama2.jpg','Llama 2 is Meta`s open source large language model (LLM)', 'Llama 2 is a collection of pretrained and fine-tuned generative text models ranging in scale from 7 billion to 70 billion parameters. This is the repository for the 7B pretrained model. Links to other models can be found in the index at the bottom.', 'Meta developed and publicly released the Llama 2 family of large language models (LLMs), a collection of pretrained and fine-tuned generative text models ranging in scale from 7 billion to 70 billion parameters. Our fine-tuned LLMs, called Llama-2-Chat, are optimized for dialogue use cases. Llama-2-Chat models outperform open-source chat models on most benchmarks we tested, and in our human evaluations for helpfulness and safety, are on par with some popular closed-source models like ChatGPT and PaLM.', 'Meta', 'Llama2-7B-GGML', 'https://huggingface.co/TheBloke/airoboros-13B-gpt4-1.4-GGML', '{"body": [{"name": "messages", "type": "array", "items": [{"type": "object", "properties": [{"name": "role", "type": "string", "example": "system", "description": "Defines the role of the message."}, {"name": "content", "type": "string", "example": "Hello, world!", "description": "Contains the content of the message."}]}], "description": "An array of messages, each containing a role and content. The latest message is always at the end of the array."}, {"name": "stream", "type": "boolean", "example": true, "description": "Indicates whether the client wants to keep the connection open for streaming."}, {"name": "max_tokens", "type": "integer", "example": 500, "description": "Defines the maximum number of tokens that the client wants to receive."}], "slug": "llm", "headers": {"accept": "text/event-stream", "content-type": "application/json"}}', '{"slug": "llm", "type": "object", "properties": [{"name": "id", "type": "string", "example": "chatcmpl-4c4e5eb5-bf53-4dbc-9136-1cf69fc5fd7c", "description": "The unique identifier of the chat completion chunk."}, {"name": "model", "type": "string", "example": "gpt-3.5-turbo", "description": "The name of the GPT model used to generate the completion."}, {"name": "created", "type": "integer", "example": 1692169988, "description": "The Unix timestamp representing the time when the completion was generated."}, {"name": "object", "type": "string", "example": "chat.completion.chunk", "description": "A string indicating the type of the chat completion chunk."}, {"name": "choices", "type": "array", "items": [{"type": "object", "properties": [{"name": "index", "type": "integer", "example": 0, "description": "The index of the choice made by the GPT model."}, {"name": "delta", "type": "object", "properties": [{"name": "content", "type": "string", "example": "What", "description": "The content generated by the GPT model."}], "description": "A JSON object containing the content generated by the GPT model."}, {"name": "finish_reason", "type": "string", "example": null, "description": "A string indicating why the GPT model stopped generating content."}]}], "description": "An array containing the choices made by the GPT model to generate the completion."}], "description": "A JSON object representing a chat completion chunk."}', '👋I’m a versatile AI trained on a wide range of topics, here to answer your questions about the universe. What are you curious about today?')
-ON CONFLICT (slug) DO NOTHING;
diff --git a/app-backend/hasura/migrations/jandb/1694417949946_insert_collections_migration/down.sql b/app-backend/hasura/migrations/jandb/1694417949946_insert_collections_migration/down.sql
deleted file mode 100644
index a089fbbf6..000000000
--- a/app-backend/hasura/migrations/jandb/1694417949946_insert_collections_migration/down.sql
+++ /dev/null
@@ -1 +0,0 @@
--- DO NOTHING WITH DATA MIGRATION DOWN
\ No newline at end of file
diff --git a/app-backend/hasura/migrations/jandb/1694417949946_insert_collections_migration/up.sql b/app-backend/hasura/migrations/jandb/1694417949946_insert_collections_migration/up.sql
deleted file mode 100644
index de5d10f45..000000000
--- a/app-backend/hasura/migrations/jandb/1694417949946_insert_collections_migration/up.sql
+++ /dev/null
@@ -1,3 +0,0 @@
-INSERT INTO public.collections ("slug", "name", "description") VALUES
-('conversational', 'Conversational', 'Converse with these models and get answers.')
-ON CONFLICT (slug) DO NOTHING;
diff --git a/app-backend/hasura/migrations/jandb/1694417968422_insert_collection_products_migration/down.sql b/app-backend/hasura/migrations/jandb/1694417968422_insert_collection_products_migration/down.sql
deleted file mode 100644
index a089fbbf6..000000000
--- a/app-backend/hasura/migrations/jandb/1694417968422_insert_collection_products_migration/down.sql
+++ /dev/null
@@ -1 +0,0 @@
--- DO NOTHING WITH DATA MIGRATION DOWN
\ No newline at end of file
diff --git a/app-backend/hasura/migrations/jandb/1694417968422_insert_collection_products_migration/up.sql b/app-backend/hasura/migrations/jandb/1694417968422_insert_collection_products_migration/up.sql
deleted file mode 100644
index c0c28cc8b..000000000
--- a/app-backend/hasura/migrations/jandb/1694417968422_insert_collection_products_migration/up.sql
+++ /dev/null
@@ -1,4 +0,0 @@
-INSERT INTO public.collection_products (collection_id, product_id)
-SELECT (SELECT id FROM public.collections WHERE slug = 'conversational') AS collection_id, id AS product_id
-FROM public.products
-WHERE slug IN ('llama2') ON CONFLICT (collection_id, product_id) DO NOTHING;
diff --git a/app-backend/hasura/migrations/jandb/1694417980224_insert_prompts_migration/down.sql b/app-backend/hasura/migrations/jandb/1694417980224_insert_prompts_migration/down.sql
deleted file mode 100644
index a089fbbf6..000000000
--- a/app-backend/hasura/migrations/jandb/1694417980224_insert_prompts_migration/down.sql
+++ /dev/null
@@ -1 +0,0 @@
--- DO NOTHING WITH DATA MIGRATION DOWN
\ No newline at end of file
diff --git a/app-backend/hasura/migrations/jandb/1694417980224_insert_prompts_migration/up.sql b/app-backend/hasura/migrations/jandb/1694417980224_insert_prompts_migration/up.sql
deleted file mode 100644
index acf4ae014..000000000
--- a/app-backend/hasura/migrations/jandb/1694417980224_insert_prompts_migration/up.sql
+++ /dev/null
@@ -1,8 +0,0 @@
-INSERT INTO public.prompts ("slug", "content", "image_url") VALUES
-('conversational-ai-future', 'What are possible developments for AI technology in the next decade?', ''),
-('conversational-managing-stress', 'What are some tips for managing stress?', ''),
-('conversational-postapoc-robot', 'Let''s role play. You are a robot in a post-apocalyptic world.', ''),
-('conversational-python-pytorch', 'What is the difference between Python and Pytorch?', ''),
-('conversational-quadratic-equation', 'Can you explain how to solve a quadratic equation?', ''),
-('conversational-roman-history', 'What is the history of the Roman Empire?', '')
-ON CONFLICT (slug) DO NOTHING;
diff --git a/app-backend/hasura/migrations/jandb/1694417991834_insert_product_prompts_migration/down.sql b/app-backend/hasura/migrations/jandb/1694417991834_insert_product_prompts_migration/down.sql
deleted file mode 100644
index a089fbbf6..000000000
--- a/app-backend/hasura/migrations/jandb/1694417991834_insert_product_prompts_migration/down.sql
+++ /dev/null
@@ -1 +0,0 @@
--- DO NOTHING WITH DATA MIGRATION DOWN
\ No newline at end of file
diff --git a/app-backend/hasura/migrations/jandb/1694417991834_insert_product_prompts_migration/up.sql b/app-backend/hasura/migrations/jandb/1694417991834_insert_product_prompts_migration/up.sql
deleted file mode 100644
index 6d3206ce5..000000000
--- a/app-backend/hasura/migrations/jandb/1694417991834_insert_product_prompts_migration/up.sql
+++ /dev/null
@@ -1,9 +0,0 @@
-INSERT INTO public.product_prompts (product_id, prompt_id)
-SELECT p.id AS product_id, r.id AS prompt_id
-FROM public.products p
-JOIN public.prompts r
-ON (p.id
- IN (SELECT x.id FROM public.products x INNER JOIN public.collection_products y ON x.id = y.product_id
- INNER JOIN public.collections z ON y.collection_id = z.id
- WHERE z.slug = 'conversational'))
-WHERE r.image_url IS NULL OR r.image_url = '' ON CONFLICT (product_id, prompt_id) DO NOTHING;
diff --git a/app-backend/sample.env b/app-backend/sample.env
deleted file mode 100644
index 9a7b438f1..000000000
--- a/app-backend/sample.env
+++ /dev/null
@@ -1,23 +0,0 @@
-## postgres database to store Hasura metadata
-HASURA_GRAPHQL_METADATA_DATABASE_URL=postgres://postgres:postgrespassword@postgres:5432/postgres
-## this env var can be used to add the above postgres database to Hasura as a data source. this can be removed/updated based on your needs
-PG_DATABASE_URL=postgres://postgres:postgrespassword@postgres:5432/postgres
-## enable the console served by server
-HASURA_GRAPHQL_ENABLE_CONSOLE="true" # set to "false" to disable console
-## enable debugging mode. It is recommended to disable this in production
-HASURA_GRAPHQL_DEV_MODE="true"
-HASURA_GRAPHQL_ENABLED_LOG_TYPES=startup, http-log, webhook-log, websocket-log, query-log
-## uncomment next line to run console offline (i.e load console assets from server instead of CDN)
-# HASURA_GRAPHQL_CONSOLE_ASSETS_DIR: /srv/console-assets
-## uncomment next line to set an admin secret
-HASURA_GRAPHQL_ADMIN_SECRET=myadminsecretkey
-HASURA_GRAPHQL_UNAUTHORIZED_ROLE="public"
-HASURA_GRAPHQL_METADATA_DEFAULTS='{"backend_configs":{"dataconnector":{"athena":{"uri":"http://data-connector-agent:8081/api/v1/athena"},"mariadb":{"uri":"http://data-connector-agent:8081/api/v1/mariadb"},"mysql8":{"uri":"http://data-connector-agent:8081/api/v1/mysql"},"oracle":{"uri":"http://data-connector-agent:8081/api/v1/oracle"},"snowflake":{"uri":"http://data-connector-agent:8081/api/v1/snowflake"}}}}'
-HASURA_GRAPHQL_JWT_SECRET={"type": "RS256", "key": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"}
-
-# Environment variable for auto migrate
-HASURA_GRAPHQL_MIGRATIONS_DIR=/migrations
-HASURA_GRAPHQL_METADATA_DIR=/metadata
-HASURA_GRAPHQL_ENABLE_CONSOLE='true'
-HASURA_ACTION_STABLE_DIFFUSION_URL=http://sd:8000
-HASURA_EVENTS_HOOK_URL="http://worker:8787"
\ No newline at end of file
diff --git a/app-backend/sample.env_postgresql b/app-backend/sample.env_postgresql
deleted file mode 100644
index 39aa93db5..000000000
--- a/app-backend/sample.env_postgresql
+++ /dev/null
@@ -1 +0,0 @@
-POSTGRES_PASSWORD=postgrespassword
\ No newline at end of file
diff --git a/conf/db/docker_psql_init.sql b/conf/db/docker_psql_init.sql
deleted file mode 100644
index db34e8393..000000000
--- a/conf/db/docker_psql_init.sql
+++ /dev/null
@@ -1,26 +0,0 @@
-CREATE DATABASE "jan-keycloak"
- WITH
- OWNER = postgres
- ENCODING = 'UTF8'
- LC_COLLATE = 'en_US.utf8'
- LC_CTYPE = 'en_US.utf8'
- TABLESPACE = pg_default
- CONNECTION LIMIT = -1;
-
-CREATE DATABASE "jan-hasura-data"
- WITH
- OWNER = postgres
- ENCODING = 'UTF8'
- LC_COLLATE = 'en_US.utf8'
- LC_CTYPE = 'en_US.utf8'
- TABLESPACE = pg_default
- CONNECTION LIMIT = -1;
-
-CREATE DATABASE "jan-hasura-metadata"
- WITH
- OWNER = postgres
- ENCODING = 'UTF8'
- LC_COLLATE = 'en_US.utf8'
- LC_CTYPE = 'en_US.utf8'
- TABLESPACE = pg_default
- CONNECTION LIMIT = -1;
\ No newline at end of file
diff --git a/conf/keycloak_conf/example-realm.json b/conf/keycloak_conf/example-realm.json
deleted file mode 100644
index bc10dac64..000000000
--- a/conf/keycloak_conf/example-realm.json
+++ /dev/null
@@ -1,2311 +0,0 @@
-{
- "id": "d3ebd3d9-2493-4aa2-942b-3118a216b430",
- "realm": "hasura",
- "notBefore": 0,
- "defaultSignatureAlgorithm": "RS256",
- "revokeRefreshToken": false,
- "refreshTokenMaxReuse": 0,
- "accessTokenLifespan": 300,
- "accessTokenLifespanForImplicitFlow": 900,
- "ssoSessionIdleTimeout": 1800,
- "ssoSessionMaxLifespan": 36000,
- "ssoSessionIdleTimeoutRememberMe": 0,
- "ssoSessionMaxLifespanRememberMe": 0,
- "offlineSessionIdleTimeout": 2592000,
- "offlineSessionMaxLifespanEnabled": false,
- "offlineSessionMaxLifespan": 5184000,
- "clientSessionIdleTimeout": 0,
- "clientSessionMaxLifespan": 0,
- "clientOfflineSessionIdleTimeout": 0,
- "clientOfflineSessionMaxLifespan": 0,
- "accessCodeLifespan": 60,
- "accessCodeLifespanUserAction": 300,
- "accessCodeLifespanLogin": 1800,
- "actionTokenGeneratedByAdminLifespan": 43200,
- "actionTokenGeneratedByUserLifespan": 300,
- "oauth2DeviceCodeLifespan": 600,
- "oauth2DevicePollingInterval": 5,
- "enabled": true,
- "sslRequired": "external",
- "registrationAllowed": false,
- "registrationEmailAsUsername": false,
- "rememberMe": false,
- "verifyEmail": false,
- "loginWithEmailAllowed": true,
- "duplicateEmailsAllowed": false,
- "resetPasswordAllowed": false,
- "editUsernameAllowed": false,
- "bruteForceProtected": false,
- "permanentLockout": false,
- "maxFailureWaitSeconds": 900,
- "minimumQuickLoginWaitSeconds": 60,
- "waitIncrementSeconds": 60,
- "quickLoginCheckMilliSeconds": 1000,
- "maxDeltaTimeSeconds": 43200,
- "failureFactor": 30,
- "roles": {
- "realm": [
- {
- "id": "7219280c-2d93-4cb5-9ff9-fa4b639716fd",
- "name": "uma_authorization",
- "description": "${role_uma_authorization}",
- "composite": false,
- "clientRole": false,
- "containerId": "d3ebd3d9-2493-4aa2-942b-3118a216b430",
- "attributes": {}
- },
- {
- "id": "7fb465bc-beae-431b-8a38-06afa8b4c75c",
- "name": "user",
- "description": "",
- "composite": false,
- "clientRole": false,
- "containerId": "d3ebd3d9-2493-4aa2-942b-3118a216b430",
- "attributes": {}
- },
- {
- "id": "b34240e1-92b8-48e9-aaa5-82eecbdcb283",
- "name": "default-roles-hasura",
- "description": "${role_default-roles}",
- "composite": true,
- "composites": {
- "realm": ["offline_access", "uma_authorization"],
- "client": {
- "account": ["manage-account", "view-profile"]
- }
- },
- "clientRole": false,
- "containerId": "d3ebd3d9-2493-4aa2-942b-3118a216b430",
- "attributes": {}
- },
- {
- "id": "3ece9760-6a06-4d75-9efc-27164c8253c8",
- "name": "offline_access",
- "description": "${role_offline-access}",
- "composite": false,
- "clientRole": false,
- "containerId": "d3ebd3d9-2493-4aa2-942b-3118a216b430",
- "attributes": {}
- }
- ],
- "client": {
- "realm-management": [
- {
- "id": "c8c637d4-39a5-4170-8d2f-1aef0a2f9720",
- "name": "query-groups",
- "description": "${role_query-groups}",
- "composite": false,
- "clientRole": true,
- "containerId": "5d7dda49-d56d-4c64-8a01-ee5dd5310c81",
- "attributes": {}
- },
- {
- "id": "e81f684f-c4ff-425e-861d-80507258b321",
- "name": "manage-authorization",
- "description": "${role_manage-authorization}",
- "composite": false,
- "clientRole": true,
- "containerId": "5d7dda49-d56d-4c64-8a01-ee5dd5310c81",
- "attributes": {}
- },
- {
- "id": "88d97cbc-e7e1-4561-ad9c-8cf1161a55a0",
- "name": "query-realms",
- "description": "${role_query-realms}",
- "composite": false,
- "clientRole": true,
- "containerId": "5d7dda49-d56d-4c64-8a01-ee5dd5310c81",
- "attributes": {}
- },
- {
- "id": "67063d5f-8023-4be9-8db2-37f81dfeb90b",
- "name": "view-identity-providers",
- "description": "${role_view-identity-providers}",
- "composite": false,
- "clientRole": true,
- "containerId": "5d7dda49-d56d-4c64-8a01-ee5dd5310c81",
- "attributes": {}
- },
- {
- "id": "024c8120-8208-4ad4-b098-27c7c9d549df",
- "name": "view-realm",
- "description": "${role_view-realm}",
- "composite": false,
- "clientRole": true,
- "containerId": "5d7dda49-d56d-4c64-8a01-ee5dd5310c81",
- "attributes": {}
- },
- {
- "id": "a2fe97c7-b0d2-444f-bdba-efb2bfecda0f",
- "name": "create-client",
- "description": "${role_create-client}",
- "composite": false,
- "clientRole": true,
- "containerId": "5d7dda49-d56d-4c64-8a01-ee5dd5310c81",
- "attributes": {}
- },
- {
- "id": "bc2425ed-2269-431c-81f6-eb4401020952",
- "name": "query-users",
- "description": "${role_query-users}",
- "composite": false,
- "clientRole": true,
- "containerId": "5d7dda49-d56d-4c64-8a01-ee5dd5310c81",
- "attributes": {}
- },
- {
- "id": "a2cb5869-dc32-4373-8a10-71b9132c850e",
- "name": "view-authorization",
- "description": "${role_view-authorization}",
- "composite": false,
- "clientRole": true,
- "containerId": "5d7dda49-d56d-4c64-8a01-ee5dd5310c81",
- "attributes": {}
- },
- {
- "id": "a8b11879-261c-4e4f-8b05-414069988810",
- "name": "manage-identity-providers",
- "description": "${role_manage-identity-providers}",
- "composite": false,
- "clientRole": true,
- "containerId": "5d7dda49-d56d-4c64-8a01-ee5dd5310c81",
- "attributes": {}
- },
- {
- "id": "0810aa4f-a32c-432a-9c16-0374233f17e6",
- "name": "view-clients",
- "description": "${role_view-clients}",
- "composite": true,
- "composites": {
- "client": {
- "realm-management": ["query-clients"]
- }
- },
- "clientRole": true,
- "containerId": "5d7dda49-d56d-4c64-8a01-ee5dd5310c81",
- "attributes": {}
- },
- {
- "id": "53b48727-828f-406c-aa7e-b0d1c4109d0b",
- "name": "impersonation",
- "description": "${role_impersonation}",
- "composite": false,
- "clientRole": true,
- "containerId": "5d7dda49-d56d-4c64-8a01-ee5dd5310c81",
- "attributes": {}
- },
- {
- "id": "27751c74-74f3-4fbf-b42a-124de7207dc0",
- "name": "manage-clients",
- "description": "${role_manage-clients}",
- "composite": false,
- "clientRole": true,
- "containerId": "5d7dda49-d56d-4c64-8a01-ee5dd5310c81",
- "attributes": {}
- },
- {
- "id": "44c4d09a-8b0e-413e-91a4-c383ef17b345",
- "name": "query-clients",
- "description": "${role_query-clients}",
- "composite": false,
- "clientRole": true,
- "containerId": "5d7dda49-d56d-4c64-8a01-ee5dd5310c81",
- "attributes": {}
- },
- {
- "id": "837d8209-0fbf-406d-a1f4-d3672879fd7c",
- "name": "manage-realm",
- "description": "${role_manage-realm}",
- "composite": false,
- "clientRole": true,
- "containerId": "5d7dda49-d56d-4c64-8a01-ee5dd5310c81",
- "attributes": {}
- },
- {
- "id": "90b904a0-ddf1-4542-902e-f0f0cd4bc23e",
- "name": "manage-users",
- "description": "${role_manage-users}",
- "composite": false,
- "clientRole": true,
- "containerId": "5d7dda49-d56d-4c64-8a01-ee5dd5310c81",
- "attributes": {}
- },
- {
- "id": "49328548-01c4-477e-90e8-5cdec6ede9d2",
- "name": "view-events",
- "description": "${role_view-events}",
- "composite": false,
- "clientRole": true,
- "containerId": "5d7dda49-d56d-4c64-8a01-ee5dd5310c81",
- "attributes": {}
- },
- {
- "id": "5f381d33-4e28-4b50-87be-0cc0e4887e2f",
- "name": "realm-admin",
- "description": "${role_realm-admin}",
- "composite": true,
- "composites": {
- "client": {
- "realm-management": [
- "query-groups",
- "manage-authorization",
- "view-realm",
- "view-identity-providers",
- "query-realms",
- "create-client",
- "view-authorization",
- "query-users",
- "manage-identity-providers",
- "view-clients",
- "impersonation",
- "manage-clients",
- "query-clients",
- "manage-realm",
- "manage-users",
- "view-events",
- "view-users",
- "manage-events"
- ]
- }
- },
- "clientRole": true,
- "containerId": "5d7dda49-d56d-4c64-8a01-ee5dd5310c81",
- "attributes": {}
- },
- {
- "id": "13b75adb-cf15-4f02-8c1e-b70bf405b692",
- "name": "view-users",
- "description": "${role_view-users}",
- "composite": true,
- "composites": {
- "client": {
- "realm-management": ["query-groups", "query-users"]
- }
- },
- "clientRole": true,
- "containerId": "5d7dda49-d56d-4c64-8a01-ee5dd5310c81",
- "attributes": {}
- },
- {
- "id": "b5126b18-1e29-4989-a0ce-3350feb02b42",
- "name": "manage-events",
- "description": "${role_manage-events}",
- "composite": false,
- "clientRole": true,
- "containerId": "5d7dda49-d56d-4c64-8a01-ee5dd5310c81",
- "attributes": {}
- }
- ],
- "security-admin-console": [],
- "admin-cli": [],
- "account-console": [],
- "broker": [
- {
- "id": "ba86f97b-0032-465d-b52d-06d0cb1514b5",
- "name": "read-token",
- "description": "${role_read-token}",
- "composite": false,
- "clientRole": true,
- "containerId": "4a3abf8b-7ce0-4eb3-8733-0b76bde3125d",
- "attributes": {}
- }
- ],
- "account": [
- {
- "id": "8107bb62-629a-4c05-a6b3-9ec05db9feab",
- "name": "manage-consent",
- "description": "${role_manage-consent}",
- "composite": true,
- "composites": {
- "client": {
- "account": ["view-consent"]
- }
- },
- "clientRole": true,
- "containerId": "ce02d0ff-b633-4d79-8a33-2c8241017d72",
- "attributes": {}
- },
- {
- "id": "cb761f1a-f422-4292-9f27-aeca063ae6ad",
- "name": "view-applications",
- "description": "${role_view-applications}",
- "composite": false,
- "clientRole": true,
- "containerId": "ce02d0ff-b633-4d79-8a33-2c8241017d72",
- "attributes": {}
- },
- {
- "id": "b87631c3-f3bb-4684-825e-53b7b784687d",
- "name": "manage-account",
- "description": "${role_manage-account}",
- "composite": true,
- "composites": {
- "client": {
- "account": ["manage-account-links"]
- }
- },
- "clientRole": true,
- "containerId": "ce02d0ff-b633-4d79-8a33-2c8241017d72",
- "attributes": {}
- },
- {
- "id": "81a519c5-d106-4b4c-986d-3416537d91b6",
- "name": "view-groups",
- "description": "${role_view-groups}",
- "composite": false,
- "clientRole": true,
- "containerId": "ce02d0ff-b633-4d79-8a33-2c8241017d72",
- "attributes": {}
- },
- {
- "id": "e563f5f3-e86f-491c-882a-7acc3d84c6f0",
- "name": "view-consent",
- "description": "${role_view-consent}",
- "composite": false,
- "clientRole": true,
- "containerId": "ce02d0ff-b633-4d79-8a33-2c8241017d72",
- "attributes": {}
- },
- {
- "id": "4434c108-c594-43e9-b8d4-8ded56196cc5",
- "name": "view-profile",
- "description": "${role_view-profile}",
- "composite": false,
- "clientRole": true,
- "containerId": "ce02d0ff-b633-4d79-8a33-2c8241017d72",
- "attributes": {}
- },
- {
- "id": "c342ec0b-6885-4d8d-b036-1aafdc376c06",
- "name": "delete-account",
- "description": "${role_delete-account}",
- "composite": false,
- "clientRole": true,
- "containerId": "ce02d0ff-b633-4d79-8a33-2c8241017d72",
- "attributes": {}
- },
- {
- "id": "5c200c4e-fd80-4cd1-8723-562ba7430398",
- "name": "manage-account-links",
- "description": "${role_manage-account-links}",
- "composite": false,
- "clientRole": true,
- "containerId": "ce02d0ff-b633-4d79-8a33-2c8241017d72",
- "attributes": {}
- }
- ],
- "hasura": [
- {
- "id": "e971ec56-92a0-4c06-a8a2-a35946852ee6",
- "name": "uma_protection",
- "composite": false,
- "clientRole": true,
- "containerId": "4be1cdcc-b0b3-4581-957b-54fa17c0d929",
- "attributes": {}
- },
- {
- "id": "8cbd62f4-daa2-4809-850b-88870e2c70fc",
- "name": "user",
- "description": "",
- "composite": false,
- "clientRole": true,
- "containerId": "4be1cdcc-b0b3-4581-957b-54fa17c0d929",
- "attributes": {}
- }
- ]
- }
- },
- "groups": [],
- "defaultRole": {
- "id": "b34240e1-92b8-48e9-aaa5-82eecbdcb283",
- "name": "default-roles-hasura",
- "description": "${role_default-roles}",
- "composite": true,
- "clientRole": false,
- "containerId": "d3ebd3d9-2493-4aa2-942b-3118a216b430"
- },
- "requiredCredentials": ["password"],
- "otpPolicyType": "totp",
- "otpPolicyAlgorithm": "HmacSHA1",
- "otpPolicyInitialCounter": 0,
- "otpPolicyDigits": 6,
- "otpPolicyLookAheadWindow": 1,
- "otpPolicyPeriod": 30,
- "otpPolicyCodeReusable": false,
- "otpSupportedApplications": [
- "totpAppGoogleName",
- "totpAppMicrosoftAuthenticatorName",
- "totpAppFreeOTPName"
- ],
- "webAuthnPolicyRpEntityName": "keycloak",
- "webAuthnPolicySignatureAlgorithms": ["ES256"],
- "webAuthnPolicyRpId": "",
- "webAuthnPolicyAttestationConveyancePreference": "not specified",
- "webAuthnPolicyAuthenticatorAttachment": "not specified",
- "webAuthnPolicyRequireResidentKey": "not specified",
- "webAuthnPolicyUserVerificationRequirement": "not specified",
- "webAuthnPolicyCreateTimeout": 0,
- "webAuthnPolicyAvoidSameAuthenticatorRegister": false,
- "webAuthnPolicyAcceptableAaguids": [],
- "webAuthnPolicyPasswordlessRpEntityName": "keycloak",
- "webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"],
- "webAuthnPolicyPasswordlessRpId": "",
- "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified",
- "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified",
- "webAuthnPolicyPasswordlessRequireResidentKey": "not specified",
- "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified",
- "webAuthnPolicyPasswordlessCreateTimeout": 0,
- "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false,
- "webAuthnPolicyPasswordlessAcceptableAaguids": [],
- "users": [
- {
- "username": "username",
- "enabled": true,
- "emailVerified": true,
- "credentials": [
- {
- "type": "password",
- "value": "password"
- }
- ],
- "realmRoles": [
- "user",
- "default-roles-hasura"
- ],
- "clientRoles": {
- "hasura": [
- "user"
- ]
- }
- },
- {
- "id": "8728e38e-6b60-44d2-8830-f1e2ad284a02",
- "createdTimestamp": 1692777925764,
- "username": "service-account-hasura",
- "enabled": true,
- "totp": false,
- "emailVerified": false,
- "serviceAccountClientId": "hasura",
- "disableableCredentialTypes": [],
- "requiredActions": [],
- "realmRoles": ["user", "default-roles-hasura"],
- "clientRoles": {
- "hasura": ["uma_protection"]
- },
- "notBefore": 0,
- "groups": []
- }
- ],
- "scopeMappings": [
- {
- "clientScope": "offline_access",
- "roles": ["offline_access"]
- },
- {
- "clientScope": "hasura",
- "roles": ["user"]
- }
- ],
- "clientScopeMappings": {
- "account": [
- {
- "client": "account-console",
- "roles": ["manage-account", "view-groups"]
- }
- ]
- },
- "clients": [
- {
- "id": "ce02d0ff-b633-4d79-8a33-2c8241017d72",
- "clientId": "account",
- "name": "${client_account}",
- "rootUrl": "${authBaseUrl}",
- "baseUrl": "/realms/hasura/account/",
- "surrogateAuthRequired": false,
- "enabled": true,
- "alwaysDisplayInConsole": false,
- "clientAuthenticatorType": "client-secret",
- "redirectUris": ["/realms/hasura/account/*"],
- "webOrigins": [],
- "notBefore": 0,
- "bearerOnly": false,
- "consentRequired": false,
- "standardFlowEnabled": true,
- "implicitFlowEnabled": false,
- "directAccessGrantsEnabled": false,
- "serviceAccountsEnabled": false,
- "publicClient": true,
- "frontchannelLogout": false,
- "protocol": "openid-connect",
- "attributes": {
- "post.logout.redirect.uris": "+"
- },
- "authenticationFlowBindingOverrides": {},
- "fullScopeAllowed": false,
- "nodeReRegistrationTimeout": 0,
- "defaultClientScopes": [
- "web-origins",
- "acr",
- "roles",
- "profile",
- "email"
- ],
- "optionalClientScopes": [
- "address",
- "phone",
- "offline_access",
- "microprofile-jwt"
- ]
- },
- {
- "id": "101df256-3450-4b5c-8d0f-d29b531e3499",
- "clientId": "account-console",
- "name": "${client_account-console}",
- "rootUrl": "${authBaseUrl}",
- "baseUrl": "/realms/hasura/account/",
- "surrogateAuthRequired": false,
- "enabled": true,
- "alwaysDisplayInConsole": false,
- "clientAuthenticatorType": "client-secret",
- "redirectUris": ["/realms/hasura/account/*"],
- "webOrigins": [],
- "notBefore": 0,
- "bearerOnly": false,
- "consentRequired": false,
- "standardFlowEnabled": true,
- "implicitFlowEnabled": false,
- "directAccessGrantsEnabled": false,
- "serviceAccountsEnabled": false,
- "publicClient": true,
- "frontchannelLogout": false,
- "protocol": "openid-connect",
- "attributes": {
- "post.logout.redirect.uris": "+",
- "pkce.code.challenge.method": "S256"
- },
- "authenticationFlowBindingOverrides": {},
- "fullScopeAllowed": false,
- "nodeReRegistrationTimeout": 0,
- "protocolMappers": [
- {
- "id": "b06a98d0-6d91-4260-9406-aea123f842dc",
- "name": "audience resolve",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-audience-resolve-mapper",
- "consentRequired": false,
- "config": {}
- }
- ],
- "defaultClientScopes": [
- "web-origins",
- "acr",
- "roles",
- "profile",
- "email"
- ],
- "optionalClientScopes": [
- "address",
- "phone",
- "offline_access",
- "microprofile-jwt"
- ]
- },
- {
- "id": "db12bda8-7624-46cc-a84f-9c83cf99b370",
- "clientId": "admin-cli",
- "name": "${client_admin-cli}",
- "surrogateAuthRequired": false,
- "enabled": true,
- "alwaysDisplayInConsole": false,
- "clientAuthenticatorType": "client-secret",
- "redirectUris": [],
- "webOrigins": [],
- "notBefore": 0,
- "bearerOnly": false,
- "consentRequired": false,
- "standardFlowEnabled": false,
- "implicitFlowEnabled": false,
- "directAccessGrantsEnabled": true,
- "serviceAccountsEnabled": false,
- "publicClient": true,
- "frontchannelLogout": false,
- "protocol": "openid-connect",
- "attributes": {},
- "authenticationFlowBindingOverrides": {},
- "fullScopeAllowed": false,
- "nodeReRegistrationTimeout": 0,
- "defaultClientScopes": [
- "web-origins",
- "acr",
- "roles",
- "profile",
- "email"
- ],
- "optionalClientScopes": [
- "address",
- "phone",
- "offline_access",
- "microprofile-jwt"
- ]
- },
- {
- "id": "4a3abf8b-7ce0-4eb3-8733-0b76bde3125d",
- "clientId": "broker",
- "name": "${client_broker}",
- "surrogateAuthRequired": false,
- "enabled": true,
- "alwaysDisplayInConsole": false,
- "clientAuthenticatorType": "client-secret",
- "redirectUris": [],
- "webOrigins": [],
- "notBefore": 0,
- "bearerOnly": true,
- "consentRequired": false,
- "standardFlowEnabled": true,
- "implicitFlowEnabled": false,
- "directAccessGrantsEnabled": false,
- "serviceAccountsEnabled": false,
- "publicClient": false,
- "frontchannelLogout": false,
- "protocol": "openid-connect",
- "attributes": {},
- "authenticationFlowBindingOverrides": {},
- "fullScopeAllowed": false,
- "nodeReRegistrationTimeout": 0,
- "defaultClientScopes": [
- "web-origins",
- "acr",
- "roles",
- "profile",
- "email"
- ],
- "optionalClientScopes": [
- "address",
- "phone",
- "offline_access",
- "microprofile-jwt"
- ]
- },
- {
- "id": "4be1cdcc-b0b3-4581-957b-54fa17c0d929",
- "clientId": "hasura",
- "name": "Hasura",
- "description": "",
- "rootUrl": "",
- "adminUrl": "",
- "baseUrl": "",
- "surrogateAuthRequired": false,
- "enabled": true,
- "alwaysDisplayInConsole": false,
- "clientAuthenticatorType": "client-secret",
- "secret": "oMtCPAV7diKpE564SBspgKj4HqlKM4Hy",
- "redirectUris": ["http://localhost:3000/*"],
- "webOrigins": ["http://localhost:3000"],
- "notBefore": 0,
- "bearerOnly": false,
- "consentRequired": false,
- "standardFlowEnabled": true,
- "implicitFlowEnabled": false,
- "directAccessGrantsEnabled": true,
- "serviceAccountsEnabled": true,
- "authorizationServicesEnabled": true,
- "publicClient": false,
- "frontchannelLogout": true,
- "protocol": "openid-connect",
- "attributes": {
- "oidc.ciba.grant.enabled": "false",
- "oauth2.device.authorization.grant.enabled": "false",
- "client.secret.creation.time": "1692777925",
- "backchannel.logout.session.required": "true",
- "backchannel.logout.revoke.offline.tokens": "false"
- },
- "authenticationFlowBindingOverrides": {},
- "fullScopeAllowed": true,
- "nodeReRegistrationTimeout": -1,
- "protocolMappers": [
- {
- "id": "c4016925-f659-455a-9cdb-29bba88327e9",
- "name": "Client ID",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usersessionmodel-note-mapper",
- "consentRequired": false,
- "config": {
- "user.session.note": "client_id",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "client_id",
- "jsonType.label": "String"
- }
- },
- {
- "id": "e80e0f4c-cd0d-4bb8-a1e4-108a49f4d1a7",
- "name": "Client Host",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usersessionmodel-note-mapper",
- "consentRequired": false,
- "config": {
- "user.session.note": "clientHost",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "clientHost",
- "jsonType.label": "String"
- }
- },
- {
- "id": "3cb8ba1a-81f2-42b7-91b0-3fbe29126262",
- "name": "Client IP Address",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usersessionmodel-note-mapper",
- "consentRequired": false,
- "config": {
- "user.session.note": "clientAddress",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "clientAddress",
- "jsonType.label": "String"
- }
- }
- ],
- "defaultClientScopes": [
- "web-origins",
- "acr",
- "roles",
- "profile",
- "hasura",
- "email"
- ],
- "optionalClientScopes": [
- "address",
- "phone",
- "offline_access",
- "microprofile-jwt"
- ],
- "authorizationSettings": {
- "allowRemoteResourceManagement": true,
- "policyEnforcementMode": "ENFORCING",
- "resources": [
- {
- "name": "Default Resource",
- "type": "urn:hasura:resources:default",
- "ownerManagedAccess": false,
- "attributes": {},
- "_id": "088f6856-3491-4600-ac5d-0287da57511c",
- "uris": ["/*"]
- }
- ],
- "policies": [
- {
- "id": "f82ec078-33ce-4204-83c9-627a802c0821",
- "name": "Default Policy",
- "description": "A policy that grants access only for users within this realm",
- "type": "role",
- "logic": "POSITIVE",
- "decisionStrategy": "AFFIRMATIVE",
- "config": {
- "roles": "[{\"id\":\"default-roles-hasura\",\"required\":false}]"
- }
- },
- {
- "id": "2cad4a9f-b0f6-4067-98a7-4f42ed5e33ef",
- "name": "Default Permission",
- "description": "A permission that applies to the default resource type",
- "type": "resource",
- "logic": "POSITIVE",
- "decisionStrategy": "UNANIMOUS",
- "config": {
- "defaultResourceType": "urn:hasura:resources:default",
- "applyPolicies": "[\"Default Policy\"]"
- }
- }
- ],
- "scopes": [],
- "decisionStrategy": "UNANIMOUS"
- }
- },
- {
- "id": "5d7dda49-d56d-4c64-8a01-ee5dd5310c81",
- "clientId": "realm-management",
- "name": "${client_realm-management}",
- "surrogateAuthRequired": false,
- "enabled": true,
- "alwaysDisplayInConsole": false,
- "clientAuthenticatorType": "client-secret",
- "redirectUris": [],
- "webOrigins": [],
- "notBefore": 0,
- "bearerOnly": true,
- "consentRequired": false,
- "standardFlowEnabled": true,
- "implicitFlowEnabled": false,
- "directAccessGrantsEnabled": false,
- "serviceAccountsEnabled": false,
- "publicClient": false,
- "frontchannelLogout": false,
- "protocol": "openid-connect",
- "attributes": {},
- "authenticationFlowBindingOverrides": {},
- "fullScopeAllowed": false,
- "nodeReRegistrationTimeout": 0,
- "defaultClientScopes": [
- "web-origins",
- "acr",
- "roles",
- "profile",
- "email"
- ],
- "optionalClientScopes": [
- "address",
- "phone",
- "offline_access",
- "microprofile-jwt"
- ]
- },
- {
- "id": "29f3fff6-2a63-4f7b-9474-d2385f0eff32",
- "clientId": "security-admin-console",
- "name": "${client_security-admin-console}",
- "rootUrl": "${authAdminUrl}",
- "baseUrl": "/admin/hasura/console/",
- "surrogateAuthRequired": false,
- "enabled": true,
- "alwaysDisplayInConsole": false,
- "clientAuthenticatorType": "client-secret",
- "redirectUris": ["/admin/hasura/console/*"],
- "webOrigins": ["+"],
- "notBefore": 0,
- "bearerOnly": false,
- "consentRequired": false,
- "standardFlowEnabled": true,
- "implicitFlowEnabled": false,
- "directAccessGrantsEnabled": false,
- "serviceAccountsEnabled": false,
- "publicClient": true,
- "frontchannelLogout": false,
- "protocol": "openid-connect",
- "attributes": {
- "post.logout.redirect.uris": "+",
- "pkce.code.challenge.method": "S256"
- },
- "authenticationFlowBindingOverrides": {},
- "fullScopeAllowed": false,
- "nodeReRegistrationTimeout": 0,
- "protocolMappers": [
- {
- "id": "795b2582-0f87-46ac-8be0-13518ad27445",
- "name": "locale",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-attribute-mapper",
- "consentRequired": false,
- "config": {
- "userinfo.token.claim": "true",
- "user.attribute": "locale",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "locale",
- "jsonType.label": "String"
- }
- }
- ],
- "defaultClientScopes": [
- "web-origins",
- "acr",
- "roles",
- "profile",
- "email"
- ],
- "optionalClientScopes": [
- "address",
- "phone",
- "offline_access",
- "microprofile-jwt"
- ]
- }
- ],
- "clientScopes": [
- {
- "id": "7df17b55-8301-4667-b9f3-41f9520a92cf",
- "name": "offline_access",
- "description": "OpenID Connect built-in scope: offline_access",
- "protocol": "openid-connect",
- "attributes": {
- "consent.screen.text": "${offlineAccessScopeConsentText}",
- "display.on.consent.screen": "true"
- }
- },
- {
- "id": "9cf833ae-3238-4aa3-9a9a-bbb8bc3767e3",
- "name": "roles",
- "description": "OpenID Connect scope for add user roles to the access token",
- "protocol": "openid-connect",
- "attributes": {
- "include.in.token.scope": "false",
- "display.on.consent.screen": "true",
- "consent.screen.text": "${rolesScopeConsentText}"
- },
- "protocolMappers": [
- {
- "id": "8d49f2bc-7ffc-4ee0-ae71-d82623330caf",
- "name": "client roles",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-client-role-mapper",
- "consentRequired": false,
- "config": {
- "user.attribute": "foo",
- "access.token.claim": "true",
- "claim.name": "resource_access.${client_id}.roles",
- "jsonType.label": "String",
- "multivalued": "true"
- }
- },
- {
- "id": "7e1a84a8-47d0-4cd8-ba0f-dc689588b7ec",
- "name": "realm roles",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-realm-role-mapper",
- "consentRequired": false,
- "config": {
- "user.attribute": "foo",
- "access.token.claim": "true",
- "claim.name": "realm_access.roles",
- "jsonType.label": "String",
- "multivalued": "true"
- }
- },
- {
- "id": "807ae7c0-7c18-4d46-b1c6-9d191294bf7b",
- "name": "audience resolve",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-audience-resolve-mapper",
- "consentRequired": false,
- "config": {}
- }
- ]
- },
- {
- "id": "ad8bfcfe-469b-4e72-82ab-2e8b564efc14",
- "name": "acr",
- "description": "OpenID Connect scope for add acr (authentication context class reference) to the token",
- "protocol": "openid-connect",
- "attributes": {
- "include.in.token.scope": "false",
- "display.on.consent.screen": "false"
- },
- "protocolMappers": [
- {
- "id": "96589cee-a8cc-4919-8b60-00a77a34ff87",
- "name": "acr loa level",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-acr-mapper",
- "consentRequired": false,
- "config": {
- "id.token.claim": "true",
- "access.token.claim": "true"
- }
- }
- ]
- },
- {
- "id": "2a8948cb-65cf-4ce4-ae11-fae5d9391fd3",
- "name": "microprofile-jwt",
- "description": "Microprofile - JWT built-in scope",
- "protocol": "openid-connect",
- "attributes": {
- "include.in.token.scope": "true",
- "display.on.consent.screen": "false"
- },
- "protocolMappers": [
- {
- "id": "6679f3f8-c764-4cd4-ae63-9da31200e7c5",
- "name": "upn",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-attribute-mapper",
- "consentRequired": false,
- "config": {
- "userinfo.token.claim": "true",
- "user.attribute": "username",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "upn",
- "jsonType.label": "String"
- }
- },
- {
- "id": "09a8bf42-8326-40a5-b1d3-0a65fe6236c3",
- "name": "groups",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-realm-role-mapper",
- "consentRequired": false,
- "config": {
- "multivalued": "true",
- "user.attribute": "foo",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "groups",
- "jsonType.label": "String"
- }
- }
- ]
- },
- {
- "id": "794f2bc0-4b14-4cb3-a7f2-730b2fabc84d",
- "name": "hasura",
- "description": "",
- "protocol": "openid-connect",
- "attributes": {
- "include.in.token.scope": "true",
- "display.on.consent.screen": "true",
- "gui.order": "",
- "consent.screen.text": ""
- },
- "protocolMappers": [
- {
- "id": "74c7a728-24a3-480e-bbb9-2129bd2dccb1",
- "name": "x-hasura-default-role",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-hardcoded-claim-mapper",
- "consentRequired": false,
- "config": {
- "claim.value": "user",
- "userinfo.token.claim": "true",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "https://hasura\\.io/jwt/claims.x-hasura-default-role",
- "jsonType.label": "String",
- "access.tokenResponse.claim": "false"
- }
- },
- {
- "id": "b451832d-670a-4e0d-878e-904c2b1598c2",
- "name": "x-hasura-user-id",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-property-mapper",
- "consentRequired": false,
- "config": {
- "userinfo.token.claim": "true",
- "user.attribute": "id",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "https://hasura\\.io/jwt/claims.x-hasura-user-id",
- "jsonType.label": "String"
- }
- },
- {
- "id": "eb62cfc4-ea5f-454a-9c78-384cb5e80373",
- "name": "x-hasura-allowed-roles",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-client-role-mapper",
- "consentRequired": false,
- "config": {
- "multivalued": "true",
- "userinfo.token.claim": "true",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "https://hasura\\.io/jwt/claims.x-hasura-allowed-roles",
- "jsonType.label": "String",
- "usermodel.clientRoleMapping.clientId": "hasura"
- }
- }
- ]
- },
- {
- "id": "8321f73d-4f9e-486e-ae03-cde49fc78bc1",
- "name": "role_list",
- "description": "SAML role list",
- "protocol": "saml",
- "attributes": {
- "consent.screen.text": "${samlRoleListScopeConsentText}",
- "display.on.consent.screen": "true"
- },
- "protocolMappers": [
- {
- "id": "56bb56c7-0237-4263-b858-7f9cdba57a1d",
- "name": "role list",
- "protocol": "saml",
- "protocolMapper": "saml-role-list-mapper",
- "consentRequired": false,
- "config": {
- "single": "false",
- "attribute.nameformat": "Basic",
- "attribute.name": "Role"
- }
- }
- ]
- },
- {
- "id": "c2c39673-f405-4fed-81a5-3bc3aa333b8b",
- "name": "email",
- "description": "OpenID Connect built-in scope: email",
- "protocol": "openid-connect",
- "attributes": {
- "include.in.token.scope": "true",
- "display.on.consent.screen": "true",
- "consent.screen.text": "${emailScopeConsentText}"
- },
- "protocolMappers": [
- {
- "id": "01e5705c-f668-476d-8526-7d8851531832",
- "name": "email",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-attribute-mapper",
- "consentRequired": false,
- "config": {
- "userinfo.token.claim": "true",
- "user.attribute": "email",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "email",
- "jsonType.label": "String"
- }
- },
- {
- "id": "66d6d45a-d4ca-4574-bcbe-b26982b4c7f8",
- "name": "email verified",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-property-mapper",
- "consentRequired": false,
- "config": {
- "userinfo.token.claim": "true",
- "user.attribute": "emailVerified",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "email_verified",
- "jsonType.label": "boolean"
- }
- }
- ]
- },
- {
- "id": "f03591f6-3ea2-405b-b47c-6eb797ec5926",
- "name": "profile",
- "description": "OpenID Connect built-in scope: profile",
- "protocol": "openid-connect",
- "attributes": {
- "include.in.token.scope": "true",
- "display.on.consent.screen": "true",
- "consent.screen.text": "${profileScopeConsentText}"
- },
- "protocolMappers": [
- {
- "id": "3c61cc63-50f9-4cfb-b45e-422c59161709",
- "name": "full name",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-full-name-mapper",
- "consentRequired": false,
- "config": {
- "id.token.claim": "true",
- "access.token.claim": "true",
- "userinfo.token.claim": "true"
- }
- },
- {
- "id": "2dfbc3d5-81f1-4160-9b0a-c58fd997fe68",
- "name": "picture",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-attribute-mapper",
- "consentRequired": false,
- "config": {
- "userinfo.token.claim": "true",
- "user.attribute": "picture",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "picture",
- "jsonType.label": "String"
- }
- },
- {
- "id": "a5e7f48a-219c-4bca-a491-d54e8788d8be",
- "name": "username",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-attribute-mapper",
- "consentRequired": false,
- "config": {
- "userinfo.token.claim": "true",
- "user.attribute": "username",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "preferred_username",
- "jsonType.label": "String"
- }
- },
- {
- "id": "837efb21-32d2-47e5-8b1c-fdf5d3ea17bc",
- "name": "website",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-attribute-mapper",
- "consentRequired": false,
- "config": {
- "userinfo.token.claim": "true",
- "user.attribute": "website",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "website",
- "jsonType.label": "String"
- }
- },
- {
- "id": "d1200744-27c1-426c-8332-ce4b0c433c50",
- "name": "zoneinfo",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-attribute-mapper",
- "consentRequired": false,
- "config": {
- "userinfo.token.claim": "true",
- "user.attribute": "zoneinfo",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "zoneinfo",
- "jsonType.label": "String"
- }
- },
- {
- "id": "63eae02f-f44b-4fe6-91f2-a16d096c46db",
- "name": "birthdate",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-attribute-mapper",
- "consentRequired": false,
- "config": {
- "userinfo.token.claim": "true",
- "user.attribute": "birthdate",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "birthdate",
- "jsonType.label": "String"
- }
- },
- {
- "id": "5cbb20a8-2947-420d-9a5d-1ce4eb928117",
- "name": "given name",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-attribute-mapper",
- "consentRequired": false,
- "config": {
- "userinfo.token.claim": "true",
- "user.attribute": "firstName",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "given_name",
- "jsonType.label": "String"
- }
- },
- {
- "id": "fdb72b90-48f8-4dcf-be77-e246fc77814c",
- "name": "family name",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-attribute-mapper",
- "consentRequired": false,
- "config": {
- "userinfo.token.claim": "true",
- "user.attribute": "lastName",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "family_name",
- "jsonType.label": "String"
- }
- },
- {
- "id": "a61f7495-5229-48a8-a1d2-0596f13d1c06",
- "name": "nickname",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-attribute-mapper",
- "consentRequired": false,
- "config": {
- "userinfo.token.claim": "true",
- "user.attribute": "nickname",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "nickname",
- "jsonType.label": "String"
- }
- },
- {
- "id": "ac4150b4-0b5f-4187-a81f-21e582fde2b4",
- "name": "profile",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-attribute-mapper",
- "consentRequired": false,
- "config": {
- "userinfo.token.claim": "true",
- "user.attribute": "profile",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "profile",
- "jsonType.label": "String"
- }
- },
- {
- "id": "2f8cf479-e0f5-49a6-a46b-f4708f60840e",
- "name": "middle name",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-attribute-mapper",
- "consentRequired": false,
- "config": {
- "userinfo.token.claim": "true",
- "user.attribute": "middleName",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "middle_name",
- "jsonType.label": "String"
- }
- },
- {
- "id": "2530d28b-3135-442d-aabe-3dcd20d6370c",
- "name": "locale",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-attribute-mapper",
- "consentRequired": false,
- "config": {
- "userinfo.token.claim": "true",
- "user.attribute": "locale",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "locale",
- "jsonType.label": "String"
- }
- },
- {
- "id": "68ca4cca-c6d3-4fa4-8079-ee0c81286577",
- "name": "gender",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-attribute-mapper",
- "consentRequired": false,
- "config": {
- "userinfo.token.claim": "true",
- "user.attribute": "gender",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "gender",
- "jsonType.label": "String"
- }
- },
- {
- "id": "ef95b412-17ff-4ef5-b9e9-8b95f7cc44b3",
- "name": "updated at",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-attribute-mapper",
- "consentRequired": false,
- "config": {
- "userinfo.token.claim": "true",
- "user.attribute": "updatedAt",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "updated_at",
- "jsonType.label": "long"
- }
- }
- ]
- },
- {
- "id": "8b3ba733-fa23-489f-b954-a0b89fba8b54",
- "name": "web-origins",
- "description": "OpenID Connect scope for add allowed web origins to the access token",
- "protocol": "openid-connect",
- "attributes": {
- "include.in.token.scope": "false",
- "display.on.consent.screen": "false",
- "consent.screen.text": ""
- },
- "protocolMappers": [
- {
- "id": "43ee6247-a7fd-4649-8770-5c966a23b831",
- "name": "allowed web origins",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-allowed-origins-mapper",
- "consentRequired": false,
- "config": {}
- }
- ]
- },
- {
- "id": "818f4f96-a63c-4c1d-9251-d999517ede72",
- "name": "address",
- "description": "OpenID Connect built-in scope: address",
- "protocol": "openid-connect",
- "attributes": {
- "include.in.token.scope": "true",
- "display.on.consent.screen": "true",
- "consent.screen.text": "${addressScopeConsentText}"
- },
- "protocolMappers": [
- {
- "id": "f9fbf5ed-195e-44f1-a5d3-6bf64919bb2b",
- "name": "address",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-address-mapper",
- "consentRequired": false,
- "config": {
- "user.attribute.formatted": "formatted",
- "user.attribute.country": "country",
- "user.attribute.postal_code": "postal_code",
- "userinfo.token.claim": "true",
- "user.attribute.street": "street",
- "id.token.claim": "true",
- "user.attribute.region": "region",
- "access.token.claim": "true",
- "user.attribute.locality": "locality"
- }
- }
- ]
- },
- {
- "id": "e3f2c7fe-ca50-4b01-b999-098c82beb362",
- "name": "phone",
- "description": "OpenID Connect built-in scope: phone",
- "protocol": "openid-connect",
- "attributes": {
- "include.in.token.scope": "true",
- "display.on.consent.screen": "true",
- "consent.screen.text": "${phoneScopeConsentText}"
- },
- "protocolMappers": [
- {
- "id": "7ae1a4ac-99db-4a27-8f18-f537a8020e4f",
- "name": "phone number",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-attribute-mapper",
- "consentRequired": false,
- "config": {
- "userinfo.token.claim": "true",
- "user.attribute": "phoneNumber",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "phone_number",
- "jsonType.label": "String"
- }
- },
- {
- "id": "edd5d134-b2c1-47d8-9cc5-3558559cf407",
- "name": "phone number verified",
- "protocol": "openid-connect",
- "protocolMapper": "oidc-usermodel-attribute-mapper",
- "consentRequired": false,
- "config": {
- "userinfo.token.claim": "true",
- "user.attribute": "phoneNumberVerified",
- "id.token.claim": "true",
- "access.token.claim": "true",
- "claim.name": "phone_number_verified",
- "jsonType.label": "boolean"
- }
- }
- ]
- }
- ],
- "defaultDefaultClientScopes": [
- "role_list",
- "profile",
- "email",
- "roles",
- "web-origins",
- "acr",
- "hasura"
- ],
- "defaultOptionalClientScopes": [
- "offline_access",
- "address",
- "phone",
- "microprofile-jwt"
- ],
- "browserSecurityHeaders": {
- "contentSecurityPolicyReportOnly": "",
- "xContentTypeOptions": "nosniff",
- "referrerPolicy": "no-referrer",
- "xRobotsTag": "none",
- "xFrameOptions": "SAMEORIGIN",
- "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';",
- "xXSSProtection": "1; mode=block",
- "strictTransportSecurity": "max-age=31536000; includeSubDomains"
- },
- "smtpServer": {},
- "eventsEnabled": false,
- "eventsListeners": ["jboss-logging"],
- "enabledEventTypes": [],
- "adminEventsEnabled": false,
- "adminEventsDetailsEnabled": false,
- "identityProviders": [],
- "identityProviderMappers": [],
- "components": {
- "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [
- {
- "id": "e2254ce7-52d6-4450-aa77-5ba2b40b8e26",
- "name": "Allowed Client Scopes",
- "providerId": "allowed-client-templates",
- "subType": "anonymous",
- "subComponents": {},
- "config": {
- "allow-default-scopes": ["true"]
- }
- },
- {
- "id": "b88f3a6b-b2a8-476d-9433-904ca547f64c",
- "name": "Allowed Protocol Mapper Types",
- "providerId": "allowed-protocol-mappers",
- "subType": "anonymous",
- "subComponents": {},
- "config": {
- "allowed-protocol-mapper-types": [
- "saml-user-attribute-mapper",
- "oidc-usermodel-property-mapper",
- "oidc-sha256-pairwise-sub-mapper",
- "oidc-full-name-mapper",
- "saml-user-property-mapper",
- "saml-role-list-mapper",
- "oidc-usermodel-attribute-mapper",
- "oidc-address-mapper"
- ]
- }
- },
- {
- "id": "d9ba5290-8c0e-4fa7-bf53-afc4fab4ec4b",
- "name": "Allowed Client Scopes",
- "providerId": "allowed-client-templates",
- "subType": "authenticated",
- "subComponents": {},
- "config": {
- "allow-default-scopes": ["true"]
- }
- },
- {
- "id": "1c24034f-5374-4020-b8e2-ba2eb4c0a944",
- "name": "Full Scope Disabled",
- "providerId": "scope",
- "subType": "anonymous",
- "subComponents": {},
- "config": {}
- },
- {
- "id": "e66a399a-317f-42c0-8ec7-9e2ba781a53d",
- "name": "Consent Required",
- "providerId": "consent-required",
- "subType": "anonymous",
- "subComponents": {},
- "config": {}
- },
- {
- "id": "eec0d3e9-e1e2-41c0-8a03-cc5f06c0f77e",
- "name": "Trusted Hosts",
- "providerId": "trusted-hosts",
- "subType": "anonymous",
- "subComponents": {},
- "config": {
- "host-sending-registration-request-must-match": ["true"],
- "client-uris-must-match": ["true"]
- }
- },
- {
- "id": "01f2436b-aa02-4f55-ac64-6132dd6ee375",
- "name": "Allowed Protocol Mapper Types",
- "providerId": "allowed-protocol-mappers",
- "subType": "authenticated",
- "subComponents": {},
- "config": {
- "allowed-protocol-mapper-types": [
- "oidc-usermodel-property-mapper",
- "saml-user-property-mapper",
- "oidc-usermodel-attribute-mapper",
- "saml-user-attribute-mapper",
- "oidc-full-name-mapper",
- "oidc-address-mapper",
- "oidc-sha256-pairwise-sub-mapper",
- "saml-role-list-mapper"
- ]
- }
- },
- {
- "id": "86fe59f0-cb96-4a39-8476-ae092479fb69",
- "name": "Max Clients Limit",
- "providerId": "max-clients",
- "subType": "anonymous",
- "subComponents": {},
- "config": {
- "max-clients": ["200"]
- }
- }
- ],
- "org.keycloak.keys.KeyProvider": [
- {
- "id": "796db70c-b8e9-4efa-b225-91d8cc8099ea",
- "name": "rsa-enc-generated",
- "providerId": "rsa-enc-generated",
- "subComponents": {},
- "config": {
- "priority": ["100"],
- "algorithm": ["RSA-OAEP"]
- }
- },
- {
- "id": "06ed66b9-a1ac-4662-afb4-889d176189b3",
- "name": "hmac-generated",
- "providerId": "hmac-generated",
- "subComponents": {},
- "config": {
- "priority": ["100"],
- "algorithm": ["HS256"]
- }
- },
- {
- "id": "fc8169fe-cb2b-4edb-befa-fa136215a184",
- "name": "aes-generated",
- "providerId": "aes-generated",
- "subComponents": {},
- "config": {
- "priority": ["100"]
- }
- },
- {
- "id": "9c40d8de-d867-4f6a-9637-b99500551f15",
- "name": "rsa-generated",
- "providerId": "rsa-generated",
- "subComponents": {},
- "config": {
- "priority": ["100"]
- }
- }
- ]
- },
- "internationalizationEnabled": false,
- "supportedLocales": [],
- "authenticationFlows": [
- {
- "id": "da3e4aa5-578c-4a70-a0ee-6bd0ac54acd5",
- "alias": "Account verification options",
- "description": "Method with which to verity the existing account",
- "providerId": "basic-flow",
- "topLevel": false,
- "builtIn": true,
- "authenticationExecutions": [
- {
- "authenticator": "idp-email-verification",
- "authenticatorFlow": false,
- "requirement": "ALTERNATIVE",
- "priority": 10,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticatorFlow": true,
- "requirement": "ALTERNATIVE",
- "priority": 20,
- "autheticatorFlow": true,
- "flowAlias": "Verify Existing Account by Re-authentication",
- "userSetupAllowed": false
- }
- ]
- },
- {
- "id": "489875e5-f48e-484a-8a82-d49e8af1834a",
- "alias": "Browser - Conditional OTP",
- "description": "Flow to determine if the OTP is required for the authentication",
- "providerId": "basic-flow",
- "topLevel": false,
- "builtIn": true,
- "authenticationExecutions": [
- {
- "authenticator": "conditional-user-configured",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 10,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticator": "auth-otp-form",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 20,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- }
- ]
- },
- {
- "id": "43d1a926-1785-4391-bf16-519da996ab47",
- "alias": "Direct Grant - Conditional OTP",
- "description": "Flow to determine if the OTP is required for the authentication",
- "providerId": "basic-flow",
- "topLevel": false,
- "builtIn": true,
- "authenticationExecutions": [
- {
- "authenticator": "conditional-user-configured",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 10,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticator": "direct-grant-validate-otp",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 20,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- }
- ]
- },
- {
- "id": "27d47660-5e77-4732-b781-36e88d2f6baf",
- "alias": "First broker login - Conditional OTP",
- "description": "Flow to determine if the OTP is required for the authentication",
- "providerId": "basic-flow",
- "topLevel": false,
- "builtIn": true,
- "authenticationExecutions": [
- {
- "authenticator": "conditional-user-configured",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 10,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticator": "auth-otp-form",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 20,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- }
- ]
- },
- {
- "id": "a7d1c2ef-57c8-4907-b21b-0d2df675c9c0",
- "alias": "Handle Existing Account",
- "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
- "providerId": "basic-flow",
- "topLevel": false,
- "builtIn": true,
- "authenticationExecutions": [
- {
- "authenticator": "idp-confirm-link",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 10,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticatorFlow": true,
- "requirement": "REQUIRED",
- "priority": 20,
- "autheticatorFlow": true,
- "flowAlias": "Account verification options",
- "userSetupAllowed": false
- }
- ]
- },
- {
- "id": "5f97b94f-e60d-498e-8874-d719adbcc985",
- "alias": "Reset - Conditional OTP",
- "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
- "providerId": "basic-flow",
- "topLevel": false,
- "builtIn": true,
- "authenticationExecutions": [
- {
- "authenticator": "conditional-user-configured",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 10,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticator": "reset-otp",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 20,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- }
- ]
- },
- {
- "id": "22b24e38-53ff-4788-ae8a-dcbce66819a7",
- "alias": "User creation or linking",
- "description": "Flow for the existing/non-existing user alternatives",
- "providerId": "basic-flow",
- "topLevel": false,
- "builtIn": true,
- "authenticationExecutions": [
- {
- "authenticatorConfig": "create unique user config",
- "authenticator": "idp-create-user-if-unique",
- "authenticatorFlow": false,
- "requirement": "ALTERNATIVE",
- "priority": 10,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticatorFlow": true,
- "requirement": "ALTERNATIVE",
- "priority": 20,
- "autheticatorFlow": true,
- "flowAlias": "Handle Existing Account",
- "userSetupAllowed": false
- }
- ]
- },
- {
- "id": "aa67bda7-bfc9-4cd2-84a5-22e371e89135",
- "alias": "Verify Existing Account by Re-authentication",
- "description": "Reauthentication of existing account",
- "providerId": "basic-flow",
- "topLevel": false,
- "builtIn": true,
- "authenticationExecutions": [
- {
- "authenticator": "idp-username-password-form",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 10,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticatorFlow": true,
- "requirement": "CONDITIONAL",
- "priority": 20,
- "autheticatorFlow": true,
- "flowAlias": "First broker login - Conditional OTP",
- "userSetupAllowed": false
- }
- ]
- },
- {
- "id": "f9b90830-656d-4378-bd98-c11d8a20295c",
- "alias": "browser",
- "description": "browser based authentication",
- "providerId": "basic-flow",
- "topLevel": true,
- "builtIn": true,
- "authenticationExecutions": [
- {
- "authenticator": "auth-cookie",
- "authenticatorFlow": false,
- "requirement": "ALTERNATIVE",
- "priority": 10,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticator": "auth-spnego",
- "authenticatorFlow": false,
- "requirement": "DISABLED",
- "priority": 20,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticator": "identity-provider-redirector",
- "authenticatorFlow": false,
- "requirement": "ALTERNATIVE",
- "priority": 25,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticatorFlow": true,
- "requirement": "ALTERNATIVE",
- "priority": 30,
- "autheticatorFlow": true,
- "flowAlias": "forms",
- "userSetupAllowed": false
- }
- ]
- },
- {
- "id": "9b00a800-deea-4b4a-b9cb-530886a7098a",
- "alias": "clients",
- "description": "Base authentication for clients",
- "providerId": "client-flow",
- "topLevel": true,
- "builtIn": true,
- "authenticationExecutions": [
- {
- "authenticator": "client-secret",
- "authenticatorFlow": false,
- "requirement": "ALTERNATIVE",
- "priority": 10,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticator": "client-jwt",
- "authenticatorFlow": false,
- "requirement": "ALTERNATIVE",
- "priority": 20,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticator": "client-secret-jwt",
- "authenticatorFlow": false,
- "requirement": "ALTERNATIVE",
- "priority": 30,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticator": "client-x509",
- "authenticatorFlow": false,
- "requirement": "ALTERNATIVE",
- "priority": 40,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- }
- ]
- },
- {
- "id": "88c2fd63-c912-4727-8323-63d31333efaf",
- "alias": "direct grant",
- "description": "OpenID Connect Resource Owner Grant",
- "providerId": "basic-flow",
- "topLevel": true,
- "builtIn": true,
- "authenticationExecutions": [
- {
- "authenticator": "direct-grant-validate-username",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 10,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticator": "direct-grant-validate-password",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 20,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticatorFlow": true,
- "requirement": "CONDITIONAL",
- "priority": 30,
- "autheticatorFlow": true,
- "flowAlias": "Direct Grant - Conditional OTP",
- "userSetupAllowed": false
- }
- ]
- },
- {
- "id": "12a8d6fe-ad7e-4443-9b40-b8855535def0",
- "alias": "docker auth",
- "description": "Used by Docker clients to authenticate against the IDP",
- "providerId": "basic-flow",
- "topLevel": true,
- "builtIn": true,
- "authenticationExecutions": [
- {
- "authenticator": "docker-http-basic-authenticator",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 10,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- }
- ]
- },
- {
- "id": "c64af03f-9326-4689-ad7e-d804b3fe0d60",
- "alias": "first broker login",
- "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
- "providerId": "basic-flow",
- "topLevel": true,
- "builtIn": true,
- "authenticationExecutions": [
- {
- "authenticatorConfig": "review profile config",
- "authenticator": "idp-review-profile",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 10,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticatorFlow": true,
- "requirement": "REQUIRED",
- "priority": 20,
- "autheticatorFlow": true,
- "flowAlias": "User creation or linking",
- "userSetupAllowed": false
- }
- ]
- },
- {
- "id": "b0e1e734-a068-4ab1-b643-9ac5e18b54b9",
- "alias": "forms",
- "description": "Username, password, otp and other auth forms.",
- "providerId": "basic-flow",
- "topLevel": false,
- "builtIn": true,
- "authenticationExecutions": [
- {
- "authenticator": "auth-username-password-form",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 10,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticatorFlow": true,
- "requirement": "CONDITIONAL",
- "priority": 20,
- "autheticatorFlow": true,
- "flowAlias": "Browser - Conditional OTP",
- "userSetupAllowed": false
- }
- ]
- },
- {
- "id": "8978b81a-5145-40bf-9a02-4b9f5c2270cf",
- "alias": "registration",
- "description": "registration flow",
- "providerId": "basic-flow",
- "topLevel": true,
- "builtIn": true,
- "authenticationExecutions": [
- {
- "authenticator": "registration-page-form",
- "authenticatorFlow": true,
- "requirement": "REQUIRED",
- "priority": 10,
- "autheticatorFlow": true,
- "flowAlias": "registration form",
- "userSetupAllowed": false
- }
- ]
- },
- {
- "id": "dd0879d0-a1c7-47dd-8343-5495259359dc",
- "alias": "registration form",
- "description": "registration form",
- "providerId": "form-flow",
- "topLevel": false,
- "builtIn": true,
- "authenticationExecutions": [
- {
- "authenticator": "registration-user-creation",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 20,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticator": "registration-profile-action",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 40,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticator": "registration-password-action",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 50,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticator": "registration-recaptcha-action",
- "authenticatorFlow": false,
- "requirement": "DISABLED",
- "priority": 60,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- }
- ]
- },
- {
- "id": "2b5b0164-695d-406a-831d-de8e0864ba8b",
- "alias": "reset credentials",
- "description": "Reset credentials for a user if they forgot their password or something",
- "providerId": "basic-flow",
- "topLevel": true,
- "builtIn": true,
- "authenticationExecutions": [
- {
- "authenticator": "reset-credentials-choose-user",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 10,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticator": "reset-credential-email",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 20,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticator": "reset-password",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 30,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- },
- {
- "authenticatorFlow": true,
- "requirement": "CONDITIONAL",
- "priority": 40,
- "autheticatorFlow": true,
- "flowAlias": "Reset - Conditional OTP",
- "userSetupAllowed": false
- }
- ]
- },
- {
- "id": "b26ac227-95e1-4dea-9673-9cf6f72950d7",
- "alias": "saml ecp",
- "description": "SAML ECP Profile Authentication Flow",
- "providerId": "basic-flow",
- "topLevel": true,
- "builtIn": true,
- "authenticationExecutions": [
- {
- "authenticator": "http-basic-authenticator",
- "authenticatorFlow": false,
- "requirement": "REQUIRED",
- "priority": 10,
- "autheticatorFlow": false,
- "userSetupAllowed": false
- }
- ]
- }
- ],
- "authenticatorConfig": [
- {
- "id": "2c59769e-dc23-4813-8ab7-deaa7cde4de5",
- "alias": "create unique user config",
- "config": {
- "require.password.update.after.registration": "false"
- }
- },
- {
- "id": "49f039b3-3d77-43d1-9a32-706285b18e3d",
- "alias": "review profile config",
- "config": {
- "update.profile.on.first.login": "missing"
- }
- }
- ],
- "requiredActions": [
- {
- "alias": "CONFIGURE_TOTP",
- "name": "Configure OTP",
- "providerId": "CONFIGURE_TOTP",
- "enabled": true,
- "defaultAction": false,
- "priority": 10,
- "config": {}
- },
- {
- "alias": "TERMS_AND_CONDITIONS",
- "name": "Terms and Conditions",
- "providerId": "TERMS_AND_CONDITIONS",
- "enabled": false,
- "defaultAction": false,
- "priority": 20,
- "config": {}
- },
- {
- "alias": "UPDATE_PASSWORD",
- "name": "Update Password",
- "providerId": "UPDATE_PASSWORD",
- "enabled": true,
- "defaultAction": false,
- "priority": 30,
- "config": {}
- },
- {
- "alias": "UPDATE_PROFILE",
- "name": "Update Profile",
- "providerId": "UPDATE_PROFILE",
- "enabled": true,
- "defaultAction": false,
- "priority": 40,
- "config": {}
- },
- {
- "alias": "VERIFY_EMAIL",
- "name": "Verify Email",
- "providerId": "VERIFY_EMAIL",
- "enabled": true,
- "defaultAction": false,
- "priority": 50,
- "config": {}
- },
- {
- "alias": "delete_account",
- "name": "Delete Account",
- "providerId": "delete_account",
- "enabled": false,
- "defaultAction": false,
- "priority": 60,
- "config": {}
- },
- {
- "alias": "webauthn-register",
- "name": "Webauthn Register",
- "providerId": "webauthn-register",
- "enabled": true,
- "defaultAction": false,
- "priority": 70,
- "config": {}
- },
- {
- "alias": "webauthn-register-passwordless",
- "name": "Webauthn Register Passwordless",
- "providerId": "webauthn-register-passwordless",
- "enabled": true,
- "defaultAction": false,
- "priority": 80,
- "config": {}
- },
- {
- "alias": "update_user_locale",
- "name": "Update User Locale",
- "providerId": "update_user_locale",
- "enabled": true,
- "defaultAction": false,
- "priority": 1000,
- "config": {}
- }
- ],
- "browserFlow": "browser",
- "registrationFlow": "registration",
- "directGrantFlow": "direct grant",
- "resetCredentialsFlow": "reset credentials",
- "clientAuthenticationFlow": "clients",
- "dockerAuthenticationFlow": "docker auth",
- "attributes": {
- "cibaBackchannelTokenDeliveryMode": "poll",
- "cibaExpiresIn": "120",
- "cibaAuthRequestedUserHint": "login_hint",
- "oauth2DeviceCodeLifespan": "600",
- "oauth2DevicePollingInterval": "5",
- "parRequestUriLifespan": "60",
- "cibaInterval": "5",
- "realmReusableOtpCode": "false"
- },
- "keycloakVersion": "22.0.0",
- "userManagedAccessAllowed": false,
- "clientProfiles": {
- "profiles": []
- },
- "clientPolicies": {
- "policies": []
- }
-}
diff --git a/conf/keycloak_theme/keywind/.DS_Store b/conf/keycloak_theme/keywind/.DS_Store
deleted file mode 100644
index 694ba58ec..000000000
Binary files a/conf/keycloak_theme/keywind/.DS_Store and /dev/null differ
diff --git a/conf/keycloak_theme/keywind/login/assets/icons/arrow-top-right-on-square.ftl b/conf/keycloak_theme/keywind/login/assets/icons/arrow-top-right-on-square.ftl
deleted file mode 100644
index 81c4bf81d..000000000
--- a/conf/keycloak_theme/keywind/login/assets/icons/arrow-top-right-on-square.ftl
+++ /dev/null
@@ -1,7 +0,0 @@
-<#-- https://github.com/tailwindlabs/heroicons/blob/master/src/20/solid/arrow-top-right-on-square.svg -->
-<#macro kw>
-
-
-
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/icons/chevron-down.ftl b/conf/keycloak_theme/keywind/login/assets/icons/chevron-down.ftl
deleted file mode 100644
index 673ef1191..000000000
--- a/conf/keycloak_theme/keywind/login/assets/icons/chevron-down.ftl
+++ /dev/null
@@ -1,6 +0,0 @@
-<#-- https://github.com/tailwindlabs/heroicons/blob/master/src/20/solid/chevron-down.svg -->
-<#macro kw>
-
-
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/bitbucket.ftl b/conf/keycloak_theme/keywind/login/assets/providers/bitbucket.ftl
deleted file mode 100644
index 068bc73d8..000000000
--- a/conf/keycloak_theme/keywind/login/assets/providers/bitbucket.ftl
+++ /dev/null
@@ -1,14 +0,0 @@
-<#-- https://atlassian.design/resources/logo-library -->
-<#macro kw name="Bitbucket">
-
- ${name}
-
-
-
-
-
-
-
-
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/discord.ftl b/conf/keycloak_theme/keywind/login/assets/providers/discord.ftl
deleted file mode 100644
index 8ebecaa48..000000000
--- a/conf/keycloak_theme/keywind/login/assets/providers/discord.ftl
+++ /dev/null
@@ -1,7 +0,0 @@
-<#-- https://discord.com/branding -->
-<#macro kw name="Discord">
-
- ${name}
-
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/facebook.ftl b/conf/keycloak_theme/keywind/login/assets/providers/facebook.ftl
deleted file mode 100644
index bc692e759..000000000
--- a/conf/keycloak_theme/keywind/login/assets/providers/facebook.ftl
+++ /dev/null
@@ -1,8 +0,0 @@
-<#-- https://www.facebook.com/brand/resources/facebookapp/logo -->
-<#macro kw name="Facebook">
-
- ${name}
-
-
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/github.ftl b/conf/keycloak_theme/keywind/login/assets/providers/github.ftl
deleted file mode 100644
index 9523103c4..000000000
--- a/conf/keycloak_theme/keywind/login/assets/providers/github.ftl
+++ /dev/null
@@ -1,7 +0,0 @@
-<#-- https://github.com/logos -->
-<#macro kw name="GitHub">
-
- ${name}
-
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/gitlab.ftl b/conf/keycloak_theme/keywind/login/assets/providers/gitlab.ftl
deleted file mode 100644
index 4acfc132d..000000000
--- a/conf/keycloak_theme/keywind/login/assets/providers/gitlab.ftl
+++ /dev/null
@@ -1,10 +0,0 @@
-<#-- https://about.gitlab.com/press/press-kit -->
-<#macro kw name="GitLab">
-
- ${name}
-
-
-
-
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/google.ftl b/conf/keycloak_theme/keywind/login/assets/providers/google.ftl
deleted file mode 100644
index b536cdbb8..000000000
--- a/conf/keycloak_theme/keywind/login/assets/providers/google.ftl
+++ /dev/null
@@ -1,10 +0,0 @@
-<#-- https://developers.google.com/identity/branding-guidelines -->
-<#macro kw name="Google">
-
- ${name}
-
-
-
-
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/instagram.ftl b/conf/keycloak_theme/keywind/login/assets/providers/instagram.ftl
deleted file mode 100644
index c4996d880..000000000
--- a/conf/keycloak_theme/keywind/login/assets/providers/instagram.ftl
+++ /dev/null
@@ -1,35 +0,0 @@
-<#-- https://www.facebook.com/brand/resources/instagram/instagram-brand -->
-<#macro kw name="Instagram">
-
- ${name}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/linkedin.ftl b/conf/keycloak_theme/keywind/login/assets/providers/linkedin.ftl
deleted file mode 100644
index 944d14335..000000000
--- a/conf/keycloak_theme/keywind/login/assets/providers/linkedin.ftl
+++ /dev/null
@@ -1,7 +0,0 @@
-<#-- https://brand.linkedin.com/downloads -->
-<#macro kw name="LinkedIn">
-
- ${name}
-
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/microsoft.ftl b/conf/keycloak_theme/keywind/login/assets/providers/microsoft.ftl
deleted file mode 100644
index 408635b84..000000000
--- a/conf/keycloak_theme/keywind/login/assets/providers/microsoft.ftl
+++ /dev/null
@@ -1,10 +0,0 @@
-<#-- https://learn.microsoft.com/azure/active-directory/develop/howto-add-branding-in-azure-ad-apps -->
-<#macro kw name="Microsoft">
-
- ${name}
-
-
-
-
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/oidc.ftl b/conf/keycloak_theme/keywind/login/assets/providers/oidc.ftl
deleted file mode 100644
index f7954ff5e..000000000
--- a/conf/keycloak_theme/keywind/login/assets/providers/oidc.ftl
+++ /dev/null
@@ -1,9 +0,0 @@
-<#-- https://openid.net/add-openid/logos -->
-<#macro kw name="OpenID">
-
- ${name}
-
-
-
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/openshift.ftl b/conf/keycloak_theme/keywind/login/assets/providers/openshift.ftl
deleted file mode 100644
index e85ddef7e..000000000
--- a/conf/keycloak_theme/keywind/login/assets/providers/openshift.ftl
+++ /dev/null
@@ -1,11 +0,0 @@
-<#-- https://www.redhat.com/technologies/cloud-computing/openshift -->
-<#macro kw name="Red Hat OpenShift">
-
- ${name}
-
-
-
-
-
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/paypal.ftl b/conf/keycloak_theme/keywind/login/assets/providers/paypal.ftl
deleted file mode 100644
index 7946e03cd..000000000
--- a/conf/keycloak_theme/keywind/login/assets/providers/paypal.ftl
+++ /dev/null
@@ -1,9 +0,0 @@
-<#-- https://www.paypal.com -->
-<#macro kw name="PayPal">
-
- ${name}
-
-
-
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/providers.ftl b/conf/keycloak_theme/keywind/login/assets/providers/providers.ftl
deleted file mode 100644
index b9c55f6c8..000000000
--- a/conf/keycloak_theme/keywind/login/assets/providers/providers.ftl
+++ /dev/null
@@ -1,79 +0,0 @@
-<#import "./bitbucket.ftl" as bitbucketIcon>
-<#import "./discord.ftl" as discordIcon>
-<#import "./facebook.ftl" as facebookIcon>
-<#import "./github.ftl" as githubIcon>
-<#import "./gitlab.ftl" as gitlabIcon>
-<#import "./google.ftl" as googleIcon>
-<#import "./instagram.ftl" as instagramIcon>
-<#import "./linkedin.ftl" as linkedinIcon>
-<#import "./microsoft.ftl" as microsoftIcon>
-<#import "./oidc.ftl" as oidcIcon>
-<#import "./openshift.ftl" as openshiftIcon>
-<#import "./paypal.ftl" as paypalIcon>
-<#import "./slack.ftl" as slackIcon>
-<#import "./stackoverflow.ftl" as stackoverflowIcon>
-<#import "./twitter.ftl" as twitterIcon>
-
-<#macro bitbucket>
- <@bitbucketIcon.kw />
-#macro>
-
-<#macro discord>
- <@discordIcon.kw />
-#macro>
-
-<#macro facebook>
- <@facebookIcon.kw />
-#macro>
-
-<#macro github>
- <@githubIcon.kw />
-#macro>
-
-<#macro gitlab>
- <@gitlabIcon.kw />
-#macro>
-
-<#macro google>
- <@googleIcon.kw />
-#macro>
-
-<#macro instagram>
- <@instagramIcon.kw />
-#macro>
-
-<#macro linkedin>
- <@linkedinIcon.kw />
-#macro>
-
-<#macro microsoft>
- <@microsoftIcon.kw />
-#macro>
-
-<#macro oidc>
- <@oidcIcon.kw />
-#macro>
-
-<#macro "openshift-v3">
- <@openshiftIcon.kw />
-#macro>
-
-<#macro "openshift-v4">
- <@openshiftIcon.kw />
-#macro>
-
-<#macro paypal>
- <@paypalIcon.kw />
-#macro>
-
-<#macro slack>
- <@slackIcon.kw />
-#macro>
-
-<#macro stackoverflow>
- <@stackoverflowIcon.kw />
-#macro>
-
-<#macro twitter>
- <@twitterIcon.kw />
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/slack.ftl b/conf/keycloak_theme/keywind/login/assets/providers/slack.ftl
deleted file mode 100644
index d4dffe3bb..000000000
--- a/conf/keycloak_theme/keywind/login/assets/providers/slack.ftl
+++ /dev/null
@@ -1,14 +0,0 @@
-<#-- https://slack.com/media-kit -->
-<#macro kw name="Slack">
-
- ${name}
-
-
-
-
-
-
-
-
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/stackoverflow.ftl b/conf/keycloak_theme/keywind/login/assets/providers/stackoverflow.ftl
deleted file mode 100644
index 1ffad8d6a..000000000
--- a/conf/keycloak_theme/keywind/login/assets/providers/stackoverflow.ftl
+++ /dev/null
@@ -1,8 +0,0 @@
-<#-- https://stackoverflow.design/brand/logo -->
-<#macro kw name="Stack Overflow">
-
- ${name}
-
-
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/twitter.ftl b/conf/keycloak_theme/keywind/login/assets/providers/twitter.ftl
deleted file mode 100644
index 2bc7e7e49..000000000
--- a/conf/keycloak_theme/keywind/login/assets/providers/twitter.ftl
+++ /dev/null
@@ -1,7 +0,0 @@
-<#-- https://about.twitter.com/en/who-we-are/brand-toolkit -->
-<#macro kw name="Twitter">
-
- ${name}
-
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/alert.ftl b/conf/keycloak_theme/keywind/login/components/atoms/alert.ftl
deleted file mode 100644
index 58e8309f0..000000000
--- a/conf/keycloak_theme/keywind/login/components/atoms/alert.ftl
+++ /dev/null
@@ -1,22 +0,0 @@
-<#macro kw color="">
- <#switch color>
- <#case "error">
- <#assign colorClass="bg-red-100 text-red-600">
- <#break>
- <#case "info">
- <#assign colorClass="bg-blue-100 text-blue-600">
- <#break>
- <#case "success">
- <#assign colorClass="bg-green-100 text-green-600">
- <#break>
- <#case "warning">
- <#assign colorClass="bg-orange-100 text-orange-600">
- <#break>
- <#default>
- <#assign colorClass="bg-blue-100 text-blue-600">
- #switch>
-
-
- <#nested>
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/body.ftl b/conf/keycloak_theme/keywind/login/components/atoms/body.ftl
deleted file mode 100644
index dcc94a06e..000000000
--- a/conf/keycloak_theme/keywind/login/components/atoms/body.ftl
+++ /dev/null
@@ -1,5 +0,0 @@
-<#macro kw>
-
- <#nested>
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/button-group.ftl b/conf/keycloak_theme/keywind/login/components/atoms/button-group.ftl
deleted file mode 100644
index 459120917..000000000
--- a/conf/keycloak_theme/keywind/login/components/atoms/button-group.ftl
+++ /dev/null
@@ -1,5 +0,0 @@
-<#macro kw>
-
- <#nested>
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/button.ftl b/conf/keycloak_theme/keywind/login/components/atoms/button.ftl
deleted file mode 100644
index eeb0af7b7..000000000
--- a/conf/keycloak_theme/keywind/login/components/atoms/button.ftl
+++ /dev/null
@@ -1,33 +0,0 @@
-<#macro kw color="" component="button" size="" rest...>
- <#switch color>
- <#case "primary">
- <#assign colorClass="bg-primary-600 text-white focus:ring-primary-600 hover:bg-primary-700">
- <#break>
- <#case "secondary">
- <#assign colorClass="bg-secondary-100 text-secondary-600 focus:ring-secondary-600 hover:bg-secondary-200 hover:text-secondary-900">
- <#break>
- <#default>
- <#assign colorClass="bg-primary-600 text-white focus:ring-primary-600 hover:bg-primary-700">
- #switch>
-
- <#switch size>
- <#case "medium">
- <#assign sizeClass="px-4 py-2 text-sm">
- <#break>
- <#case "small">
- <#assign sizeClass="px-2 py-1 text-xs">
- <#break>
- <#default>
- <#assign sizeClass="px-4 py-2 text-sm">
- #switch>
-
- <${component}
- class="${colorClass} ${sizeClass} flex justify-center relative rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-offset-2"
-
- <#list rest as attrName, attrValue>
- ${attrName}="${attrValue}"
- #list>
- >
- <#nested>
- ${component}>
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/card.ftl b/conf/keycloak_theme/keywind/login/components/atoms/card.ftl
deleted file mode 100644
index c1e808df1..000000000
--- a/conf/keycloak_theme/keywind/login/components/atoms/card.ftl
+++ /dev/null
@@ -1,19 +0,0 @@
-<#macro kw content="" footer="" header="">
-
- <#if header?has_content>
-
- ${header}
-
- #if>
- <#if content?has_content>
-
- ${content}
-
- #if>
- <#if footer?has_content>
-
- ${footer}
-
- #if>
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/checkbox.ftl b/conf/keycloak_theme/keywind/login/components/atoms/checkbox.ftl
deleted file mode 100644
index e47fd619a..000000000
--- a/conf/keycloak_theme/keywind/login/components/atoms/checkbox.ftl
+++ /dev/null
@@ -1,19 +0,0 @@
-<#macro kw checked=false label="" name="" rest...>
-
- checked#if>
-
- class="border-secondary-200 h-4 rounded text-primary-600 w-4 focus:ring-primary-200 focus:ring-opacity-50"
- id="${name}"
- name="${name}"
- type="checkbox"
-
- <#list rest as attrName, attrValue>
- ${attrName}="${attrValue}"
- #list>
- >
-
- ${label}
-
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/container.ftl b/conf/keycloak_theme/keywind/login/components/atoms/container.ftl
deleted file mode 100644
index 34ead183c..000000000
--- a/conf/keycloak_theme/keywind/login/components/atoms/container.ftl
+++ /dev/null
@@ -1,5 +0,0 @@
-<#macro kw>
-
- <#nested>
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/form.ftl b/conf/keycloak_theme/keywind/login/components/atoms/form.ftl
deleted file mode 100644
index 014bb4f1c..000000000
--- a/conf/keycloak_theme/keywind/login/components/atoms/form.ftl
+++ /dev/null
@@ -1,11 +0,0 @@
-<#macro kw rest...>
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/heading.ftl b/conf/keycloak_theme/keywind/login/components/atoms/heading.ftl
deleted file mode 100644
index 7665c0196..000000000
--- a/conf/keycloak_theme/keywind/login/components/atoms/heading.ftl
+++ /dev/null
@@ -1,5 +0,0 @@
-<#macro kw>
-
- <#nested>
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/input.ftl b/conf/keycloak_theme/keywind/login/components/atoms/input.ftl
deleted file mode 100644
index 01e2897ad..000000000
--- a/conf/keycloak_theme/keywind/login/components/atoms/input.ftl
+++ /dev/null
@@ -1,37 +0,0 @@
-<#macro
- kw
- autofocus=false
- disabled=false
- invalid=false
- label=""
- message=""
- name=""
- required=true
- rest...
->
-
-
- ${label}
-
-
autofocus#if>
- <#if disabled>disabled#if>
- <#if required>required#if>
-
- aria-invalid="${invalid?c}"
- class="block border-secondary-200 mt-1 rounded-md w-full focus:border-primary-300 focus:ring focus:ring-primary-200 focus:ring-opacity-50 sm:text-sm"
- id="${name}"
- name="${name}"
- placeholder="${label}"
-
- <#list rest as attrName, attrValue>
- ${attrName}="${attrValue}"
- #list>
- >
- <#if invalid?? && message??>
-
- ${message?no_esc}
-
- #if>
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/link.ftl b/conf/keycloak_theme/keywind/login/components/atoms/link.ftl
deleted file mode 100644
index bde766653..000000000
--- a/conf/keycloak_theme/keywind/login/components/atoms/link.ftl
+++ /dev/null
@@ -1,30 +0,0 @@
-<#macro kw color="" component="a" size="" rest...>
- <#switch color>
- <#case "primary">
- <#assign colorClass="text-primary-600 hover:text-primary-500">
- <#break>
- <#case "secondary">
- <#assign colorClass="text-secondary-600 hover:text-secondary-900">
- <#break>
- <#default>
- <#assign colorClass="text-primary-600 hover:text-primary-500">
- #switch>
-
- <#switch size>
- <#case "small">
- <#assign sizeClass="text-sm">
- <#break>
- <#default>
- <#assign sizeClass="">
- #switch>
-
- <${component}
- class="<#compress>${colorClass} ${sizeClass} inline-flex#compress>"
-
- <#list rest as attrName, attrValue>
- ${attrName}="${attrValue}"
- #list>
- >
- <#nested>
- ${component}>
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/logo.ftl b/conf/keycloak_theme/keywind/login/components/atoms/logo.ftl
deleted file mode 100644
index f166403e6..000000000
--- a/conf/keycloak_theme/keywind/login/components/atoms/logo.ftl
+++ /dev/null
@@ -1,5 +0,0 @@
-<#macro kw>
-
- <#nested>
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/nav.ftl b/conf/keycloak_theme/keywind/login/components/atoms/nav.ftl
deleted file mode 100644
index 81a4abf63..000000000
--- a/conf/keycloak_theme/keywind/login/components/atoms/nav.ftl
+++ /dev/null
@@ -1,5 +0,0 @@
-<#macro kw>
-
- <#nested>
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/radio.ftl b/conf/keycloak_theme/keywind/login/components/atoms/radio.ftl
deleted file mode 100644
index 5596d5c4b..000000000
--- a/conf/keycloak_theme/keywind/login/components/atoms/radio.ftl
+++ /dev/null
@@ -1,18 +0,0 @@
-<#macro kw checked=false id="" label="" rest...>
-
- checked#if>
-
- class="border-secondary-200 focus:ring-primary-600"
- id="${id}"
- type="radio"
-
- <#list rest as attrName, attrValue>
- ${attrName}="${attrValue}"
- #list>
- >
-
- ${label}
-
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/molecules/identity-provider.ftl b/conf/keycloak_theme/keywind/login/components/molecules/identity-provider.ftl
deleted file mode 100644
index 50c9c81c7..000000000
--- a/conf/keycloak_theme/keywind/login/components/molecules/identity-provider.ftl
+++ /dev/null
@@ -1,78 +0,0 @@
-<#import "/assets/providers/providers.ftl" as providerIcons>
-
-<#macro kw providers=[]>
-
- ${msg("identity-provider-login-label")}
-
-
- <#list providers as provider>
- <#switch provider.alias>
- <#case "bitbucket">
- <#assign colorClass="hover:bg-provider-bitbucket/10">
- <#break>
- <#case "discord">
- <#assign colorClass="hover:bg-provider-discord/10">
- <#break>
- <#case "facebook">
- <#assign colorClass="hover:bg-provider-facebook/10">
- <#break>
- <#case "github">
- <#assign colorClass="hover:bg-provider-github/10">
- <#break>
- <#case "gitlab">
- <#assign colorClass="hover:bg-provider-gitlab/10">
- <#break>
- <#case "google">
- <#assign colorClass="hover:bg-provider-google/10">
- <#break>
- <#case "instagram">
- <#assign colorClass="hover:bg-provider-instagram/10">
- <#break>
- <#case "linkedin">
- <#assign colorClass="hover:bg-provider-linkedin/10">
- <#break>
- <#case "microsoft">
- <#assign colorClass="hover:bg-provider-microsoft/10">
- <#break>
- <#case "oidc">
- <#assign colorClass="hover:bg-provider-oidc/10">
- <#break>
- <#case "openshift-v3">
- <#assign colorClass="hover:bg-provider-openshift/10">
- <#break>
- <#case "openshift-v4">
- <#assign colorClass="hover:bg-provider-openshift/10">
- <#break>
- <#case "paypal">
- <#assign colorClass="hover:bg-provider-paypal/10">
- <#break>
- <#case "slack">
- <#assign colorClass="hover:bg-provider-slack/10">
- <#break>
- <#case "stackoverflow">
- <#assign colorClass="hover:bg-provider-stackoverflow/10">
- <#break>
- <#case "twitter">
- <#assign colorClass="hover:bg-provider-twitter/10">
- <#break>
- <#default>
- <#assign colorClass="hover:bg-secondary-100">
- #switch>
-
-
- <#if providerIcons[provider.alias]??>
-
- <@providerIcons[provider.alias] />
-
- <#else>
- ${provider.displayName!}
- #if>
-
- #list>
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/molecules/locale-provider.ftl b/conf/keycloak_theme/keywind/login/components/molecules/locale-provider.ftl
deleted file mode 100644
index 198e5be10..000000000
--- a/conf/keycloak_theme/keywind/login/components/molecules/locale-provider.ftl
+++ /dev/null
@@ -1,29 +0,0 @@
-<#import "/assets/icons/chevron-down.ftl" as icon>
-<#import "/components/atoms/link.ftl" as link>
-
-<#macro kw currentLocale="" locales=[]>
-
- <@link.kw @click="open = true" color="secondary" component="button" type="button">
-
- ${currentLocale}
- <@icon.kw />
-
- @link.kw>
-
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/molecules/username.ftl b/conf/keycloak_theme/keywind/login/components/molecules/username.ftl
deleted file mode 100644
index ba6339389..000000000
--- a/conf/keycloak_theme/keywind/login/components/molecules/username.ftl
+++ /dev/null
@@ -1,15 +0,0 @@
-<#import "/assets/icons/arrow-top-right-on-square.ftl" as icon>
-<#import "/components/atoms/link.ftl" as link>
-
-<#macro kw linkHref="" linkTitle="" name="">
-
- ${name}
- <@link.kw
- color="primary"
- href=linkHref
- title=linkTitle
- >
- <@icon.kw />
- @link.kw>
-
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/document.ftl b/conf/keycloak_theme/keywind/login/document.ftl
deleted file mode 100644
index 188e16a31..000000000
--- a/conf/keycloak_theme/keywind/login/document.ftl
+++ /dev/null
@@ -1,35 +0,0 @@
-<#macro kw script="">
- ${msg("loginTitle", (realm.displayName!""))}
-
-
-
-
-
- <#if properties.meta?has_content>
- <#list properties.meta?split(" ") as meta>
-
- #list>
- #if>
-
- <#if properties.favicons?has_content>
- <#list properties.favicons?split(" ") as favicon>
-
- #list>
- #if>
-
- <#if properties.styles?has_content>
- <#list properties.styles?split(" ") as style>
-
- #list>
- #if>
-
- <#if script?has_content>
-
- #if>
-
- <#if properties.scripts?has_content>
- <#list properties.scripts?split(" ") as script>
-
- #list>
- #if>
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/error.ftl b/conf/keycloak_theme/keywind/login/error.ftl
deleted file mode 100644
index 52af9c1e6..000000000
--- a/conf/keycloak_theme/keywind/login/error.ftl
+++ /dev/null
@@ -1,18 +0,0 @@
-<#import "template.ftl" as layout>
-<#import "components/atoms/alert.ftl" as alert>
-<#import "components/atoms/link.ftl" as link>
-
-<@layout.registrationLayout displayMessage=false; section>
- <#if section="header">
- ${kcSanitize(msg("errorTitle"))?no_esc}
- <#elseif section="form">
- <@alert.kw color="error">${kcSanitize(message.summary)?no_esc}@alert.kw>
- <#if !skipLink??>
- <#if client?? && client.baseUrl?has_content>
- <@link.kw color="secondary" href=client.baseUrl size="small">
- ${kcSanitize(msg("backToApplication"))?no_esc}
- @link.kw>
- #if>
- #if>
- #if>
-@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/features/labels/totp-device.ftl b/conf/keycloak_theme/keywind/login/features/labels/totp-device.ftl
deleted file mode 100644
index 98ae12f8d..000000000
--- a/conf/keycloak_theme/keywind/login/features/labels/totp-device.ftl
+++ /dev/null
@@ -1,5 +0,0 @@
-<#macro kw>
- <#compress>
- ${msg("loginTotpDeviceName")} <#if totp.otpCredentials?size gte 1>*#if>
- #compress>
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/features/labels/totp.ftl b/conf/keycloak_theme/keywind/login/features/labels/totp.ftl
deleted file mode 100644
index be5158ebe..000000000
--- a/conf/keycloak_theme/keywind/login/features/labels/totp.ftl
+++ /dev/null
@@ -1,5 +0,0 @@
-<#macro kw>
- <#compress>
- ${msg("authenticatorCode")} *
- #compress>
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/features/labels/username.ftl b/conf/keycloak_theme/keywind/login/features/labels/username.ftl
deleted file mode 100644
index 6c01d6b34..000000000
--- a/conf/keycloak_theme/keywind/login/features/labels/username.ftl
+++ /dev/null
@@ -1,11 +0,0 @@
-<#macro kw>
- <#compress>
- <#if !realm.loginWithEmailAllowed>
- ${msg("username")}
- <#elseif !realm.registrationEmailAsUsername>
- ${msg("usernameOrEmail")}
- <#else>
- ${msg("email")}
- #if>
- #compress>
-#macro>
diff --git a/conf/keycloak_theme/keywind/login/login-config-totp.ftl b/conf/keycloak_theme/keywind/login/login-config-totp.ftl
deleted file mode 100644
index e0b64c634..000000000
--- a/conf/keycloak_theme/keywind/login/login-config-totp.ftl
+++ /dev/null
@@ -1,110 +0,0 @@
-<#import "template.ftl" as layout>
-<#import "components/atoms/button.ftl" as button>
-<#import "components/atoms/button-group.ftl" as buttonGroup>
-<#import "components/atoms/form.ftl" as form>
-<#import "components/atoms/input.ftl" as input>
-<#import "components/atoms/link.ftl" as link>
-<#import "features/labels/totp.ftl" as totpLabel>
-<#import "features/labels/totp-device.ftl" as totpDeviceLabel>
-
-<#assign totpLabel><@totpLabel.kw />#assign>
-<#assign totpDeviceLabel><@totpDeviceLabel.kw />#assign>
-
-<@layout.registrationLayout
- displayMessage=!messagesPerField.existsError("totp", "userLabel")
- displayRequiredFields=false
- ;
- section
->
- <#if section="header">
- ${msg("loginTotpTitle")}
- <#elseif section="form">
-
-
- ${msg("loginTotpStep1")}
-
- <#list totp.supportedApplications as app>
- ${msg(app)}
- #list>
-
-
- <#if mode?? && mode="manual">
-
- ${msg("loginTotpManualStep2")}
- ${totp.totpSecretEncoded}
-
-
- <@link.kw color="primary" href=totp.qrUrl>
- ${msg("loginTotpScanBarcode")}
- @link.kw>
-
-
- ${msg("loginTotpManualStep3")}
-
- ${msg("loginTotpType")}: ${msg("loginTotp." + totp.policy.type)}
- ${msg("loginTotpAlgorithm")}: ${totp.policy.getAlgorithmKey()}
- ${msg("loginTotpDigits")}: ${totp.policy.digits}
- <#if totp.policy.type="totp">
- ${msg("loginTotpInterval")}: ${totp.policy.period}
- <#elseif totp.policy.type="hotp">
- ${msg("loginTotpCounter")}: ${totp.policy.initialCounter}
- #if>
-
-
- <#else>
-
- ${msg("loginTotpStep2")}
-
- <@link.kw color="primary" href=totp.manualUrl>
- ${msg("loginTotpUnableToScan")}
- @link.kw>
-
- #if>
- ${msg("loginTotpStep3")}
- ${msg("loginTotpStep3DeviceName")}
-
- <@form.kw action=url.loginAction method="post">
-
- <#if mode??>
-
- #if>
- <@input.kw
- autocomplete="off"
- autofocus=true
- invalid=messagesPerField.existsError("totp")
- label=totpLabel
- message=kcSanitize(messagesPerField.get("totp"))
- name="totp"
- required=false
- type="text"
- />
- <@input.kw
- autocomplete="off"
- invalid=messagesPerField.existsError("userLabel")
- label=totpDeviceLabel
- message=kcSanitize(messagesPerField.get("userLabel"))
- name="userLabel"
- required=false
- type="text"
- />
- <@buttonGroup.kw>
- <#if isAppInitiatedAction??>
- <@button.kw color="primary" type="submit">
- ${msg("doSubmit")}
- @button.kw>
- <@button.kw color="secondary" name="cancel-aia" type="submit" value="true">
- ${msg("doCancel")}
- @button.kw>
- <#else>
- <@button.kw color="primary" type="submit">
- ${msg("doSubmit")}
- @button.kw>
- #if>
- @buttonGroup.kw>
- @form.kw>
- #if>
-@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login-idp-link-confirm.ftl b/conf/keycloak_theme/keywind/login/login-idp-link-confirm.ftl
deleted file mode 100644
index 9a2554d51..000000000
--- a/conf/keycloak_theme/keywind/login/login-idp-link-confirm.ftl
+++ /dev/null
@@ -1,18 +0,0 @@
-<#import "template.ftl" as layout>
-<#import "components/atoms/button.ftl" as button>
-<#import "components/atoms/form.ftl" as form>
-
-<@layout.registrationLayout; section>
- <#if section="header">
- ${msg("confirmLinkIdpTitle")}
- <#elseif section="form">
- <@form.kw action=url.loginAction method="post">
- <@button.kw color="primary" name="submitAction" type="submit" value="updateProfile">
- ${msg("confirmLinkIdpReviewProfile")}
- @button.kw>
- <@button.kw color="primary" name="submitAction" type="submit" value="linkAccount">
- ${msg("confirmLinkIdpContinue", idpDisplayName)}
- @button.kw>
- @form.kw>
- #if>
-@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login-oauth-grant.ftl b/conf/keycloak_theme/keywind/login/login-oauth-grant.ftl
deleted file mode 100644
index aa4173cbf..000000000
--- a/conf/keycloak_theme/keywind/login/login-oauth-grant.ftl
+++ /dev/null
@@ -1,62 +0,0 @@
-<#import "template.ftl" as layout>
-<#import "components/atoms/button.ftl" as button>
-<#import "components/atoms/button-group.ftl" as buttonGroup>
-<#import "components/atoms/form.ftl" as form>
-
-<@layout.registrationLayout; section>
- <#if section="header">
- <#if client.attributes.logoUri??>
-
- #if>
-
- <#if client.name?has_content>
- ${msg("oauthGrantTitle", advancedMsg(client.name))}
- <#else>
- ${msg("oauthGrantTitle", client.clientId)}
- #if>
-
- <#elseif section="form">
- ${msg("oauthGrantRequest")}
-
- <#if oauth.clientScopesRequested??>
- <#list oauth.clientScopesRequested as clientScope>
-
- <#if !clientScope.dynamicScopeParameter??>
- ${advancedMsg(clientScope.consentScreenText)}
- <#else>
- ${advancedMsg(clientScope.consentScreenText)}: ${clientScope.dynamicScopeParameter}
- #if>
-
- #list>
- #if>
-
- <#if client.attributes.policyUri?? || client.attributes.tosUri??>
-
- <#if client.name?has_content>
- ${msg("oauthGrantInformation",advancedMsg(client.name))}
- <#else>
- ${msg("oauthGrantInformation",client.clientId)}
- #if>
- <#if client.attributes.tosUri??>
- ${msg("oauthGrantReview")}
- ${msg("oauthGrantTos")}
- #if>
- <#if client.attributes.policyUri??>
- ${msg("oauthGrantReview")}
- ${msg("oauthGrantPolicy")}
- #if>
-
- #if>
- <@form.kw action=url.oauthAction method="post">
-
- <@buttonGroup.kw>
- <@button.kw color="primary" name="accept" type="submit">
- ${msg("doYes")}
- @button.kw>
- <@button.kw color="secondary" name="cancel" type="submit">
- ${msg("doNo")}
- @button.kw>
- @buttonGroup.kw>
- @form.kw>
- #if>
-@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login-otp.ftl b/conf/keycloak_theme/keywind/login/login-otp.ftl
deleted file mode 100644
index b1bb3b975..000000000
--- a/conf/keycloak_theme/keywind/login/login-otp.ftl
+++ /dev/null
@@ -1,50 +0,0 @@
-<#import "template.ftl" as layout>
-<#import "components/atoms/button.ftl" as button>
-<#import "components/atoms/button-group.ftl" as buttonGroup>
-<#import "components/atoms/form.ftl" as form>
-<#import "components/atoms/input.ftl" as input>
-<#import "components/atoms/radio.ftl" as radio>
-<#import "features/labels/totp.ftl" as totpLabel>
-
-<#assign totpLabel><@totpLabel.kw />#assign>
-
-<@layout.registrationLayout
- displayMessage=!messagesPerField.existsError("totp")
- ;
- section
->
- <#if section="header">
- ${msg("doLogIn")}
- <#elseif section="form">
- <@form.kw action=url.loginAction method="post">
- <#if otpLogin.userOtpCredentials?size gt 1>
-
- <#list otpLogin.userOtpCredentials as otpCredential>
- <@radio.kw
- checked=(otpCredential.id == otpLogin.selectedCredentialId)
- id="kw-otp-credential-${otpCredential?index}"
- label=otpCredential.userLabel
- name="selectedCredentialId"
- tabindex=otpCredential?index
- value=otpCredential.id
- />
- #list>
-
- #if>
- <@input.kw
- autocomplete="off"
- autofocus=true
- invalid=messagesPerField.existsError("totp")
- label=totpLabel
- message=kcSanitize(messagesPerField.get("totp"))
- name="otp"
- type="text"
- />
- <@buttonGroup.kw>
- <@button.kw color="primary" name="submitAction" type="submit">
- ${msg("doLogIn")}
- @button.kw>
- @buttonGroup.kw>
- @form.kw>
- #if>
-@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login-page-expired.ftl b/conf/keycloak_theme/keywind/login/login-page-expired.ftl
deleted file mode 100644
index 2b6288d94..000000000
--- a/conf/keycloak_theme/keywind/login/login-page-expired.ftl
+++ /dev/null
@@ -1,18 +0,0 @@
-<#import "template.ftl" as layout>
-<#import "components/atoms/button.ftl" as button>
-<#import "components/atoms/button-group.ftl" as buttonGroup>
-
-<@layout.registrationLayout; section>
- <#if section="header">
- ${msg("pageExpiredTitle")}
- <#elseif section="form">
- <@buttonGroup.kw>
- <@button.kw color="primary" component="a" href=url.loginRestartFlowUrl>
- ${msg("doTryAgain")}
- @button.kw>
- <@button.kw color="secondary" component="a" href=url.loginAction>
- ${msg("doContinue")}
- @button.kw>
- @buttonGroup.kw>
- #if>
-@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login-password.ftl b/conf/keycloak_theme/keywind/login/login-password.ftl
deleted file mode 100644
index 54e7d9dca..000000000
--- a/conf/keycloak_theme/keywind/login/login-password.ftl
+++ /dev/null
@@ -1,39 +0,0 @@
-<#import "template.ftl" as layout>
-<#import "components/atoms/button.ftl" as button>
-<#import "components/atoms/button-group.ftl" as buttonGroup>
-<#import "components/atoms/form.ftl" as form>
-<#import "components/atoms/input.ftl" as input>
-<#import "components/atoms/link.ftl" as link>
-
-<@layout.registrationLayout displayMessage=!messagesPerField.existsError("password"); section>
- <#if section="header">
- ${msg("doLogIn")}
- <#elseif section="form">
- <@form.kw
- action=url.loginAction
- method="post"
- onsubmit="login.disabled = true; return true;"
- >
- <@input.kw
- autofocus=true
- invalid=messagesPerField.existsError("password")
- label=msg("password")
- message=kcSanitize(messagesPerField.get("password"))?no_esc
- name="password"
- type="password"
- />
- <#if realm.resetPasswordAllowed>
-
- <@link.kw color="primary" href=url.loginResetCredentialsUrl size="small">
- ${msg("doForgotPassword")}
- @link.kw>
-
- #if>
- <@buttonGroup.kw>
- <@button.kw color="primary" name="login" type="submit">
- ${msg("doLogIn")}
- @button.kw>
- @buttonGroup.kw>
- @form.kw>
- #if>
-@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login-recovery-authn-code-config.ftl b/conf/keycloak_theme/keywind/login/login-recovery-authn-code-config.ftl
deleted file mode 100644
index 186d71080..000000000
--- a/conf/keycloak_theme/keywind/login/login-recovery-authn-code-config.ftl
+++ /dev/null
@@ -1,91 +0,0 @@
-<#import "template.ftl" as layout>
-<#import "components/atoms/alert.ftl" as alert>
-<#import "components/atoms/button.ftl" as button>
-<#import "components/atoms/button-group.ftl" as buttonGroup>
-<#import "components/atoms/checkbox.ftl" as checkbox>
-<#import "components/atoms/form.ftl" as form>
-
-<@layout.registrationLayout script="dist/recoveryCodes.js"; section>
- <#if section="header">
- ${msg("recovery-code-config-header")}
- <#elseif section="form">
-
- <@alert.kw color="warning">
-
-
${msg("recovery-code-config-warning-title")}
-
${msg("recovery-code-config-warning-message")}
-
- @alert.kw>
-
- <#list recoveryAuthnCodesConfigBean.generatedRecoveryAuthnCodesList as code>
- ${code[0..3]}-${code[4..7]}-${code[8..]}
- #list>
-
-
- <@button.kw @click="print" color="secondary" size="small" type="button">
- ${msg("recovery-codes-print")}
- @button.kw>
- <@button.kw @click="download" color="secondary" size="small" type="button">
- ${msg("recovery-codes-download")}
- @button.kw>
- <@button.kw @click="copy" color="secondary" size="small" type="button">
- ${msg("recovery-codes-copy")}
- @button.kw>
-
- <@form.kw action=url.loginAction method="post">
-
-
-
- <@checkbox.kw
- label=msg("recovery-codes-confirmation-message")
- name="kcRecoveryCodesConfirmationCheck"
- required="required"
- x\-ref="confirmationCheck"
- />
- <@buttonGroup.kw>
- <#if isAppInitiatedAction??>
- <@button.kw color="primary" type="submit">
- ${msg("recovery-codes-action-complete")}
- @button.kw>
- <@button.kw
- @click="$refs.confirmationCheck.required = false"
- color="secondary"
- name="cancel-aia"
- type="submit"
- value="true"
- >
- ${msg("recovery-codes-action-cancel")}
- @button.kw>
- <#else>
- <@button.kw color="primary" type="submit">
- ${msg("recovery-codes-action-complete")}
- @button.kw>
- #if>
- @buttonGroup.kw>
- @form.kw>
-
- #if>
-@layout.registrationLayout>
-
-
diff --git a/conf/keycloak_theme/keywind/login/login-recovery-authn-code-input.ftl b/conf/keycloak_theme/keywind/login/login-recovery-authn-code-input.ftl
deleted file mode 100644
index a46bcfa09..000000000
--- a/conf/keycloak_theme/keywind/login/login-recovery-authn-code-input.ftl
+++ /dev/null
@@ -1,26 +0,0 @@
-<#import "template.ftl" as layout>
-<#import "components/atoms/button.ftl" as button>
-<#import "components/atoms/button-group.ftl" as buttonGroup>
-<#import "components/atoms/form.ftl" as form>
-<#import "components/atoms/input.ftl" as input>
-
-<@layout.registrationLayout; section>
- <#if section="header">
- ${msg("auth-recovery-code-header")}
- <#elseif section="form">
- <@form.kw action=url.loginAction method="post">
- <@input.kw
- autocomplete="off"
- autofocus=true
- label=msg("auth-recovery-code-prompt", recoveryAuthnCodesInputBean.codeNumber?c)
- name="recoveryCodeInput"
- type="text"
- />
- <@buttonGroup.kw>
- <@button.kw color="primary" name="login" type="submit">
- ${msg("doLogIn")}
- @button.kw>
- @buttonGroup.kw>
- @form.kw>
- #if>
-@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login-reset-password.ftl b/conf/keycloak_theme/keywind/login/login-reset-password.ftl
deleted file mode 100644
index b0516aae8..000000000
--- a/conf/keycloak_theme/keywind/login/login-reset-password.ftl
+++ /dev/null
@@ -1,44 +0,0 @@
-<#import "template.ftl" as layout>
-<#import "components/atoms/button.ftl" as button>
-<#import "components/atoms/button-group.ftl" as buttonGroup>
-<#import "components/atoms/form.ftl" as form>
-<#import "components/atoms/input.ftl" as input>
-<#import "components/atoms/link.ftl" as link>
-<#import "features/labels/username.ftl" as usernameLabel>
-
-<#assign usernameLabel><@usernameLabel.kw />#assign>
-
-<@layout.registrationLayout
- displayInfo=true
- displayMessage=!messagesPerField.existsError("username")
- ;
- section
->
- <#if section="header">
- ${msg("emailForgotTitle")}
- <#elseif section="form">
- <@form.kw action=url.loginAction method="post">
- <@input.kw
- autocomplete=realm.loginWithEmailAllowed?string("email", "username")
- autofocus=true
- invalid=messagesPerField.existsError("username")
- label=usernameLabel
- message=kcSanitize(messagesPerField.get("username"))
- name="username"
- type="text"
- value=(auth?has_content && auth.showUsername())?then(auth.attemptedUsername, '')
- />
- <@buttonGroup.kw>
- <@button.kw color="primary" type="submit">
- ${msg("doSubmit")}
- @button.kw>
- @buttonGroup.kw>
- @form.kw>
- <#elseif section="info">
- ${msg("emailInstruction")}
- <#elseif section="nav">
- <@link.kw color="secondary" href=url.loginUrl size="small">
- ${kcSanitize(msg("backToLogin"))?no_esc}
- @link.kw>
- #if>
-@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login-update-password.ftl b/conf/keycloak_theme/keywind/login/login-update-password.ftl
deleted file mode 100644
index ed82380e2..000000000
--- a/conf/keycloak_theme/keywind/login/login-update-password.ftl
+++ /dev/null
@@ -1,64 +0,0 @@
-<#import "template.ftl" as layout>
-<#import "components/atoms/button.ftl" as button>
-<#import "components/atoms/button-group.ftl" as buttonGroup>
-<#import "components/atoms/checkbox.ftl" as checkbox>
-<#import "components/atoms/form.ftl" as form>
-<#import "components/atoms/input.ftl" as input>
-
-<@layout.registrationLayout
- displayMessage=!messagesPerField.existsError("password", "password-confirm")
- ;
- section
->
- <#if section="header">
- ${msg("updatePasswordTitle")}
- <#elseif section="form">
- <@form.kw action=url.loginAction method="post">
-
-
- <@input.kw
- autocomplete="new-password"
- autofocus=true
- invalid=messagesPerField.existsError("password", "password-confirm")
- label=msg("passwordNew")
- name="password-new"
- type="password"
- />
- <@input.kw
- autocomplete="new-password"
- invalid=messagesPerField.existsError("password-confirm")
- label=msg("passwordConfirm")
- message=kcSanitize(messagesPerField.get("password-confirm"))
- name="password-confirm"
- type="password"
- />
- <#if isAppInitiatedAction??>
- <@checkbox.kw
- checked=true
- label=msg("logoutOtherSessions")
- name="logout-sessions"
- value="on"
- />
- #if>
- <@buttonGroup.kw>
- <#if isAppInitiatedAction??>
- <@button.kw color="primary" type="submit">
- ${msg("doSubmit")}
- @button.kw>
- <@button.kw color="secondary" name="cancel-aia" type="submit" value="true">
- ${msg("doCancel")}
- @button.kw>
- <#else>
- <@button.kw color="primary" type="submit">
- ${msg("doSubmit")}
- @button.kw>
- #if>
- @buttonGroup.kw>
- @form.kw>
- #if>
-@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login-update-profile.ftl b/conf/keycloak_theme/keywind/login/login-update-profile.ftl
deleted file mode 100644
index 306bad944..000000000
--- a/conf/keycloak_theme/keywind/login/login-update-profile.ftl
+++ /dev/null
@@ -1,71 +0,0 @@
-<#import "template.ftl" as layout>
-<#import "components/atoms/button.ftl" as button>
-<#import "components/atoms/button-group.ftl" as buttonGroup>
-<#import "components/atoms/form.ftl" as form>
-<#import "components/atoms/input.ftl" as input>
-
-<@layout.registrationLayout
- displayMessage=!messagesPerField.existsError("email", "firstName", "lastName", "username")
- ;
- section
->
- <#if section="header">
- ${msg("loginProfileTitle")}
- <#elseif section="form">
- <@form.kw action=url.loginAction method="post">
- <#if user.editUsernameAllowed>
- <@input.kw
- autocomplete="username"
- autofocus=true
- invalid=messagesPerField.existsError("username")
- label=msg("username")
- message=kcSanitize(messagesPerField.get("username"))
- name="username"
- type="text"
- value=(user.username)!''
- />
- #if>
- <@input.kw
- autocomplete="email"
- invalid=messagesPerField.existsError("email")
- label=msg("email")
- message=kcSanitize(messagesPerField.get("email"))
- name="email"
- type="email"
- value=(user.email)!''
- />
- <@input.kw
- autocomplete="given-name"
- invalid=messagesPerField.existsError("firstName")
- label=msg("firstName")
- message=kcSanitize(messagesPerField.get("firstName"))
- name="firstName"
- type="text"
- value=(user.firstName)!''
- />
- <@input.kw
- autocomplete="family-name"
- invalid=messagesPerField.existsError("lastName")
- label=msg("lastName")
- message=kcSanitize(messagesPerField.get("lastName"))
- name="lastName"
- type="text"
- value=(user.lastName)!''
- />
- <@buttonGroup.kw>
- <#if isAppInitiatedAction??>
- <@button.kw color="primary" type="submit">
- ${msg("doSubmit")}
- @button.kw>
- <@button.kw color="secondary" name="cancel-aia" type="submit" value="true">
- ${msg("doCancel")}
- @button.kw>
- <#else>
- <@button.kw color="primary" type="submit">
- ${msg("doSubmit")}
- @button.kw>
- #if>
- @buttonGroup.kw>
- @form.kw>
- #if>
-@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login-username.ftl b/conf/keycloak_theme/keywind/login/login-username.ftl
deleted file mode 100644
index b8064b2da..000000000
--- a/conf/keycloak_theme/keywind/login/login-username.ftl
+++ /dev/null
@@ -1,71 +0,0 @@
-<#import "template.ftl" as layout>
-<#import "components/atoms/button.ftl" as button>
-<#import "components/atoms/button-group.ftl" as buttonGroup>
-<#import "components/atoms/checkbox.ftl" as checkbox>
-<#import "components/atoms/form.ftl" as form>
-<#import "components/atoms/input.ftl" as input>
-<#import "components/atoms/link.ftl" as link>
-<#import "components/molecules/identity-provider.ftl" as identityProvider>
-<#import "features/labels/username.ftl" as usernameLabel>
-
-<#assign usernameLabel><@usernameLabel.kw />#assign>
-
-<@layout.registrationLayout
- displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??
- displayMessage=!messagesPerField.existsError("username")
- ;
- section
->
- <#if section="header">
- ${msg("loginAccountTitle")}
- <#elseif section="form">
- <#if realm.password>
- <@form.kw
- action=url.loginAction
- method="post"
- onsubmit="login.disabled = true; return true;"
- >
- <#if !usernameHidden??>
- <@input.kw
- autocomplete=realm.loginWithEmailAllowed?string("email", "username")
- autofocus=true
- disabled=usernameEditDisabled??
- invalid=messagesPerField.existsError("username")
- label=usernameLabel
- message=kcSanitize(messagesPerField.get("username"))?no_esc
- name="username"
- type="text"
- value=(login.username)!''
- />
- #if>
- <#if realm.rememberMe && !usernameHidden??>
-
- <@checkbox.kw
- checked=login.rememberMe??
- label=msg("rememberMe")
- name="rememberMe"
- />
-
- #if>
- <@buttonGroup.kw>
- <@button.kw color="primary" name="login" type="submit">
- ${msg("doLogIn")}
- @button.kw>
- @buttonGroup.kw>
- @form.kw>
- #if>
- <#elseif section="info">
- <#if realm.password && realm.registrationAllowed && !registrationDisabled??>
-
- ${msg("noAccount")}
- <@link.kw color="primary" href=url.registrationUrl>
- ${msg("doRegister")}
- @link.kw>
-
- #if>
- <#elseif section="socialProviders">
- <#if realm.password && social.providers??>
- <@identityProvider.kw providers=social.providers />
- #if>
- #if>
-@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login.ftl b/conf/keycloak_theme/keywind/login/login.ftl
deleted file mode 100644
index 308413817..000000000
--- a/conf/keycloak_theme/keywind/login/login.ftl
+++ /dev/null
@@ -1,88 +0,0 @@
-<#import "template.ftl" as layout>
-<#import "components/atoms/button.ftl" as button>
-<#import "components/atoms/button-group.ftl" as buttonGroup>
-<#import "components/atoms/checkbox.ftl" as checkbox>
-<#import "components/atoms/form.ftl" as form>
-<#import "components/atoms/input.ftl" as input>
-<#import "components/atoms/link.ftl" as link>
-<#import "components/molecules/identity-provider.ftl" as identityProvider>
-<#import "features/labels/username.ftl" as usernameLabel>
-
-<#assign usernameLabel><@usernameLabel.kw />#assign>
-
-<@layout.registrationLayout
- displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??
- displayMessage=!messagesPerField.existsError("username", "password")
- ;
- section
->
- <#if section="header">
- ${msg("loginAccountTitle")}
- <#elseif section="form">
- <#if realm.password>
- <@form.kw
- action=url.loginAction
- method="post"
- onsubmit="login.disabled = true; return true;"
- >
-
- <@input.kw
- autocomplete=realm.loginWithEmailAllowed?string("email", "username")
- autofocus=true
- disabled=usernameEditDisabled??
- invalid=messagesPerField.existsError("username", "password")
- label=usernameLabel
- message=kcSanitize(messagesPerField.getFirstError("username", "password"))
- name="username"
- type="text"
- value=(login.username)!'username'
- />
- <@input.kw
- invalid=messagesPerField.existsError("username", "password")
- label=msg("password")
- name="password"
- type="password"
- value=(login.password)!'password'
- />
- <#if realm.rememberMe && !usernameEditDisabled?? || realm.resetPasswordAllowed>
-
- <#if realm.rememberMe && !usernameEditDisabled??>
- <@checkbox.kw
- checked=login.rememberMe??
- label=msg("rememberMe")
- name="rememberMe"
- />
- #if>
- <#if realm.resetPasswordAllowed>
- <@link.kw color="primary" href=url.loginResetCredentialsUrl size="small">
- ${msg("doForgotPassword")}
- @link.kw>
- #if>
-
- #if>
- <@buttonGroup.kw>
- <@button.kw color="primary" name="login" type="submit">
- ${msg("doLogIn")}
- @button.kw>
- @buttonGroup.kw>
- @form.kw>
- #if>
- <#elseif section="info">
- <#if realm.password && realm.registrationAllowed && !registrationDisabled??>
-
- ${msg("noAccount")}
- <@link.kw color="primary" href=url.registrationUrl>
- ${msg("doRegister")}
- @link.kw>
-
- #if>
- <#elseif section="socialProviders">
- <#if realm.password && social.providers??>
- <@identityProvider.kw providers=social.providers />
- #if>
- #if>
-@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/logout-confirm.ftl b/conf/keycloak_theme/keywind/login/logout-confirm.ftl
deleted file mode 100644
index e7ec48626..000000000
--- a/conf/keycloak_theme/keywind/login/logout-confirm.ftl
+++ /dev/null
@@ -1,25 +0,0 @@
-<#import "template.ftl" as layout>
-<#import "components/atoms/button.ftl" as button>
-<#import "components/atoms/form.ftl" as form>
-<#import "components/atoms/link.ftl" as link>
-
-<@layout.registrationLayout; section>
- <#if section="header">
- ${msg("logoutConfirmTitle")}
- <#elseif section="form">
- ${msg("logoutConfirmHeader")}
- <@form.kw action=url.logoutConfirmAction method="post">
-
- <@button.kw color="primary" name="confirmLogout" type="submit" value=msg('doLogout')>
- ${msg("doLogout")}
- @button.kw>
- @form.kw>
- <#if !logoutConfirm.skipLink>
- <#if (client.baseUrl)?has_content>
- <@link.kw color="secondary" href=client.baseUrl size="small">
- ${kcSanitize(msg("backToApplication"))?no_esc}
- @link.kw>
- #if>
- #if>
- #if>
-@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/register.ftl b/conf/keycloak_theme/keywind/login/register.ftl
deleted file mode 100644
index c1a2f061f..000000000
--- a/conf/keycloak_theme/keywind/login/register.ftl
+++ /dev/null
@@ -1,88 +0,0 @@
-<#import "template.ftl" as layout>
-<#import "components/atoms/button.ftl" as button>
-<#import "components/atoms/button-group.ftl" as buttonGroup>
-<#import "components/atoms/form.ftl" as form>
-<#import "components/atoms/input.ftl" as input>
-<#import "components/atoms/link.ftl" as link>
-
-<@layout.registrationLayout
- displayMessage=!messagesPerField.existsError("firstName", "lastName", "email", "username", "password", "password-confirm")
- ;
- section
->
- <#if section="header">
- ${msg("registerTitle")}
- <#elseif section="form">
- <@form.kw action=url.registrationAction method="post">
- <@input.kw
- autocomplete="given-name"
- autofocus=true
- invalid=messagesPerField.existsError("firstName")
- label=msg("firstName")
- message=kcSanitize(messagesPerField.get("firstName"))
- name="firstName"
- type="text"
- value=(register.formData.firstName)!''
- />
- <@input.kw
- autocomplete="family-name"
- invalid=messagesPerField.existsError("lastName")
- label=msg("lastName")
- message=kcSanitize(messagesPerField.get("lastName"))
- name="lastName"
- type="text"
- value=(register.formData.lastName)!''
- />
- <@input.kw
- autocomplete="email"
- invalid=messagesPerField.existsError("email")
- label=msg("email")
- message=kcSanitize(messagesPerField.get("email"))
- name="email"
- type="email"
- value=(register.formData.email)!''
- />
- <#if !realm.registrationEmailAsUsername>
- <@input.kw
- autocomplete="username"
- invalid=messagesPerField.existsError("username")
- label=msg("username")
- message=kcSanitize(messagesPerField.get("username"))
- name="username"
- type="text"
- value=(register.formData.username)!''
- />
- #if>
- <#if passwordRequired??>
- <@input.kw
- autocomplete="new-password"
- invalid=messagesPerField.existsError("password", "password-confirm")
- label=msg("password")
- message=kcSanitize(messagesPerField.getFirstError("password", "password-confirm"))
- name="password"
- type="password"
- />
- <@input.kw
- autocomplete="new-password"
- invalid=messagesPerField.existsError("password-confirm")
- label=msg("passwordConfirm")
- message=kcSanitize(messagesPerField.get("password-confirm"))
- name="password-confirm"
- type="password"
- />
- #if>
- <#if recaptchaRequired??>
-
- #if>
- <@buttonGroup.kw>
- <@button.kw color="primary" type="submit">
- ${msg("doRegister")}
- @button.kw>
- @buttonGroup.kw>
- @form.kw>
- <#elseif section="nav">
- <@link.kw color="secondary" href=url.loginUrl size="small">
- ${kcSanitize(msg("backToLogin"))?no_esc}
- @link.kw>
- #if>
-@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/resources/dist/assets/index-a7b84447.js b/conf/keycloak_theme/keywind/login/resources/dist/assets/index-a7b84447.js
deleted file mode 100644
index c1b2f3c66..000000000
--- a/conf/keycloak_theme/keywind/login/resources/dist/assets/index-a7b84447.js
+++ /dev/null
@@ -1 +0,0 @@
-var s={};Object.defineProperty(s,"__esModule",{value:!0});function v(e,r,a){var l;if(a===void 0&&(a={}),!r.codes){r.codes={};for(var n=0;n=8&&(t-=8,c[u++]=255&i>>t)}if(t>=r.bits||255&i<<8-t)throw new SyntaxError("Unexpected end of data");return c}function o(e,r,a){a===void 0&&(a={});for(var l=a,n=l.pad,b=n===void 0?!0:n,c=(1<r.bits;)i-=r.bits,t+=r.chars[c&u>>i];if(i&&(t+=r.chars[c&u<Te&&R.splice(t,1)}function $r(){!Me&&!Ce&&(Ce=!0,queueMicrotask(Rr))}function Rr(){Ce=!1,Me=!0;for(let e=0;ee.effect(t,{scheduler:r=>{Ie?Ir(r):r()}}),yt=e.raw}function ct(e){z=e}function Lr(e){let t=()=>{};return[n=>{let i=z(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),Z(i))},i},()=>{t()}]}var xt=[],bt=[],mt=[];function Fr(e){mt.push(e)}function wt(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,bt.push(t))}function Kr(e){xt.push(e)}function Dr(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function Et(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}var qe=new MutationObserver(Je),We=!1;function Ve(){qe.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),We=!0}function St(){Br(),qe.disconnect(),We=!1}var U=[],Ee=!1;function Br(){U=U.concat(qe.takeRecords()),U.length&&!Ee&&(Ee=!0,queueMicrotask(()=>{kr(),Ee=!1}))}function kr(){Je(U),U.length=0}function x(e){if(!We)return e();St();let t=e();return Ve(),t}var Ue=!1,ae=[];function zr(){Ue=!0}function Hr(){Ue=!1,Je(ae),ae=[]}function Je(e){if(Ue){ae=ae.concat(e);return}let t=[],r=[],n=new Map,i=new Map;for(let o=0;os.nodeType===1&&t.push(s)),e[o].removedNodes.forEach(s=>s.nodeType===1&&r.push(s))),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,u=e[o].oldValue,c=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},l=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&u===null?c():s.hasAttribute(a)?(l(),c()):l()}i.forEach((o,s)=>{Et(s,o)}),n.forEach((o,s)=>{xt.forEach(a=>a(s,o))});for(let o of r)if(!t.includes(o)&&(bt.forEach(s=>s(o)),o._x_cleanups))for(;o._x_cleanups.length;)o._x_cleanups.pop()();t.forEach(o=>{o._x_ignoreSelf=!0,o._x_ignore=!0});for(let o of t)r.includes(o)||o.isConnected&&(delete o._x_ignoreSelf,delete o._x_ignore,mt.forEach(s=>s(o)),o._x_ignore=!0,o._x_ignoreSelf=!0);t.forEach(o=>{delete o._x_ignoreSelf,delete o._x_ignore}),t=null,r=null,n=null,i=null}function At(e){return ee(K(e))}function X(e,t,r){return e._x_dataStack=[t,...K(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function lt(e,t){let r=e._x_dataStack[0];Object.entries(t).forEach(([n,i])=>{r[n]=i})}function K(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?K(e.host):e.parentNode?K(e.parentNode):[]}function ee(e){let t=new Proxy({},{ownKeys:()=>Array.from(new Set(e.flatMap(r=>Object.keys(r)))),has:(r,n)=>e.some(i=>i.hasOwnProperty(n)),get:(r,n)=>(e.find(i=>{if(i.hasOwnProperty(n)){let o=Object.getOwnPropertyDescriptor(i,n);if(o.get&&o.get._x_alreadyBound||o.set&&o.set._x_alreadyBound)return!0;if((o.get||o.set)&&o.enumerable){let s=o.get,a=o.set,u=o;s=s&&s.bind(t),a=a&&a.bind(t),s&&(s._x_alreadyBound=!0),a&&(a._x_alreadyBound=!0),Object.defineProperty(i,n,{...u,get:s,set:a})}return!0}return!1})||{})[n],set:(r,n,i)=>{let o=e.find(s=>s.hasOwnProperty(n));return o?o[n]=i:e[e.length-1][n]=i,!0}});return t}function Ot(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0)return;let u=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,u,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,u)})};return r(e)}function Ct(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>qr(n,i),s=>Pe(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let u=n.initialize(o,s,a);return r.initialValue=u,i(o,s,a)}}else r.initialValue=n;return r}}function qr(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function Pe(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),Pe(e[t[0]],t.slice(1),r)}}var Mt={};function S(e,t){Mt[e]=t}function $e(e,t){return Object.entries(Mt).forEach(([r,n])=>{Object.defineProperty(e,`$${r}`,{get(){let[i,o]=Rt(t);return i={interceptor:Ct,...i},wt(t,o),n(t,i)},enumerable:!1})}),e}function Wr(e,t,r,...n){try{return r(...n)}catch(i){Y(i,e,t)}}function Y(e,t,r=void 0){Object.assign(e,{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message}
-
-${r?'Expression: "'+r+`"
-
-`:""}`,t),setTimeout(()=>{throw e},0)}var se=!0;function Vr(e){let t=se;se=!1,e(),se=t}function F(e,t,r={}){let n;return m(e,t)(i=>n=i,r),n}function m(...e){return Tt(...e)}var Tt=It;function Ur(e){Tt=e}function It(e,t){let r={};$e(r,e);let n=[r,...K(e)],i=typeof t=="function"?Jr(n,t):Yr(n,t,e);return Wr.bind(null,e,t,i)}function Jr(e,t){return(r=()=>{},{scope:n={},params:i=[]}={})=>{let o=t.apply(ee([n,...e]),i);ue(r,o)}}var Se={};function Gr(e,t){if(Se[e])return Se[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e)||/^(let|const)\s/.test(e)?`(async()=>{ ${e} })()`:e,o=(()=>{try{return new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`)}catch(s){return Y(s,t,e),Promise.resolve()}})();return Se[e]=o,o}function Yr(e,t,r){let n=Gr(t,r);return(i=()=>{},{scope:o={},params:s=[]}={})=>{n.result=void 0,n.finished=!1;let a=ee([o,...e]);if(typeof n=="function"){let u=n(n,a).catch(c=>Y(c,r,t));n.finished?(ue(i,n.result,a,s,r),n.result=void 0):u.then(c=>{ue(i,c,a,s,r)}).catch(c=>Y(c,r,t)).finally(()=>n.result=void 0)}}}function ue(e,t,r,n,i){if(se&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>ue(e,s,r,n)).catch(s=>Y(s,i,t)):e(o)}else typeof t=="object"&&t instanceof Promise?t.then(o=>e(o)):e(t)}var Ge="x-";function H(e=""){return Ge+e}function Qr(e){Ge=e}var Re={};function g(e,t){return Re[e]=t,{before(r){if(!Re[r]){console.warn("Cannot find directive `${directive}`. `${name}` will use the default order of execution");return}const n=$.indexOf(r);$.splice(n>=0?n:$.indexOf("DEFAULT"),0,e)}}}function Ye(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,u])=>({name:a,value:u})),s=Pt(o);o=o.map(a=>s.find(u=>u.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(Lt((o,s)=>n[o]=s)).filter(Kt).map(en(n,r)).sort(tn).map(o=>Xr(e,o))}function Pt(e){return Array.from(e).map(Lt()).filter(t=>!Kt(t))}var je=!1,V=new Map,$t=Symbol();function Zr(e){je=!0;let t=Symbol();$t=t,V.set(t,[]);let r=()=>{for(;V.get(t).length;)V.get(t).shift()();V.delete(t)},n=()=>{je=!1,r()};e(r),n()}function Rt(e){let t=[],r=a=>t.push(a),[n,i]=Lr(e);return t.push(i),[{Alpine:re,effect:n,cleanup:r,evaluateLater:m.bind(m,e),evaluate:F.bind(F,e)},()=>t.forEach(a=>a())]}function Xr(e,t){let r=()=>{},n=Re[t.type]||r,[i,o]=Rt(e);Dr(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),je?V.get($t).push(n):n())};return s.runCleanups=o,s}var jt=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),Nt=e=>e;function Lt(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=Ft.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var Ft=[];function Qe(e){Ft.push(e)}function Kt({name:e}){return Dt().test(e)}var Dt=()=>new RegExp(`^${Ge}([^:^.]+)\\b`);function en(e,t){return({name:r,value:n})=>{let i=r.match(Dt()),o=r.match(/:([a-zA-Z0-9\-:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(u=>u.replace(".","")),expression:n,original:a}}}var Ne="DEFAULT",$=["ignore","ref","data","id","bind","init","for","model","modelable","transition","show","if",Ne,"teleport"];function tn(e,t){let r=$.indexOf(e.type)===-1?Ne:e.type,n=$.indexOf(t.type)===-1?Ne:t.type;return $.indexOf(r)-$.indexOf(n)}function J(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}function M(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>M(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)M(n,t),n=n.nextElementSibling}function D(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}function rn(){document.body||D("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's `
diff --git a/conf/keycloak_theme/keywind/login/webauthn-error.ftl b/conf/keycloak_theme/keywind/login/webauthn-error.ftl
deleted file mode 100644
index 852d1e3d5..000000000
--- a/conf/keycloak_theme/keywind/login/webauthn-error.ftl
+++ /dev/null
@@ -1,34 +0,0 @@
-<#import "template.ftl" as layout>
-<#import "components/atoms/button.ftl" as button>
-<#import "components/atoms/button-group.ftl" as buttonGroup>
-
-<@layout.registrationLayout displayMessage=true; section>
- <#if section="header">
- ${kcSanitize(msg("webauthn-error-title"))?no_esc}
- <#elseif section="form">
-
-
- <@buttonGroup.kw>
- <@button.kw
- @click="$refs.executionValueInput.value = '${execution}'; $refs.isSetRetryInput.value = 'retry'; $refs.errorCredentialForm.submit()"
- color="primary"
- name="try-again"
- tabindex="4"
- type="button"
- >
- ${kcSanitize(msg("doTryAgain"))?no_esc}
- @button.kw>
- <#if isAppInitiatedAction??>
-
- #if>
- @buttonGroup.kw>
-
- #if>
-@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/webauthn-register.ftl b/conf/keycloak_theme/keywind/login/webauthn-register.ftl
deleted file mode 100644
index 57f4dad87..000000000
--- a/conf/keycloak_theme/keywind/login/webauthn-register.ftl
+++ /dev/null
@@ -1,54 +0,0 @@
-<#import "template.ftl" as layout>
-<#import "components/atoms/button.ftl" as button>
-<#import "components/atoms/button-group.ftl" as buttonGroup>
-
-<@layout.registrationLayout script="dist/webAuthnRegister.js"; section>
- <#if section="title">
- title
- <#elseif section="header">
- ${kcSanitize(msg("webauthn-registration-title"))?no_esc}
- <#elseif section="form">
-
-
- <@buttonGroup.kw>
- <@button.kw @click="registerSecurityKey" color="primary" type="submit">
- ${msg("doRegister")}
- @button.kw>
- <#if !isSetRetry?has_content && isAppInitiatedAction?has_content>
-
- #if>
- @buttonGroup.kw>
-
- #if>
-@layout.registrationLayout>
-
-
diff --git a/conf/sample.env_app-backend b/conf/sample.env_app-backend
deleted file mode 100644
index e75a8c98c..000000000
--- a/conf/sample.env_app-backend
+++ /dev/null
@@ -1,23 +0,0 @@
-## postgres database to store Hasura metadata
-HASURA_GRAPHQL_METADATA_DATABASE_URL=postgres://postgres:postgrespassword@postgres:5432/jan-hasura-metadata
-## this env var can be used to add the above postgres database to Hasura as a data source. this can be removed/updated based on your needs
-PG_DATABASE_URL=postgres://postgres:postgrespassword@postgres:5432/jan-hasura-data
-## enable the console served by server
-HASURA_GRAPHQL_ENABLE_CONSOLE="true" # set to "false" to disable console
-## enable debugging mode. It is recommended to disable this in production
-HASURA_GRAPHQL_DEV_MODE="true"
-HASURA_GRAPHQL_ENABLED_LOG_TYPES=startup, http-log, webhook-log, websocket-log, query-log
-## uncomment next line to run console offline (i.e load console assets from server instead of CDN)
-# HASURA_GRAPHQL_CONSOLE_ASSETS_DIR: /srv/console-assets
-## uncomment next line to set an admin secret
-HASURA_GRAPHQL_ADMIN_SECRET=myadminsecretkey
-HASURA_GRAPHQL_UNAUTHORIZED_ROLE="public"
-HASURA_GRAPHQL_METADATA_DEFAULTS='{"backend_configs":{"dataconnector":{"athena":{"uri":"http://data-connector-agent:8081/api/v1/athena"},"mariadb":{"uri":"http://data-connector-agent:8081/api/v1/mariadb"},"mysql8":{"uri":"http://data-connector-agent:8081/api/v1/mysql"},"oracle":{"uri":"http://data-connector-agent:8081/api/v1/oracle"},"snowflake":{"uri":"http://data-connector-agent:8081/api/v1/snowflake"}}}}'
-HASURA_GRAPHQL_JWT_SECRET={"jwk_url": "http://keycloak:8088/realms/hasura/protocol/openid-connect/certs"}
-
-# Environment variable for auto migrate
-HASURA_GRAPHQL_MIGRATIONS_DIR=/migrations
-HASURA_GRAPHQL_METADATA_DIR=/metadata
-HASURA_GRAPHQL_ENABLE_CONSOLE='true'
-HASURA_ACTION_STABLE_DIFFUSION_URL=http://sd:8000
-HASURA_EVENTS_HOOK_URL="http://worker:8787"
\ No newline at end of file
diff --git a/conf/sample.env_app-backend-postgres b/conf/sample.env_app-backend-postgres
deleted file mode 100644
index 39aa93db5..000000000
--- a/conf/sample.env_app-backend-postgres
+++ /dev/null
@@ -1 +0,0 @@
-POSTGRES_PASSWORD=postgrespassword
\ No newline at end of file
diff --git a/conf/sample.env_web-client b/conf/sample.env_web-client
deleted file mode 100644
index e024bb762..000000000
--- a/conf/sample.env_web-client
+++ /dev/null
@@ -1,15 +0,0 @@
-NEXT_PUBLIC_ENV=development
-NEXT_PUBLIC_WEB_URL=http://localhost:3000
-NEXT_PUBLIC_DISCORD_INVITATION_URL=#
-NEXT_PUBLIC_DOWNLOAD_APP_IOS=#
-NEXT_PUBLIC_DOWNLOAD_APP_ANDROID=#
-NEXT_PUBLIC_GRAPHQL_ENGINE_URL=http://localhost:8080/v1/graphql
-NEXT_PUBLIC_GRAPHQL_ENGINE_WEB_SOCKET_URL=ws://localhost:8080/v1/graphql
-NEXT_PUBLIC_OPENAPI_ENDPOINT=http://localhost:8000/v1/chat/completions
-KEYCLOAK_CLIENT_ID=hasura
-KEYCLOAK_CLIENT_SECRET=oMtCPAV7diKpE564SBspgKj4HqlKM4Hy
-AUTH_ISSUER=http://localhost:8088/realms/$KEYCLOAK_CLIENT_ID
-NEXTAUTH_URL=http://localhost:3000
-NEXTAUTH_SECRET=my-secret
-END_SESSION_URL=http://localhost:8088/realms/$KEYCLOAK_CLIENT_ID/protocol/openid-connect/logout
-REFRESH_TOKEN_URL=http://localhost:8088/realms/$KEYCLOAK_CLIENT_ID/protocol/openid-connect/token
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644
index 2a0c2bad7..000000000
--- a/docker-compose.yml
+++ /dev/null
@@ -1,121 +0,0 @@
-# docker version
-version: "3"
-
-services:
- keycloak:
- image: quay.io/keycloak/keycloak:${KEYCLOAK_VERSION-22.0.0}
- command: ["start-dev", "--import-realm", "--http-port", "8088"]
- environment:
- KC_DB: postgres
- KC_DB_URL_HOST: postgres
- KC_DB_URL_DATABASE: jan-keycloak
- KC_DB_PASSWORD: postgrespassword
- KC_DB_USERNAME: postgres
- KC_DB_SCHEMA: public
- KC_HEALTH_ENABLED: "true"
- KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN-admin}
- KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD-admin}
- volumes:
- - ./conf/keycloak_conf:/opt/keycloak/data/import
- - ./conf/keycloak_theme/keywind:/opt/keycloak/themes/keywind
- ports:
- - "8088:8088"
- depends_on:
- postgres:
- condition: service_healthy
- networks:
- jan_community:
- ipv4_address: 172.20.0.9
-
- postgres:
- image: postgres:15
- command: postgres -c jit=off
- restart: always
- environment:
- POSTGRES_USER: postgres
- POSTGRES_PASSWORD: postgrespassword
- ports:
- - "5432:5432"
- healthcheck:
- test: "exit 0"
- volumes:
- - ./conf/db/docker_psql_init.sql:/docker-entrypoint-initdb.d/docker_postgres_init.sql
- networks:
- jan_community:
- ipv4_address: 172.20.0.11
-
- graphql-engine:
- image: hasura/graphql-engine:v2.31.0.cli-migrations-v3
- ports:
- - 8080:8080
- restart: always
- env_file:
- - conf/sample.env_app-backend
- volumes:
- - ./app-backend/hasura/migrations:/migrations
- - ./app-backend/hasura/metadata:/metadata
- depends_on:
- data-connector-agent:
- condition: service_healthy
- keycloak:
- condition: service_started
- healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"]
- interval: 10s
- timeout: 10s
- retries: 30
- networks:
- jan_community:
- ipv4_address: 172.20.0.12
-
- data-connector-agent:
- image: hasura/graphql-data-connector:v2.31.0
- restart: always
- ports:
- - 8081:8081
- environment:
- QUARKUS_LOG_LEVEL: ERROR # FATAL, ERROR, WARN, INFO, DEBUG, TRACE
- ## https://quarkus.io/guides/opentelemetry#configuration-reference
- QUARKUS_OPENTELEMETRY_ENABLED: "false"
- ## QUARKUS_OPENTELEMETRY_TRACER_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317
- healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8081/api/v1/athena/health"]
- interval: 5s
- timeout: 10s
- retries: 5
- start_period: 5s
- networks:
- jan_community:
- ipv4_address: 172.20.0.14
-
- web:
- build:
- context: ./web-client
- dockerfile: ./dev.Dockerfile
- restart: always
- volumes:
- - ./web-client/:/app
- - /app/node_modules
- - /app/.next
- env_file:
- - conf/sample.env_web-client
- ports:
- - 3000:3000
- environment:
- NODE_ENV: development
- extra_hosts:
- - "localhost:172.20.0.9"
- depends_on:
- graphql-engine:
- condition: service_healthy
- networks:
- jan_community:
- ipv4_address: 172.20.0.15
-
-networks:
- jan_community:
- driver: bridge
- ipam:
- driver: default
- config:
- - subnet: 172.20.0.0/16
diff --git a/docs/docs/privacy/privacy.md b/docs/docs/privacy/privacy.md
new file mode 100644
index 000000000..56e81f3a1
--- /dev/null
+++ b/docs/docs/privacy/privacy.md
@@ -0,0 +1,25 @@
+# Privacy Policy
+
+Jan is committed to protecting your privacy and ensuring that your personal information is handled in a safe and responsible way. This policy outlines how we collect, store, and use your personal information when you use our mobile application.
+
+## Data Collection and Usage
+
+When you use Jan, we may collect certain information about you, including your name, email address, and other personal information that you provide to us. We use this information to provide you with the best possible experience when using our app.
+
+We may also collect certain non-personal information, such as your device type, operating system, and app usage data. This information is used to improve our app and to provide you with a better user experience.
+
+## Data Sharing
+
+We do not share your personal information with third parties except as required by law or as necessary to provide you with the services you have requested. We may share non-personal information with third parties for the purpose of improving our app and providing you with a better user experience.
+
+## Data Security
+
+We take the security of your personal information seriously and have implemented appropriate technical and organizational measures to protect your personal information from unauthorized access, disclosure, or misuse.
+
+## Your Choices
+
+You have the right to access, update, and delete your personal information at any time. You may also opt-out of receiving marketing communications from us by following the unsubscribe link included in our emails.
+
+## Contact Us
+
+If you have any questions or concerns about our privacy policy, please contact us at hello@jan.ai.
diff --git a/docs/docs/support/support.md b/docs/docs/support/support.md
new file mode 100644
index 000000000..6f2b77548
--- /dev/null
+++ b/docs/docs/support/support.md
@@ -0,0 +1,6 @@
+# Support
+
+- Bugs & requests: file a Github ticket [here](https://github.com/janhq/jan/issues)
+- For discussion: join our Discord [here](https://discord.gg/FTk2MvZwJH)
+- For business inquiries: email hello@jan.ai
+- For jobs: please email hr@jan.ai
diff --git a/docs/package.json b/docs/package.json
index aace8464c..ed81ad1c6 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -4,7 +4,7 @@
"private": true,
"scripts": {
"docusaurus": "docusaurus",
- "start": "docusaurus start",
+ "start": "docusaurus start --port 3001",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
diff --git a/docs/src/components/Elements/dropdown.js b/docs/src/components/Elements/dropdown.js
index 95835575e..6d9bd0697 100644
--- a/docs/src/components/Elements/dropdown.js
+++ b/docs/src/components/Elements/dropdown.js
@@ -6,22 +6,22 @@ import { ChevronDownIcon } from "@heroicons/react/20/solid";
const items = [
{
name: "Download for Mac (M1/M2)",
- href: "#",
+ href: "https://github.com/janhq/jan/releases/download/v0.1.2/Jan-0.1.2-arm64.dmg",
logo: require("@site/static/img/apple-logo-white.png").default,
},
{
name: "Download for Mac (Intel)",
- href: "#",
+ href: "https://github.com/janhq/jan/releases/download/v0.1.2/Jan-0.1.2-arm64.dmg",
logo: require("@site/static/img/apple-logo-white.png").default,
},
{
name: "Download for Windows",
- href: "#",
+ href: "https://static.vecteezy.com/system/resources/previews/004/243/615/non_2x/creative-coming-soon-teaser-background-free-vector.jpg",
logo: require("@site/static/img/windows-logo-white.png").default,
},
{
name: "Download for Linux",
- href: "#",
+ href: "https://static.vecteezy.com/system/resources/previews/004/243/615/non_2x/creative-coming-soon-teaser-background-free-vector.jpg",
logo: require("@site/static/img/linux-logo-white.png").default,
},
];
@@ -34,9 +34,9 @@ export default function Dropdown() {
return (
{/* TODO dynamically detect users OS through browser */}
-
Download for Mac (Silicon)
-
+
Open OS options
diff --git a/docs/src/components/Homepage/banner.js b/docs/src/components/Homepage/banner.js
index 43bd96711..24520e747 100644
--- a/docs/src/components/Homepage/banner.js
+++ b/docs/src/components/Homepage/banner.js
@@ -1,32 +1,32 @@
import React from "react";
import { XMarkIcon } from "@heroicons/react/20/solid";
+import { useColorMode } from "@docusaurus/theme-common";
export default function HomepageBanner() {
- return (
-
-
-
- GeneriCon 2023
-
-
-
- Join us in Denver from June 7 – 9 to see what’s coming next
- →
-
-
-
-
- Dismiss
-
-
+ const { colorMode } = useColorMode();
+ const bannerText =
+ "🚧 This site is under construction - expect breaking changes! 🚧";
+ return colorMode === "dark" ? (
+
+ {bannerText}
+
+ ) : (
+
);
}
diff --git a/docs/src/components/Homepage/hero.js b/docs/src/components/Homepage/hero.js
index e285f7d75..64dbd83f2 100644
--- a/docs/src/components/Homepage/hero.js
+++ b/docs/src/components/Homepage/hero.js
@@ -1,5 +1,5 @@
import React from "react";
-import { ChevronRightIcon } from "@heroicons/react/20/solid";
+import { ArrowTopRightOnSquareIcon } from "@heroicons/react/20/solid";
import { useColorMode } from "@docusaurus/theme-common";
import Dropdown from "@site/src/components/Elements/dropdown";
@@ -47,7 +47,9 @@ export default function HomepageHero() {
Run your own AI
- Jan lets you run AI on your own hardware, and with 1-click installs for the latest models. Easy-to-use yet powerful, with helpful tools to monitor and manage software-hardware performance.
+ Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure qui
+ lorem cupidatat commodo. Elit sunt amet fugiat veniam occaecat
+ fugiat aliqua.
{/* TODO: handle mobile model download app instead */}
@@ -55,8 +57,16 @@ export default function HomepageHero() {
+ window.open(
+ "https://github.com/janhq/jan",
+ "_blank",
+ "noreferrer"
+ )
+ }
>
- Book a Demo
+ View Github
+
@@ -64,9 +74,9 @@ export default function HomepageHero() {
@@ -64,7 +66,14 @@ export default function HomepageSectionOne() {
+
-
-
- Run AI on any OS
+
+
+ AI on your own hardware means
-
+
Unlimited Use
-
- No uncontrolled cloud spending. No hidden fees. No limits.
-
- {/* Cost comparitor */}
-
-
-
-
-
-
-
-
-
- Name
-
-
- Title
-
-
- Email
-
-
-
-
- {people.map((person) => (
-
-
- {person.name}
-
-
- {person.title}
-
-
- {person.email}
-
-
- ))}
-
-
-
+
+ No uncontrolled cloud spending. No hidden fees. No limits.
+
+ {/*
+
+
+ Payment frequency
+
+ {frequencies.map((option) => (
+
+ classNames(
+ checked ? "bg-indigo-600 text-white" : "text-gray-500",
+ "cursor-pointer rounded-full px-2.5 py-1"
+ )
+ }
+ >
+ {option.label}
+
+ ))}
+
+
*/}
+
+ {tiers.map((tier) => (
+
+
+
+ {tier.name}
+
+ {tier.mostPopular ? (
+
+ Fully private
+
+ ) : null}
+
+ {tier.description}
+
+
+
+ {tier.price[frequency.value]}
+
+
+ {frequency.priceSuffix}
+
+
+ {/*
+ Buy plan
+ */}
+
+ {tier.features.map((feature) => (
+
+
+ {feature}
+
+ ))}
+
-
+ ))}
diff --git a/docs/src/components/Homepage/sectionTwo.js b/docs/src/components/Homepage/sectionTwo.js
index 616c909b9..d9ff18666 100644
--- a/docs/src/components/Homepage/sectionTwo.js
+++ b/docs/src/components/Homepage/sectionTwo.js
@@ -8,18 +8,19 @@ import {
RocketLaunchIcon,
ServerIcon,
} from "@heroicons/react/20/solid";
+import { useColorMode } from "@docusaurus/theme-common";
const features = [
{
- name: "Data security and privacy.",
+ name: "Data Security and Privacy.",
description:
- "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Maiores impedit perferendis suscipit eaque, iste dolor cupiditate blanditiis ratione.",
+ "Jan runs locally on your machine. Your data never leaves your computer. You can even run Jan offline.",
icon: CloudArrowUpIcon,
},
{
- name: "Always accessible.",
+ name: "Cross Device Compatible.",
description:
- "Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure qui lorem cupidatat commodo.",
+ "Jan runs Nitro, a C++ inference engine, that is compatible with all major operating systems (CPU and GPU).",
icon: LockClosedIcon,
},
{
@@ -30,6 +31,7 @@ const features = [
];
export default function sectionTwo() {
+ const { colorMode } = useColorMode();
return (
@@ -43,9 +45,8 @@ export default function sectionTwo() {
AI that you control
- Lorem ipsum, dolor sit amet consectetur adipisicing elit.
- Maiores impedit perferendis suscipit eaque, iste dolor
- cupiditate blanditiis ratione.
+ Jan is a source-available, cross device, and privacy focused AI
+ engine and Desktop app that runs locally on your machine.
{features.map((feature) => (
@@ -64,7 +65,12 @@ export default function sectionTwo() {
- {/*
*/}
+
-
-
+ {/*
+ */}
);
diff --git a/docs/src/pages/index.module.css b/docs/src/pages/index.module.css
index 9f71a5da7..6fdd6da59 100644
--- a/docs/src/pages/index.module.css
+++ b/docs/src/pages/index.module.css
@@ -3,7 +3,7 @@
* and scoped locally.
*/
-.heroBanner {
+/* .heroBanner {
padding: 4rem 0;
text-align: center;
position: relative;
@@ -20,4 +20,4 @@
display: flex;
align-items: center;
justify-content: center;
-}
+} */
diff --git a/docs/static/img/desktop-screenshot-dark.png b/docs/static/img/desktop-explore-models-dark.png
similarity index 100%
rename from docs/static/img/desktop-screenshot-dark.png
rename to docs/static/img/desktop-explore-models-dark.png
diff --git a/docs/static/img/desktop-screenshot.png b/docs/static/img/desktop-explore-models.png
similarity index 100%
rename from docs/static/img/desktop-screenshot.png
rename to docs/static/img/desktop-explore-models.png
diff --git a/docs/static/img/desktop-llm-chat.png b/docs/static/img/desktop-llm-chat.png
new file mode 100644
index 000000000..b235dfc0a
Binary files /dev/null and b/docs/static/img/desktop-llm-chat.png differ
diff --git a/docs/static/img/desktop-model-settings.png b/docs/static/img/desktop-model-settings.png
new file mode 100644
index 000000000..d22014991
Binary files /dev/null and b/docs/static/img/desktop-model-settings.png differ
diff --git a/docs/static/img/jan-social-card.png b/docs/static/img/jan-social-card.png
index f6e09f014..ed606e685 100644
Binary files a/docs/static/img/jan-social-card.png and b/docs/static/img/jan-social-card.png differ
diff --git a/docs/static/img/undraw_docusaurus_mountain.svg b/docs/static/img/undraw_docusaurus_mountain.svg
deleted file mode 100644
index af961c49a..000000000
--- a/docs/static/img/undraw_docusaurus_mountain.svg
+++ /dev/null
@@ -1,171 +0,0 @@
-
- Easy to Use
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/static/img/undraw_docusaurus_react.svg b/docs/static/img/undraw_docusaurus_react.svg
deleted file mode 100644
index 94b5cf08f..000000000
--- a/docs/static/img/undraw_docusaurus_react.svg
+++ /dev/null
@@ -1,170 +0,0 @@
-
- Powered by React
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/static/img/undraw_docusaurus_tree.svg b/docs/static/img/undraw_docusaurus_tree.svg
deleted file mode 100644
index d9161d339..000000000
--- a/docs/static/img/undraw_docusaurus_tree.svg
+++ /dev/null
@@ -1,40 +0,0 @@
-
- Focus on What Matters
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/electron/core/plugin-manager/execution/Activation.js b/electron/core/plugin-manager/execution/Activation.js
new file mode 100644
index 000000000..3381e428f
--- /dev/null
+++ b/electron/core/plugin-manager/execution/Activation.js
@@ -0,0 +1,37 @@
+import { callExport } from "./import-manager.js"
+
+class Activation {
+ /** @type {string} Name of the registered plugin. */
+ plugin
+
+ /** @type {string} Name of the activation point that is registered to. */
+ activationPoint
+
+ /** @type {string} location of the file containing the activation function. */
+ url
+
+ /** @type {boolean} Whether the activation has been activated. */
+ activated
+
+ constructor(plugin, activationPoint, url) {
+ this.plugin = plugin
+ this.activationPoint = activationPoint
+ this.url = url
+ this.activated = false
+ }
+
+ /**
+ * Trigger the activation function in the plugin once,
+ * providing the list of extension points or an object with the extension point's register, execute and executeSerial functions.
+ * @returns {boolean} Whether the activation has already been activated.
+ */
+ async trigger() {
+ if (!this.activated) {
+ await callExport(this.url, this.activationPoint, this.plugin)
+ this.activated = true
+ }
+ return this.activated
+ }
+}
+
+export default Activation
diff --git a/electron/core/plugin-manager/execution/ExtensionPoint.js b/electron/core/plugin-manager/execution/ExtensionPoint.js
new file mode 100644
index 000000000..b3caba012
--- /dev/null
+++ b/electron/core/plugin-manager/execution/ExtensionPoint.js
@@ -0,0 +1,145 @@
+/**
+ * @typedef {Object} Extension An extension registered to an extension point
+ * @property {string} name Unique name for the extension.
+ * @property {Object|Callback} response Object to be returned or function to be called by the extension point.
+ * @property {number} [priority] Order priority for execution used for executing in serial.
+ */
+
+/**
+ * Represents a point in the consumer's code that can be extended by a plugin.
+ * The plugin can register a callback or object to the extension point.
+ * When the extension point is triggered, the provided function will then be called or object will be returned.
+ */
+class ExtensionPoint {
+ /** @type {string} Name of the extension point */
+ name
+
+ /**
+ * @type {Array.
} The list of all extensions registered with this extension point.
+ * @private
+ */
+ _extensions = []
+
+ /**
+ * @type {Array.} A list of functions to be executed when the list of extensions changes.
+ * @private
+ */
+ #changeListeners = []
+
+ constructor(name) {
+ this.name = name
+ }
+
+ /**
+ * Register new extension with this extension point.
+ * The registered response will be executed (if callback) or returned (if object)
+ * when the extension point is executed (see below).
+ * @param {string} name Unique name for the extension.
+ * @param {Object|Callback} response Object to be returned or function to be called by the extension point.
+ * @param {number} [priority] Order priority for execution used for executing in serial.
+ * @returns {void}
+ */
+ register(name, response, priority = 0) {
+ const index = this._extensions.findIndex(p => p.priority > priority)
+ const newExt = { name, response, priority }
+ if (index > -1) {
+ this._extensions.splice(index, 0, newExt)
+ } else {
+ this._extensions.push(newExt)
+ }
+
+ this.#emitChange()
+ }
+
+ /**
+ * Remove an extension from the registry. It will no longer be part of the extension point execution.
+ * @param {RegExp } name Matcher for the name of the extension to remove.
+ * @returns {void}
+ */
+ unregister(name) {
+ const index = this._extensions.findIndex(ext => ext.name.match(name))
+ if (index > -1) this._extensions.splice(index, 1)
+
+ this.#emitChange()
+ }
+
+ /**
+ * Empty the registry of all extensions.
+ * @returns {void}
+ */
+ clear() {
+ this._extensions = []
+ this.#emitChange()
+ }
+
+ /**
+ * Get a specific extension registered with the extension point
+ * @param {string} name Name of the extension to return
+ * @returns {Object|Callback|undefined} The response of the extension. If this is a function the function is returned, not its response.
+ */
+ get(name) {
+ const ep = this._extensions.find(ext => ext.name === name)
+ return ep && ep.response
+ }
+
+ /**
+ * Execute (if callback) and return or just return (if object) the response for each extension registered to this extension point.
+ * Any asynchronous responses will be executed in parallel and the returned array will contain a promise for each of these responses.
+ * @param {*} input Input to be provided as a parameter to each response if response is a callback.
+ * @returns {Array} List of responses from the extensions.
+ */
+ execute(input) {
+ return this._extensions.map(p => {
+ if (typeof p.response === 'function') {
+ return p.response(input)
+ } else {
+ return p.response
+ }
+ })
+ }
+
+ /**
+ * Execute (if callback) and return the response, or push it to the array if the previous response is an array
+ * for each extension registered to this extension point in serial,
+ * feeding the result from the last response as input to the next.
+ * @param {*} input Input to be provided as a parameter to the 1st callback
+ * @returns {Promise.<*>} Result of the last extension that was called
+ */
+ async executeSerial(input) {
+ return await this._extensions.reduce(async (throughput, p) => {
+ let tp = await throughput
+ if (typeof p.response === 'function') {
+ tp = await p.response(tp)
+ } else if (Array.isArray(tp)) {
+ tp.push(p.response)
+ }
+ return tp
+ }, input)
+ }
+
+ /**
+ * Register a callback to be executed if the list of extensions changes.
+ * @param {string} name Name of the listener needed if it is to be removed.
+ * @param {Function} callback The callback function to trigger on a change.
+ */
+ onRegister(name, callback) {
+ if (typeof callback === 'function') this.#changeListeners.push({ name, callback })
+ }
+
+ /**
+ * Unregister a callback from the extension list changes.
+ * @param {string} name The name of the listener to remove.
+ */
+ offRegister(name) {
+ const index = this.#changeListeners.findIndex(l => l.name === name)
+ if (index > -1) this.#changeListeners.splice(index, 1)
+ }
+
+ #emitChange() {
+ for (const l of this.#changeListeners) {
+ l.callback(this)
+ }
+ }
+}
+
+export default ExtensionPoint
\ No newline at end of file
diff --git a/electron/core/plugin-manager/execution/ExtensionPoint.test.js b/electron/core/plugin-manager/execution/ExtensionPoint.test.js
new file mode 100644
index 000000000..54d7b37ec
--- /dev/null
+++ b/electron/core/plugin-manager/execution/ExtensionPoint.test.js
@@ -0,0 +1,116 @@
+import Ep from './ExtensionPoint'
+
+/** @type {Ep} */
+let ep
+const changeListener = jest.fn()
+
+const objectRsp = { foo: 'bar' }
+const funcRsp = arr => {
+ arr || (arr = [])
+ arr.push({ foo: 'baz' })
+ return arr
+}
+
+beforeEach(() => {
+ ep = new Ep('test-ep')
+ ep.register('test-ext-obj', objectRsp)
+ ep.register('test-ext-func', funcRsp, 10)
+ ep.onRegister('test', changeListener)
+})
+
+
+it('should create a new extension point by providing a name', () => {
+ expect(ep.name).toEqual('test-ep')
+})
+
+it('should register extension with extension point', () => {
+ expect(ep._extensions).toContainEqual({
+ name: 'test-ext-func',
+ response: funcRsp,
+ priority: 10
+ })
+})
+
+it('should register extension with a default priority of 0 if not provided', () => {
+ expect(ep._extensions).toContainEqual({
+ name: 'test-ext-obj',
+ response: objectRsp,
+ priority: 0
+ })
+})
+
+it('should execute the change listeners on registering a new extension', () => {
+ changeListener.mockClear()
+ ep.register('test-change-listener', true)
+ expect(changeListener.mock.calls.length).toBeTruthy()
+})
+
+it('should unregister an extension with the provided name if it exists', () => {
+ ep.unregister('test-ext-obj')
+
+ expect(ep._extensions).not.toContainEqual(
+ expect.objectContaining({
+ name: 'test-ext-obj'
+ })
+ )
+})
+
+it('should not unregister any extensions if the provided name does not exist', () => {
+ ep.unregister('test-ext-invalid')
+
+ expect(ep._extensions.length).toBe(2)
+})
+
+it('should execute the change listeners on unregistering an extension', () => {
+ changeListener.mockClear()
+ ep.unregister('test-ext-obj')
+ expect(changeListener.mock.calls.length).toBeTruthy()
+})
+
+it('should empty the registry of all extensions on clearing', () => {
+ ep.clear()
+
+ expect(ep._extensions).toEqual([])
+})
+
+it('should execute the change listeners on clearing extensions', () => {
+ changeListener.mockClear()
+ ep.clear()
+ expect(changeListener.mock.calls.length).toBeTruthy()
+})
+
+it('should return the relevant extension using the get method', () => {
+ const ext = ep.get('test-ext-obj')
+
+ expect(ext).toEqual({ foo: 'bar' })
+})
+
+it('should return the false using the get method if the extension does not exist', () => {
+ const ext = ep.get('test-ext-invalid')
+
+ expect(ext).toBeUndefined()
+})
+
+it('should provide an array with all responses, including promises where necessary, using the execute method', async () => {
+ ep.register('test-ext-async', () => new Promise(resolve => setTimeout(resolve, 0, { foo: 'delayed' })))
+ const arr = ep.execute([])
+
+ const res = await Promise.all(arr)
+
+ expect(res).toContainEqual({ foo: 'bar' })
+ expect(res).toContainEqual([{ foo: 'baz' }])
+ expect(res).toContainEqual({ foo: 'delayed' })
+ expect(res.length).toBe(3)
+})
+
+it('should provide an array including all responses in priority order, using the executeSerial method provided with an array', async () => {
+ const res = await ep.executeSerial([])
+
+ expect(res).toEqual([{ "foo": "bar" }, { "foo": "baz" }])
+})
+
+it('should provide an array including the last response using the executeSerial method provided with something other than an array', async () => {
+ const res = await ep.executeSerial()
+
+ expect(res).toEqual([{ "foo": "baz" }])
+})
diff --git a/electron/core/plugin-manager/execution/Plugin.js b/electron/core/plugin-manager/execution/Plugin.js
new file mode 100644
index 000000000..5c6462aee
--- /dev/null
+++ b/electron/core/plugin-manager/execution/Plugin.js
@@ -0,0 +1,35 @@
+import { callExport } from "./import-manager"
+
+/**
+ * A slimmed down representation of a plugin for the renderer.
+ */
+class Plugin {
+ /** @type {string} Name of the package. */
+ name
+
+ /** @type {string} The electron url where this plugin is located. */
+ url
+
+ /** @type {Array} List of activation points. */
+ activationPoints
+
+ /** @type {boolean} Whether this plugin should be activated when its activation points are triggered. */
+ active
+
+ constructor(name, url, activationPoints, active) {
+ this.name = name
+ this.url = url
+ this.activationPoints = activationPoints
+ this.active = active
+ }
+
+ /**
+ * Trigger an exported callback on the plugin's main file.
+ * @param {string} exp exported callback to trigger.
+ */
+ triggerExport(exp) {
+ callExport(this.url, exp, this.name)
+ }
+}
+
+export default Plugin
\ No newline at end of file
diff --git a/electron/core/plugin-manager/execution/Plugin.test.js b/electron/core/plugin-manager/execution/Plugin.test.js
new file mode 100644
index 000000000..7982decdc
--- /dev/null
+++ b/electron/core/plugin-manager/execution/Plugin.test.js
@@ -0,0 +1,22 @@
+import { setImporter } from "./import-manager"
+import Plugin from './Plugin'
+
+describe('triggerExport', () => {
+ it('should call the provided export on the plugin\'s main file', async () => {
+ // Set up mock importer with mock main plugin file
+ const mockExport = jest.fn()
+ const mockImporter = jest.fn(() => ({
+ lifeCycleFn: mockExport
+ }))
+ setImporter(mockImporter)
+
+ // Call triggerExport on new plugin
+ const plgUrl = 'main'
+ const plugin = new Plugin('test', plgUrl, ['ap1'], true)
+ await plugin.triggerExport('lifeCycleFn')
+
+ // Check results
+ expect(mockImporter.mock.lastCall).toEqual([plgUrl])
+ expect(mockExport.mock.calls.length).toBeTruthy()
+ })
+})
\ No newline at end of file
diff --git a/electron/core/plugin-manager/execution/activation-manager.js b/electron/core/plugin-manager/execution/activation-manager.js
new file mode 100644
index 000000000..01eeed2a2
--- /dev/null
+++ b/electron/core/plugin-manager/execution/activation-manager.js
@@ -0,0 +1,88 @@
+import Activation from "./Activation.js"
+
+/**
+ * This object contains a register of plugin registrations to an activation points, and the means to work with them.
+ * @namespace activationPoints
+ */
+
+/**
+ * @constant {Array.} activationRegister
+ * @private
+ * Store of activations used by the consumer
+ */
+const activationRegister = []
+
+/**
+ * Register a plugin with its activation points (as defined in its manifest).
+ * @param {Plugin} plugin plugin object as provided by the main process.
+ * @returns {void}
+ * @alias activationPoints.register
+ */
+export function register(plugin) {
+ if (!Array.isArray(plugin.activationPoints)) throw new Error(
+ `Plugin ${plugin.name || 'without name'} does not have any activation points set up in its manifest.`
+ )
+ for (const ap of plugin.activationPoints) {
+ // Ensure plugin is not already registered to activation point
+ const duplicate = activationRegister.findIndex(act =>
+ act.plugin === plugin.name && act.activationPoint === ap
+ )
+
+ // Create new activation and add it to the register
+ if (duplicate < 0) activationRegister.push(new Activation(plugin.name, ap, plugin.url))
+ }
+}
+
+/**
+ * Trigger all activations registered to the given activation point. See {@link Plugin}.
+ * This will call the function with the same name as the activation point on the path specified in the plugin.
+ * @param {string} activationPoint Name of the activation to trigger
+ * @returns {Promise.} Resolves to true when the activations are complete.
+ * @alias activationPoints.trigger
+ */
+export async function trigger(activationPoint) {
+ // Make sure all triggers are complete before returning
+ await Promise.all(
+ // Trigger each relevant activation point from the register and return an array of trigger promises
+ activationRegister.reduce((triggered, act) => {
+ if (act.activationPoint === activationPoint) {
+ triggered.push(act.trigger())
+ }
+ return triggered
+ }, [])
+ )
+ return true
+}
+
+/**
+ * Remove a plugin from the activations register.
+ * @param {string} plugin Name of the plugin to remove.
+ * @returns {void}
+ * @alias activationPoints.remove
+ */
+export function remove(plugin) {
+ let i = activationRegister.length
+ while (i--) {
+ if (activationRegister[i].plugin === plugin) {
+ activationRegister.splice(i, 1)
+ }
+ }
+}
+
+/**
+ * Remove all activations from the activation register.
+ * @returns {void}
+ * @alias activationPoints.clear
+ */
+export function clear() {
+ activationRegister.length = 0
+}
+
+/**
+ * Fetch all activations.
+ * @returns {Array.} Found extension points
+ * @alias activationPoints.get
+ */
+export function get() {
+ return [...activationRegister]
+}
\ No newline at end of file
diff --git a/electron/core/plugin-manager/execution/activation-manager.test.js b/electron/core/plugin-manager/execution/activation-manager.test.js
new file mode 100644
index 000000000..dd7ff9251
--- /dev/null
+++ b/electron/core/plugin-manager/execution/activation-manager.test.js
@@ -0,0 +1,307 @@
+import { setup } from './index'
+import { register, trigger, remove, clear, get } from "./activation-manager";
+import { add } from './extension-manager'
+
+let mockPlugins = {}
+setup({
+ importer(plugin) { return mockPlugins[plugin] }
+})
+
+afterEach(() => {
+ clear()
+ mockPlugins = {}
+})
+
+describe('register', () => {
+ it('should add a new activation point to the register when a new, valid plugin is registered',
+ () => {
+ register({
+ name: 'test',
+ url: 'testPkg',
+ activationPoints: ['ap1', 'ap2'],
+ active: true
+ })
+
+ expect(get()).toEqual([
+ {
+ plugin: 'test',
+ url: 'testPkg',
+ activationPoint: 'ap1',
+ activated: false
+ },
+ {
+ plugin: 'test',
+ url: 'testPkg',
+ activationPoint: 'ap2',
+ activated: false
+ }
+ ])
+ }
+ )
+
+ it('should not add an activation point to the register when an existing, valid plugin is registered',
+ () => {
+ register({
+ name: 'test',
+ url: 'testPkg',
+ activationPoints: ['ap1', 'ap2'],
+ active: true
+ })
+
+ register({
+ name: 'test',
+ url: 'testPkg',
+ activationPoints: ['ap2', 'ap3'],
+ active: true
+ })
+
+ expect(get()).toEqual([
+ {
+ plugin: 'test',
+ url: 'testPkg',
+ activationPoint: 'ap1',
+ activated: false
+ },
+ {
+ plugin: 'test',
+ url: 'testPkg',
+ activationPoint: 'ap2',
+ activated: false
+ },
+ {
+ plugin: 'test',
+ url: 'testPkg',
+ activationPoint: 'ap3',
+ activated: false
+ },
+ ])
+ }
+ )
+
+ it('should throw an error when an invalid plugin is registered',
+ () => {
+ const noActivationPoints = () => register({
+ name: 'test',
+ url: 'testPkg',
+ active: true
+ })
+
+ expect(noActivationPoints).toThrow(/does not have any activation points set up in its manifest/)
+ }
+ )
+})
+
+describe('trigger', () => {
+ it('should trigger all and only the activations with for the given execution point on triggering an execution, using the defined importer',
+ async () => {
+ const triggered = []
+
+ mockPlugins.plugin1 = {
+ ap1() { triggered.push('plugin1-ap1') }
+ }
+ mockPlugins.plugin2 = {
+ ap2() { triggered.push('plugin2-ap2') }
+ }
+ mockPlugins.plugin3 = {
+ ap1() { triggered.push('plugin3-ap1') },
+ ap2() { triggered.push('plugin3-ap2') }
+ }
+
+ register({
+ name: 'plugin1',
+ url: 'plugin1',
+ activationPoints: ['ap1'],
+ active: true
+ })
+ register({
+ name: 'plugin2',
+ url: 'plugin2',
+ activationPoints: ['ap2'],
+ active: true
+ })
+ register({
+ name: 'plugin3',
+ url: 'plugin3',
+ activationPoints: ['ap1', 'ap2'],
+ active: true
+ })
+
+ await trigger('ap1')
+
+ expect(triggered).toEqual(['plugin1-ap1', 'plugin3-ap1'])
+ }
+ )
+
+ it('should return an error if an activation point is triggered on a plugin that does not include it',
+ async () => {
+ mockPlugins.plugin1 = {
+ wrongAp() { }
+ }
+
+ register({
+ name: 'plugin1',
+ url: 'plugin1',
+ activationPoints: ['ap1']
+ })
+
+ await expect(() => trigger('ap1')).rejects.toThrow(/was triggered but does not exist on plugin/)
+ }
+ )
+
+ it('should provide the registered extension points to the triggered activation point if presetEPs is set to true in the setup',
+ async () => {
+ setup({
+ importer(plugin) { return mockPlugins[plugin] },
+ presetEPs: true,
+ })
+
+ let ap1Res
+
+ mockPlugins.plugin1 = {
+ ap1: eps => ap1Res = eps
+ }
+ register({
+ name: 'plugin1',
+ url: 'plugin1',
+ activationPoints: ['ap1']
+ })
+
+ add('ep1')
+ add('ep2')
+
+ await trigger('ap1')
+
+ expect(ap1Res.ep1.constructor.name).toEqual('ExtensionPoint')
+ expect(ap1Res.ep2.constructor.name).toEqual('ExtensionPoint')
+ }
+ )
+
+ it('should allow registration, execution and serial execution of execution points when an activation point is triggered if presetEPs is set to false in the setup',
+ async () => {
+ setup({
+ importer(plugin) { return mockPlugins[plugin] },
+ })
+
+ let ap1Res
+
+ mockPlugins.plugin1 = {
+ ap1: eps => ap1Res = eps
+ }
+ register({
+ name: 'plugin1',
+ url: 'plugin1',
+ activationPoints: ['ap1']
+ })
+
+ await trigger('ap1')
+
+ expect(typeof ap1Res.register).toBe('function')
+ expect(typeof ap1Res.execute).toBe('function')
+ expect(typeof ap1Res.executeSerial).toBe('function')
+ }
+ )
+
+ it('should not provide any reference to extension points during activation point triggering if presetEPs is set to null in the setup',
+ async () => {
+ setup({
+ importer(plugin) { return mockPlugins[plugin] },
+ presetEPs: null,
+ })
+
+ let ap1Res = true
+
+ mockPlugins.plugin1 = {
+ ap1: eps => ap1Res = eps
+ }
+ register({
+ name: 'plugin1',
+ url: 'plugin1',
+ activationPoints: ['ap1']
+ })
+
+ await trigger('ap1')
+
+ expect(ap1Res).not.toBeDefined()
+ }
+ )
+})
+
+describe('remove and clear', () => {
+
+ beforeEach(() => {
+ register({
+ name: 'plugin1',
+ url: 'plugin1',
+ activationPoints: ['ap1', 'ap2'],
+ active: true
+ })
+
+ register({
+ name: 'plugin2',
+ url: 'plugin2',
+ activationPoints: ['ap2', 'ap3'],
+ active: true
+ })
+ })
+ it('should remove all and only the activations for the given plugin from the register when removing activations',
+ () => {
+ remove('plugin1')
+
+ expect(get()).toEqual([
+ {
+ plugin: 'plugin2',
+ url: 'plugin2',
+ activationPoint: 'ap2',
+ activated: false
+ },
+ {
+ plugin: 'plugin2',
+ url: 'plugin2',
+ activationPoint: 'ap3',
+ activated: false
+ },
+ ])
+ }
+ )
+
+ it('should not remove any activations from the register if no plugin name is provided',
+ () => {
+ remove()
+
+ expect(get()).toEqual([
+ {
+ plugin: 'plugin1',
+ url: 'plugin1',
+ activationPoint: 'ap1',
+ activated: false
+ },
+ {
+ plugin: 'plugin1',
+ url: 'plugin1',
+ activationPoint: 'ap2',
+ activated: false
+ },
+ {
+ plugin: 'plugin2',
+ url: 'plugin2',
+ activationPoint: 'ap2',
+ activated: false
+ },
+ {
+ plugin: 'plugin2',
+ url: 'plugin2',
+ activationPoint: 'ap3',
+ activated: false
+ },
+ ])
+ }
+ )
+
+ it('should remove all activations from the register when clearing the register',
+ () => {
+ clear()
+
+ expect(get()).toEqual([])
+ }
+ )
+})
diff --git a/electron/core/plugin-manager/execution/extension-manager.js b/electron/core/plugin-manager/execution/extension-manager.js
new file mode 100644
index 000000000..48b2e893a
--- /dev/null
+++ b/electron/core/plugin-manager/execution/extension-manager.js
@@ -0,0 +1,98 @@
+/**
+ * This object contains a register of {@link ExtensionPoint|extension points} and the means to work with them.
+ * @namespace extensionPoints
+ */
+
+import ExtensionPoint from "./ExtensionPoint.js"
+
+/**
+ * @constant {Object.} extensionPoints
+ * @private
+ * Register of extension points created by the consumer
+ */
+const _extensionPoints = {}
+
+/**
+ * Create new extension point and add it to the registry.
+ * @param {string} name Name of the extension point.
+ * @returns {void}
+ * @alias extensionPoints.add
+ */
+export function add(name) {
+ _extensionPoints[name] = new ExtensionPoint(name)
+}
+
+/**
+ * Remove an extension point from the registry.
+ * @param {string} name Name of the extension point
+ * @returns {void}
+ * @alias extensionPoints.remove
+ */
+export function remove(name) {
+ delete _extensionPoints[name]
+}
+
+/**
+ * Create extension point if it does not exist and then register the given extension to it.
+ * @param {string} ep Name of the extension point.
+ * @param {string} extension Unique name for the extension.
+ * @param {Object|Callback} response Object to be returned or function to be called by the extension point.
+ * @param {number} [priority=0] Order priority for execution used for executing in serial.
+ * @returns {void}
+ * @alias extensionPoints.register
+ */
+export function register(ep, extension, response, priority) {
+ if (!_extensionPoints[ep]) add(ep)
+ if (_extensionPoints[ep].register) {
+ _extensionPoints[ep].register(extension, response, priority)
+ }
+}
+
+/**
+ * Remove extensions matching regular expression from all extension points.
+ * @param {RegExp} name Matcher for the name of the extension to remove.
+ * @alias extensionPoints.unregisterAll
+ */
+export function unregisterAll(name) {
+ for (const ep in _extensionPoints) _extensionPoints[ep].unregister(name)
+}
+
+/**
+ * Fetch extension point by name. or all extension points if no name is given.
+ * @param {string} [ep] Extension point to return
+ * @returns {Object. | ExtensionPoint} Found extension points
+ * @alias extensionPoints.get
+ */
+export function get(ep) {
+ return (ep ? _extensionPoints[ep] : { ..._extensionPoints })
+}
+
+/**
+ * Call all the extensions registered to an extension point synchronously. See execute on {@link ExtensionPoint}.
+ * Call this at the point in the base code where you want it to be extended.
+ * @param {string} name Name of the extension point to call
+ * @param {*} [input] Parameter to provide to the extensions if they are a function
+ * @returns {Array} Result of Promise.all or Promise.allSettled depending on exitOnError
+ * @alias extensionPoints.execute
+ */
+export function execute(name, input) {
+ if (!_extensionPoints[name] || !_extensionPoints[name].execute) throw new Error(
+ `The extension point "${name}" is not a valid extension point`
+ )
+ return _extensionPoints[name].execute(input)
+}
+
+/**
+ * Calls all the extensions registered to the extension point in serial. See executeSerial on {@link ExtensionPoint}
+ * Call this at the point in the base code where you want it to be extended.
+ * @param {string} name Name of the extension point to call
+ * @param {*} [input] Parameter to provide to the extensions if they are a function
+ * @returns {Promise.<*>} Result of the last extension that was called
+ * @alias extensionPoints.executeSerial
+ */
+export function executeSerial(name, input) {
+ if (!_extensionPoints[name] || !_extensionPoints[name].executeSerial) throw new Error(
+ `The extension point "${name}" is not a valid extension point`
+ )
+ return _extensionPoints[name].executeSerial(input)
+}
diff --git a/electron/core/plugin-manager/execution/extension-manager.test.js b/electron/core/plugin-manager/execution/extension-manager.test.js
new file mode 100644
index 000000000..dd934f872
--- /dev/null
+++ b/electron/core/plugin-manager/execution/extension-manager.test.js
@@ -0,0 +1,116 @@
+import { add, remove, register, get, execute, executeSerial, unregisterAll } from './extension-manager'
+import ExtensionPoint from './ExtensionPoint'
+
+beforeEach(() => {
+ add('ep1')
+ add('ep2')
+})
+
+afterEach(() => {
+ remove('ep1')
+ remove('ep2')
+ remove('ep3')
+})
+
+describe('get', () => {
+ it('should return the extension point with the given name if it exists', () => {
+ expect(get('ep1')).toBeInstanceOf(ExtensionPoint)
+ })
+
+ it('should return all extension points if no name is provided', () => {
+ expect(get()).toEqual(expect.objectContaining({ ep1: expect.any(ExtensionPoint) }))
+ expect(get()).toEqual(expect.objectContaining({ ep2: expect.any(ExtensionPoint) }))
+ })
+})
+
+describe('Add and remove', () => {
+ it('should add a new extension point with the given name using the add function', () => {
+ add('ep1')
+
+ expect(get('ep1')).toBeInstanceOf(ExtensionPoint)
+ })
+
+ it('should remove only the extension point with the given name using the remove function', () => {
+ remove('ep1')
+
+ expect(get()).not.toEqual(expect.objectContaining({ ep1: expect.anything() }))
+ expect(get()).toEqual(expect.objectContaining({ ep2: expect.any(ExtensionPoint) }))
+ })
+
+ it('should not remove any extension points if no name is provided using the remove function', () => {
+ remove()
+
+ expect(get()).toEqual(expect.objectContaining({ ep1: expect.any(ExtensionPoint) }))
+ expect(get()).toEqual(expect.objectContaining({ ep2: expect.any(ExtensionPoint) }))
+ })
+})
+
+describe('register', () => {
+ it('should register an extension to an existing extension point if the point has already been created', () => {
+ register('ep1', 'extension1', { foo: 'bar' })
+
+ expect(get('ep1')._extensions).toContainEqual(expect.objectContaining({ name: 'extension1' }))
+ })
+
+ it('should create an extension point and register an extension to it if the point has not yet been created', () => {
+ register('ep3', 'extension1', { foo: 'bar' })
+
+ expect(get('ep3')._extensions).toContainEqual(expect.objectContaining({ name: 'extension1' }))
+ })
+})
+
+describe('unregisterAll', () => {
+ it('should unregister all extension points matching the give name regex', () => {
+ // Register example extensions
+ register('ep1', 'remove1', { foo: 'bar' })
+ register('ep2', 'remove2', { foo: 'bar' })
+ register('ep1', 'keep', { foo: 'bar' })
+
+ // Remove matching extensions
+ unregisterAll(/remove/)
+
+ // Extract all registered extensions
+ const eps = Object.values(get()).map(ep => ep._extensions)
+ const extensions = eps.flat()
+
+ // Test extracted extensions
+ expect(extensions).toContainEqual(expect.objectContaining({ name: 'keep' }))
+ expect(extensions).not.toContainEqual(expect.objectContaining({ name: 'ep1' }))
+ expect(extensions).not.toContainEqual(expect.objectContaining({ name: 'ep2' }))
+ })
+})
+
+describe('execute', () => {
+ it('should execute the extensions registered to the named extension point with the provided input', () => {
+ const result = []
+ register('ep1', 'extension1', input => result.push(input + 'bar'))
+ register('ep1', 'extension2', input => result.push(input + 'baz'))
+
+ execute('ep1', 'foo')
+
+ expect(result).toEqual(['foobar', 'foobaz'])
+ })
+
+ it('should throw an error if the named extension point does not exist', () => {
+ register('ep1', 'extension1', { foo: 'bar' })
+
+ expect(() => execute('ep3')).toThrow(/not a valid extension point/)
+ })
+})
+
+describe('executeSerial', () => {
+ it('should execute the extensions in serial registered to the named extension point with the provided input', async () => {
+ register('ep1', 'extension1', input => input + 'bar')
+ register('ep1', 'extension2', input => input + 'baz')
+
+ const result = await executeSerial('ep1', 'foo')
+
+ expect(result).toEqual('foobarbaz')
+ })
+
+ it('should throw an error if the named extension point does not exist', () => {
+ register('ep1', 'extension1', { foo: 'bar' })
+
+ expect(() => executeSerial('ep3')).toThrow(/not a valid extension point/)
+ })
+})
diff --git a/electron/core/plugin-manager/execution/facade.js b/electron/core/plugin-manager/execution/facade.js
new file mode 100644
index 000000000..d153e283c
--- /dev/null
+++ b/electron/core/plugin-manager/execution/facade.js
@@ -0,0 +1,148 @@
+/**
+ * Helper functions to access the plugin management in the main process.
+ * Note that the facade needs to be imported separately as "pluggable-electron/facade" as described above.
+ * It is then available on the global window object as describe in the {@link https://www.electronjs.org/docs/api/context-bridge|Electron documentation}
+ * @namespace plugins
+ */
+
+import Plugin from "./Plugin";
+import { register } from "./activation-manager";
+
+/**
+ * @typedef {Object.} installOptions The {@link https://www.npmjs.com/package/pacote|pacote options}
+ * used to install the plugin.
+ * @param {string} specifier the NPM specifier that identifies the package.
+ * @param {boolean} [activate=true] Whether this plugin should be activated after installation.
+ */
+
+/**
+ * Install a new plugin.
+ * @param {Array.} plugins A list of NPM specifiers, or installation configuration objects.
+ * @returns {Promise. | false>} plugin as defined by the main process. Has property cancelled set to true if installation was cancelled in the main process.
+ * @alias plugins.install
+ */
+export async function install(plugins) {
+ if (typeof window === "undefined") {
+ return;
+ }
+ const plgList = await window.pluggableElectronIpc.install(plugins);
+ if (plgList.cancelled) return false;
+ return plgList.map((plg) => {
+ const plugin = new Plugin(
+ plg.name,
+ plg.url,
+ plg.activationPoints,
+ plg.active
+ );
+ register(plugin);
+ return plugin;
+ });
+}
+
+/**
+ * Uninstall provided plugins
+ * @param {Array.} plugins List of names of plugins to uninstall.
+ * @param {boolean} reload Whether to reload all renderers after updating the plugins.
+ * @returns {Promise.} Whether uninstalling the plugins was successful.
+ * @alias plugins.uninstall
+ */
+export function uninstall(plugins, reload = true) {
+ if (typeof window === "undefined") {
+ return;
+ }
+ return window.pluggableElectronIpc.uninstall(plugins, reload);
+}
+
+/**
+ * Fetch a list of all the active plugins.
+ * @returns {Promise.>} List of plugins as defined by the main process.
+ * @alias plugins.getActive
+ */
+export async function getActive() {
+ if (typeof window === "undefined") {
+ return;
+ }
+ const plgList = await window.pluggableElectronIpc.getActive();
+ return plgList.map(
+ (plugin) =>
+ new Plugin(
+ plugin.name,
+ plugin.url,
+ plugin.activationPoints,
+ plugin.active
+ )
+ );
+}
+
+/**
+ * Register all the active plugins.
+ * @returns {Promise.>} List of plugins as defined by the main process.
+ * @alias plugins.registerActive
+ */
+export async function registerActive() {
+ if (typeof window === "undefined") {
+ return;
+ }
+ const plgList = await window.pluggableElectronIpc.getActive();
+ plgList.forEach((plugin) =>
+ register(
+ new Plugin(
+ plugin.name,
+ plugin.url,
+ plugin.activationPoints,
+ plugin.active
+ )
+ )
+ );
+}
+
+/**
+ * Update provided plugins to its latest version.
+ * @param {Array.} plugins List of plugins to update by name.
+ * @param {boolean} reload Whether to reload all renderers after updating the plugins.
+ * @returns {Promise.>} Updated plugin as defined by the main process.
+ * @alias plugins.update
+ */
+export async function update(plugins, reload = true) {
+ if (typeof window === "undefined") {
+ return;
+ }
+ const plgList = await window.pluggableElectronIpc.update(plugins, reload);
+ return plgList.map(
+ (plugin) =>
+ new Plugin(
+ plugin.name,
+ plugin.url,
+ plugin.activationPoints,
+ plugin.active
+ )
+ );
+}
+
+/**
+ * Check if an update is available for provided plugins.
+ * @param {Array.} plugin List of plugin names to check for available updates.
+ * @returns {Object.} Object with plugins as keys and new version if update is available or false as values.
+ * @alias plugins.updatesAvailable
+ */
+export function updatesAvailable(plugin) {
+ if (typeof window === "undefined") {
+ return;
+ }
+ return window.pluggableElectronIpc.updatesAvailable(plugin);
+}
+
+/**
+ * Toggle a plugin's active state. This determines if a plugin should be loaded in initialisation.
+ * @param {String} plugin Plugin to toggle.
+ * @param {boolean} active Whether plugin should be activated (true) or deactivated (false).
+ * @returns {Promise.} Updated plugin as defined by the main process.
+ * @alias plugins.toggleActive
+ */
+export async function toggleActive(plugin, active) {
+ if (typeof window === "undefined") {
+ return;
+ }
+ const plg = await window.pluggableElectronIpc.toggleActive(plugin, active);
+ return new Plugin(plg.name, plg.url, plg.activationPoints, plg.active);
+}
diff --git a/electron/core/plugin-manager/execution/import-manager.js b/electron/core/plugin-manager/execution/import-manager.js
new file mode 100644
index 000000000..0d25297a2
--- /dev/null
+++ b/electron/core/plugin-manager/execution/import-manager.js
@@ -0,0 +1,77 @@
+import {
+ get as getEPs,
+ register,
+ execute,
+ executeSerial,
+} from "./extension-manager.js";
+/**
+ * Used to import a plugin entry point.
+ * Ensure your bundler does no try to resolve this import as the plugins are not known at build time.
+ * @callback importer
+ * @param {string} entryPoint File to be imported.
+ * @returns {module} The module containing the entry point function.
+ */
+
+/**
+ * @private
+ * @type {importer}
+ */
+export let importer;
+
+/**
+ * @private
+ * Set the plugin importer function.
+ * @param {importer} callback Callback to import plugins.
+ */
+export function setImporter(callback) {
+ importer = callback;
+}
+
+/**
+ * @private
+ * @type {Boolean|null}
+ */
+export let presetEPs;
+
+/**
+ * @private
+ * Define how extension points are accessed.
+ * @param {Boolean|null} peps Whether extension points are predefined.
+ */
+export function definePresetEps(peps) {
+ presetEPs = peps === null || peps === true ? peps : false;
+}
+
+/**
+ * @private
+ * Call exported function on imported module.
+ * @param {string} url @see Activation
+ * @param {string} exp Export to call
+ * @param {string} [plugin] @see Activation
+ */
+export async function callExport(url, exp, plugin) {
+ if (!importer) throw new Error("Importer callback has not been set");
+
+ const main = await importer(url);
+ if (!main || typeof main[exp] !== "function") {
+ throw new Error(
+ `Activation point "${exp}" was triggered but does not exist on ${
+ plugin ? "plugin " + plugin : "unknown plugin"
+ }`
+ );
+ }
+ const activate = main[exp];
+ switch (presetEPs) {
+ case true:
+ activate(getEPs());
+ break;
+
+ case null:
+ activate();
+ break;
+
+ default:
+ activate({ register, execute, executeSerial });
+ break;
+ }
+}
diff --git a/electron/core/plugin-manager/execution/index.js b/electron/core/plugin-manager/execution/index.js
new file mode 100644
index 000000000..83fe0ce17
--- /dev/null
+++ b/electron/core/plugin-manager/execution/index.js
@@ -0,0 +1,24 @@
+import { definePresetEps, setImporter } from "./import-manager.js";
+
+export * as extensionPoints from "./extension-manager.js";
+export * as activationPoints from "./activation-manager.js";
+export * as plugins from "./facade.js";
+export { default as ExtensionPoint } from "./ExtensionPoint.js";
+
+if (typeof window !== "undefined" && !window.pluggableElectronIpc)
+ console.warn(
+ "Facade is not registered in preload. Facade functions will throw an error if used."
+ );
+
+/**
+ * Set the renderer options for Pluggable Electron. Should be called before any other Pluggable Electron function in the renderer
+ * @param {Object} options
+ * @param {importer} options.importer The callback function used to import the plugin entry points.
+ * @param {Boolean|null} [options.presetEPs=false] Whether the Extension Points have been predefined (true),
+ * can be created on the fly(false) or should not be provided through the input at all (null).
+ * @returns {void}
+ */
+export function setup(options) {
+ setImporter(options.importer);
+ definePresetEps(options.presetEPs);
+}
diff --git a/electron/core/plugin-manager/execution/index.test.js b/electron/core/plugin-manager/execution/index.test.js
new file mode 100644
index 000000000..75f8e5933
--- /dev/null
+++ b/electron/core/plugin-manager/execution/index.test.js
@@ -0,0 +1,28 @@
+import { setup } from "."
+import { importer, presetEPs } from "./import-manager"
+
+describe('setup', () => {
+ const mockImporter = jest.fn()
+
+ it('should store the importer function', () => {
+ setup({ importer: mockImporter })
+
+ expect(importer).toBe(mockImporter)
+ })
+
+ it('should set presetEPS to false if not provided', () => {
+ expect(presetEPs).toBe(false)
+ })
+
+ it('should set presetEPS to the provided value if it is true', () => {
+ setup({ presetEPs: true })
+
+ expect(presetEPs).toBe(true)
+ })
+
+ it('should set presetEPS to the provided value if it is null', () => {
+ setup({ presetEPs: null })
+
+ expect(presetEPs).toBe(null)
+ })
+})
\ No newline at end of file
diff --git a/electron/core/plugin-manager/facade/index.js b/electron/core/plugin-manager/facade/index.js
new file mode 100644
index 000000000..2257fbd4d
--- /dev/null
+++ b/electron/core/plugin-manager/facade/index.js
@@ -0,0 +1,30 @@
+import { ipcRenderer, contextBridge } from "electron"
+
+export default function useFacade() {
+ const interfaces = {
+ install(plugins) {
+ return ipcRenderer.invoke('pluggable:install', plugins)
+ },
+ uninstall(plugins, reload) {
+ return ipcRenderer.invoke('pluggable:uninstall', plugins, reload)
+ },
+ getActive() {
+ return ipcRenderer.invoke('pluggable:getActivePlugins')
+ },
+ update(plugins, reload) {
+ return ipcRenderer.invoke('pluggable:update', plugins, reload)
+ },
+ updatesAvailable(plugin) {
+ return ipcRenderer.invoke('pluggable:updatesAvailable', plugin)
+ },
+ toggleActive(plugin, active) {
+ return ipcRenderer.invoke('pluggable:togglePluginActive', plugin, active)
+ },
+ }
+
+ if (contextBridge) {
+ contextBridge.exposeInMainWorld('pluggableElectronIpc', interfaces)
+ }
+
+ return interfaces
+}
diff --git a/electron/core/plugin-manager/facade/index.test.js b/electron/core/plugin-manager/facade/index.test.js
new file mode 100644
index 000000000..0e4bae62d
--- /dev/null
+++ b/electron/core/plugin-manager/facade/index.test.js
@@ -0,0 +1,196 @@
+jest.mock('electron', () => {
+ const handlers = {}
+
+ return {
+ ipcMain: {
+ handle(channel, callback) {
+ handlers[channel] = callback
+ }
+ },
+ ipcRenderer: {
+ invoke(channel, ...args) {
+ return Promise.resolve(handlers[channel].call(undefined, 'event', ...args))
+ }
+ },
+ webContents: {
+ getAllWebContents: jest.fn(() => [])
+ },
+ contextBridge: {
+ exposeInMainWorld(key, val) {
+ global.window = { [key]: val }
+ }
+ }
+ }
+})
+
+jest.mock('../pluginMgr/store', () => {
+ const setActive = jest.fn(() => true)
+ const uninstall = jest.fn()
+ const update = jest.fn(() => true)
+ const isUpdateAvailable = jest.fn(() => false)
+
+ class Plugin {
+ constructor(name) {
+ this.name = name
+ this.activationPoints = ['test']
+ }
+ setActive = setActive
+ uninstall = uninstall
+ update = update
+ isUpdateAvailable = isUpdateAvailable
+ }
+
+ return {
+ getPlugin: jest.fn(name => new Plugin(name)),
+ getActivePlugins: jest.fn(() => [new Plugin('test')]),
+ installPlugins: jest.fn(async plugins => plugins.map(name => new Plugin(name))),
+ removePlugin: jest.fn()
+ }
+})
+
+const { rmSync } = require('fs')
+const { webContents } = require('electron')
+const useFacade = require('./index')
+const { getActive, install, toggleActive, uninstall, update, updatesAvailable, registerActive } = require('../execution/facade')
+const { setPluginsPath, setConfirmInstall } = require('../pluginMgr/globals')
+const router = require('../pluginMgr/router')
+const { getPlugin, getActivePlugins, removePlugin } = require('../pluginMgr/store')
+const { get: getActivations } = require('../execution/activation-manager')
+
+const pluginsPath = './testPlugins'
+const confirmInstall = jest.fn(() => true)
+
+beforeAll(async () => {
+ setPluginsPath(pluginsPath)
+ router()
+ useFacade()
+})
+
+afterAll(() => {
+ rmSync(pluginsPath, { recursive: true })
+})
+
+describe('install', () => {
+ it('should return cancelled state if the confirmPlugin callback returns falsy', async () => {
+ setConfirmInstall(() => false)
+ const plugins = await install(['test-install'])
+ expect(plugins).toEqual(false)
+ })
+
+ it('should perform a security check of the install using confirmInstall if facade is used', async () => {
+ setConfirmInstall(confirmInstall)
+ await install(['test-install'])
+ expect(confirmInstall.mock.calls.length).toBeTruthy()
+ })
+
+ it('should register all installed plugins', async () => {
+ const pluginName = 'test-install'
+ await install([pluginName])
+ expect(getActivations()).toContainEqual(expect.objectContaining({
+ plugin: pluginName
+ }))
+ })
+
+ it('should return a list of plugins', async () => {
+ setConfirmInstall(confirmInstall)
+ const pluginName = 'test-install'
+ const plugins = await install([pluginName])
+ expect(plugins).toEqual([expect.objectContaining({ name: pluginName })])
+ })
+})
+
+describe('uninstall', () => {
+ it('should uninstall all plugins with the provided name, remove it from the store and refresh all renderers', async () => {
+ // Reset mock functions
+ const mockUninstall = getPlugin().uninstall
+ mockUninstall.mockClear()
+ removePlugin.mockClear()
+ webContents.getAllWebContents.mockClear()
+ getPlugin.mockClear()
+
+ // Uninstall plugins
+ const specs = ['test-uninstall-1', 'test-uninstall-2']
+ await uninstall(specs)
+
+ // Test result
+ expect(getPlugin.mock.calls).toEqual(specs.map(spec => [spec]))
+ expect(mockUninstall.mock.calls.length).toBeTruthy()
+ expect(removePlugin.mock.calls.length).toBeTruthy()
+ expect(webContents.getAllWebContents.mock.calls.length).toBeTruthy()
+ })
+})
+
+describe('getActive', () => {
+ it('should return all active plugins', async () => {
+ getActivePlugins.mockClear()
+ await getActive()
+ expect(getActivePlugins.mock.calls.length).toBeTruthy()
+ })
+})
+
+describe('registerActive', () => {
+ it('should register all active plugins', async () => {
+ await registerActive()
+ expect(getActivations()).toContainEqual(expect.objectContaining({
+ plugin: 'test'
+ }))
+ })
+})
+
+describe('update', () => {
+ const specs = ['test-uninstall-1', 'test-uninstall-2']
+ const mockUpdate = getPlugin().update
+
+ beforeAll(async () => {
+ // Reset mock functions
+ mockUpdate.mockClear()
+ webContents.getAllWebContents.mockClear()
+ getPlugin.mockClear()
+
+ // Update plugins
+ await update(specs)
+ })
+
+ it('should call the update function on all provided plugins', async () => {
+ // Check result
+ expect(getPlugin.mock.calls).toEqual(specs.map(spec => [spec]))
+ expect(mockUpdate.mock.calls.length).toBe(2)
+ })
+
+ it('should reload the renderers if reload is true', () => {
+ expect(webContents.getAllWebContents.mock.calls.length).toBeTruthy()
+ })
+
+ it('should not reload the renderer if reload is false', async () => {
+ webContents.getAllWebContents.mockClear()
+ await update(['test-uninstall'], false)
+ expect(webContents.getAllWebContents.mock.calls.length).toBeFalsy()
+ })
+})
+
+describe('toggleActive', () => {
+ it('call the setActive function on the plugin with the provided name, with the provided active state', async () => {
+ await toggleActive('test-toggleActive', true)
+ expect(getPlugin.mock.lastCall).toEqual(['test-toggleActive'])
+ const mockSetActive = getPlugin().setActive
+ expect(mockSetActive.mock.lastCall).toEqual([true])
+ })
+})
+
+describe('updatesAvailable', () => {
+ it('should return the new versions for the provided plugins if provided', async () => {
+ // Reset mock functions
+ const mockIsUpdAvailable = getPlugin().isUpdateAvailable
+ mockIsUpdAvailable.mockClear()
+ getPlugin.mockClear()
+
+ // Get available updates
+ const testPlugin1 = 'test-plugin-1'
+ const testPlugin2 = 'test-update-2'
+ const updates = await updatesAvailable([testPlugin1, testPlugin2])
+ expect(updates).toEqual({
+ [testPlugin1]: false,
+ [testPlugin2]: false,
+ })
+ })
+})
diff --git a/electron/core/plugin-manager/pluginMgr/Plugin.js b/electron/core/plugin-manager/pluginMgr/Plugin.js
new file mode 100644
index 000000000..3e9ca609f
--- /dev/null
+++ b/electron/core/plugin-manager/pluginMgr/Plugin.js
@@ -0,0 +1,189 @@
+import { rmdir } from "fs/promises"
+import { resolve, join } from "path"
+import { manifest, extract } from "pacote"
+import Arborist from '@npmcli/arborist'
+
+import { pluginsPath } from "./globals"
+
+/**
+ * An NPM package that can be used as a Pluggable Electron plugin.
+ * Used to hold all the information and functions necessary to handle the plugin lifecycle.
+ */
+class Plugin {
+ /**
+ * @property {string} origin Original specification provided to fetch the package.
+ * @property {Object} installOptions Options provided to pacote when fetching the manifest.
+ * @property {name} name The name of the plugin as defined in the manifest.
+ * @property {string} url Electron URL where the package can be accessed.
+ * @property {string} version Version of the package as defined in the manifest.
+ * @property {Array} activationPoints List of {@link ./Execution-API#activationPoints|activation points}.
+ * @property {string} main The entry point as defined in the main entry of the manifest.
+ */
+
+ /** @private */
+ _active = false
+
+ /**
+ * @private
+ * @property {Object.} #listeners A list of callbacks to be executed when the Plugin is updated.
+ */
+ #listeners = {}
+
+ /**
+ * Set installOptions with defaults for options that have not been provided.
+ * @param {string} [origin] Original specification provided to fetch the package.
+ * @param {Object} [options] Options provided to pacote when fetching the manifest.
+ */
+ constructor(origin, options = {}) {
+ const defaultOpts = {
+ version: false,
+ fullMetadata: false,
+ Arborist
+ }
+
+ this.origin = origin
+ this.installOptions = { ...defaultOpts, ...options }
+ }
+
+ /**
+ * Package name with version number.
+ * @type {string}
+ */
+ get specifier() {
+ return this.origin + (this.installOptions.version ? '@' + this.installOptions.version : '')
+ }
+
+ /**
+ * Whether the plugin should be registered with its activation points.
+ * @type {boolean}
+ */
+ get active() {
+ return this._active
+ }
+
+ /**
+ * Set Package details based on it's manifest
+ * @returns {Promise.} Resolves to true when the action completed
+ */
+ async #getManifest() {
+ // Get the package's manifest (package.json object)
+ try {
+ const mnf = await manifest(this.specifier, this.installOptions)
+
+ // set the Package properties based on the it's manifest
+ this.name = mnf.name
+ this.version = mnf.version
+ this.activationPoints = mnf.activationPoints || null
+ this.main = mnf.main
+
+ } catch (error) {
+ throw new Error(`Package ${this.origin} does not contain a valid manifest: ${error}`)
+ }
+
+ return true
+ }
+
+ /**
+ * Extract plugin to plugins folder.
+ * @returns {Promise.} This plugin
+ * @private
+ */
+ async _install() {
+ try {
+ // import the manifest details
+ await this.#getManifest()
+
+ // Install the package in a child folder of the given folder
+ await extract(this.specifier, join(pluginsPath, this.name), this.installOptions)
+
+ if (!Array.isArray(this.activationPoints))
+ throw new Error('The plugin does not contain any activation points')
+
+ // Set the url using the custom plugins protocol
+ this.url = `plugin://${this.name}/${this.main}`
+
+ this.#emitUpdate()
+
+ } catch (err) {
+ // Ensure the plugin is not stored and the folder is removed if the installation fails
+ this.setActive(false)
+ throw err
+ }
+
+ return [this]
+ }
+
+ /**
+ * Subscribe to updates of this plugin
+ * @param {string} name name of the callback to register
+ * @param {callback} cb The function to execute on update
+ */
+ subscribe(name, cb) {
+ this.#listeners[name] = cb
+ }
+
+ /**
+ * Remove subscription
+ * @param {string} name name of the callback to remove
+ */
+ unsubscribe(name) {
+ delete this.#listeners[name]
+ }
+
+ /**
+ * Execute listeners
+ */
+ #emitUpdate() {
+ for (const cb in this.#listeners) {
+ this.#listeners[cb].call(null, this)
+ }
+ }
+
+ /**
+ * Check for updates and install if available.
+ * @param {string} version The version to update to.
+ * @returns {boolean} Whether an update was performed.
+ */
+ async update(version = false) {
+ if (this.isUpdateAvailable()) {
+ this.installOptions.version = version
+ await this._install(false)
+ return true
+ }
+
+ return false
+ }
+
+ /**
+ * Check if a new version of the plugin is available at the origin.
+ * @returns the latest available version if a new version is available or false if not.
+ */
+ async isUpdateAvailable() {
+ const mnf = await manifest(this.origin)
+ return mnf.version !== this.version ? mnf.version : false
+ }
+
+ /**
+ * Remove plugin and refresh renderers.
+ * @returns {Promise}
+ */
+ async uninstall() {
+ const plgPath = resolve(pluginsPath, this.name)
+ await rmdir(plgPath, { recursive: true })
+
+ this.#emitUpdate()
+ }
+
+ /**
+ * Set a plugin's active state. This determines if a plugin should be loaded on initialisation.
+ * @param {boolean} active State to set _active to
+ * @returns {Plugin} This plugin
+ */
+ setActive(active) {
+ this._active = active
+ this.#emitUpdate()
+ return this
+ }
+}
+
+export default Plugin
diff --git a/electron/core/plugin-manager/pluginMgr/Plugin.test.js b/electron/core/plugin-manager/pluginMgr/Plugin.test.js
new file mode 100644
index 000000000..c0326bd06
--- /dev/null
+++ b/electron/core/plugin-manager/pluginMgr/Plugin.test.js
@@ -0,0 +1,212 @@
+import { init } from "."
+import { join } from 'path'
+import Plugin from "./Plugin"
+import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from "fs"
+
+const pluginsDir = './testPlugins'
+const testPluginDir = './testPluginSrc'
+const testPluginName = 'test-plugin'
+const manifest = join(testPluginDir, 'package.json')
+const main = 'index'
+
+/** @type Plugin */
+let plugin
+
+beforeAll(() => {
+ init({
+ confirmInstall: () => true,
+ pluginsPath: pluginsDir,
+ })
+
+ mkdirSync(testPluginDir)
+
+ writeFileSync(manifest, JSON.stringify({
+ name: testPluginName,
+ activationPoints: [],
+ main,
+ }), 'utf8')
+
+ plugin = new Plugin(testPluginDir)
+})
+
+afterAll(() => {
+ rmSync(pluginsDir, { recursive: true })
+ rmSync(testPluginDir, { recursive: true })
+})
+
+
+describe('subscribe', () => {
+ let res = false
+ it('should register the provided callback', () => {
+ plugin.subscribe('test', () => res = true)
+ plugin.setActive(true)
+
+ expect(res).toBeTruthy()
+ })
+})
+
+describe('unsubscribe', () => {
+ it(`should remove the provided callback from the register
+ after which it should not be executed anymore when the plugin is updated`, () => {
+ let res = false
+ plugin.subscribe('test', () => res = true)
+ plugin.unsubscribe('test')
+ plugin.setActive(true)
+
+ expect(res).toBeFalsy()
+ })
+})
+
+describe('install', () => {
+ beforeAll(async () => {
+ await plugin._install()
+ })
+
+ it('should store all the relevant manifest values on the plugin', async () => {
+ expect(plugin).toMatchObject({
+ origin: testPluginDir,
+ installOptions: {
+ version: false,
+ fullMetadata: false,
+ },
+ name: testPluginName,
+ url: `plugin://${testPluginName}/${main}`,
+ activationPoints: []
+ })
+ })
+
+ it('should create a folder for the plugin if it does not yet exist and copy the plugin files to it', () => {
+ expect(existsSync(join(pluginsDir, testPluginName))).toBeTruthy()
+ })
+
+ it('should replace the existing plugin files in the plugin folder if it already exist', async () => {
+ writeFileSync(manifest, JSON.stringify({
+ name: testPluginName,
+ activationPoints: [],
+ main: 'updated',
+ }), 'utf8')
+
+ await plugin._install()
+
+ const savedPkg = JSON.parse(readFileSync(join(pluginsDir, testPluginName, 'package.json')))
+
+ expect(savedPkg.main).toBe('updated')
+ })
+
+ it('should throw an error and the plugin should be set to inactive if no manifest could be found', async () => {
+ rmSync(join(testPluginDir, 'package.json'))
+
+ await expect(() => plugin._install()).rejects.toThrow(/does not contain a valid manifest/)
+ })
+
+ it('should throw an error and the plugin should be set to inactive if plugin does not contain any activation points', async () => {
+ writeFileSync(manifest, JSON.stringify({
+ name: testPluginName,
+ main,
+ }), 'utf8')
+
+ await expect(() => plugin._install()).rejects.toThrow('The plugin does not contain any activation points')
+ expect(plugin.active).toBe(false)
+ })
+})
+
+describe('update', () => {
+ let updatedPlugin
+ let subscription = false
+ let beforeUpd
+
+ beforeAll(async () => {
+ writeFileSync(manifest, JSON.stringify({
+ name: testPluginName,
+ activationPoints: [],
+ version: '0.0.1',
+ main,
+ }), 'utf8')
+
+ await plugin._install()
+
+ plugin.subscribe('test', () => subscription = true)
+ beforeUpd = Object.assign({}, plugin)
+
+ await plugin.update()
+ })
+
+ it('should not do anything if no version update is available', () => {
+ expect(beforeUpd).toMatchObject(plugin)
+ })
+
+ it('should update the plugin files to the latest version if there is a new version available for the plugin', async () => {
+ writeFileSync(manifest, JSON.stringify({
+ name: testPluginName,
+ activationPoints: [],
+ version: '0.0.2',
+ main,
+ }), 'utf8')
+
+ await plugin.update()
+
+ expect(plugin).toMatchObject({
+ origin: testPluginDir,
+ installOptions: {
+ version: false,
+ fullMetadata: false,
+ },
+ name: testPluginName,
+ version: '0.0.2',
+ url: `plugin://${testPluginName}/${main}`,
+ activationPoints: []
+ })
+ })
+
+ it('should execute callbacks subscribed to this plugin, providing the plugin as a parameter', () => {
+ expect(subscription).toBeTruthy()
+ })
+})
+
+describe('isUpdateAvailable', () => {
+ it('should return false if no new version is available', async () => {
+ await expect(plugin.isUpdateAvailable()).resolves.toBe(false)
+ })
+
+ it('should return the latest version number if a new version is available', async () => {
+ writeFileSync(manifest, JSON.stringify({
+ name: testPluginName,
+ activationPoints: [],
+ version: '0.0.3',
+ main,
+ }), 'utf8')
+
+ await expect(plugin.isUpdateAvailable()).resolves.toBe('0.0.3')
+ })
+})
+
+describe('setActive', () => {
+ it('should set the plugin to be active', () => {
+ plugin.setActive(true)
+ expect(plugin.active).toBeTruthy()
+ })
+
+ it('should execute callbacks subscribed to this plugin, providing the plugin as a parameter', () => {
+ let res = false
+ plugin.subscribe('test', () => res = true)
+ plugin.setActive(true)
+
+ expect(res).toBeTruthy()
+ })
+})
+
+describe('uninstall', () => {
+ let subscription = false
+ beforeAll(async () => {
+ plugin.subscribe('test', () => subscription = true)
+ await plugin.uninstall()
+ })
+
+ it('should remove the installed plugin from the plugins folder', () => {
+ expect(existsSync(join(pluginsDir, testPluginName))).toBe(false)
+ })
+
+ it('should execute callbacks subscribed to this plugin, providing the plugin as a parameter', () => {
+ expect(subscription).toBeTruthy()
+ })
+})
diff --git a/electron/core/plugin-manager/pluginMgr/globals.js b/electron/core/plugin-manager/pluginMgr/globals.js
new file mode 100644
index 000000000..a0fc3718d
--- /dev/null
+++ b/electron/core/plugin-manager/pluginMgr/globals.js
@@ -0,0 +1,57 @@
+import { existsSync, mkdirSync, writeFileSync } from "fs"
+import { join, resolve } from "path"
+
+export let pluginsPath = null
+
+/**
+ * @private
+ * Set path to plugins directory and create the directory if it does not exist.
+ * @param {string} plgPath path to plugins directory
+ */
+export function setPluginsPath(plgPath) {
+ // Create folder if it does not exist
+ let plgDir
+ try {
+ plgDir = resolve(plgPath)
+ if (plgDir.length < 2) throw new Error()
+
+ if (!existsSync(plgDir)) mkdirSync(plgDir)
+
+ const pluginsJson = join(plgDir, 'plugins.json')
+ if (!existsSync(pluginsJson)) writeFileSync(pluginsJson, '{}', 'utf8')
+
+ pluginsPath = plgDir
+
+ } catch (error) {
+ throw new Error('Invalid path provided to the plugins folder')
+ }
+
+}
+
+/**
+* @private
+ * Get the path to the plugins.json file.
+ * @returns location of plugins.json
+ */
+export function getPluginsFile() { return join(pluginsPath, 'plugins.json') }
+
+
+export let confirmInstall = function () {
+ return new Error(
+ 'The facade.confirmInstall callback needs to be set in when initializing Pluggable Electron in the main process.'
+ )
+}
+
+/**
+ * @private
+ * Set callback to use as confirmInstall.
+ * @param {confirmInstall} cb Callback
+ */
+export function setConfirmInstall(cb) { confirmInstall = cb }
+
+/**
+ * This function is executed when plugins are installed to verify that the user indeed wants to install the plugin.
+ * @callback confirmInstall
+ * @param {Array.} plg The specifiers used to locate the packages (from NPM or local file)
+ * @returns {Promise} Whether to proceed with the plugin installation
+ */
diff --git a/electron/core/plugin-manager/pluginMgr/index.js b/electron/core/plugin-manager/pluginMgr/index.js
new file mode 100644
index 000000000..ae7bff759
--- /dev/null
+++ b/electron/core/plugin-manager/pluginMgr/index.js
@@ -0,0 +1,123 @@
+import { readFileSync } from "fs"
+import { protocol } from 'electron'
+import { normalize } from "path"
+
+import Plugin from "./Plugin"
+import { getAllPlugins, removePlugin, persistPlugins, installPlugins, getPlugin, getActivePlugins, addPlugin } from "./store"
+import { pluginsPath as storedPluginsPath, setPluginsPath, getPluginsFile, setConfirmInstall } from './globals'
+import router from "./router"
+
+/**
+ * Sets up the required communication between the main and renderer processes.
+ * Additionally sets the plugins up using {@link usePlugins} if a pluginsPath is provided.
+ * @param {Object} options configuration for setting up the renderer facade.
+ * @param {confirmInstall} [options.confirmInstall] Function to validate that a plugin should be installed.
+ * @param {Boolean} [options.useFacade=true] Whether to make a facade to the plugins available in the renderer.
+ * @param {string} [options.pluginsPath] Optional path to the plugins folder.
+ * @returns {pluginManager|Object} A set of functions used to manage the plugin lifecycle if usePlugins is provided.
+ * @function
+ */
+export function init(options) {
+ if (!Object.prototype.hasOwnProperty.call(options, 'useFacade') || options.useFacade) {
+ // Store the confirmInstall function
+ setConfirmInstall(options.confirmInstall)
+ // Enable IPC to be used by the facade
+ router()
+ }
+
+ // Create plugins protocol to serve plugins to renderer
+ registerPluginProtocol()
+
+ // perform full setup if pluginsPath is provided
+ if (options.pluginsPath) {
+ return usePlugins(options.pluginsPath)
+ }
+
+ return {}
+
+}
+
+/**
+ * Create plugins protocol to provide plugins to renderer
+ * @private
+ * @returns {boolean} Whether the protocol registration was successful
+ */
+function registerPluginProtocol() {
+ return protocol.registerFileProtocol('plugin', (request, callback) => {
+ const entry = request.url.substr(8)
+ const url = normalize(storedPluginsPath + entry)
+ callback({ path: url })
+ })
+}
+
+/**
+ * Set Pluggable Electron up to run from the pluginPath folder if it is provided and
+ * load plugins persisted in that folder.
+ * @param {string} pluginsPath Path to the plugins folder. Required if not yet set up.
+ * @returns {pluginManager} A set of functions used to manage the plugin lifecycle.
+ */
+export function usePlugins(pluginsPath) {
+ if (!pluginsPath) throw Error('A path to the plugins folder is required to use Pluggable Electron')
+ // Store the path to the plugins folder
+ setPluginsPath(pluginsPath)
+
+ // Remove any registered plugins
+ for (const plugin of getAllPlugins()) {
+ removePlugin(plugin.name, false)
+ }
+
+ // Read plugin list from plugins folder
+ const plugins = JSON.parse(readFileSync(getPluginsFile()))
+ try {
+ // Create and store a Plugin instance for each plugin in list
+ for (const p in plugins) {
+ loadPlugin(plugins[p])
+ }
+ persistPlugins()
+
+ } catch (error) {
+ // Throw meaningful error if plugin loading fails
+ throw new Error('Could not successfully rebuild list of installed plugins.\n'
+ + error
+ + '\nPlease check the plugins.json file in the plugins folder.')
+ }
+
+ // Return the plugin lifecycle functions
+ return getStore()
+}
+
+/**
+ * Check the given plugin object. If it is marked for uninstalling, the plugin files are removed.
+ * Otherwise a Plugin instance for the provided object is created and added to the store.
+ * @private
+ * @param {Object} plg Plugin info
+ */
+function loadPlugin(plg) {
+ // Create new plugin, populate it with plg details and save it to the store
+ const plugin = new Plugin()
+
+ for (const key in plg) {
+ plugin[key] = plg[key]
+ }
+
+ addPlugin(plugin, false)
+ plugin.subscribe('pe-persist', persistPlugins)
+}
+
+/**
+ * Returns the publicly available store functions.
+ * @returns {pluginManager} A set of functions used to manage the plugin lifecycle.
+ */
+export function getStore() {
+ if (!storedPluginsPath) {
+ throw new Error('The plugin path has not yet been set up. Please run usePlugins before accessing the store')
+ }
+
+ return {
+ installPlugins,
+ getPlugin,
+ getAllPlugins,
+ getActivePlugins,
+ removePlugin,
+ }
+}
diff --git a/electron/core/plugin-manager/pluginMgr/index.test.js b/electron/core/plugin-manager/pluginMgr/index.test.js
new file mode 100644
index 000000000..37055db62
--- /dev/null
+++ b/electron/core/plugin-manager/pluginMgr/index.test.js
@@ -0,0 +1,150 @@
+import { usePlugins, getStore, init } from './index'
+import { installPlugins, getPlugin, getAllPlugins, getActivePlugins, addPlugin, removePlugin } from './store'
+import Plugin from './Plugin'
+import { existsSync, rmSync, mkdirSync, writeFileSync } from 'fs'
+import { join } from 'path'
+import { protocol } from 'electron'
+
+// Set up variables for test folders and test plugins.
+const pluginDir = './testPlugins'
+const registeredPluginName = 'registered-plugin'
+const demoPlugin = {
+ origin: ".\\demo-plugin\\demo-plugin-1.5.0.tgz",
+ installOptions: {
+ version: false,
+ fullMetadata: false
+ },
+ name: "demoPlugin",
+ version: "1.5.0",
+ activationPoints: [
+ "init"
+ ],
+ main: "index.js",
+ _active: true,
+ url: "plugin://demo-plugin/index.js"
+}
+
+describe('before setting a plugin path', () => {
+ describe('getStore', () => {
+ it('should throw an error if called without a plugin path set', () => {
+ expect(() => getStore()).toThrowError('The plugin path has not yet been set up. Please run usePlugins before accessing the store')
+ })
+ })
+
+ describe('usePlugins', () => {
+ it('should throw an error if called without a plugin path whilst no plugin path is set', () => {
+ expect(() => usePlugins()).toThrowError('A path to the plugins folder is required to use Pluggable Electron')
+ })
+
+ it('should throw an error if called with an invalid plugin path', () => {
+ expect(() => usePlugins('http://notsupported')).toThrowError('Invalid path provided to the plugins folder')
+ })
+
+ it('should create the plugin path if it does not yet exist', () => {
+ // Execute usePlugins with a folder that does not exist
+ const newPluginDir = './test-new-plugins'
+ usePlugins(newPluginDir)
+ expect(existsSync(newPluginDir)).toBe(true)
+
+ // Remove created folder to clean up
+ rmSync(newPluginDir, { recursive: true })
+ })
+ })
+})
+
+describe('after setting a plugin path', () => {
+ let pm
+
+ beforeAll(() => {
+ // Create folders to contain plugins
+ mkdirSync(pluginDir)
+
+ // Create initial
+ writeFileSync(join(pluginDir, 'plugins.json'), JSON.stringify({ demoPlugin }), 'utf8')
+
+ // Register a plugin before using plugins
+ const registeredPLugin = new Plugin(registeredPluginName)
+ registeredPLugin.name = registeredPluginName
+ addPlugin(registeredPLugin, false)
+
+ // Load plugins
+ pm = usePlugins(pluginDir)
+ })
+
+ afterAll(() => {
+ rmSync(pluginDir, { recursive: true })
+ })
+
+ describe('getStore', () => {
+ it('should return the plugin lifecycle functions if no plugin path is provided', () => {
+ expect(getStore()).toEqual({
+ installPlugins,
+ getPlugin,
+ getAllPlugins,
+ getActivePlugins,
+ removePlugin,
+ })
+ })
+ })
+
+ describe('usePlugins', () => {
+ it('should return the plugin lifecycle functions if a plugin path is provided', () => {
+ expect(pm).toEqual({
+ installPlugins,
+ getPlugin,
+ getAllPlugins,
+ getActivePlugins,
+ removePlugin,
+ })
+ })
+
+ it('should load the plugins defined in plugins.json in the provided plugins folder if a plugin path is provided', () => {
+ expect(getPlugin('demoPlugin')).toEqual(demoPlugin)
+ })
+
+ it('should unregister any registered plugins before registering the new ones if a plugin path is provided', () => {
+ expect(() => getPlugin(registeredPluginName)).toThrowError(`Plugin ${registeredPluginName} does not exist`)
+ })
+ })
+})
+
+describe('init', () => {
+ // Enabling the facade and registering the confirm install function is tested with the router.
+ let pm
+
+ beforeAll(() => {
+ // Create test plugins folder
+ mkdirSync(pluginDir)
+
+ // Initialize Pluggable Electron without a plugin folder
+ pm = init({ confirmInstall: () => true })
+ })
+
+ afterAll(() => {
+ // Remove test plugins folder
+ rmSync(pluginDir, { recursive: true })
+ })
+
+ it('should make the plugin files available through the plugin protocol', async () => {
+ expect(protocol.isProtocolRegistered('plugin')).toBeTruthy()
+ })
+
+ it('should return an empty object if no plugin path is provided', () => {
+ expect(pm).toEqual({})
+ })
+
+ it('should return the plugin lifecycle functions if a plugin path is provided', () => {
+ pm = init({
+ confirmInstall: () => true,
+ pluginsPath: pluginDir,
+ })
+
+ expect(pm).toEqual({
+ installPlugins,
+ getPlugin,
+ getAllPlugins,
+ getActivePlugins,
+ removePlugin,
+ })
+ })
+})
\ No newline at end of file
diff --git a/electron/core/plugin-manager/pluginMgr/router.js b/electron/core/plugin-manager/pluginMgr/router.js
new file mode 100644
index 000000000..17e5ba2f5
--- /dev/null
+++ b/electron/core/plugin-manager/pluginMgr/router.js
@@ -0,0 +1,91 @@
+import { ipcMain, webContents } from "electron"
+
+import { getPlugin, getActivePlugins, installPlugins, removePlugin, getAllPlugins } from "./store"
+import { pluginsPath, confirmInstall } from './globals'
+
+// Throw an error if pluginsPath has not yet been provided by usePlugins.
+const checkPluginsPath = () => {
+ if (!pluginsPath) throw Error('Path to plugins folder has not yet been set up.')
+}
+let active = false
+/**
+ * Provide the renderer process access to the plugins.
+ **/
+export default function () {
+ if (active) return
+ // Register IPC route to install a plugin
+ ipcMain.handle('pluggable:install', async (e, plugins) => {
+ checkPluginsPath()
+
+ // Validate install request from backend for security.
+ const specs = plugins.map(plg => typeof plg === 'object' ? plg.specifier : plg)
+ const conf = await confirmInstall(specs)
+ if (!conf) return { cancelled: true }
+
+ // Install and activate all provided plugins
+ const installed = await installPlugins(plugins)
+ return JSON.parse(JSON.stringify(installed))
+ })
+
+ // Register IPC route to uninstall a plugin
+ ipcMain.handle('pluggable:uninstall', async (e, plugins, reload) => {
+ checkPluginsPath()
+
+ // Uninstall all provided plugins
+ for (const plg of plugins) {
+ const plugin = getPlugin(plg)
+ await plugin.uninstall()
+ removePlugin(plugin.name)
+ }
+
+ // Reload all renderer pages if needed
+ reload && webContents.getAllWebContents().forEach(wc => wc.reload())
+ return true
+ })
+
+ // Register IPC route to update a plugin
+ ipcMain.handle('pluggable:update', (e, plugins, reload) => {
+ checkPluginsPath()
+
+ // Update all provided plugins
+ let updated = []
+ for (const plg of plugins) {
+ const plugin = getPlugin(plg)
+ const res = plugin.update()
+ if (res) updated.push(plugin)
+ }
+
+ // Reload all renderer pages if needed
+ if (updated.length && reload) webContents.getAllWebContents().forEach(wc => wc.reload())
+
+ return JSON.parse(JSON.stringify(updated))
+ })
+
+ // Register IPC route to check if updates are available for a plugin
+ ipcMain.handle('pluggable:updatesAvailable', (e, names) => {
+ checkPluginsPath()
+
+ const plugins = names ? names.map(name => getPlugin(name)) : getAllPlugins()
+
+ const updates = {}
+ for (const plugin of plugins) {
+ updates[plugin.name] = plugin.isUpdateAvailable()
+ }
+ return updates
+ })
+
+ // Register IPC route to get the list of active plugins
+ ipcMain.handle('pluggable:getActivePlugins', () => {
+ checkPluginsPath()
+ return JSON.parse(JSON.stringify(getActivePlugins()))
+ })
+
+ // Register IPC route to toggle the active state of a plugin
+ ipcMain.handle('pluggable:togglePluginActive', (e, plg, active) => {
+ checkPluginsPath()
+ const plugin = getPlugin(plg)
+ return JSON.parse(JSON.stringify(plugin.setActive(active)))
+ })
+
+ active = true
+}
diff --git a/electron/core/plugin-manager/pluginMgr/store.js b/electron/core/plugin-manager/pluginMgr/store.js
new file mode 100644
index 000000000..16a647930
--- /dev/null
+++ b/electron/core/plugin-manager/pluginMgr/store.js
@@ -0,0 +1,129 @@
+/**
+ * Provides access to the plugins stored by Pluggable Electron
+ * @typedef {Object} pluginManager
+ * @prop {getPlugin} getPlugin
+ * @prop {getAllPlugins} getAllPlugins
+ * @prop {getActivePlugins} getActivePlugins
+ * @prop {installPlugins} installPlugins
+ * @prop {removePlugin} removePlugin
+ */
+
+import { writeFileSync } from "fs"
+import Plugin from "./Plugin"
+import { getPluginsFile } from './globals'
+
+/**
+ * @module store
+ * @private
+ */
+
+/**
+ * Register of installed plugins
+ * @type {Object.} plugin - List of installed plugins
+ */
+const plugins = {}
+
+/**
+ * Get a plugin from the stored plugins.
+ * @param {string} name Name of the plugin to retrieve
+ * @returns {Plugin} Retrieved plugin
+ * @alias pluginManager.getPlugin
+ */
+export function getPlugin(name) {
+ if (!Object.prototype.hasOwnProperty.call(plugins, name)) {
+ throw new Error(`Plugin ${name} does not exist`)
+ }
+
+ return plugins[name]
+}
+
+/**
+ * Get list of all plugin objects.
+ * @returns {Array.} All plugin objects
+ * @alias pluginManager.getAllPlugins
+ */
+export function getAllPlugins() { return Object.values(plugins) }
+
+/**
+ * Get list of active plugin objects.
+ * @returns {Array.} Active plugin objects
+ * @alias pluginManager.getActivePlugins
+ */
+export function getActivePlugins() {
+ return Object.values(plugins).filter(plugin => plugin.active)
+}
+
+/**
+ * Remove plugin from store and maybe save stored plugins to file
+ * @param {string} name Name of the plugin to remove
+ * @param {boolean} persist Whether to save the changes to plugins to file
+ * @returns {boolean} Whether the delete was successful
+ * @alias pluginManager.removePlugin
+ */
+export function removePlugin(name, persist = true) {
+ const del = delete plugins[name]
+ if (persist) persistPlugins()
+ return del
+}
+
+/**
+ * Add plugin to store and maybe save stored plugins to file
+ * @param {Plugin} plugin Plugin to add to store
+ * @param {boolean} persist Whether to save the changes to plugins to file
+ * @returns {void}
+ */
+export function addPlugin(plugin, persist = true) {
+ plugins[plugin.name] = plugin
+ if (persist) {
+ persistPlugins()
+ plugin.subscribe('pe-persist', persistPlugins)
+ }
+}
+
+/**
+ * Save stored plugins to file
+ * @returns {void}
+ */
+export function persistPlugins() {
+ const persistData = {}
+ for (const name in plugins) {
+ persistData[name] = plugins[name]
+ }
+ writeFileSync(getPluginsFile(), JSON.stringify(persistData), 'utf8')
+}
+
+/**
+ * Create and install a new plugin for the given specifier.
+ * @param {Array.} plugins A list of NPM specifiers, or installation configuration objects.
+ * @param {boolean} [store=true] Whether to store the installed plugins in the store
+ * @returns {Promise.>} New plugin
+ * @alias pluginManager.installPlugins
+ */
+export async function installPlugins(plugins, store = true) {
+ const installed = []
+ for (const plg of plugins) {
+ // Set install options and activation based on input type
+ const isObject = typeof plg === 'object'
+ const spec = isObject ? [plg.specifier, plg] : [plg]
+ const activate = isObject ? plg.activate !== false : true
+
+ // Install and possibly activate plugin
+ const plugin = new Plugin(...spec)
+ await plugin._install()
+ if (activate) plugin.setActive(true)
+
+ // Add plugin to store if needed
+ if (store) addPlugin(plugin)
+ installed.push(plugin)
+ }
+
+ // Return list of all installed plugins
+ return installed
+}
+
+/**
+ * @typedef {Object.} installOptions The {@link https://www.npmjs.com/package/pacote|pacote}
+ * options used to install the plugin with some extra options.
+ * @param {string} specifier the NPM specifier that identifies the package.
+ * @param {boolean} [activate] Whether this plugin should be activated after installation. Defaults to true.
+ */
\ No newline at end of file
diff --git a/electron/core/plugin-manager/pluginMgr/store.test.js b/electron/core/plugin-manager/pluginMgr/store.test.js
new file mode 100644
index 000000000..e99cb7c7d
--- /dev/null
+++ b/electron/core/plugin-manager/pluginMgr/store.test.js
@@ -0,0 +1,108 @@
+import { getActivePlugins, getAllPlugins, getPlugin, installPlugins } from './store'
+import { init } from "."
+import { join } from 'path'
+import Plugin from "./Plugin"
+import { mkdirSync, writeFileSync, rmSync } from "fs"
+
+// Temporary directory to install plugins to
+const pluginsDir = './testPlugins'
+
+// Temporary directory containing the active plugin to install
+const activePluginDir = './activePluginSrc'
+const activePluginName = 'active-plugin'
+const activeManifest = join(activePluginDir, 'package.json')
+
+// Temporary directory containing the inactive plugin to install
+const inactivePluginDir = './inactivePluginSrc'
+const inactivePluginName = 'inactive-plugin'
+const inactiveManifest = join(inactivePluginDir, 'package.json')
+
+// Mock name for the entry file in the plugins
+const main = 'index'
+
+/** @type Array. */
+let activePlugins
+/** @type Array. */
+let inactivePlugins
+
+beforeAll(async () => {
+ // Initialize pluggable Electron
+ init({
+ confirmInstall: () => true,
+ pluginsPath: pluginsDir,
+ })
+
+ // Create active plugin
+ mkdirSync(activePluginDir)
+ writeFileSync(activeManifest, JSON.stringify({
+ name: activePluginName,
+ activationPoints: [],
+ main,
+ }), 'utf8')
+
+ // Create active plugin
+ mkdirSync(inactivePluginDir)
+ writeFileSync(inactiveManifest, JSON.stringify({
+ name: inactivePluginName,
+ activationPoints: [],
+ main,
+ }), 'utf8')
+
+ // Install plugins
+ activePlugins = await installPlugins([activePluginDir], true)
+ activePlugins[0].setActive(true)
+ inactivePlugins = await installPlugins([{
+ specifier: inactivePluginDir,
+ activate: false
+ }], true)
+})
+
+afterAll(() => {
+ // Remove all test files and folders
+ rmSync(pluginsDir, { recursive: true })
+ rmSync(activePluginDir, { recursive: true })
+ rmSync(inactivePluginDir, { recursive: true })
+})
+
+describe('installPlugins', () => {
+ it('should create a new plugin found at the given location and return it if store is false', async () => {
+ const res = await installPlugins([activePluginDir], false)
+
+ expect(res[0]).toBeInstanceOf(Plugin)
+ })
+
+ it('should create a new plugin found at the given location and register it if store is true', () => {
+ expect(activePlugins[0]).toBeInstanceOf(Plugin)
+ expect(getPlugin(activePluginName)).toBe(activePlugins[0])
+ })
+
+ it('should activate the installed plugin by default', () => {
+ expect(getPlugin(activePluginName).active).toBe(true)
+ })
+
+ it('should set plugin to inactive if activate is set to false in the install options', async () => {
+ expect(inactivePlugins[0].active).toBe(false)
+ })
+})
+
+describe('getPlugin', () => {
+ it('should return the plugin with the given name if it is registered', () => {
+ expect(getPlugin(activePluginName)).toBeInstanceOf(Plugin)
+ })
+
+ it('should return an error if the plugin with the given name is not registered', () => {
+ expect(() => getPlugin('wrongName')).toThrowError('Plugin wrongName does not exist')
+ })
+})
+
+describe('getAllPlugins', () => {
+ it('should return a list of all registered plugins', () => {
+ expect(getAllPlugins()).toEqual([activePlugins[0], inactivePlugins[0]])
+ })
+})
+
+describe('getActivePlugins', () => {
+ it('should return a list of all and only the registered plugins that are active', () => {
+ expect(getActivePlugins()).toEqual(activePlugins)
+ })
+})
\ No newline at end of file
diff --git a/electron/core/plugins/data-plugin/README.md b/electron/core/plugins/data-plugin/README.md
new file mode 100644
index 000000000..2197d9ad2
--- /dev/null
+++ b/electron/core/plugins/data-plugin/README.md
@@ -0,0 +1,8 @@
+## Database handler plugin for Jan App
+
+**Notice**: please only install dependencies and run build using npm and not yarn.
+
+- index.ts: Main entry point for the plugin.
+- module.ts: Defines the plugin module which would be executed by the main node process.
+- package.json: Defines the plugin metadata.
+- tsconfig.json: Defines the typescript configuration.
diff --git a/electron/core/plugins/data-plugin/index.ts b/electron/core/plugins/data-plugin/index.ts
new file mode 100644
index 000000000..c2f91d89a
--- /dev/null
+++ b/electron/core/plugins/data-plugin/index.ts
@@ -0,0 +1,152 @@
+// Provide an async method to manipulate the price provided by the extension point
+const MODULE_PATH = "data-plugin/dist/module.js";
+
+const storeModel = (model: any) =>
+ new Promise((resolve) => {
+ if (window && window.electronAPI) {
+ window.electronAPI
+ .invokePluginFunc(MODULE_PATH, "storeModel", model)
+ .then((res: any) => resolve(res));
+ }
+ });
+
+const getFinishedDownloadModels = () =>
+ new Promise((resolve) => {
+ if (window && window.electronAPI) {
+ window.electronAPI
+ .invokePluginFunc(MODULE_PATH, "getFinishedDownloadModels")
+ .then((res: any) => resolve(res));
+ }
+ });
+
+const getModelById = (modelId: string) =>
+ new Promise((resolve) => {
+ if (window && window.electronAPI) {
+ window.electronAPI
+ .invokePluginFunc(MODULE_PATH, "getModelById", modelId)
+ .then((res: any) => resolve(res));
+ }
+ });
+
+const updateFinishedDownloadAt = (fileName: string) =>
+ new Promise((resolve) => {
+ if (window && window.electronAPI) {
+ window.electronAPI
+ .invokePluginFunc(
+ MODULE_PATH,
+ "updateFinishedDownloadAt",
+ fileName,
+ Date.now()
+ )
+ .then((res: any) => resolve(res));
+ }
+ });
+
+const getUnfinishedDownloadModels = () =>
+ new Promise((resolve) => {
+ if (window && window.electronAPI) {
+ window.electronAPI
+ .invokePluginFunc(MODULE_PATH, "getUnfinishedDownloadModels")
+ .then((res: any[]) => resolve(res));
+ } else {
+ resolve([]);
+ }
+ });
+
+const deleteDownloadModel = (modelId: string) =>
+ new Promise((resolve) => {
+ if (window && window.electronAPI) {
+ window.electronAPI
+ .invokePluginFunc(MODULE_PATH, "deleteDownloadModel", modelId)
+ .then((res: any) => resolve(res));
+ }
+ });
+
+const getConversations = () =>
+ new Promise((resolve) => {
+ if (window && window.electronAPI) {
+ window.electronAPI
+ .invokePluginFunc(MODULE_PATH, "getConversations")
+ .then((res: any[]) => resolve(res));
+ } else {
+ resolve([]);
+ }
+ });
+const getConversationMessages = (id: any) =>
+ new Promise((resolve) => {
+ if (window && window.electronAPI) {
+ window.electronAPI
+ .invokePluginFunc(MODULE_PATH, "getConversationMessages", id)
+ .then((res: any[]) => resolve(res));
+ } else {
+ resolve([]);
+ }
+ });
+
+const createConversation = (conversation: any) =>
+ new Promise((resolve) => {
+ if (window && window.electronAPI) {
+ window.electronAPI
+ .invokePluginFunc(MODULE_PATH, "storeConversation", conversation)
+ .then((res: any) => {
+ resolve(res);
+ });
+ } else {
+ resolve("-");
+ }
+ });
+const createMessage = (message: any) =>
+ new Promise((resolve) => {
+ if (window && window.electronAPI) {
+ window.electronAPI
+ .invokePluginFunc(MODULE_PATH, "storeMessage", message)
+ .then((res: any) => resolve(res));
+ } else {
+ resolve("-");
+ }
+ });
+
+const deleteConversation = (id: any) =>
+ new Promise((resolve) => {
+ if (window && window.electronAPI) {
+ window.electronAPI
+ .invokePluginFunc(MODULE_PATH, "deleteConversation", id)
+ .then((res: any) => {
+ resolve(res);
+ });
+ } else {
+ resolve("-");
+ }
+ });
+
+const setupDb = () => {
+ window.electronAPI.invokePluginFunc(MODULE_PATH, "init");
+};
+
+// Register all the above functions and objects with the relevant extension points
+export function init({ register }: { register: any }) {
+ setupDb();
+ register("getConversations", "getConv", getConversations, 1);
+ register("createConversation", "insertConv", createConversation);
+ register("deleteConversation", "deleteConv", deleteConversation);
+ register("createMessage", "insertMessage", createMessage);
+ register("getConversationMessages", "getMessages", getConversationMessages);
+ register("storeModel", "storeModel", storeModel);
+ register(
+ "updateFinishedDownloadAt",
+ "updateFinishedDownloadAt",
+ updateFinishedDownloadAt
+ );
+ register(
+ "getUnfinishedDownloadModels",
+ "getUnfinishedDownloadModels",
+ getUnfinishedDownloadModels
+ );
+ register("deleteDownloadModel", "deleteDownloadModel", deleteDownloadModel);
+ register("getModelById", "getModelById", getModelById);
+ register(
+ "getFinishedDownloadModels",
+ "getFinishedDownloadModels",
+ getFinishedDownloadModels
+ );
+}
diff --git a/electron/core/plugins/data-plugin/module.ts b/electron/core/plugins/data-plugin/module.ts
new file mode 100644
index 000000000..384e538ae
--- /dev/null
+++ b/electron/core/plugins/data-plugin/module.ts
@@ -0,0 +1,371 @@
+const sqlite3 = require("sqlite3").verbose();
+const path = require("path");
+const { app } = require("electron");
+
+const MODEL_TABLE_CREATION = `
+CREATE TABLE IF NOT EXISTS models (
+ id TEXT PRIMARY KEY,
+ slug TEXT NOT NULL,
+ name TEXT NOT NULL,
+ description TEXT NOT NULL,
+ avatar_url TEXT,
+ long_description TEXT NOT NULL,
+ technical_description TEXT NOT NULL,
+ author TEXT NOT NULL,
+ version TEXT NOT NULL,
+ model_url TEXT NOT NULL,
+ nsfw INTEGER NOT NULL,
+ greeting TEXT NOT NULL,
+ type TEXT NOT NULL,
+ file_name TEXT NOT NULL,
+ download_url TEXT NOT NULL,
+ start_download_at INTEGER DEFAULT -1,
+ finish_download_at INTEGER DEFAULT -1,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+);`;
+
+const MODEL_TABLE_INSERTION = `
+INSERT INTO models (
+ id,
+ slug,
+ name,
+ description,
+ avatar_url,
+ long_description,
+ technical_description,
+ author,
+ version,
+ model_url,
+ nsfw,
+ greeting,
+ type,
+ file_name,
+ download_url,
+ start_download_at
+) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`;
+
+function init() {
+ const db = new sqlite3.Database(path.join(app.getPath("userData"), "jan.db"));
+ console.log(
+ `Database located at ${path.join(app.getPath("userData"), "jan.db")}`
+ );
+
+ db.serialize(() => {
+ db.run(MODEL_TABLE_CREATION);
+ db.run(
+ "CREATE TABLE IF NOT EXISTS conversations ( id INTEGER PRIMARY KEY, name TEXT, model_id TEXT, image TEXT, message TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP);"
+ );
+ db.run(
+ "CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY, name TEXT, conversation_id INTEGER, user TEXT, message TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP);"
+ );
+ });
+
+ const stmt = db.prepare(
+ "INSERT INTO conversations (name, model_id, image, message) VALUES (?, ?, ?, ?)"
+ );
+ stmt.finalize();
+ db.close();
+}
+
+/**
+ * Store a model in the database when user start downloading it
+ *
+ * @param model Product
+ */
+function storeModel(model: any) {
+ return new Promise((res) => {
+ const db = new sqlite3.Database(
+ path.join(app.getPath("userData"), "jan.db")
+ );
+ console.debug("Inserting", JSON.stringify(model));
+ db.serialize(() => {
+ const stmt = db.prepare(MODEL_TABLE_INSERTION);
+ stmt.run(
+ model.id,
+ model.slug,
+ model.name,
+ model.description,
+ model.avatarUrl,
+ model.longDescription,
+ model.technicalDescription,
+ model.author,
+ model.version,
+ model.modelUrl,
+ model.nsfw,
+ model.greeting,
+ model.type,
+ model.fileName,
+ model.downloadUrl,
+ Date.now(),
+ function (err: any) {
+ if (err) {
+ // Handle the insertion error here
+ console.error(err.message);
+ res(undefined);
+ return;
+ }
+ // @ts-ignoreF
+ const id = this.lastID;
+ res(id);
+ return;
+ }
+ );
+ stmt.finalize();
+ });
+
+ db.close();
+ });
+}
+
+/**
+ * Update the finished download time of a model
+ *
+ * @param model Product
+ */
+function updateFinishedDownloadAt(fileName: string, time: number) {
+ return new Promise((res) => {
+ const db = new sqlite3.Database(
+ path.join(app.getPath("userData"), "jan.db")
+ );
+ console.debug(`Updating fileName ${fileName} to ${time}`);
+ const stmt = `UPDATE models SET finish_download_at = ? WHERE file_name = ?`;
+ db.run(stmt, [time, fileName], (err: any) => {
+ if (err) {
+ console.log(err);
+ } else {
+ console.log("Updated 1 row");
+ res("Updated");
+ }
+ });
+
+ db.close();
+ });
+}
+
+/**
+ * Get all unfinished models from the database
+ */
+function getUnfinishedDownloadModels() {
+ return new Promise((res) => {
+ const db = new sqlite3.Database(
+ path.join(app.getPath("userData"), "jan.db")
+ );
+
+ const query = `SELECT * FROM models WHERE finish_download_at = -1 ORDER BY start_download_at DESC`;
+ db.all(query, (err: Error, row: any) => {
+ res(row);
+ });
+ db.close();
+ });
+}
+
+function getFinishedDownloadModels() {
+ return new Promise((res) => {
+ const db = new sqlite3.Database(
+ path.join(app.getPath("userData"), "jan.db")
+ );
+
+ const query = `SELECT * FROM models WHERE finish_download_at != -1 ORDER BY finish_download_at DESC`;
+ db.all(query, (err: Error, row: any) => {
+ res(row);
+ });
+ db.close();
+ });
+}
+
+function deleteDownloadModel(modelId: string) {
+ return new Promise((res) => {
+ const db = new sqlite3.Database(
+ path.join(app.getPath("userData"), "jan.db")
+ );
+ console.log(`Deleting ${modelId}`);
+ db.serialize(() => {
+ const stmt = db.prepare("DELETE FROM models WHERE id = ?");
+ stmt.run(modelId);
+ stmt.finalize();
+ });
+
+ db.close();
+ });
+}
+
+function getModelById(modelId: string) {
+ return new Promise((res) => {
+ const db = new sqlite3.Database(
+ path.join(app.getPath("userData"), "jan.db")
+ );
+
+ console.debug("Get model by id", modelId);
+ db.get(
+ `SELECT * FROM models WHERE id = ?`,
+ [modelId],
+ (err: any, row: any) => {
+ console.debug("Get model by id result", row);
+
+ if (row) {
+ const product = {
+ id: row.id,
+ slug: row.slug,
+ name: row.name,
+ description: row.description,
+ avatarUrl: row.avatar_url,
+ longDescription: row.long_description,
+ technicalDescription: row.technical_description,
+ author: row.author,
+ version: row.version,
+ modelUrl: row.model_url,
+ nsfw: row.nsfw,
+ greeting: row.greeting,
+ type: row.type,
+ inputs: row.inputs,
+ outputs: row.outputs,
+ createdAt: new Date(row.created_at),
+ updatedAt: new Date(row.updated_at),
+ fileName: row.file_name,
+ downloadUrl: row.download_url,
+ };
+ res(product);
+ }
+ }
+ );
+
+ db.close();
+ });
+}
+
+function getConversations() {
+ return new Promise((res) => {
+ const db = new sqlite3.Database(
+ path.join(app.getPath("userData"), "jan.db")
+ );
+
+ db.all(
+ "SELECT * FROM conversations ORDER BY created_at DESC",
+ (err: any, row: any) => {
+ res(row);
+ }
+ );
+ db.close();
+ });
+}
+function storeConversation(conversation: any) {
+ return new Promise((res) => {
+ const db = new sqlite3.Database(
+ path.join(app.getPath("userData"), "jan.db")
+ );
+
+ db.serialize(() => {
+ const stmt = db.prepare(
+ "INSERT INTO conversations (name, model_id, image, message) VALUES (?, ?, ?, ?)"
+ );
+ stmt.run(
+ conversation.name,
+ conversation.model_id,
+ conversation.image,
+ conversation.message,
+ function (err: any) {
+ if (err) {
+ // Handle the insertion error here
+ console.error(err.message);
+ res(undefined);
+ return;
+ }
+ // @ts-ignoreF
+ const id = this.lastID;
+ res(id);
+ return;
+ }
+ );
+ stmt.finalize();
+ });
+
+ db.close();
+ });
+}
+
+function storeMessage(message: any) {
+ return new Promise((res) => {
+ const db = new sqlite3.Database(
+ path.join(app.getPath("userData"), "jan.db")
+ );
+
+ db.serialize(() => {
+ const stmt = db.prepare(
+ "INSERT INTO messages (name, conversation_id, user, message) VALUES (?, ?, ?, ?)"
+ );
+ stmt.run(
+ message.name,
+ message.conversation_id,
+ message.user,
+ message.message,
+ (err: any) => {
+ if (err) {
+ // Handle the insertion error here
+ console.error(err.message);
+ res(undefined);
+ return;
+ }
+ //@ts-ignore
+ const id = this.lastID;
+ res(id);
+ return;
+ }
+ );
+ stmt.finalize();
+ });
+
+ db.close();
+ });
+}
+
+function deleteConversation(id: any) {
+ return new Promise((res) => {
+ const db = new sqlite3.Database(
+ path.join(app.getPath("userData"), "jan.db")
+ );
+
+ db.serialize(() => {
+ const deleteConv = db.prepare("DELETE FROM conversations WHERE id = ?");
+ deleteConv.run(id);
+ deleteConv.finalize();
+ const deleteMessages = db.prepare(
+ "DELETE FROM messages WHERE conversation_id = ?"
+ );
+ deleteMessages.run(id);
+ deleteMessages.finalize();
+ res([]);
+ });
+
+ db.close();
+ });
+}
+
+function getConversationMessages(conversation_id: any) {
+ return new Promise((res) => {
+ const db = new sqlite3.Database(
+ path.join(app.getPath("userData"), "jan.db")
+ );
+
+ const query = `SELECT * FROM messages WHERE conversation_id = ${conversation_id} ORDER BY created_at DESC`;
+ db.all(query, (err: Error, row: any) => {
+ res(row);
+ });
+ db.close();
+ });
+}
+
+module.exports = {
+ init,
+ getConversations,
+ deleteConversation,
+ storeConversation,
+ storeMessage,
+ getConversationMessages,
+ storeModel,
+ updateFinishedDownloadAt,
+ getUnfinishedDownloadModels,
+ getFinishedDownloadModels,
+ deleteDownloadModel,
+ getModelById,
+};
diff --git a/electron/core/plugins/data-plugin/package.json b/electron/core/plugins/data-plugin/package.json
new file mode 100644
index 000000000..83f148f7e
--- /dev/null
+++ b/electron/core/plugins/data-plugin/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "data-plugin",
+ "version": "2.1.0",
+ "description": "",
+ "main": "dist/index.js",
+ "author": "Jan",
+ "license": "MIT",
+ "activationPoints": [
+ "init"
+ ],
+ "scripts": {
+ "build": "tsc -b . && webpack --config webpack.config.js",
+ "build:package": "rimraf ./data-plugin*.tgz && npm run build && npm pack",
+ "build:publish": "npm run build:package && cpx *.tgz ../../pre-install"
+ },
+ "exports": {
+ ".": "./dist/index.js",
+ "./main": "./dist/module.js"
+ },
+ "devDependencies": {
+ "cpx": "^1.5.0",
+ "rimraf": "^3.0.2",
+ "ts-loader": "^9.4.4",
+ "ts-node": "^10.9.1",
+ "typescript": "^5.2.2",
+ "webpack": "^5.88.2",
+ "webpack-cli": "^5.1.4"
+ },
+ "bundledDependencies": [
+ "sql.js",
+ "sqlite3"
+ ],
+ "files": [
+ "dist/**",
+ "package.json",
+ "node_modules"
+ ],
+ "dependencies": {
+ "sqlite3": "^5.1.6"
+ }
+}
diff --git a/electron/core/plugins/data-plugin/tsconfig.json b/electron/core/plugins/data-plugin/tsconfig.json
new file mode 100644
index 000000000..72d6f3732
--- /dev/null
+++ b/electron/core/plugins/data-plugin/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ /* Visit https://aka.ms/tsconfig to read more about this file */
+ /* Language and Environment */
+ "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
+ /* Modules */
+ "module": "ES6" /* Specify what module code is generated. */,
+ // "rootDir": "./", /* Specify the root folder within your source files. */
+ // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
+ // "baseUrl": "." /* Specify the base directory to resolve non-relative module names. */,
+ // "paths": {} /* Specify a set of entries that re-map imports to additional lookup locations. */,
+ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
+ // "resolveJsonModule": true, /* Enable importing .json files. */
+
+ "outDir": "./dist" /* Specify an output folder for all emitted files. */,
+ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
+ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
+ /* Type Checking */
+ "strict": false /* Enable all strict type-checking options. */,
+ "skipLibCheck": true /* Skip type checking all .d.ts files. */
+ }
+}
diff --git a/electron/core/plugins/data-plugin/types/index.d.ts b/electron/core/plugins/data-plugin/types/index.d.ts
new file mode 100644
index 000000000..a7ac3c8ac
--- /dev/null
+++ b/electron/core/plugins/data-plugin/types/index.d.ts
@@ -0,0 +1,7 @@
+export {};
+
+declare global {
+ interface Window {
+ electronAPI?: any | undefined;
+ }
+}
diff --git a/electron/core/plugins/data-plugin/webpack.config.js b/electron/core/plugins/data-plugin/webpack.config.js
new file mode 100644
index 000000000..18696aa34
--- /dev/null
+++ b/electron/core/plugins/data-plugin/webpack.config.js
@@ -0,0 +1,25 @@
+const path = require("path");
+
+module.exports = {
+ experiments: { outputModule: true },
+ entry: "./index.ts", // Adjust the entry point to match your project's main file
+ mode: "production",
+ module: {
+ rules: [
+ {
+ test: /\.tsx?$/,
+ use: "ts-loader",
+ exclude: /node_modules/,
+ },
+ ],
+ },
+ output: {
+ filename: "index.js", // Adjust the output file name as needed
+ path: path.resolve(__dirname, "dist"),
+ library: { type: "module" }, // Specify ESM output format
+ },
+ resolve: {
+ extensions: [".ts", ".js"],
+ },
+ // Add loaders and other configuration as needed for your project
+};
diff --git a/electron/core/plugins/inference-plugin/index.js b/electron/core/plugins/inference-plugin/index.js
new file mode 100644
index 000000000..76c1445fc
--- /dev/null
+++ b/electron/core/plugins/inference-plugin/index.js
@@ -0,0 +1,25 @@
+const MODULE_PATH = "inference-plugin/dist/module.js";
+
+const prompt = async (prompt) =>
+ new Promise(async (resolve) => {
+ if (window.electronAPI) {
+ window.electronAPI
+ .invokePluginFunc(MODULE_PATH, "prompt", prompt)
+ .then((res) => resolve(res));
+ }
+ });
+
+const initModel = async (product) =>
+ new Promise(async (resolve) => {
+ if (window.electronAPI) {
+ window.electronAPI
+ .invokePluginFunc(MODULE_PATH, "initModel", product)
+ .then((res) => resolve(res));
+ }
+ });
+
+// Register all the above functions and objects with the relevant extension points
+export function init({ register }) {
+ register("initModel", "initModel", initModel);
+ register("prompt", "prompt", prompt);
+}
diff --git a/electron/core/plugins/inference-plugin/module.js b/electron/core/plugins/inference-plugin/module.js
new file mode 100644
index 000000000..763c632cc
--- /dev/null
+++ b/electron/core/plugins/inference-plugin/module.js
@@ -0,0 +1,55 @@
+const path = require("path");
+const { app, dialog } = require("electron");
+const _importDynamic = new Function("modulePath", "return import(modulePath)");
+
+let llamaSession = null;
+
+async function initModel(product) {
+ // fileName fallback
+ if (!product.fileName) {
+ product.fileName = product.file_name;
+ }
+
+ if (!product.fileName) {
+ await dialog.showMessageBox({
+ message: "Selected model does not have file name..",
+ });
+
+ return;
+ }
+
+ console.info(`Initializing model: ${product.name}..`);
+ _importDynamic("../node_modules/node-llama-cpp/dist/index.js")
+ .then(({ LlamaContext, LlamaChatSession, LlamaModel }) => {
+ const modelPath = path.join(app.getPath("userData"), product.fileName);
+ const model = new LlamaModel({ modelPath });
+ const context = new LlamaContext({ model });
+ llamaSession = new LlamaChatSession({ context });
+ console.info(`Init model ${product.name} successfully!`);
+ })
+ .catch(async (e) => {
+ console.error(e);
+ await dialog.showMessageBox({
+ message: "Failed to import LLM module",
+ });
+ });
+}
+
+async function prompt(prompt) {
+ if (!llamaSession) {
+ await dialog.showMessageBox({
+ message: "Model not initialized",
+ });
+
+ return;
+ }
+ console.log("prompt: ", prompt);
+ const response = await llamaSession.prompt(prompt);
+ console.log("response: ", response);
+ return response;
+}
+
+module.exports = {
+ initModel,
+ prompt,
+};
diff --git a/electron/core/plugins/inference-plugin/package.json b/electron/core/plugins/inference-plugin/package.json
new file mode 100644
index 000000000..170a2f0eb
--- /dev/null
+++ b/electron/core/plugins/inference-plugin/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "inference-plugin",
+ "version": "0.0.1",
+ "description": "",
+ "main": "dist/index.js",
+ "author": "James",
+ "license": "MIT",
+ "activationPoints": [
+ "init"
+ ],
+ "scripts": {
+ "build": "webpack --config webpack.config.js",
+ "build:package": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && npm pack",
+ "build:publish": "yarn build:package && cpx *.tgz ../../pre-install"
+ },
+ "devDependencies": {
+ "cpx": "^1.5.0",
+ "rimraf": "^3.0.2",
+ "webpack": "^5.88.2",
+ "webpack-cli": "^5.1.4"
+ },
+ "bundledDependencies": [
+ "electron-is-dev",
+ "node-llama-cpp"
+ ],
+ "dependencies": {
+ "electron-is-dev": "^2.0.0",
+ "node-llama-cpp": "^2.4.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "files": [
+ "dist/*",
+ "package.json",
+ "README.md"
+ ]
+}
diff --git a/electron/core/plugins/inference-plugin/webpack.config.js b/electron/core/plugins/inference-plugin/webpack.config.js
new file mode 100644
index 000000000..2821852d4
--- /dev/null
+++ b/electron/core/plugins/inference-plugin/webpack.config.js
@@ -0,0 +1,25 @@
+const path = require("path");
+
+module.exports = {
+ experiments: { outputModule: true },
+ entry: "./index.js", // Adjust the entry point to match your project's main file
+ mode: "production",
+ module: {
+ rules: [
+ {
+ test: /\.tsx?$/,
+ use: "ts-loader",
+ exclude: /node_modules/,
+ },
+ ],
+ },
+ output: {
+ filename: "index.js", // Adjust the output file name as needed
+ path: path.resolve(__dirname, "dist"),
+ library: { type: "module" }, // Specify ESM output format
+ },
+ resolve: {
+ extensions: [".js"],
+ },
+ // Add loaders and other configuration as needed for your project
+};
diff --git a/electron/core/plugins/model-management-plugin/index.js b/electron/core/plugins/model-management-plugin/index.js
new file mode 100644
index 000000000..0d2449931
--- /dev/null
+++ b/electron/core/plugins/model-management-plugin/index.js
@@ -0,0 +1,47 @@
+const MODULE_PATH = "model-management-plugin/dist/module.js";
+
+const getDownloadedModels = async () =>
+ new Promise(async (resolve) => {
+ if (window.electronAPI) {
+ window.electronAPI
+ .invokePluginFunc(MODULE_PATH, "getDownloadedModels")
+ .then((res) => resolve(res));
+ }
+ });
+
+const getAvailableModels = async () =>
+ new Promise(async (resolve) => {
+ if (window.electronAPI) {
+ window.electronAPI
+ .invokePluginFunc(MODULE_PATH, "getAvailableModels")
+ .then((res) => resolve(res));
+ }
+ });
+
+const downloadModel = async (product) =>
+ new Promise(async (resolve) => {
+ if (window && window.electronAPI) {
+ window.electronAPI
+ .downloadFile(product.downloadUrl, product.fileName)
+ .then((res) => resolve(res));
+ } else {
+ resolve("-");
+ }
+ });
+
+const deleteModel = async (path) =>
+ new Promise(async (resolve) => {
+ if (window.electronAPI) {
+ console.debug(`Delete model model management plugin: ${path}`);
+ const response = await window.electronAPI.deleteFile(path);
+ resolve(response);
+ }
+ });
+
+// Register all the above functions and objects with the relevant extension points
+export function init({ register }) {
+ register("getDownloadedModels", "getDownloadedModels", getDownloadedModels);
+ register("getAvailableModels", "getAvailableModels", getAvailableModels);
+ register("downloadModel", "downloadModel", downloadModel);
+ register("deleteModel", "deleteModel", deleteModel);
+}
diff --git a/electron/core/plugins/model-management-plugin/module.js b/electron/core/plugins/model-management-plugin/module.js
new file mode 100644
index 000000000..d1472656b
--- /dev/null
+++ b/electron/core/plugins/model-management-plugin/module.js
@@ -0,0 +1,101 @@
+const path = require("path");
+const { readdirSync, lstatSync } = require("fs");
+const { app } = require("electron");
+
+const ALL_MODELS = [
+ {
+ id: "llama-2-7b-chat.Q4_K_M.gguf.bin",
+ slug: "llama-2-7b-chat.Q4_K_M.gguf.bin",
+ name: "Llama 2 7B Chat - GGUF",
+ description: "medium, balanced quality - recommended",
+ avatarUrl:
+ "https://aeiljuispo.cloudimg.io/v7/https://cdn-uploads.huggingface.co/production/uploads/6426d3f3a7723d62b53c259b/tvPikpAzKTKGN5wrpadOJ.jpeg?w=200&h=200&f=face",
+ longDescription:
+ "GGUF is a new format introduced by the llama.cpp team on August 21st 2023. It is a replacement for GGML, which is no longer supported by llama.cpp. GGUF offers numerous advantages over GGML, such as better tokenisation, and support for special tokens. It is also supports metadata, and is designed to be extensible.",
+ technicalDescription:
+ 'GGML_TYPE_Q4_K - "type-1" 4-bit quantization in super-blocks containing 8 blocks, each block having 32 weights. Scales and mins are quantized with 6 bits. This ends up using 4.5 bpw.',
+ author: "The Bloke",
+ version: "1.0.0",
+ modelUrl: "https://google.com",
+ nsfw: false,
+ greeting: "Hello there",
+ type: "LLM",
+ inputs: undefined,
+ outputs: undefined,
+ createdAt: 0,
+ updatedAt: undefined,
+ fileName: "llama-2-7b-chat.Q4_K_M.gguf.bin",
+ downloadUrl:
+ "https://huggingface.co/TheBloke/Llama-2-7b-Chat-GGUF/resolve/main/llama-2-7b-chat.Q4_K_M.gguf",
+ },
+ {
+ id: "llama-2-13b-chat.Q4_K_M.gguf",
+ slug: "llama-2-13b-chat.Q4_K_M.gguf",
+ name: "Llama 2 13B Chat - GGUF",
+ description: "medium, balanced quality - recommended",
+ avatarUrl:
+ "https://aeiljuispo.cloudimg.io/v7/https://cdn-uploads.huggingface.co/production/uploads/6426d3f3a7723d62b53c259b/tvPikpAzKTKGN5wrpadOJ.jpeg?w=200&h=200&f=face",
+ longDescription:
+ "GGUF is a new format introduced by the llama.cpp team on August 21st 2023. It is a replacement for GGML, which is no longer supported by llama.cpp. GGUF offers numerous advantages over GGML, such as better tokenisation, and support for special tokens. It is also supports metadata, and is designed to be extensible.",
+ technicalDescription:
+ 'GGML_TYPE_Q4_K - "type-1" 4-bit quantization in super-blocks containing 8 blocks, each block having 32 weights. Scales and mins are quantized with 6 bits. This ends up using 4.5 bpw.',
+ author: "The Bloke",
+ version: "1.0.0",
+ modelUrl: "https://google.com",
+ nsfw: false,
+ greeting: "Hello there",
+ type: "LLM",
+ inputs: undefined,
+ outputs: undefined,
+ createdAt: 0,
+ updatedAt: undefined,
+ fileName: "llama-2-13b-chat.Q4_K_M.gguf.bin",
+ downloadUrl:
+ "https://huggingface.co/TheBloke/Llama-2-13B-chat-GGUF/resolve/main/llama-2-13b-chat.Q4_K_M.gguf",
+ },
+];
+
+function getDownloadedModels() {
+ const userDataPath = app.getPath("userData");
+
+ const allBinariesName = [];
+ var files = readdirSync(userDataPath);
+ for (var i = 0; i < files.length; i++) {
+ var filename = path.join(userDataPath, files[i]);
+ var stat = lstatSync(filename);
+ if (stat.isDirectory()) {
+ // ignore
+ } else if (filename.endsWith(".bin")) {
+ var binaryName = path.basename(filename);
+ allBinariesName.push(binaryName);
+ }
+ }
+
+ const downloadedModels = ALL_MODELS.map((model) => {
+ if (
+ model.fileName &&
+ allBinariesName
+ .map((t) => t.toLowerCase())
+ .includes(model.fileName.toLowerCase())
+ ) {
+ return model;
+ }
+ return undefined;
+ }).filter((m) => m !== undefined);
+
+ return downloadedModels;
+}
+
+function getAvailableModels() {
+ const downloadedModelIds = getDownloadedModels().map((model) => model.id);
+ return ALL_MODELS.filter((model) => {
+ if (!downloadedModelIds.includes(model.id)) {
+ return model;
+ }
+ });
+}
+
+module.exports = {
+ getDownloadedModels,
+ getAvailableModels,
+};
diff --git a/electron/core/plugins/model-management-plugin/package.json b/electron/core/plugins/model-management-plugin/package.json
new file mode 100644
index 000000000..dd7d61d3c
--- /dev/null
+++ b/electron/core/plugins/model-management-plugin/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "model-management-plugin",
+ "version": "0.0.1",
+ "description": "",
+ "main": "dist/index.js",
+ "author": "James",
+ "license": "MIT",
+ "activationPoints": [
+ "init"
+ ],
+ "scripts": {
+ "build": "webpack --config webpack.config.js",
+ "build:package": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && npm pack",
+ "build:publish": "yarn build:package && cpx *.tgz ../../pre-install"
+ },
+ "devDependencies": {
+ "cpx": "^1.5.0",
+ "rimraf": "^3.0.2",
+ "webpack": "^5.88.2",
+ "webpack-cli": "^5.1.4"
+ },
+ "files": [
+ "dist/*",
+ "package.json",
+ "README.md"
+ ]
+}
diff --git a/electron/core/plugins/model-management-plugin/webpack.config.js b/electron/core/plugins/model-management-plugin/webpack.config.js
new file mode 100644
index 000000000..2821852d4
--- /dev/null
+++ b/electron/core/plugins/model-management-plugin/webpack.config.js
@@ -0,0 +1,25 @@
+const path = require("path");
+
+module.exports = {
+ experiments: { outputModule: true },
+ entry: "./index.js", // Adjust the entry point to match your project's main file
+ mode: "production",
+ module: {
+ rules: [
+ {
+ test: /\.tsx?$/,
+ use: "ts-loader",
+ exclude: /node_modules/,
+ },
+ ],
+ },
+ output: {
+ filename: "index.js", // Adjust the output file name as needed
+ path: path.resolve(__dirname, "dist"),
+ library: { type: "module" }, // Specify ESM output format
+ },
+ resolve: {
+ extensions: [".js"],
+ },
+ // Add loaders and other configuration as needed for your project
+};
diff --git a/electron/core/plugins/monitoring-plugin/index.js b/electron/core/plugins/monitoring-plugin/index.js
new file mode 100644
index 000000000..ac47a9adc
--- /dev/null
+++ b/electron/core/plugins/monitoring-plugin/index.js
@@ -0,0 +1,36 @@
+// Provide an async method to manipulate the price provided by the extension point
+const PLUGIN_NAME = "monitoring-plugin/dist/module.js";
+
+const getResourcesInfo = () => {
+ return new Promise((resolve) => {
+ if (window && window.electronAPI) {
+ window.electronAPI
+ .invokePluginFunc(PLUGIN_NAME, "getResourcesInfo")
+ .then((res) => {
+ resolve(res);
+ });
+ } else {
+ resolve({});
+ }
+ });
+};
+
+const getCurrentLoad = () => {
+ return new Promise((resolve) => {
+ if (window && window.electronAPI) {
+ window.electronAPI
+ .invokePluginFunc(PLUGIN_NAME, "getCurrentLoad")
+ .then((res) => {
+ resolve(res);
+ });
+ } else {
+ resolve({});
+ }
+ });
+};
+
+// Register all the above functions and objects with the relevant extension points
+export function init({ register }) {
+ register("getResourcesInfo", "getResourcesInfo", getResourcesInfo);
+ register("getCurrentLoad", "getCurrentLoad", getCurrentLoad);
+}
diff --git a/electron/core/plugins/monitoring-plugin/module.js b/electron/core/plugins/monitoring-plugin/module.js
new file mode 100644
index 000000000..6fe5881c2
--- /dev/null
+++ b/electron/core/plugins/monitoring-plugin/module.js
@@ -0,0 +1,25 @@
+const si = require("systeminformation");
+
+const getResourcesInfo = async () =>
+ new Promise(async (resolve) => {
+ const cpu = await si.cpu();
+ const mem = await si.mem();
+ const gpu = await si.graphics();
+ const response = {
+ cpu,
+ mem,
+ gpu,
+ };
+ resolve(response);
+ });
+
+const getCurrentLoad = async () =>
+ new Promise(async (resolve) => {
+ const currentLoad = await si.currentLoad();
+ resolve(currentLoad);
+ });
+
+module.exports = {
+ getResourcesInfo,
+ getCurrentLoad,
+};
diff --git a/electron/core/plugins/monitoring-plugin/package.json b/electron/core/plugins/monitoring-plugin/package.json
new file mode 100644
index 000000000..34d110ab5
--- /dev/null
+++ b/electron/core/plugins/monitoring-plugin/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "monitoring-plugin",
+ "version": "0.0.1",
+ "description": "",
+ "main": "dist/bundle.js",
+ "author": "Jan",
+ "license": "MIT",
+ "activationPoints": [
+ "init"
+ ],
+ "scripts": {
+ "build": "webpack --config webpack.config.js",
+ "build:package": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && npm pack",
+ "build:publish": "yarn build:package && cpx *.tgz ../../pre-install"
+ },
+ "devDependencies": {
+ "rimraf": "^3.0.2",
+ "webpack": "^5.88.2",
+ "webpack-cli": "^5.1.4"
+ },
+ "dependencies": {
+ "systeminformation": "^5.21.8"
+ },
+ "bundledDependencies": [
+ "systeminformation"
+ ],
+ "files": [
+ "dist/*",
+ "package.json",
+ "README.md"
+ ]
+}
diff --git a/electron/core/plugins/monitoring-plugin/webpack.config.js b/electron/core/plugins/monitoring-plugin/webpack.config.js
new file mode 100644
index 000000000..7248a0990
--- /dev/null
+++ b/electron/core/plugins/monitoring-plugin/webpack.config.js
@@ -0,0 +1,15 @@
+const path = require("path");
+
+module.exports = {
+ experiments: { outputModule: true },
+ entry: "./index.js", // Adjust the entry point to match your project's main file
+ output: {
+ filename: "bundle.js", // Adjust the output file name as needed
+ path: path.resolve(__dirname, "dist"),
+ library: { type: "module" }, // Specify ESM output format
+ },
+ resolve: {
+ extensions: [".js"],
+ },
+ // Add loaders and other configuration as needed for your project
+};
diff --git a/models/.gitkeep b/electron/core/pre-install/.gitkeep
similarity index 100%
rename from models/.gitkeep
rename to electron/core/pre-install/.gitkeep
diff --git a/electron/icon.png b/electron/icon.png
new file mode 100644
index 000000000..289f99ded
Binary files /dev/null and b/electron/icon.png differ
diff --git a/electron/main.ts b/electron/main.ts
new file mode 100644
index 000000000..5419668a5
--- /dev/null
+++ b/electron/main.ts
@@ -0,0 +1,204 @@
+import {
+ app,
+ BrowserWindow,
+ screen as electronScreen,
+ ipcMain,
+ dialog,
+ shell,
+} from "electron";
+import { readdirSync } from "fs";
+import { resolve, join, extname } from "path";
+import { unlink, createWriteStream } from "fs";
+import isDev = require("electron-is-dev");
+import { init } from "./core/plugin-manager/pluginMgr";
+const { autoUpdater } = require("electron-updater");
+// @ts-ignore
+import request = require("request");
+// @ts-ignore
+import progress = require("request-progress");
+
+let mainWindow: BrowserWindow | undefined = undefined;
+
+autoUpdater.autoDownload = false;
+autoUpdater.autoInstallOnAppQuit = true;
+
+const createMainWindow = () => {
+ mainWindow = new BrowserWindow({
+ width: electronScreen.getPrimaryDisplay().workArea.width,
+ height: electronScreen.getPrimaryDisplay().workArea.height,
+ show: false,
+ backgroundColor: "white",
+ webPreferences: {
+ nodeIntegration: true,
+ preload: join(__dirname, "preload.js"),
+ },
+ });
+
+ ipcMain.handle(
+ "invokePluginFunc",
+ async (event, modulePath, method, ...args) => {
+ const module = join(app.getPath("userData"), "plugins", modulePath);
+ return await import(/* webpackIgnore: true */ module)
+ .then((plugin) => {
+ if (typeof plugin[method] === "function") {
+ return plugin[method](...args);
+ } else {
+ console.log(plugin[method]);
+ console.error(`Function "${method}" does not exist in the module.`);
+ }
+ })
+ .then((res) => {
+ return res;
+ })
+ .catch((err) => console.log(err));
+ }
+ );
+
+ const startURL = isDev
+ ? "http://localhost:3000"
+ : `file://${join(__dirname, "../renderer/index.html")}`;
+
+ mainWindow.loadURL(startURL);
+
+ mainWindow.once("ready-to-show", () => mainWindow?.show());
+ mainWindow.on("closed", () => {
+ if (process.platform !== "darwin") app.quit();
+ });
+
+ if (isDev) mainWindow.webContents.openDevTools();
+};
+
+app.whenReady().then(() => {
+ createMainWindow();
+ setupPlugins();
+ autoUpdater.checkForUpdates();
+
+ ipcMain.handle("basePlugins", async (event) => {
+ const basePluginPath = join(
+ __dirname,
+ "../",
+ isDev ? "/core/pre-install" : "../app.asar.unpacked/core/pre-install"
+ );
+ return readdirSync(basePluginPath)
+ .filter((file) => extname(file) === ".tgz")
+ .map((file) => join(basePluginPath, file));
+ });
+
+ ipcMain.handle("pluginPath", async (event) => {
+ return join(app.getPath("userData"), "plugins");
+ });
+ ipcMain.handle("appVersion", async (event) => {
+ return app.getVersion();
+ });
+ ipcMain.handle("openExternalUrl", async (event, url) => {
+ shell.openExternal(url);
+ });
+
+ /**
+ * Used to delete a file from the user data folder
+ */
+ ipcMain.handle("deleteFile", async (_event, filePath) => {
+ const userDataPath = app.getPath("userData");
+ const fullPath = join(userDataPath, filePath);
+
+ let result = "NULL";
+ unlink(fullPath, function (err) {
+ if (err && err.code == "ENOENT") {
+ result = `File not exist: ${err}`;
+ } else if (err) {
+ result = `File delete error: ${err}`;
+ } else {
+ result = "File deleted successfully";
+ }
+ console.log(`Delete file ${filePath} from ${fullPath} result: ${result}`);
+ });
+
+ return result;
+ });
+
+ /**
+ * Used to download a file from a given url
+ */
+ ipcMain.handle("downloadFile", async (_event, url, fileName) => {
+ const userDataPath = app.getPath("userData");
+ const destination = resolve(userDataPath, fileName);
+
+ progress(request(url), {})
+ .on("progress", function (state: any) {
+ mainWindow?.webContents.send("FILE_DOWNLOAD_UPDATE", {
+ ...state,
+ fileName,
+ });
+ })
+ .on("error", function (err: Error) {
+ mainWindow?.webContents.send("FILE_DOWNLOAD_ERROR", {
+ fileName,
+ err,
+ });
+ })
+ .on("end", function () {
+ mainWindow?.webContents.send("FILE_DOWNLOAD_COMPLETE", {
+ fileName,
+ });
+ })
+ .pipe(createWriteStream(destination));
+ });
+
+ app.on("activate", () => {
+ if (!BrowserWindow.getAllWindows().length) {
+ createMainWindow();
+ }
+ });
+});
+
+/*New Update Available*/
+autoUpdater.on("update-available", async (info: any) => {
+ const action = await dialog.showMessageBox({
+ message: `Update available. Do you want to download the latest update?`,
+ buttons: ["Download", "Later"],
+ });
+ if (action.response === 0) await autoUpdater.downloadUpdate();
+});
+
+/*App Update Completion Message*/
+autoUpdater.on("update-downloaded", async (info: any) => {
+ mainWindow?.webContents.send("APP_UPDATE_COMPLETE", {});
+ const action = await dialog.showMessageBox({
+ message: `Update downloaded. Please restart the application to apply the updates.`,
+ buttons: ["Restart", "Later"],
+ });
+ if (action.response === 0) {
+ autoUpdater.quitAndInstall();
+ }
+});
+
+/*App Update Error */
+autoUpdater.on("error", (info: any) => {
+ dialog.showMessageBox({ message: info.message });
+ mainWindow?.webContents.send("APP_UPDATE_ERROR", {});
+});
+
+/*App Update Progress */
+autoUpdater.on("download-progress", (progress: any) => {
+ console.log("app update progress: ", progress.percent);
+ mainWindow?.webContents.send("APP_UPDATE_PROGRESS", {
+ percent: progress.percent,
+ });
+});
+
+app.on("window-all-closed", () => {
+ if (process.platform !== "darwin") {
+ app.quit();
+ }
+});
+
+function setupPlugins() {
+ init({
+ // Function to check from the main process that user wants to install a plugin
+ confirmInstall: async (plugins: string[]) => {
+ return true;
+ },
+ // Path to install plugin to
+ pluginsPath: join(app.getPath("userData"), "plugins"),
+ });
+}
diff --git a/electron/package.json b/electron/package.json
new file mode 100644
index 000000000..abb8ac016
--- /dev/null
+++ b/electron/package.json
@@ -0,0 +1,55 @@
+{
+ "name": "jan-electron",
+ "version": "0.1.1",
+ "main": "./build/main.js",
+ "author": "Jan",
+ "license": "MIT",
+ "homepage": "./",
+ "build": {
+ "appId": "jan.ai.app",
+ "productName": "Jan",
+ "files": [
+ "renderer/**/*",
+ "build/*.{js,map}",
+ "build/core/plugin-manager/**/*",
+ "core/pre-install"
+ ],
+ "asarUnpack": [
+ "core/pre-install"
+ ],
+ "publish": [
+ {
+ "provider": "github",
+ "owner": "janhq",
+ "repo": "jan"
+ }
+ ],
+ "extends": null,
+ "mac": {
+ "type": "distribution"
+ }
+ },
+ "scripts": {
+ "dev": "tsc -p . && electron .",
+ "build": "tsc -p . && electron-builder -p never -mw",
+ "build:publish": "tsc -p . && electron-builder -p onTagOrDraft -mw",
+ "postinstall": "electron-builder install-app-deps"
+ },
+ "dependencies": {
+ "electron-is-dev": "^2.0.0",
+ "electron-updater": "^6.1.4",
+ "node-llama-cpp": "^2.4.1",
+ "pluggable-electron": "^0.6.0",
+ "request": "^2.88.2",
+ "request-progress": "^3.0.0"
+ },
+ "devDependencies": {
+ "concurrently": "^8.2.1",
+ "electron": "26.2.1",
+ "electron-builder": "^24.6.4",
+ "wait-on": "^7.0.1"
+ },
+ "installConfig": {
+ "hoistingLimits": "workspaces"
+ }
+}
diff --git a/electron/preload.ts b/electron/preload.ts
new file mode 100644
index 000000000..18a9e78a0
--- /dev/null
+++ b/electron/preload.ts
@@ -0,0 +1,43 @@
+/* eslint-disable react-hooks/rules-of-hooks */
+// Make Pluggable Electron's facade available to the renderer on window.plugins
+//@ts-ignore
+const useFacade = require("pluggable-electron/facade");
+useFacade();
+//@ts-ignore
+const { contextBridge, ipcRenderer } = require("electron");
+
+contextBridge.exposeInMainWorld("electronAPI", {
+ invokePluginFunc: (plugin: any, method: any, ...args: any[]) =>
+ ipcRenderer.invoke("invokePluginFunc", plugin, method, ...args),
+
+ basePlugins: () => ipcRenderer.invoke("basePlugins"),
+
+ pluginPath: () => ipcRenderer.invoke("pluginPath"),
+
+ appVersion: () => ipcRenderer.invoke("appVersion"),
+
+ openExternalUrl: (url: string) => ipcRenderer.invoke("openExternalUrl", url),
+
+ deleteFile: (filePath: string) => ipcRenderer.invoke("deleteFile", filePath),
+
+ downloadFile: (url: string, path: string) =>
+ ipcRenderer.invoke("downloadFile", url, path),
+
+ onFileDownloadUpdate: (callback: any) =>
+ ipcRenderer.on("FILE_DOWNLOAD_UPDATE", callback),
+
+ onFileDownloadError: (callback: any) =>
+ ipcRenderer.on("FILE_DOWNLOAD_ERROR", callback),
+
+ onFileDownloadSuccess: (callback: any) =>
+ ipcRenderer.on("FILE_DOWNLOAD_COMPLETE", callback),
+
+ onAppUpdateDownloadUpdate: (callback: any) =>
+ ipcRenderer.on("APP_UPDATE_PROGRESS", callback),
+
+ onAppUpdateDownloadError: (callback: any) =>
+ ipcRenderer.on("APP_UPDATE_ERROR", callback),
+
+ onAppUpdateDownloadSuccess: (callback: any) =>
+ ipcRenderer.on("APP_UPDATE_COMPLETE", callback),
+});
diff --git a/electron/tsconfig.json b/electron/tsconfig.json
new file mode 100644
index 000000000..675a6abec
--- /dev/null
+++ b/electron/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "module": "commonjs",
+ "sourceMap": true,
+ "strict": true,
+ "outDir": "./build",
+ "rootDir": "./",
+ "noEmitOnError": true,
+ "allowJs": true,
+ "typeRoots": ["node_modules/@types"]
+ },
+ "exclude": ["core", "build", "node_modules"]
+}
diff --git a/node_modules/.yarn-integrity b/node_modules/.yarn-integrity
new file mode 100644
index 000000000..42a6cb985
--- /dev/null
+++ b/node_modules/.yarn-integrity
@@ -0,0 +1,12 @@
+{
+ "systemParams": "darwin-arm64-93",
+ "modulesFolders": [
+ "node_modules"
+ ],
+ "flags": [],
+ "linkedModules": [],
+ "topLevelPatterns": [],
+ "lockfileEntries": {},
+ "files": [],
+ "artifacts": {}
+}
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..827d81d8a
--- /dev/null
+++ b/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "jan-app",
+ "private": true,
+ "workspaces": {
+ "packages": [
+ "electron",
+ "web"
+ ],
+ "nohoist": [
+ "electron",
+ "electron/**",
+ "web",
+ "web/**"
+ ]
+ },
+ "scripts": {
+ "dev:electron": "yarn workspace jan-electron dev",
+ "dev:web": "yarn workspace jan-web dev",
+ "dev": "concurrently --kill-others-on-fail \"yarn dev:web\" \"wait-on http://localhost:3000 && yarn dev:electron\"",
+ "build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
+ "build:electron": "yarn workspace jan-electron build",
+ "build:plugins": "rm -f ./electron/core/pre-install/*.tgz && concurrently \"cd ./electron/core/plugins/data-plugin && npm install && npm run build:publish\" \"cd ./electron/core/plugins/inference-plugin && npm install && npm run build:publish\" \"cd ./electron/core/plugins/model-management-plugin && npm install && npm run build:publish\" \"cd ./electron/core/plugins/monitoring-plugin && npm install && npm run build:publish\"",
+ "build": "yarn build:web && yarn build:electron",
+ "build:publish": "yarn build:web && yarn workspace jan-electron build:publish"
+ },
+ "devDependencies": {
+ "concurrently": "^8.2.1",
+ "cpx": "^1.5.0",
+ "wait-on": "^7.0.1"
+ },
+ "version": "0.0.0"
+}
diff --git a/run.sh b/run.sh
deleted file mode 100755
index 2856f8770..000000000
--- a/run.sh
+++ /dev/null
@@ -1,179 +0,0 @@
-#!/bin/bash
-### Clean sub-processes on exit
-trap "trap - SIGTERM && kill -- -$$" SIGINT
-
-MAX_STEPS=13
-progress() {
- local BAR_SIZE="##########"
- local MAX_BAR_SIZE="${#BAR_SIZE}"
- local CLEAR_LINE="\\033[K"
- spin[0]="-"
- spin[1]="\\"
- spin[2]="|"
- spin[3]="/"
- perc=$((($3 + 1) * 100 / MAX_STEPS))
- percBar=$((perc * MAX_BAR_SIZE / 100))
-
- eval "$1" >/dev/null 2>error.log &
- pid=$!
-
- echo -ne "\\r- [$3/$MAX_STEPS] [ ] $2 ...$CLEAR_LINE\\n"
- while kill -0 $pid >/dev/null 2>&1; do
- for i in "${spin[@]}"; do
- echo -ne "\\r\\033[1A- [$3/$MAX_STEPS] [$i] $2 $CLEAR_LINE\\n"
- sleep 0.1
- done
- done
- if [ -s "error.log" ] && [[ "$(cat error.log)" != "WARNING"* ]]; then
- echo -ne "\\r\\033[1A- [$3/$MAX_STEPS] [x] $2\\n $(cat error.log)"
- exit 1
- fi
- echo -ne "\\r\\033[1A- [$3/$MAX_STEPS] [✔] $2 $CLEAR_LINE\\n"
-}
-step=1
-
-mkdir -p models/
-
-### macOS setup
-if [[ "$OSTYPE" == "darwin"* ]]; then
- MAX_STEPS=13
- if [[ ! -x "$(command -v brew)" ]]; then
- progress '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' "Installing Homebrew" 1
- else
- progress '' "Homebrew - Installed" $((step++))
- fi
-
- xcode-select -p &>/dev/null
- if [ $? -ne 0 ]; then
- progress 'xcode-select --install' "Installing Xcode Command Line Tools" $((step++))
- else
- progress '' "Xcode Command Line Tools - Installed" $((step++))
- fi
-
- if [[ ! -x "$(command -v git)" ]]; then
- progress 'brew install git' "Installing Git" $((step++))
- else
- progress '' "Git - Installed" $((step++))
- fi
-
- if [[ ! -x "$(command -v wget)" ]]; then
- progress 'brew install wget' "Installing Wget" $((step++))
- else
- progress '' "Wget - Installed" $((step++))
- fi
-
- if [[ ! -x "$(command -v docker)" ]]; then
- progress 'brew install --cask docker' "Installing Docker" $((step++))
- else
- progress '' "Docker - Installed" $((step++))
- fi
-
- docker compose version &>/dev/null
- if [ $? -ne 0 ] && [ ! -x "$(command -v docker-compose)" ]; then
- progress 'brew install docker-compose' "Installing Docker Compose" $((step++))
- else
- progress '' "docker-compose - Installed" $((step++))
- fi
-fi
-###
-
-### Debian setup
-if [[ "$OSTYPE" == "linux"* ]]; then
- MAX_STEPS=12
- progress "sudo apt update 2>/dev/null" "Apt Updating" $((step++))
-
- if [[ ! -x "$(command -v git)" ]]; then
- progress 'sudo apt install git' "Installing Git" $((step++))
- else
- progress '' "Git - Installed" $((step++))
- fi
-
- if [[ ! -x "$(command -v wget)" ]]; then
- progress 'sudo apt install wget' "Installing Wget" $((step++))
- else
- progress '' "Wget - Installed" $((step++))
- fi
-
- if [[ ! -x "$(command -v docker)" ]]; then
- progress '/bin/bash -c "$(curl -fsSL https://get.docker.com/) 2>/dev/null"' "Installing Docker" $((step++))
- else
- progress '' "Docker - Installed" $((step++))
- fi
-
- docker compose version &>/dev/null
- if [ $? -ne 0 ] && [ ! -x "$(command -v docker-compose)" ]; then
- progress 'sudo apt install docker-compose' "Installing Docker Compose" $((step++))
- else
- progress '' "docker-compose - Installed" $((step++))
- fi
-fi
-###
-
-### Pull Jan
-if [ -d "jan" ]; then
- cd jan
- progress 'git pull 2>/dev/null' "Git pull" $((step++))
-else
- progress 'git clone --quiet https://github.com/janhq/jan' "Git clone" $((step++))
- cd jan
-fi
-
-progress 'git submodule update --init --recursive' "Pull submodule" $((step++))
-###
-
-### Prepare environment
-progress 'cp -f sample.env .env' "Prepare .env file" $((step++))
-###
-
-### Download Model
-if [ -f "/models/llama-2-7b.Q4_K_S.gguf" ]; then
- progress '' "Llama model - Installed" $((step++))
-else
- progress 'wget https://huggingface.co/TheBloke/Llama-2-7B-GGUF/resolve/main/llama-2-7b.Q4_K_S.gguf -P models' "Download Llama model" $((step++))
-fi
-###
-
-### Launch Docker & Docker compose up
-if [[ "$OSTYPE" == "darwin"* ]]; then
- progress $'
- if (! docker stats --no-stream 2>/dev/null ); then
- open /Applications/Docker.app
- while (! docker stats --no-stream 2>/dev/null ); do
- sleep 0.3
- done
- fi' "Waiting for docker to launch" $((step++))
-elif [[ "$OSTYPE" == "linux"* ]]; then
- progress 'sudo service docker start 2>/dev/null' "Starting Docker Service" $((step++))
-fi
-
-docker compose version &>/dev/null
-if [[ "$OSTYPE" == "darwin"* ]]; then
- if [ $? == 0 ]; then
- progress 'docker compose up -d --quiet-pull --remove-orphans 2>/dev/null' "Docker compose up" $((step++))
- elif [[ -x "$(command -v docker-compose)" ]]; then
- progress 'docker-compose up -d --quiet-pull --remove-orphans 2>/dev/null' "Docker compose up" $((step++))
- fi
-elif [[ "$OSTYPE" == "linux"* ]]; then
- if [[ $? == 0 ]]; then
- progress 'sudo docker compose up -d --quiet-pull --remove-orphans 2>/dev/null' "Docker compose up" $((step++))
- elif [[ -x "$(command -v docker-compose)" ]]; then
- progress 'sudo docker-compose up -d --quiet-pull --remove-orphans 2>/dev/null' "Docker compose up" $((step++))
- fi
-else
- echo >&2 "Can not find docker compose runner"
- exit 2
-fi
-
-###
-
-### Wait for service ready
-progress $'
- while (true); do
- if curl -sL -w "%{http_code}\\n" "http://localhost:3000" -o /dev/null | grep -q "200"; then
- break
- fi
-done
-' "Waiting for service ready" $((step++))
-###
-
-echo -ne "\\r You can now view Jan app in the browser: http://localhost:3000 \\n"
diff --git a/sample.env b/sample.env
deleted file mode 100644
index ab897ce1a..000000000
--- a/sample.env
+++ /dev/null
@@ -1,8 +0,0 @@
-KEYCLOAK_VERSION=22.0.0
-KEYCLOAK_ADMIN=admin
-KEYCLOAK_ADMIN_PASSWORD=admin
-
-# Inference
-## LLM
-LLM_MODEL_URL=https://huggingface.co/TheBloke/CodeLlama-13B-GGUF/resolve/main/codellama-13b.Q3_K_L.gguf
-LLM_MODEL_FILE=llama-2-7b-chat.ggmlv3.q4_1.bin
\ No newline at end of file
diff --git a/web-client/README.md b/web-client/README.md
deleted file mode 100644
index ef0d70d5e..000000000
--- a/web-client/README.md
+++ /dev/null
@@ -1,105 +0,0 @@
-# Jan Web
-
-Jan Web is a Next.js application designed to provide users with the ability to interact with the Language Model (LLM) through chat or generate art using Stable Diffusion. This application runs as a single-page application (SPA) and is encapsulated within a Docker container for easy local deployment.
-
-## Features
-
-- Chat with the Language Model: Engage in interactive conversations with the Language Model. Ask questions, seek information, or simply have a chat.
-
-- Generate Art with Stable Diffusion: Utilize the power of Stable Diffusion to generate unique and captivating pieces of art. Experiment with various parameters to achieve desired results.
-
-## Installation and Usage
-
-### Use as complete suite
-For using our complete solution, check [this](https://github.com/janhq/jan)
-
-### For interactive development
-
-1. **Clone the Repository:**
-
- ```
- git clone https://github.com/your-username/jan-web.git
- cd jan-web
- ```
-
-2. **Install dependencies:**
-
- ```
- yarn
- ```
-
-3. **Run development:**
-
- ```
- yarn dev
- ```
-4. **Regenerate Graphql:**
-
- ```
- HASURA_ADMIN_TOKEN="[hasura_admin_secret_key]" yarn generate
- ```
-
-5. **Access Jan Web:**
-
- Open your web browser and navigate to `http://localhost:3000` to access the Jan Web application.
-
-## Configuration
-
-You can customize the endpoint of the Jan Web application through environment file. These options can be found in the `.env` file located in the project's root directory.
-
-```env
-// .env
-
-KEYCLOAK_CLIENT_ID=hasura
-KEYCLOAK_CLIENT_SECRET=**********
-AUTH_ISSUER=http://localhost:8088/realms/hasura
-NEXTAUTH_URL=http://localhost:3000
-NEXTAUTH_SECRET=my-secret
-END_SESSION_URL=http://localhost:8088/realms/hasura/protocol/openid-connect/logout
-REFRESH_TOKEN_URL=http://localhost:8088/realms/hasura/protocol/openid-connect/token
-HASURA_ADMIN_TOKEN=myadminsecretkey
-NEXT_PUBLIC_GRAPHQL_ENGINE_URL=localhost:8080
-```
-
-Replace above configuration with your actual infrastructure.
-
-## Dependencies
-
-|Library| Category | Version | Description |
-|--|--|--|--|
-| [next](https://nextjs.org/) | Framework | 13.4.10 |
-| [typescript](https://www.typescriptlang.org/) | Language | 5.1.6 |
-| [tailwindcss](https://tailwindcss.com/) | UI | 3.3.3 |
-| [Tailwind UI](https://tailwindui.com/) | UI | |
-| [react-hook-form](https://www.react-hook-form.com/) | UI | ^7.45.4 |
-| [@headlessui/react](https://headlessui.com/) | UI | ^1.7.15 |
-| [@heroicons/react](https://heroicons.com/) | UI | ^2.0.18 |
-| [@tailwindcss/typography](https://tailwindcss.com/docs/typography-plugin) | UI | ^0.5.9 |
-| [embla-carousel](https://www.embla-carousel.com/) | UI | ^8.0.0-rc11 |
-| [@apollo/client](https://www.apollographql.com/docs/react/) | State management | ^3.8.1 |
-| [jotai](https://jotai.org/) | State management | ^2.4.0 |
-
-
-## Deploy to Netlify
-Clone this repository on own GitHub account and deploy to Netlify:
-
-[](https://app.netlify.com/start/deploy?repository=https://github.com/janhq/jan-web)
-
-## Deploy to Vercel
-
-Deploy Jan Web on Vercel in one click:
-
-[](https://vercel.com/new/clone?repository-url=https://github.com/janhq/jan-web)
-
-
-## Contributing
-
-Contributions are welcome! If you find a bug or have suggestions for improvements, feel free to open an issue or submit a pull request on the [GitHub repository](https://github.com/janhq/jan-web/tree/6337306c54e735a4a5c2132dcd1377f21fd76a33).
-
-## License
-
-This project is licensed under the Fair-code License - see the [License](https://faircode.io/#licenses) for more details.
-
----
-
-Feel free to reach out [Discord](https://jan.ai/discord) if you have any questions or need further assistance. Happy coding with Jan Web and exploring the capabilities of the Language Model and Stable Diffusion! 🚀🎨🤖
\ No newline at end of file
diff --git a/web-client/app/_components/ActionButton/index.tsx b/web-client/app/_components/ActionButton/index.tsx
deleted file mode 100644
index a23bb51e2..000000000
--- a/web-client/app/_components/ActionButton/index.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import classNames from "classnames";
-import Image from "next/image";
-
-type Props = {
- title: string;
- icon: string;
- isLoading?: boolean;
- onClick: () => void;
-};
-
-const ActionButton: React.FC = (props) => {
- return (
- <>
- {!props.isLoading && (
-
-
- {props.title}
-
- )}
- {props.isLoading && (
-
-
-
- )}
- >
- );
-};
-
-export default ActionButton;
diff --git a/web-client/app/_components/ApiPane/index.tsx b/web-client/app/_components/ApiPane/index.tsx
deleted file mode 100644
index dc451a487..000000000
--- a/web-client/app/_components/ApiPane/index.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import Image from "next/image";
-import { useState } from "react";
-import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
-import js from "react-syntax-highlighter/dist/esm/languages/hljs/javascript";
-import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
-import useGetModelApiInfo from "@/_hooks/useGetModelApiInfo";
-
-SyntaxHighlighter.registerLanguage("javascript", js);
-
-const ApiPane: React.FC = () => {
- const [expend, setExpend] = useState(true);
- const { data } = useGetModelApiInfo();
- const [highlightCode, setHighlightCode] = useState(data[0]);
-
- return (
-
-
-
setExpend(!expend)}
- className="flex items-center flex-none"
- >
-
- Request
-
-
-
-
- {data.map((item, index) => (
- setHighlightCode(item)}
- >
- {item.type}
-
- ))}
-
-
- navigator.clipboard.writeText(highlightCode?.stringCode)
- }
- >
-
-
-
-
- {highlightCode?.stringCode}
-
-
-
-
- );
-};
-
-export default ApiPane;
\ No newline at end of file
diff --git a/web-client/app/_components/ChatContainer/index.tsx b/web-client/app/_components/ChatContainer/index.tsx
deleted file mode 100644
index 391ad60c2..000000000
--- a/web-client/app/_components/ChatContainer/index.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-"use client";
-
-import ModelDetailSideBar from "../ModelDetailSideBar";
-import ProductOverview from "../ProductOverview";
-import { useAtomValue } from "jotai";
-import {
- getActiveConvoIdAtom,
- showingProductDetailAtom,
-} from "@/_helpers/JotaiWrapper";
-import { ReactNode } from "react";
-
-type Props = {
- children: ReactNode;
-};
-
-export default function ChatContainer({ children }: Props) {
- const activeConvoId = useAtomValue(getActiveConvoIdAtom);
- const showingProductDetail = useAtomValue(showingProductDetailAtom);
-
- if (!activeConvoId) {
- return ;
- }
-
- return (
-
- {children}
- {showingProductDetail ? : null}
-
- );
-}
diff --git a/web-client/app/_components/GenerativeSampleContainer/index.tsx b/web-client/app/_components/GenerativeSampleContainer/index.tsx
deleted file mode 100644
index f42be8be3..000000000
--- a/web-client/app/_components/GenerativeSampleContainer/index.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import JanWelcomeTitle from "../JanWelcomeTitle";
-import { GetProductPromptsQuery, GetProductPromptsDocument } from "@/graphql";
-import { useQuery } from "@apollo/client";
-import { Product } from "@/_models/Product";
-import { useSetAtom } from "jotai";
-import { currentPromptAtom } from "@/_helpers/JotaiWrapper";
-
-type Props = {
- product: Product;
-};
-
-const GenerativeSampleContainer: React.FC = ({ product }) => {
- const setCurrentPrompt = useSetAtom(currentPromptAtom);
- const { data } = useQuery(GetProductPromptsDocument, {
- variables: { productSlug: product.slug },
- });
-
- return (
-
-
-
-
- Create now
-
-
- {data?.prompts.map((item) => (
-
setCurrentPrompt(item.content ?? "")}
- className="w-full h-full"
- >
-
-
- ))}
-
-
-
- );
-};
-
-export default GenerativeSampleContainer;
diff --git a/web-client/app/_components/Header/index.tsx b/web-client/app/_components/Header/index.tsx
deleted file mode 100644
index 98a3a125d..000000000
--- a/web-client/app/_components/Header/index.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from "react";
-import UserProfileDropDown from "../UserProfileDropDown";
-import LoginButton from "../LoginButton";
-import HamburgerButton from "../HamburgerButton";
-
-const Header: React.FC = () => (
-
-);
-
-export default Header;
diff --git a/web-client/app/_components/HistoryEmpty/index.tsx b/web-client/app/_components/HistoryEmpty/index.tsx
deleted file mode 100644
index ed4f07f2c..000000000
--- a/web-client/app/_components/HistoryEmpty/index.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import Image from "next/image";
-import Link from "next/link";
-import React from "react";
-
-const HistoryEmpty: React.FC = () => {
- return (
-
-
-
- Jan allows you to use 100s of AIs on your mobile phone
-
-
- Explore AIs
-
-
- );
-};
-
-export default React.memo(HistoryEmpty);
diff --git a/web-client/app/_components/LeftContainer/index.tsx b/web-client/app/_components/LeftContainer/index.tsx
deleted file mode 100644
index 69f4c6aab..000000000
--- a/web-client/app/_components/LeftContainer/index.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-"use client";
-
-import { useAtomValue } from "jotai";
-import AdvancedPrompt from "../AdvancedPrompt";
-import CompactSideBar from "../CompactSideBar";
-import LeftSidebar from "../LeftSidebar";
-import { showingAdvancedPromptAtom } from "@/_helpers/JotaiWrapper";
-
-const LeftContainer: React.FC = () => {
- const isShowingAdvPrompt = useAtomValue(showingAdvancedPromptAtom);
-
- if (isShowingAdvPrompt) {
- return (
-
- );
- }
-
- return ;
-};
-
-export default LeftContainer;
diff --git a/web-client/app/_components/LoadingIndicator.tsx b/web-client/app/_components/LoadingIndicator.tsx
deleted file mode 100644
index 21220305d..000000000
--- a/web-client/app/_components/LoadingIndicator.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-const LoadingIndicator = () => {
- let circleCommonClasses = "h-1.5 w-1.5 bg-current rounded-full";
-
- return (
-
- );
-};
-
-export default LoadingIndicator;
diff --git a/web-client/app/_components/LoginButton/index.tsx b/web-client/app/_components/LoginButton/index.tsx
deleted file mode 100644
index f2712eeaf..000000000
--- a/web-client/app/_components/LoginButton/index.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-"use client";
-
-import useGetCurrentUser from "@/_hooks/useGetCurrentUser";
-import useSignIn from "@/_hooks/useSignIn";
-
-const LoginButton: React.FC = () => {
- const { signInWithKeyCloak } = useSignIn();
- const { user, loading } = useGetCurrentUser();
-
- if (loading || user) {
- return
;
- }
-
- return (
-
-
- Login
-
-
- );
-};
-
-export default LoginButton;
diff --git a/web-client/app/_components/MenuHeader/index.tsx b/web-client/app/_components/MenuHeader/index.tsx
deleted file mode 100644
index f04f5d1b4..000000000
--- a/web-client/app/_components/MenuHeader/index.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import Link from "next/link";
-import { Popover, Transition } from "@headlessui/react";
-import { Fragment } from "react";
-import useGetCurrentUser from "@/_hooks/useGetCurrentUser";
-import { useSetAtom } from "jotai";
-import { showConfirmSignOutModalAtom } from "@/_helpers/JotaiWrapper";
-
-export const MenuHeader: React.FC = () => {
- const setShowConfirmSignOutModal = useSetAtom(showConfirmSignOutModalAtom);
- const { user } = useGetCurrentUser();
-
- if (!user) {
- return
;
- }
-
- return (
-
-
-
-
- {user.displayName}
-
-
- {user.email}
-
-
-
- setShowConfirmSignOutModal(true)}
- className="px-4 py-3 text-sm w-full text-left text-gray-700"
- >
- Sign Out
-
-
-
-
-
Privacy
-
-
-
-
Support
-
-
-
-
- );
-};
diff --git a/web-client/app/_components/ProductOverview/index.tsx b/web-client/app/_components/ProductOverview/index.tsx
deleted file mode 100644
index 2ab063456..000000000
--- a/web-client/app/_components/ProductOverview/index.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import React from "react";
-import Slider from "../Slider";
-import ConversationalList from "../ConversationalList";
-import GenerateImageList from "../GenerateImageList";
-import Image from "next/image";
-import useGetProducts from "@/_hooks/useGetProducts";
-
-const ProductOverview: React.FC = () => {
- const { loading, featured, conversational, generativeArts } =
- useGetProducts();
-
- if (loading) {
- return (
-
-
-
- );
- }
-
- return (
-
-
-
-
-
- );
-};
-
-export default ProductOverview;
diff --git a/web-client/app/_components/ShortcutList/index.tsx b/web-client/app/_components/ShortcutList/index.tsx
deleted file mode 100644
index 2c2e9930f..000000000
--- a/web-client/app/_components/ShortcutList/index.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-"use client";
-
-import React, { useEffect, useState } from "react";
-import ShortcutItem from "../ShortcutItem";
-import { GetProductsDocument, GetProductsQuery } from "@/graphql";
-import ExpandableHeader from "../ExpandableHeader";
-import { useQuery } from "@apollo/client";
-import { useAtomValue } from "jotai";
-import { searchAtom } from "@/_helpers/JotaiWrapper";
-import { Product, toProduct } from "@/_models/Product";
-
-const ShortcutList: React.FC = () => {
- const searchText = useAtomValue(searchAtom);
- const { data } = useQuery(GetProductsDocument);
- const [expand, setExpand] = useState(true);
- const [featuredProducts, setFeaturedProducts] = useState([]);
-
- useEffect(() => {
- if (data?.products) {
- const products: Product[] = data.products.map((p) => toProduct(p));
- setFeaturedProducts(
- [...(products || [])]
- .sort(() => 0.5 - Math.random())
- .slice(0, 3)
- .filter(
- (e) =>
- searchText === "" ||
- e.name.toLowerCase().includes(searchText.toLowerCase())
- ) || []
- );
- }
- }, [data?.products, searchText]);
-
- return (
-
-
setExpand(!expand)}
- />
- {expand ? (
-
- {featuredProducts.map((product) => (
-
- ))}
-
- ) : null}
-
- );
-};
-
-export default ShortcutList;
diff --git a/web-client/app/_components/TryItYourself/index.tsx b/web-client/app/_components/TryItYourself/index.tsx
deleted file mode 100644
index 707569a51..000000000
--- a/web-client/app/_components/TryItYourself/index.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { currentProductAtom, currentPromptAtom } from "@/_helpers/JotaiWrapper";
-import { GetProductPromptsQuery, GetProductPromptsDocument } from "@/graphql";
-import { useQuery } from "@apollo/client";
-import { useAtomValue, useSetAtom } from "jotai";
-
-const TryItYourself = () => {
- const setCurrentPrompt = useSetAtom(currentPromptAtom);
- const product = useAtomValue(currentProductAtom);
- const { data } = useQuery(GetProductPromptsDocument, {
- variables: { productSlug: product?.slug ?? "" },
- });
-
- if (!data || data.prompts.length === 0) {
- return
;
- }
-
- const promps = data.prompts;
-
- return (
-
-
Try it yourself
-
- {promps.map((prompt, index) => (
- setCurrentPrompt(prompt.content ?? "")}
- key={prompt.slug}
- className={`text-sm text-gray-500 leading-[20px] flex gap-[10px] border-b-[${
- index !== promps.length - 1 ? "1" : "0"
- }px] border-[#E5E7EB] hover:text-blue-400 text-left p-3 w-full`}
- >
- {prompt.content}
-
- ))}
-
-
- );
-};
-
-export default TryItYourself;
diff --git a/web-client/app/_components/UserProfileDropDown/index.tsx b/web-client/app/_components/UserProfileDropDown/index.tsx
deleted file mode 100644
index 3b2f10ca5..000000000
--- a/web-client/app/_components/UserProfileDropDown/index.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-"use client";
-
-import React from "react";
-import { Popover } from "@headlessui/react";
-import { MenuHeader } from "../MenuHeader";
-import useGetCurrentUser from "@/_hooks/useGetCurrentUser";
-
-const UserProfileDropDown: React.FC = () => {
- const { loading, user } = useGetCurrentUser();
-
- if (loading || !user) {
- return
;
- }
-
- return (
-
-
-
-
-
-
- {user.displayName}
-
-
-
-
-
-
- );
-};
-
-export default UserProfileDropDown;
diff --git a/web-client/app/_helpers/ApolloWrapper.tsx b/web-client/app/_helpers/ApolloWrapper.tsx
deleted file mode 100644
index 8afcd2295..000000000
--- a/web-client/app/_helpers/ApolloWrapper.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-"use client";
-
-import {
- ApolloProvider,
- ApolloClient,
- InMemoryCache,
- HttpLink,
- concat,
- split,
-} from "@apollo/client";
-import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
-import { setContext } from "@apollo/client/link/context";
-import { createClient } from "graphql-ws";
-import { getMainDefinition } from "@apollo/client/utilities";
-import { getAccessToken } from "@/_utils/tokenAccessor";
-import { ReactNode } from "react";
-
-const authMiddleware = setContext(async (_, { headers }) => {
- const token = await getAccessToken();
- return {
- headers: {
- ...headers,
- ...(token && { authorization: token ? `Bearer ${token}` : "" }),
- },
- };
-});
-
-const wsLink =
- typeof window !== "undefined"
- ? new GraphQLWsLink(
- createClient({
- url: `${process.env.NEXT_PUBLIC_GRAPHQL_ENGINE_WEB_SOCKET_URL}`,
- connectionParams: async () => {
- const token = await getAccessToken();
- return {
- headers: {
- Authorization: `Bearer ${token}`,
- },
- };
- },
- })
- )
- : null;
-const httpLink = new HttpLink({
- uri: `${process.env.NEXT_PUBLIC_GRAPHQL_ENGINE_URL}`,
-});
-
-const link =
- typeof window !== "undefined" && wsLink != null
- ? split(
- ({ query }) => {
- const definition = getMainDefinition(query);
- return (
- definition.kind === "OperationDefinition" &&
- definition.operation === "subscription"
- );
- },
- wsLink,
- httpLink
- )
- : httpLink;
-
-type Props = {
- children: ReactNode;
-};
-
-export const ApolloWrapper: React.FC = ({ children }) => {
- const client = new ApolloClient({
- link: concat(authMiddleware, link),
- cache: new InMemoryCache(),
- });
-
- return {children} ;
-};
diff --git a/web-client/app/_hooks/useChatMessageSubscription.ts b/web-client/app/_hooks/useChatMessageSubscription.ts
deleted file mode 100644
index 0cbb54bde..000000000
--- a/web-client/app/_hooks/useChatMessageSubscription.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import {
- SubscribeMessageSubscription,
- SubscribeMessageDocument,
-} from "@/graphql";
-import { useSubscription } from "@apollo/client";
-
-const useChatMessageSubscription = (messageId: string) => {
- const { data, loading, error } =
- useSubscription(SubscribeMessageDocument, {
- variables: { id: messageId },
- });
-
- return { data, loading, error };
-};
-
-export default useChatMessageSubscription;
diff --git a/web-client/app/_hooks/useChatMessages.ts b/web-client/app/_hooks/useChatMessages.ts
deleted file mode 100644
index 47914edc0..000000000
--- a/web-client/app/_hooks/useChatMessages.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import {
- addOldMessagesAtom,
- conversationStatesAtom,
- currentConversationAtom,
- updateConversationHasMoreAtom,
-} from "@/_helpers/JotaiWrapper";
-import { ChatMessage, toChatMessage } from "@/_models/ChatMessage";
-import { MESSAGE_PER_PAGE } from "@/_utils/const";
-import {
- GetConversationMessagesQuery,
- GetConversationMessagesDocument,
- GetConversationMessagesQueryVariables,
- MessageDetailFragment,
-} from "@/graphql";
-import { useLazyQuery } from "@apollo/client";
-import { useAtomValue, useSetAtom } from "jotai";
-import { useEffect } from "react";
-
-/**
- * Custom hooks to get chat messages for current(active) conversation
- *
- * @param offset for pagination purpose
- * @returns
- */
-const useChatMessages = (offset = 0) => {
- const addOldChatMessages = useSetAtom(addOldMessagesAtom);
- const currentConvo = useAtomValue(currentConversationAtom);
- if (!currentConvo) {
- throw new Error("activeConversation is null");
- }
- const convoStates = useAtomValue(conversationStatesAtom);
- const updateConvoHasMore = useSetAtom(updateConversationHasMoreAtom);
- const [getConversationMessages, { loading, error }] =
- useLazyQuery(GetConversationMessagesDocument);
-
- useEffect(() => {
- const hasMore = convoStates[currentConvo.id]?.hasMore ?? true;
- if (!hasMore) return;
-
- const variables: GetConversationMessagesQueryVariables = {
- conversation_id: currentConvo.id,
- limit: MESSAGE_PER_PAGE,
- offset: offset,
- };
-
- getConversationMessages({ variables }).then((data) => {
- parseMessages(data.data?.messages ?? []).then((newMessages) => {
- const isHasMore = newMessages.length === MESSAGE_PER_PAGE;
- addOldChatMessages(newMessages);
- updateConvoHasMore(currentConvo.id, isHasMore);
- });
- });
- }, [offset, currentConvo.id]);
-
- return {
- loading,
- error,
- hasMore: convoStates[currentConvo.id]?.hasMore ?? true,
- };
-};
-
-async function parseMessages(
- messages: MessageDetailFragment[]
-): Promise {
- const newMessages: ChatMessage[] = [];
- for (const m of messages) {
- const chatMessage = await toChatMessage(m);
- newMessages.push(chatMessage);
- }
- return newMessages;
-}
-
-export default useChatMessages;
diff --git a/web-client/app/_hooks/useCreateConversation.ts b/web-client/app/_hooks/useCreateConversation.ts
deleted file mode 100644
index badf3ff4c..000000000
--- a/web-client/app/_hooks/useCreateConversation.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import {
- CreateConversationMutation,
- CreateConversationDocument,
- CreateConversationMutationVariables,
-} from "@/graphql";
-import useGetCurrentUser from "./useGetCurrentUser";
-import { useMutation } from "@apollo/client";
-import useSignIn from "./useSignIn";
-import { useAtom, useSetAtom } from "jotai";
-import {
- addNewConversationStateAtom,
- setActiveConvoIdAtom,
- userConversationsAtom,
-} from "@/_helpers/JotaiWrapper";
-import { Conversation } from "@/_models/Conversation";
-import { Product } from "@/_models/Product";
-import { MessageSenderType, MessageType } from "@/_models/ChatMessage";
-
-const useCreateConversation = () => {
- const [userConversations, setUserConversations] = useAtom(
- userConversationsAtom
- );
- const setActiveConvoId = useSetAtom(setActiveConvoIdAtom);
- const addNewConvoState = useSetAtom(addNewConversationStateAtom);
- const { user } = useGetCurrentUser();
- const { signInWithKeyCloak } = useSignIn();
- const [createConversation] = useMutation(
- CreateConversationDocument
- );
-
- const requestCreateConvo = async (
- product: Product,
- forceCreate: boolean = false
- ) => {
- if (!user) {
- signInWithKeyCloak();
- return;
- }
-
- // search if any fresh convo with particular product id
- const convo = userConversations.find(
- (convo) => convo.product.slug === product.slug
- );
-
- if (convo && !forceCreate) {
- setActiveConvoId(convo.id);
- return;
- }
-
- const variables: CreateConversationMutationVariables = {
- data: {
- product_id: product.id,
- user_id: user.id,
- last_image_url: "",
- last_text_message: product.greeting,
- conversation_messages: {
- data: [
- {
- content: product.greeting || "Hello there 👋",
- sender: MessageSenderType.Ai,
- sender_name: product.name,
- sender_avatar_url: product.avatarUrl,
- message_type: MessageType.Text,
- message_sender_type: MessageSenderType.Ai,
- },
- ],
- },
- },
- };
- const result = await createConversation({
- variables,
- });
- const newConvo = result.data?.insert_conversations_one;
-
- if (newConvo) {
- const mappedConvo: Conversation = {
- id: newConvo.id,
- product: product,
- user: {
- id: user.id,
- displayName: user.displayName,
- },
- lastTextMessage: newConvo.last_text_message ?? "",
- createdAt: Date.now(),
- updatedAt: Date.now(),
- };
- addNewConvoState(newConvo.id, {
- hasMore: true,
- waitingForResponse: false,
- });
- setUserConversations([...userConversations, mappedConvo]);
- setActiveConvoId(newConvo.id);
- }
- // if not found, create new convo and set it as current
- };
-
- return {
- requestCreateConvo,
- };
-};
-
-export default useCreateConversation;
diff --git a/web-client/app/_hooks/useGetProducts.ts b/web-client/app/_hooks/useGetProducts.ts
deleted file mode 100644
index a87c6537e..000000000
--- a/web-client/app/_hooks/useGetProducts.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { ProductType, toProduct } from "@/_models/Product";
-import { GetProductsDocument, GetProductsQuery } from "@/graphql";
-import { useQuery } from "@apollo/client";
-
-export default function useGetProducts() {
- const { loading, data } = useQuery(GetProductsDocument, {
- variables: { slug: "conversational" },
- });
-
- const allProducts = (data?.products ?? []).map((e) => toProduct(e));
-
- const featured = allProducts.sort(() => 0.5 - Math.random()).slice(0, 3);
- const conversational = allProducts.filter((e) => e.type === ProductType.LLM);
- const generativeArts = allProducts.filter(
- (e) => e.type === ProductType.GenerativeArt
- );
-
- return {
- loading,
- featured,
- conversational,
- generativeArts,
- };
-}
diff --git a/web-client/app/_hooks/useGetUserConversations.ts b/web-client/app/_hooks/useGetUserConversations.ts
deleted file mode 100644
index 883518d9b..000000000
--- a/web-client/app/_hooks/useGetUserConversations.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { GetConversationsQuery, GetConversationsDocument } from "@/graphql";
-import { useLazyQuery } from "@apollo/client";
-import { ConversationState, toConversation } from "@/_models/Conversation";
-import { useSetAtom } from "jotai";
-import {
- conversationStatesAtom,
- userConversationsAtom,
-} from "@/_helpers/JotaiWrapper";
-
-const useGetUserConversations = () => {
- const setConversationStates = useSetAtom(conversationStatesAtom);
- const setConversations = useSetAtom(userConversationsAtom);
- const [getConvos] = useLazyQuery(
- GetConversationsDocument
- );
-
- const getUserConversations = async () => {
- const results = await getConvos();
- if (!results || !results.data || results.data.conversations.length === 0) {
- return;
- }
-
- const convos = results.data.conversations.map((e) => toConversation(e));
- const convoStates: Record = {};
- convos.forEach((convo) => {
- convoStates[convo.id] = {
- hasMore: true,
- waitingForResponse: false,
- };
- });
- setConversationStates(convoStates);
- setConversations(convos);
- };
-
- return {
- getUserConversations,
- };
-};
-
-export default useGetUserConversations;
diff --git a/web-client/app/_hooks/useSendChatMessage.ts b/web-client/app/_hooks/useSendChatMessage.ts
deleted file mode 100644
index c67f7c668..000000000
--- a/web-client/app/_hooks/useSendChatMessage.ts
+++ /dev/null
@@ -1,383 +0,0 @@
-import {
- addNewMessageAtom,
- currentChatMessagesAtom,
- currentConversationAtom,
- currentPromptAtom,
- currentStreamingMessageAtom,
- setConvoLastImageAtom,
- setConvoUpdatedAtAtom,
- updateConversationWaitingForResponseAtom,
- updateMessageAtom,
- userConversationsAtom,
-} from "@/_helpers/JotaiWrapper";
-import {
- ChatMessage,
- MessageSenderType,
- MessageStatus,
- MessageType,
-} from "@/_models/ChatMessage";
-import { Conversation } from "@/_models/Conversation";
-import { ProductType } from "@/_models/Product";
-import {
- CreateMessageDocument,
- CreateMessageMutation,
- CreateMessageMutationVariables,
- GenerateImageDocument,
- GenerateImageMutation,
- GenerateImageMutationVariables,
- UpdateMessageMutation,
- UpdateMessageDocument,
- UpdateMessageMutationVariables,
- UpdateConversationMutation,
- UpdateConversationDocument,
- UpdateConversationMutationVariables,
-} from "@/graphql";
-import { useMutation } from "@apollo/client";
-import { useAtom, useAtomValue, useSetAtom } from "jotai";
-import useSignIn from "./useSignIn";
-import useGetCurrentUser from "./useGetCurrentUser";
-import { Role } from "@/_models/User";
-
-export default function useSendChatMessage() {
- const { user } = useGetCurrentUser();
- const { signInWithKeyCloak } = useSignIn();
- const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom);
- const [userConversations, setUserConversations] = useAtom(
- userConversationsAtom
- );
- const addNewMessage = useSetAtom(addNewMessageAtom);
- const activeConversation = useAtomValue(currentConversationAtom);
- const currentMessages = useAtomValue(currentChatMessagesAtom);
- const [createMessageMutation] = useMutation(
- CreateMessageDocument
- );
- const [updateMessageMutation] = useMutation(
- UpdateMessageDocument
- );
- const [updateConversationMutation] = useMutation(
- UpdateConversationDocument
- );
- const [imageGenerationMutation] = useMutation(
- GenerateImageDocument
- );
- const updateConvoWaitingState = useSetAtom(
- updateConversationWaitingForResponseAtom
- );
- const updateMessageText = useSetAtom(updateMessageAtom);
- const [, setTextMessage] = useAtom(currentStreamingMessageAtom);
- const setConvoLastImageUrl = useSetAtom(setConvoLastImageAtom);
- const setConvoUpdateAt = useSetAtom(setConvoUpdatedAtAtom);
-
- const sendTextToTextMessage = async (
- conversation: Conversation,
- latestUserMessage: ChatMessage
- ) => {
- // TODO: handle case timeout using higher order function
- const messageToSend = [
- latestUserMessage,
- ...currentMessages.slice(0, 4),
- ].reverse();
- const latestMessages = messageToSend.map((e) => ({
- role:
- e.messageSenderType === MessageSenderType.User
- ? Role.User
- : Role.Assistant,
- content: e.text,
- }));
-
- const variables: CreateMessageMutationVariables = {
- data: {
- conversation_id: conversation.id,
- sender: MessageSenderType.Ai,
- message_sender_type: MessageSenderType.Ai,
- message_type: MessageType.Text,
- sender_avatar_url: conversation.product.avatarUrl,
- sender_name: conversation.product.name,
- prompt_cache: latestMessages,
- status: MessageStatus.Pending,
- },
- };
- const result = await createMessageMutation({
- variables,
- });
-
- if (!result.data?.insert_messages_one?.id) {
- console.error(
- "Error creating user message",
- JSON.stringify(result.errors)
- );
- updateConvoWaitingState(conversation.id, false);
- return;
- }
-
- const aiResponseMessage: ChatMessage = {
- id: result.data.insert_messages_one.id,
- conversationId: conversation.id,
- messageType: MessageType.Text,
- messageSenderType: MessageSenderType.Ai,
- senderUid: conversation.product.slug,
- senderName: conversation.product.name,
- senderAvatarUrl: conversation.product.avatarUrl ?? "/icons/app_icon.svg",
- text: "",
- status: MessageStatus.Pending,
- createdAt: Date.now(),
- };
-
- setTextMessage(aiResponseMessage);
- addNewMessage(aiResponseMessage);
-
- try {
- const response = await fetch(
- `${process.env.NEXT_PUBLIC_OPENAPI_ENDPOINT}`,
- {
- method: "POST",
- cache: "no-cache",
- keepalive: true,
- headers: {
- "Content-Type": "application/json",
- Accept: "text/event-stream",
- },
- body: JSON.stringify({
- messages: latestMessages,
- model: "gpt-3.5-turbo",
- stream: true,
- max_tokens: 500,
- }),
- }
- );
- if (!response.ok) {
- updateMessageText(
- aiResponseMessage.id,
- conversation.id,
- "There is an error while retrieving the result. Please try again later."
- );
- } else {
- const data = response.body;
- if (!data) {
- return;
- }
- const reader = data.getReader();
- const decoder = new TextDecoder();
- let done = false;
-
- let currentResponse: string = "";
- updateConvoWaitingState(conversation.id, false);
- while (!done) {
- const { value, done: doneReading } = await reader.read();
- done = doneReading;
- const chunkValue = decoder.decode(value);
- chunkValue.split("\n").forEach((chunk) => {
- console.log("chunk", chunk);
- const text = parsedBuffer(chunk) ?? "";
- currentResponse += text;
- updateMessageText(
- aiResponseMessage.id,
- conversation.id,
- currentResponse
- );
- });
- }
- mutateMessageText(
- aiResponseMessage.id,
- conversation.id,
- currentResponse
- );
- }
- } catch (err) {
- const errorText =
- "There is an error while retrieving the result. Please try again later.";
- updateMessageText(aiResponseMessage.id, conversation.id, errorText);
- mutateMessageText(aiResponseMessage.id, conversation.id, errorText);
- }
- updateConvoWaitingState(conversation.id, false);
- };
-
- const sendTextToImageMessage = async (conversation: Conversation) => {
- // TODO: handle case timeout using higher order function
- const variables: GenerateImageMutationVariables = {
- model: conversation.product.slug,
- prompt: currentPrompt,
- neg_prompt: "",
- seed: Math.floor(Math.random() * 429496729),
- steps: 30,
- width: 512,
- height: 512,
- };
-
- const data = await imageGenerationMutation({
- variables,
- });
-
- if (!data.data?.imageGeneration?.url) {
- // TODO: display error
- console.error("Error creating user message", JSON.stringify(data.errors));
- updateConvoWaitingState(conversation.id, false);
- return;
- }
-
- const imageUrl: string = data.data.imageGeneration.url;
-
- const createMessageVariables: CreateMessageMutationVariables = {
- data: {
- conversation_id: conversation.id,
- content: currentPrompt,
- sender: MessageSenderType.Ai,
- message_sender_type: MessageSenderType.Ai,
- message_type: MessageType.Image,
- sender_avatar_url: conversation.product.avatarUrl,
- sender_name: conversation.product.name,
- status: MessageStatus.Ready,
- message_medias: {
- data: [
- {
- media_url: imageUrl,
- mime_type: "image/jpeg",
- },
- ],
- },
- },
- };
- const result = await createMessageMutation({
- variables: createMessageVariables,
- });
-
- if (!result.data?.insert_messages_one?.id) {
- // TODO: display error
- console.error(
- "Error creating user message",
- JSON.stringify(result.errors)
- );
- updateConvoWaitingState(conversation.id, false);
- return;
- }
-
- const imageResponseMessage: ChatMessage = {
- id: result.data.insert_messages_one.id,
- conversationId: conversation.id,
- messageType: MessageType.Image,
- messageSenderType: MessageSenderType.Ai,
- senderUid: conversation.product.slug,
- senderName: conversation.product.name,
- senderAvatarUrl: conversation.product.avatarUrl,
- text: currentPrompt,
- imageUrls: [imageUrl],
- createdAt: Date.now(),
- status: MessageStatus.Ready,
- };
-
- addNewMessage(imageResponseMessage);
- setConvoUpdateAt(conversation.id);
- setConvoLastImageUrl(conversation.id, imageUrl);
- updateConvoWaitingState(conversation.id, false);
- };
-
- const sendChatMessage = async () => {
- if (!user) {
- signInWithKeyCloak();
- return;
- }
- if (currentPrompt.trim().length === 0) return;
-
- if (!activeConversation) {
- console.error("No active conversation");
- return;
- }
-
- updateConvoWaitingState(activeConversation.id, true);
- const variables: CreateMessageMutationVariables = {
- data: {
- conversation_id: activeConversation.id,
- content: currentPrompt,
- sender: user.id,
- message_sender_type: MessageSenderType.User,
- message_type: MessageType.Text,
- sender_avatar_url: user.avatarUrl,
- sender_name: user.displayName,
- },
- };
- const result = await createMessageMutation({ variables });
-
- if (!result.data?.insert_messages_one?.id) {
- // TODO: display error
- console.error(
- "Error creating user message",
- JSON.stringify(result.errors)
- );
- updateConvoWaitingState(activeConversation.id, false);
- return;
- }
-
- const userMesssage: ChatMessage = {
- id: result.data.insert_messages_one.id,
- conversationId: activeConversation.id,
- messageType: MessageType.Text,
- messageSenderType: MessageSenderType.User,
- senderUid: user.id,
- senderName: user.displayName,
- senderAvatarUrl: user.avatarUrl ?? "/icons/app_icon.svg",
- text: currentPrompt,
- createdAt: Date.now(),
- status: MessageStatus.Ready,
- };
-
- addNewMessage(userMesssage);
- const newUserConversations = userConversations.map((e) => {
- if (e.id === activeConversation.id) {
- e.lastTextMessage = userMesssage.text;
- }
- return e;
- });
-
- setUserConversations(newUserConversations);
-
- if (activeConversation.product.type === ProductType.LLM) {
- await sendTextToTextMessage(activeConversation, userMesssage);
- setCurrentPrompt("");
- } else if (activeConversation.product.type === ProductType.GenerativeArt) {
- await sendTextToImageMessage(activeConversation);
- setCurrentPrompt("");
- } else {
- console.error(
- "We do not support this model type yet:",
- activeConversation.product.type
- );
- }
- };
-
- const parsedBuffer = (buffer: string) => {
- try {
- const json = buffer.replace("data: ", "");
- return JSON.parse(json).choices[0].delta.content;
- } catch (e) {
- return "";
- }
- };
-
- const mutateMessageText = (
- messageId: string,
- convId: string,
- text: string
- ) => {
- const variables: UpdateMessageMutationVariables = {
- data: {
- content: text,
- status: MessageStatus.Ready,
- },
- id: messageId,
- };
- updateMessageMutation({
- variables,
- });
-
- updateConversationMutation({
- variables: {
- id: convId,
- lastMessageText: text,
- },
- });
- };
-
- return {
- sendChatMessage,
- };
-}
diff --git a/web-client/app/_models/ChatMessage.ts b/web-client/app/_models/ChatMessage.ts
deleted file mode 100644
index 083fa1d2c..000000000
--- a/web-client/app/_models/ChatMessage.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { MessageDetailFragment } from "@/graphql";
-import { remark } from "remark";
-import html from "remark-html";
-
-export enum MessageType {
- Text = "Text",
- Image = "Image",
- ImageWithText = "ImageWithText",
- Error = "Error",
-}
-
-export enum MessageSenderType {
- Ai = "Ai",
- User = "User",
-}
-
-export enum MessageStatus {
- Ready = "ready",
- Pending = "pending",
-}
-
-export interface ChatMessage {
- id: string;
- conversationId: string;
- messageType: MessageType;
- messageSenderType: MessageSenderType;
- senderUid: string;
- senderName: string;
- senderAvatarUrl: string;
- text: string | undefined;
- imageUrls?: string[] | undefined;
- createdAt: number;
- status: MessageStatus;
-}
-
-export const toChatMessage = async (
- m: MessageDetailFragment
-): Promise => {
- const createdAt = new Date(m.created_at).getTime();
- const imageUrls: string[] = [];
- const imageUrl =
- m.message_medias.length > 0 ? m.message_medias[0].media_url : null;
- if (imageUrl) {
- imageUrls.push(imageUrl);
- }
-
- const messageType = m.message_type
- ? MessageType[m.message_type as keyof typeof MessageType]
- : MessageType.Text;
- const messageSenderType = m.message_sender_type
- ? MessageSenderType[m.message_sender_type as keyof typeof MessageSenderType]
- : MessageSenderType.Ai;
-
- const content = m.content ?? "";
- const processedContent = await remark().use(html).process(content);
- const contentHtml = processedContent.toString();
-
- return {
- id: m.id,
- conversationId: m.conversation_id,
- messageType: messageType,
- messageSenderType: messageSenderType,
- senderUid: m.sender,
- senderName: m.sender_name ?? "",
- senderAvatarUrl: m.sender_avatar_url ?? "/icons/app_icon.svg",
- text: contentHtml,
- imageUrls: imageUrls,
- createdAt: createdAt,
- status: m.status as MessageStatus,
- };
-};
diff --git a/web-client/app/_models/Conversation.ts b/web-client/app/_models/Conversation.ts
deleted file mode 100644
index 890c1c9e6..000000000
--- a/web-client/app/_models/Conversation.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { ConversationDetailFragment } from "@/graphql";
-import { Product, toProduct } from "./Product";
-
-export interface Conversation {
- id: string;
- product: Product;
- createdAt: number;
- updatedAt?: number;
- lastImageUrl?: string;
- lastTextMessage?: string;
-}
-
-/**
- * Store the state of conversation like fetching, waiting for response, etc.
- */
-export type ConversationState = {
- hasMore: boolean;
- waitingForResponse: boolean;
-};
-
-export const toConversation = (
- convo: ConversationDetailFragment
-): Conversation => {
- const product = convo.conversation_product;
- if (!product) {
- throw new Error("Product is not defined");
- }
- return {
- id: convo.id,
- product: toProduct(product),
- lastImageUrl: convo.last_image_url ?? undefined,
- lastTextMessage: convo.last_text_message ?? undefined,
- createdAt: new Date(convo.created_at).getTime(),
- updatedAt: convo.updated_at
- ? new Date(convo.updated_at).getTime()
- : undefined,
- };
-};
diff --git a/web-client/app/_models/Product.ts b/web-client/app/_models/Product.ts
deleted file mode 100644
index 570e0b3e3..000000000
--- a/web-client/app/_models/Product.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import { ProductDetailFragment } from "@/graphql";
-import { ProductInput } from "./ProductInput";
-import { ProductOutput } from "./ProductOutput";
-
-export enum ProductType {
- LLM = "LLM",
- GenerativeArt = "GenerativeArt",
- ControlNet = "ControlNet",
-}
-
-export interface Product {
- id: string;
- slug: string;
- name: string;
- description: string;
- avatarUrl: string;
- longDescription: string;
- technicalDescription: string;
- author: string;
- version: string;
- modelUrl: string;
- nsfw: boolean;
- greeting: string;
- type: ProductType;
- inputs?: ProductInput;
- outputs?: ProductOutput;
- createdAt: number;
- updatedAt?: number;
-}
-
-export function toProduct(
- productDetailFragment: ProductDetailFragment
-): Product {
- const {
- id,
- slug,
- name,
- description,
- image_url,
- long_description,
- technical_description,
- author,
- version,
- source_url,
- nsfw,
- greeting,
- created_at,
- updated_at,
- } = productDetailFragment;
- let modelType: ProductType | undefined = undefined;
- if (productDetailFragment.inputs.slug === "llm") {
- modelType = ProductType.LLM;
- } else if (productDetailFragment.inputs.slug === "sd") {
- modelType = ProductType.GenerativeArt;
- } else if (productDetailFragment.inputs.slug === "controlnet") {
- modelType = ProductType.ControlNet;
- } else {
- throw new Error("Model type not supported");
- }
-
- const product: Product = {
- id,
- slug,
- name,
- description: description ?? "",
- avatarUrl: image_url ?? "/icons/app_icon.svg",
- longDescription: long_description ?? "",
- technicalDescription: technical_description ?? "",
- author: author ?? "",
- version: version ?? "",
- modelUrl: source_url ?? "",
- nsfw: nsfw ?? false,
- greeting: greeting ?? "",
- type: modelType,
- createdAt: new Date(created_at).getTime(),
- updatedAt: new Date(updated_at).getTime(),
- };
-
- return product;
-}
diff --git a/web-client/app/_services/controlnet.ts b/web-client/app/_services/controlnet.ts
deleted file mode 100644
index 86c0a365a..000000000
--- a/web-client/app/_services/controlnet.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-export const controlNetRequest = async (
- token: string,
- prompt: string,
- negPrompt: string,
- fileInput: any
-): Promise | undefined> => {
- const formData = new FormData();
-
- const advancedPrompt: AdvancedPrompt = {
- prompt: prompt,
- neg_prompt: negPrompt ?? "",
- control_net_model: "controlnet_canny",
- seed: 1024,
- steps: 20,
- control_scale: 1.0,
- };
-
- formData.append("file", fileInput);
- formData.append("data", JSON.stringify(advancedPrompt));
-
- const res = await fetch("https://sd-inference.jan.ai/controlnet_inference", {
- method: "POST",
- body: formData,
- });
-
- if (!res.ok) {
- console.error("fetchConversations error", res);
- return;
- }
- const body = await res.json();
-
- return body.url;
-};
-
-export type AdvancedPrompt = {
- prompt: string;
- neg_prompt: string;
- control_net_model: string;
- seed: number;
- steps: number;
- control_scale: number;
-};
diff --git a/web-client/app/_utils/tokenAccessor.ts b/web-client/app/_utils/tokenAccessor.ts
deleted file mode 100644
index 90b9c88b6..000000000
--- a/web-client/app/_utils/tokenAccessor.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { getSession } from "next-auth/react";
-
-export async function getAccessToken() {
- const session = await getSession();
- if (session) {
- // @ts-ignore
- return session.access_token;
- }
- return null;
-}
-
-export async function getIdToken() {
- const session = await getSession();
- if (session) {
- // @ts-ignore
- return session.id_token;
- }
- return null;
-}
diff --git a/web-client/app/api/auth/[...nextauth]/route.ts b/web-client/app/api/auth/[...nextauth]/route.ts
deleted file mode 100644
index 474673d8b..000000000
--- a/web-client/app/api/auth/[...nextauth]/route.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-// @ts-nocheck
-import NextAuth from "next-auth/next";
-import KeycloakProvider from "next-auth/providers/keycloak";
-import jwt_decode from "jwt-decode";
-
-async function refreshAccessToken(token) {
- const resp = await fetch(`${process.env.REFRESH_TOKEN_URL}`, {
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
- body: new URLSearchParams({
- client_id: process.env.KEYCLOAK_CLIENT_ID,
- client_secret: process.env.KEYCLOAK_CLIENT_SECRET,
- grant_type: "refresh_token",
- refresh_token: token.refresh_token,
- }),
- method: "POST",
- });
- const refreshToken = await resp.json();
- if (!resp.ok) throw refreshToken;
- return {
- ...token,
- access_token: refreshToken.access_token,
- decoded: jwt_decode(refreshToken.access_token),
- id_token: refreshToken.id_token,
- expired_at: Math.floor(Date.now() / 1000) + refreshToken.expired_in,
- refresh_token: refreshToken.refresh_token,
- };
-}
-
-export const authOptions = {
- providers: [
- KeycloakProvider({
- clientId: `${process.env.KEYCLOAK_CLIENT_ID}`,
- clientSecret: `${process.env.KEYCLOAK_CLIENT_SECRET}`,
- issuer: `${process.env.AUTH_ISSUER}`,
- }),
- ],
- secret: `${process.env.NEXTAUTH_SECRET}`,
- callbacks: {
- async jwt({ token, account }) {
- const nowTimestamp = Math.floor(Date.now() / 1000);
- if (account) {
- token.decoded = jwt_decode(account.id_token);
- token.access_token = account.access_token;
- token.id_token = account.id_token;
- token.expires_at = account.expires_at;
- token.refresh_token = account.refresh_token;
- return token;
- } else if (nowTimestamp < token.expires_at) {
- return token;
- } else {
- console.log("token is expired, refresh it");
- try {
- const refreshedToken = await refreshAccessToken(token);
- console.log("token is refreshed");
- return refreshedToken;
- } catch (e) {
- console.error("Error refreshing access token", e);
- return { ...token, error: "RefreshAccessTokenError" };
- }
- }
- },
- async session({ session, token }) {
- session.access_token = token.access_token;
- session.id_token = token.id_token;
- session.user.user_id = token.sub;
- session.error = token.error;
- return session;
- },
- },
-};
-
-const handler = NextAuth(authOptions);
-
-export { handler as GET, handler as POST };
diff --git a/web-client/app/api/auth/logout/route.ts b/web-client/app/api/auth/logout/route.ts
deleted file mode 100644
index 8f1f02d0b..000000000
--- a/web-client/app/api/auth/logout/route.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-// @ts-nocheck
-import { getServerSession } from "next-auth";
-import { authOptions } from "../[...nextauth]/route";
-
-export async function GET() {
- const session = await getServerSession(authOptions);
- if (session) {
- const url = `${process.env.END_SESSION_URL}?id_token_hint=${
- session.id_token
- }&post_logout_redirect_uri=${encodeURIComponent(
- process.env.NEXTAUTH_URL ?? "/"
- )}`;
-
- try {
- await fetch(url, { method: "GET" });
- } catch (e) {
- console.error(e);
- return new Response({ status: 500 });
- }
- }
-
- return new Response({ status: 200 });
-}
diff --git a/web-client/app/api/openai/route.ts b/web-client/app/api/openai/route.ts
deleted file mode 100644
index 4ed892ccb..000000000
--- a/web-client/app/api/openai/route.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { OpenAI } from "openai-streams";
-
-export async function POST(req: Request) {
- const { messages } = await req.json();
- if (!messages) {
- return new Response(null, {
- status: 400,
- statusText: "Did not include `messages` parameter",
- });
- }
- const completionsStream = await OpenAI(
- "chat",
- {
- model: "gpt-3.5-turbo",
- stream: true,
- messages,
- max_tokens: 500,
- },
- {
- apiBase: process.env.OPENAPI_ENDPOINT,
- apiKey: process.env.OPENAPI_KEY,
- }
- );
-
- return new Response(completionsStream);
-}
diff --git a/web-client/app/download/page.tsx b/web-client/app/download/page.tsx
deleted file mode 100644
index a14d34612..000000000
--- a/web-client/app/download/page.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-"use client";
-
-import { useEffect } from "react";
-import { redirect } from "next/navigation";
-
-const Page = () => {
- useEffect(() => {
- var userAgent = navigator.userAgent || navigator.vendor;
- // iOS detection from: http://stackoverflow.com/a/9039885/177710
- if (/iPad|iPhone|iPod/.test(userAgent)) {
- window.open(process.env.NEXT_PUBLIC_DOWNLOAD_APP_IOS, "_blank_");
- } else {
- window.open(process.env.NEXT_PUBLIC_DOWNLOAD_APP_ANDROID, "_blank_");
- }
- redirect("/", undefined);
- }, []);
- return <>>;
-};
-export default Page;
diff --git a/web-client/app/layout.tsx b/web-client/app/layout.tsx
deleted file mode 100644
index 1ebe5f4d0..000000000
--- a/web-client/app/layout.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import "./globals.css";
-import { Inter } from "next/font/google";
-import classNames from "classnames";
-import { Metadata } from "next";
-import SessionProviderWrapper from "@/_components/SessionProviderWrapper";
-
-const inter = Inter({ subsets: ["latin"] });
-
-export const metadata: Metadata = {
- title: "Jan",
- description:
- "Self-hosted, local, AI Inference Platform that scales from personal use to production deployments for a team.",
- metadataBase: new URL(
- process.env.NEXT_PUBLIC_WEB_URL ?? "https://cloud.jan.ai"
- ),
- openGraph: {
- images: "/images/preview.jpg",
- },
-};
-
-type Props = {
- children: React.ReactNode;
-};
-
-export default function RootLayout({ children }: Props) {
- return (
-
- {/* suppressHydrationWarning is for next-themes */}
-
- {children}
-
-
- );
-}
diff --git a/web-client/app/page.tsx b/web-client/app/page.tsx
deleted file mode 100644
index 8112809d1..000000000
--- a/web-client/app/page.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { ApolloWrapper } from "./_helpers/ApolloWrapper";
-import { ThemeWrapper } from "./_helpers/ThemeWrapper";
-import JotaiWrapper from "./_helpers/JotaiWrapper";
-import LeftContainer from "./_components/LeftContainer";
-import RightContainer from "./_components/RightContainer";
-import { ModalWrapper } from "./_helpers/ModalWrapper";
-
-const Page: React.FC = () => (
-
-
-
-
-
-
-
-
-
-
-
-
-);
-
-export default Page;
diff --git a/web-client/codegen.ts b/web-client/codegen.ts
deleted file mode 100644
index 521d0fe29..000000000
--- a/web-client/codegen.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import type { CodegenConfig } from "@graphql-codegen/cli";
-import dotenv from "dotenv";
-dotenv.config();
-
-const config: CodegenConfig = {
- overwrite: true,
- schema: {
- "http://localhost:8080/v1/graphql": {
- headers: {
- "x-hasura-admin-secret": process.env.HASURA_ADMIN_TOKEN || "",
- },
- },
- },
- documents: "graphql/**/*.graphql",
- generates: {
- "graphql/generated/": {
- preset: "client",
- plugins: [],
- presetConfig: {
- gqlTagName: "gql",
- fragmentMasking: false,
- },
- },
- },
-};
-
-export default config;
diff --git a/web-client/graphql/fragments/collection.graphql b/web-client/graphql/fragments/collection.graphql
deleted file mode 100644
index fdbb34cd5..000000000
--- a/web-client/graphql/fragments/collection.graphql
+++ /dev/null
@@ -1,4 +0,0 @@
-fragment CollectionDetail on collections {
- slug
- name
-}
\ No newline at end of file
diff --git a/web-client/graphql/fragments/conversation.graphql b/web-client/graphql/fragments/conversation.graphql
deleted file mode 100644
index 621c0cb71..000000000
--- a/web-client/graphql/fragments/conversation.graphql
+++ /dev/null
@@ -1,12 +0,0 @@
-fragment ConversationDetail on conversations {
- id
- product_id
- user_id
- last_image_url
- last_text_message
- created_at
- updated_at
- conversation_product {
- ...ProductDetail
- }
-}
\ No newline at end of file
diff --git a/web-client/graphql/fragments/media.graphql b/web-client/graphql/fragments/media.graphql
deleted file mode 100644
index bb905ef98..000000000
--- a/web-client/graphql/fragments/media.graphql
+++ /dev/null
@@ -1,7 +0,0 @@
-fragment MessageMedia on message_medias {
- id
- message_id
- media_url
- mime_type
- updated_at
-}
diff --git a/web-client/graphql/fragments/message.graphql b/web-client/graphql/fragments/message.graphql
deleted file mode 100644
index eee01ec93..000000000
--- a/web-client/graphql/fragments/message.graphql
+++ /dev/null
@@ -1,16 +0,0 @@
-fragment MessageDetail on messages {
- id
- conversation_id
- sender
- sender_name
- sender_avatar_url
- content
- message_type
- message_sender_type
- created_at
- updated_at
- status
- message_medias {
- ...MessageMedia
- }
-}
\ No newline at end of file
diff --git a/web-client/graphql/fragments/product.graphql b/web-client/graphql/fragments/product.graphql
deleted file mode 100644
index 9717b63f5..000000000
--- a/web-client/graphql/fragments/product.graphql
+++ /dev/null
@@ -1,18 +0,0 @@
-fragment ProductDetail on products {
- id
- name
- slug
- description
- long_description
- technical_description
- image_url
- author
- greeting
- source_url
- version
- inputs
- outputs
- nsfw
- created_at
- updated_at
-}
\ No newline at end of file
diff --git a/web-client/graphql/fragments/prompt.graphql b/web-client/graphql/fragments/prompt.graphql
deleted file mode 100644
index 4281e844a..000000000
--- a/web-client/graphql/fragments/prompt.graphql
+++ /dev/null
@@ -1,5 +0,0 @@
-fragment PromptDetail on prompts {
- slug
- content
- image_url
-}
diff --git a/web-client/graphql/generated/gql.ts b/web-client/graphql/generated/gql.ts
deleted file mode 100644
index 425e7cc21..000000000
--- a/web-client/graphql/generated/gql.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-/* eslint-disable */
-import * as types from './graphql';
-import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
-
-/**
- * Map of all GraphQL operations in the project.
- *
- * This map has several performance disadvantages:
- * 1. It is not tree-shakeable, so it will include all operations in the project.
- * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
- * 3. It does not support dead code elimination, so it will add unused operations.
- *
- * Therefore it is highly recommended to use the babel or swc plugin for production.
- */
-const documents = {
- "fragment CollectionDetail on collections {\n slug\n name\n}": types.CollectionDetailFragmentDoc,
- "fragment ConversationDetail on conversations {\n id\n product_id\n user_id\n last_image_url\n last_text_message\n created_at\n updated_at\n conversation_product {\n ...ProductDetail\n }\n}": types.ConversationDetailFragmentDoc,
- "fragment MessageMedia on message_medias {\n id\n message_id\n media_url\n mime_type\n updated_at\n}": types.MessageMediaFragmentDoc,
- "fragment MessageDetail on messages {\n id\n conversation_id\n sender\n sender_name\n sender_avatar_url\n content\n message_type\n message_sender_type\n created_at\n updated_at\n status\n message_medias {\n ...MessageMedia\n }\n}": types.MessageDetailFragmentDoc,
- "fragment ProductDetail on products {\n id\n name\n slug\n description\n long_description\n technical_description\n image_url\n author\n greeting\n source_url\n version\n inputs\n outputs\n nsfw\n created_at\n updated_at\n}": types.ProductDetailFragmentDoc,
- "fragment PromptDetail on prompts {\n slug\n content\n image_url\n}": types.PromptDetailFragmentDoc,
- "mutation createConversation($data: conversations_insert_input!) {\n insert_conversations_one(object: $data) {\n ...ConversationDetail\n }\n}": types.CreateConversationDocument,
- "mutation createMessage($data: messages_insert_input!) {\n insert_messages_one(object: $data) {\n ...MessageDetail\n }\n}": types.CreateMessageDocument,
- "mutation deleteConversation($id: uuid!) {\n delete_conversations_by_pk(id: $id) {\n id\n }\n}": types.DeleteConversationDocument,
- "mutation generateImage($model: String = \"\", $neg_prompt: String = \"\", $prompt: String = \"\", $seed: Int = 10, $steps: Int = 10, $width: Int = 512, $height: Int = 512) {\n imageGeneration(\n input: {model: $model, neg_prompt: $neg_prompt, prompt: $prompt, seed: $seed, steps: $steps, width: $width, height: $height}\n ) {\n url\n }\n}": types.GenerateImageDocument,
- "mutation updateConversation($id: uuid!, $lastMessageText: String, $lastMessageUrl: String) {\n update_conversations_by_pk(\n pk_columns: {id: $id}\n _set: {last_text_message: $lastMessageText, last_image_url: $lastMessageUrl}\n ) {\n ...ConversationDetail\n }\n}": types.UpdateConversationDocument,
- "mutation updateMessage($id: uuid = \"\", $data: messages_set_input!) {\n update_messages_by_pk(pk_columns: {id: $id}, _set: $data) {\n ...MessageDetail\n }\n}": types.UpdateMessageDocument,
- "query getCollections {\n collections {\n ...CollectionDetail\n collection_products {\n products {\n ...ProductDetail\n product_prompts {\n prompts {\n ...PromptDetail\n }\n }\n }\n }\n }\n}": types.GetCollectionsDocument,
- "query getConversationMessages($conversation_id: uuid = \"\", $limit: Int = 100, $offset: Int = 100) {\n messages(\n offset: $offset\n limit: $limit\n where: {conversation_id: {_eq: $conversation_id}}\n order_by: {created_at: desc}\n ) {\n ...MessageDetail\n }\n}": types.GetConversationMessagesDocument,
- "query getConversations {\n conversations(order_by: {updated_at: desc}) {\n ...ConversationDetail\n conversation_messages {\n ...MessageDetail\n message_medias {\n ...MessageMedia\n }\n }\n }\n}": types.GetConversationsDocument,
- "query getProductsByCollectionSlug($slug: String = \"\") {\n products(where: {product_collections: {collections: {slug: {_eq: $slug}}}}) {\n ...ProductDetail\n product_prompts {\n prompts {\n ...PromptDetail\n }\n }\n product_collections {\n collections {\n ...CollectionDetail\n }\n }\n }\n}": types.GetProductsByCollectionSlugDocument,
- "query getProductPrompts($productSlug: String = \"\") {\n prompts(where: {prompt_products: {products: {slug: {_eq: $productSlug}}}}) {\n ...PromptDetail\n }\n}": types.GetProductPromptsDocument,
- "query getProducts {\n products {\n ...ProductDetail\n product_prompts {\n prompts {\n ...PromptDetail\n }\n }\n product_collections {\n collections {\n ...CollectionDetail\n }\n }\n }\n}": types.GetProductsDocument,
- "query getProductsIn($_in: [String!] = \"\") {\n products(where: {slug: {_in: $_in}}) {\n ...ProductDetail\n }\n}": types.GetProductsInDocument,
- "subscription subscribeMessage($id: uuid = \"\") {\n messages_by_pk(id: $id) {\n id\n content\n status\n }\n}": types.SubscribeMessageDocument,
-};
-
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- *
- *
- * @example
- * ```ts
- * const query = gql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
- * ```
- *
- * The query argument is unknown!
- * Please regenerate the types.
- */
-export function gql(source: string): unknown;
-
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function gql(source: "fragment CollectionDetail on collections {\n slug\n name\n}"): (typeof documents)["fragment CollectionDetail on collections {\n slug\n name\n}"];
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function gql(source: "fragment ConversationDetail on conversations {\n id\n product_id\n user_id\n last_image_url\n last_text_message\n created_at\n updated_at\n conversation_product {\n ...ProductDetail\n }\n}"): (typeof documents)["fragment ConversationDetail on conversations {\n id\n product_id\n user_id\n last_image_url\n last_text_message\n created_at\n updated_at\n conversation_product {\n ...ProductDetail\n }\n}"];
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function gql(source: "fragment MessageMedia on message_medias {\n id\n message_id\n media_url\n mime_type\n updated_at\n}"): (typeof documents)["fragment MessageMedia on message_medias {\n id\n message_id\n media_url\n mime_type\n updated_at\n}"];
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function gql(source: "fragment MessageDetail on messages {\n id\n conversation_id\n sender\n sender_name\n sender_avatar_url\n content\n message_type\n message_sender_type\n created_at\n updated_at\n status\n message_medias {\n ...MessageMedia\n }\n}"): (typeof documents)["fragment MessageDetail on messages {\n id\n conversation_id\n sender\n sender_name\n sender_avatar_url\n content\n message_type\n message_sender_type\n created_at\n updated_at\n status\n message_medias {\n ...MessageMedia\n }\n}"];
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function gql(source: "fragment ProductDetail on products {\n id\n name\n slug\n description\n long_description\n technical_description\n image_url\n author\n greeting\n source_url\n version\n inputs\n outputs\n nsfw\n created_at\n updated_at\n}"): (typeof documents)["fragment ProductDetail on products {\n id\n name\n slug\n description\n long_description\n technical_description\n image_url\n author\n greeting\n source_url\n version\n inputs\n outputs\n nsfw\n created_at\n updated_at\n}"];
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function gql(source: "fragment PromptDetail on prompts {\n slug\n content\n image_url\n}"): (typeof documents)["fragment PromptDetail on prompts {\n slug\n content\n image_url\n}"];
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function gql(source: "mutation createConversation($data: conversations_insert_input!) {\n insert_conversations_one(object: $data) {\n ...ConversationDetail\n }\n}"): (typeof documents)["mutation createConversation($data: conversations_insert_input!) {\n insert_conversations_one(object: $data) {\n ...ConversationDetail\n }\n}"];
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function gql(source: "mutation createMessage($data: messages_insert_input!) {\n insert_messages_one(object: $data) {\n ...MessageDetail\n }\n}"): (typeof documents)["mutation createMessage($data: messages_insert_input!) {\n insert_messages_one(object: $data) {\n ...MessageDetail\n }\n}"];
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function gql(source: "mutation deleteConversation($id: uuid!) {\n delete_conversations_by_pk(id: $id) {\n id\n }\n}"): (typeof documents)["mutation deleteConversation($id: uuid!) {\n delete_conversations_by_pk(id: $id) {\n id\n }\n}"];
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function gql(source: "mutation generateImage($model: String = \"\", $neg_prompt: String = \"\", $prompt: String = \"\", $seed: Int = 10, $steps: Int = 10, $width: Int = 512, $height: Int = 512) {\n imageGeneration(\n input: {model: $model, neg_prompt: $neg_prompt, prompt: $prompt, seed: $seed, steps: $steps, width: $width, height: $height}\n ) {\n url\n }\n}"): (typeof documents)["mutation generateImage($model: String = \"\", $neg_prompt: String = \"\", $prompt: String = \"\", $seed: Int = 10, $steps: Int = 10, $width: Int = 512, $height: Int = 512) {\n imageGeneration(\n input: {model: $model, neg_prompt: $neg_prompt, prompt: $prompt, seed: $seed, steps: $steps, width: $width, height: $height}\n ) {\n url\n }\n}"];
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function gql(source: "mutation updateConversation($id: uuid!, $lastMessageText: String, $lastMessageUrl: String) {\n update_conversations_by_pk(\n pk_columns: {id: $id}\n _set: {last_text_message: $lastMessageText, last_image_url: $lastMessageUrl}\n ) {\n ...ConversationDetail\n }\n}"): (typeof documents)["mutation updateConversation($id: uuid!, $lastMessageText: String, $lastMessageUrl: String) {\n update_conversations_by_pk(\n pk_columns: {id: $id}\n _set: {last_text_message: $lastMessageText, last_image_url: $lastMessageUrl}\n ) {\n ...ConversationDetail\n }\n}"];
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function gql(source: "mutation updateMessage($id: uuid = \"\", $data: messages_set_input!) {\n update_messages_by_pk(pk_columns: {id: $id}, _set: $data) {\n ...MessageDetail\n }\n}"): (typeof documents)["mutation updateMessage($id: uuid = \"\", $data: messages_set_input!) {\n update_messages_by_pk(pk_columns: {id: $id}, _set: $data) {\n ...MessageDetail\n }\n}"];
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function gql(source: "query getCollections {\n collections {\n ...CollectionDetail\n collection_products {\n products {\n ...ProductDetail\n product_prompts {\n prompts {\n ...PromptDetail\n }\n }\n }\n }\n }\n}"): (typeof documents)["query getCollections {\n collections {\n ...CollectionDetail\n collection_products {\n products {\n ...ProductDetail\n product_prompts {\n prompts {\n ...PromptDetail\n }\n }\n }\n }\n }\n}"];
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function gql(source: "query getConversationMessages($conversation_id: uuid = \"\", $limit: Int = 100, $offset: Int = 100) {\n messages(\n offset: $offset\n limit: $limit\n where: {conversation_id: {_eq: $conversation_id}}\n order_by: {created_at: desc}\n ) {\n ...MessageDetail\n }\n}"): (typeof documents)["query getConversationMessages($conversation_id: uuid = \"\", $limit: Int = 100, $offset: Int = 100) {\n messages(\n offset: $offset\n limit: $limit\n where: {conversation_id: {_eq: $conversation_id}}\n order_by: {created_at: desc}\n ) {\n ...MessageDetail\n }\n}"];
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function gql(source: "query getConversations {\n conversations(order_by: {updated_at: desc}) {\n ...ConversationDetail\n conversation_messages {\n ...MessageDetail\n message_medias {\n ...MessageMedia\n }\n }\n }\n}"): (typeof documents)["query getConversations {\n conversations(order_by: {updated_at: desc}) {\n ...ConversationDetail\n conversation_messages {\n ...MessageDetail\n message_medias {\n ...MessageMedia\n }\n }\n }\n}"];
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function gql(source: "query getProductsByCollectionSlug($slug: String = \"\") {\n products(where: {product_collections: {collections: {slug: {_eq: $slug}}}}) {\n ...ProductDetail\n product_prompts {\n prompts {\n ...PromptDetail\n }\n }\n product_collections {\n collections {\n ...CollectionDetail\n }\n }\n }\n}"): (typeof documents)["query getProductsByCollectionSlug($slug: String = \"\") {\n products(where: {product_collections: {collections: {slug: {_eq: $slug}}}}) {\n ...ProductDetail\n product_prompts {\n prompts {\n ...PromptDetail\n }\n }\n product_collections {\n collections {\n ...CollectionDetail\n }\n }\n }\n}"];
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function gql(source: "query getProductPrompts($productSlug: String = \"\") {\n prompts(where: {prompt_products: {products: {slug: {_eq: $productSlug}}}}) {\n ...PromptDetail\n }\n}"): (typeof documents)["query getProductPrompts($productSlug: String = \"\") {\n prompts(where: {prompt_products: {products: {slug: {_eq: $productSlug}}}}) {\n ...PromptDetail\n }\n}"];
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function gql(source: "query getProducts {\n products {\n ...ProductDetail\n product_prompts {\n prompts {\n ...PromptDetail\n }\n }\n product_collections {\n collections {\n ...CollectionDetail\n }\n }\n }\n}"): (typeof documents)["query getProducts {\n products {\n ...ProductDetail\n product_prompts {\n prompts {\n ...PromptDetail\n }\n }\n product_collections {\n collections {\n ...CollectionDetail\n }\n }\n }\n}"];
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function gql(source: "query getProductsIn($_in: [String!] = \"\") {\n products(where: {slug: {_in: $_in}}) {\n ...ProductDetail\n }\n}"): (typeof documents)["query getProductsIn($_in: [String!] = \"\") {\n products(where: {slug: {_in: $_in}}) {\n ...ProductDetail\n }\n}"];
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function gql(source: "subscription subscribeMessage($id: uuid = \"\") {\n messages_by_pk(id: $id) {\n id\n content\n status\n }\n}"): (typeof documents)["subscription subscribeMessage($id: uuid = \"\") {\n messages_by_pk(id: $id) {\n id\n content\n status\n }\n}"];
-
-export function gql(source: string) {
- return (documents as any)[source] ?? {};
-}
-
-export type DocumentType> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never;
\ No newline at end of file
diff --git a/web-client/graphql/generated/graphql.ts b/web-client/graphql/generated/graphql.ts
deleted file mode 100644
index 1734c7b91..000000000
--- a/web-client/graphql/generated/graphql.ts
+++ /dev/null
@@ -1,3774 +0,0 @@
-/* eslint-disable */
-import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
-export type Maybe = T | null;
-export type InputMaybe = Maybe;
-export type Exact = { [K in keyof T]: T[K] };
-export type MakeOptional = Omit & { [SubKey in K]?: Maybe };
-export type MakeMaybe = Omit & { [SubKey in K]: Maybe };
-export type MakeEmpty = { [_ in K]?: never };
-export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
-/** All built-in and custom scalars, mapped to their actual values */
-export type Scalars = {
- ID: { input: string; output: string; }
- String: { input: string; output: string; }
- Boolean: { input: boolean; output: boolean; }
- Int: { input: number; output: number; }
- Float: { input: number; output: number; }
- jsonb: { input: any; output: any; }
- timestamptz: { input: any; output: any; }
- uuid: { input: any; output: any; }
-};
-
-/** Boolean expression to compare columns of type "Boolean". All fields are combined with logical 'AND'. */
-export type Boolean_Comparison_Exp = {
- _eq?: InputMaybe;
- _gt?: InputMaybe;
- _gte?: InputMaybe;
- _in?: InputMaybe>;
- _is_null?: InputMaybe;
- _lt?: InputMaybe;
- _lte?: InputMaybe;
- _neq?: InputMaybe;
- _nin?: InputMaybe>;
-};
-
-export type ImageGenerationInput = {
- height: Scalars['Int']['input'];
- model: Scalars['String']['input'];
- neg_prompt: Scalars['String']['input'];
- prompt: Scalars['String']['input'];
- seed: Scalars['Int']['input'];
- steps: Scalars['Int']['input'];
- width: Scalars['Int']['input'];
-};
-
-export type ImageGenerationOutput = {
- __typename?: 'ImageGenerationOutput';
- url: Scalars['String']['output'];
-};
-
-/** Boolean expression to compare columns of type "Int". All fields are combined with logical 'AND'. */
-export type Int_Comparison_Exp = {
- _eq?: InputMaybe;
- _gt?: InputMaybe;
- _gte?: InputMaybe;
- _in?: InputMaybe>;
- _is_null?: InputMaybe;
- _lt?: InputMaybe;
- _lte?: InputMaybe;
- _neq?: InputMaybe;
- _nin?: InputMaybe>;
-};
-
-/** Boolean expression to compare columns of type "String". All fields are combined with logical 'AND'. */
-export type String_Comparison_Exp = {
- _eq?: InputMaybe;
- _gt?: InputMaybe;
- _gte?: InputMaybe;
- /** does the column match the given case-insensitive pattern */
- _ilike?: InputMaybe;
- _in?: InputMaybe>;
- /** does the column match the given POSIX regular expression, case insensitive */
- _iregex?: InputMaybe;
- _is_null?: InputMaybe;
- /** does the column match the given pattern */
- _like?: InputMaybe;
- _lt?: InputMaybe;
- _lte?: InputMaybe;
- _neq?: InputMaybe;
- /** does the column NOT match the given case-insensitive pattern */
- _nilike?: InputMaybe;
- _nin?: InputMaybe>;
- /** does the column NOT match the given POSIX regular expression, case insensitive */
- _niregex?: InputMaybe;
- /** does the column NOT match the given pattern */
- _nlike?: InputMaybe;
- /** does the column NOT match the given POSIX regular expression, case sensitive */
- _nregex?: InputMaybe;
- /** does the column NOT match the given SQL regular expression */
- _nsimilar?: InputMaybe;
- /** does the column match the given POSIX regular expression, case sensitive */
- _regex?: InputMaybe;
- /** does the column match the given SQL regular expression */
- _similar?: InputMaybe;
-};
-
-/** columns and relationships of "collection_products" */
-export type Collection_Products = {
- __typename?: 'collection_products';
- collection_id: Scalars['uuid']['output'];
- /** An array relationship */
- collections: Array;
- /** An aggregate relationship */
- collections_aggregate: Collections_Aggregate;
- created_at: Scalars['timestamptz']['output'];
- id: Scalars['uuid']['output'];
- product_id: Scalars['uuid']['output'];
- /** An array relationship */
- products: Array;
- /** An aggregate relationship */
- products_aggregate: Products_Aggregate;
- updated_at: Scalars['timestamptz']['output'];
-};
-
-
-/** columns and relationships of "collection_products" */
-export type Collection_ProductsCollectionsArgs = {
- distinct_on?: InputMaybe>;
- limit?: InputMaybe;
- offset?: InputMaybe;
- order_by?: InputMaybe>;
- where?: InputMaybe;
-};
-
-
-/** columns and relationships of "collection_products" */
-export type Collection_ProductsCollections_AggregateArgs = {
- distinct_on?: InputMaybe>;
- limit?: InputMaybe;
- offset?: InputMaybe;
- order_by?: InputMaybe>;
- where?: InputMaybe;
-};
-
-
-/** columns and relationships of "collection_products" */
-export type Collection_ProductsProductsArgs = {
- distinct_on?: InputMaybe>;
- limit?: InputMaybe;
- offset?: InputMaybe;
- order_by?: InputMaybe>;
- where?: InputMaybe;
-};
-
-
-/** columns and relationships of "collection_products" */
-export type Collection_ProductsProducts_AggregateArgs = {
- distinct_on?: InputMaybe>;
- limit?: InputMaybe;
- offset?: InputMaybe;
- order_by?: InputMaybe>;
- where?: InputMaybe;
-};
-
-/** aggregated selection of "collection_products" */
-export type Collection_Products_Aggregate = {
- __typename?: 'collection_products_aggregate';
- aggregate?: Maybe;
- nodes: Array;
-};
-
-export type Collection_Products_Aggregate_Bool_Exp = {
- count?: InputMaybe;
-};
-
-export type Collection_Products_Aggregate_Bool_Exp_Count = {
- arguments?: InputMaybe>;
- distinct?: InputMaybe;
- filter?: InputMaybe;
- predicate: Int_Comparison_Exp;
-};
-
-/** aggregate fields of "collection_products" */
-export type Collection_Products_Aggregate_Fields = {
- __typename?: 'collection_products_aggregate_fields';
- count: Scalars['Int']['output'];
- max?: Maybe;
- min?: Maybe;
-};
-
-
-/** aggregate fields of "collection_products" */
-export type Collection_Products_Aggregate_FieldsCountArgs = {
- columns?: InputMaybe>;
- distinct?: InputMaybe;
-};
-
-/** order by aggregate values of table "collection_products" */
-export type Collection_Products_Aggregate_Order_By = {
- count?: InputMaybe;
- max?: InputMaybe;
- min?: InputMaybe;
-};
-
-/** input type for inserting array relation for remote table "collection_products" */
-export type Collection_Products_Arr_Rel_Insert_Input = {
- data: Array;
- /** upsert condition */
- on_conflict?: InputMaybe;
-};
-
-/** Boolean expression to filter rows from the table "collection_products". All fields are combined with a logical 'AND'. */
-export type Collection_Products_Bool_Exp = {
- _and?: InputMaybe>;
- _not?: InputMaybe;
- _or?: InputMaybe>;
- collection_id?: InputMaybe;
- collections?: InputMaybe;
- collections_aggregate?: InputMaybe;
- created_at?: InputMaybe;
- id?: InputMaybe;
- product_id?: InputMaybe;
- products?: InputMaybe;
- products_aggregate?: InputMaybe;
- updated_at?: InputMaybe;
-};
-
-/** unique or primary key constraints on table "collection_products" */
-export enum Collection_Products_Constraint {
- /** unique or primary key constraint on columns "product_id", "collection_id" */
- CollectionProductsCollectionIdProductIdKey = 'collection_products_collection_id_product_id_key',
- /** unique or primary key constraint on columns "id" */
- CollectionProductsPkey = 'collection_products_pkey'
-}
-
-/** input type for inserting data into table "collection_products" */
-export type Collection_Products_Insert_Input = {
- collection_id?: InputMaybe;
- collections?: InputMaybe;
- created_at?: InputMaybe;
- id?: InputMaybe;
- product_id?: InputMaybe;
- products?: InputMaybe;
- updated_at?: InputMaybe;
-};
-
-/** aggregate max on columns */
-export type Collection_Products_Max_Fields = {
- __typename?: 'collection_products_max_fields';
- collection_id?: Maybe;
- created_at?: Maybe;
- id?: Maybe;
- product_id?: Maybe;
- updated_at?: Maybe;
-};
-
-/** order by max() on columns of table "collection_products" */
-export type Collection_Products_Max_Order_By = {
- collection_id?: InputMaybe;
- created_at?: InputMaybe;
- id?: InputMaybe;
- product_id?: InputMaybe;
- updated_at?: InputMaybe;
-};
-
-/** aggregate min on columns */
-export type Collection_Products_Min_Fields = {
- __typename?: 'collection_products_min_fields';
- collection_id?: Maybe;
- created_at?: Maybe;
- id?: Maybe;
- product_id?: Maybe;
- updated_at?: Maybe;
-};
-
-/** order by min() on columns of table "collection_products" */
-export type Collection_Products_Min_Order_By = {
- collection_id?: InputMaybe;
- created_at?: InputMaybe;
- id?: InputMaybe;
- product_id?: InputMaybe;
- updated_at?: InputMaybe;
-};
-
-/** response of any mutation on the table "collection_products" */
-export type Collection_Products_Mutation_Response = {
- __typename?: 'collection_products_mutation_response';
- /** number of rows affected by the mutation */
- affected_rows: Scalars['Int']['output'];
- /** data from the rows affected by the mutation */
- returning: Array;
-};
-
-/** on_conflict condition type for table "collection_products" */
-export type Collection_Products_On_Conflict = {
- constraint: Collection_Products_Constraint;
- update_columns?: Array;
- where?: InputMaybe;
-};
-
-/** Ordering options when selecting data from "collection_products". */
-export type Collection_Products_Order_By = {
- collection_id?: InputMaybe;
- collections_aggregate?: InputMaybe;
- created_at?: InputMaybe;
- id?: InputMaybe;
- product_id?: InputMaybe;
- products_aggregate?: InputMaybe;
- updated_at?: InputMaybe;
-};
-
-/** primary key columns input for table: collection_products */
-export type Collection_Products_Pk_Columns_Input = {
- id: Scalars['uuid']['input'];
-};
-
-/** select columns of table "collection_products" */
-export enum Collection_Products_Select_Column {
- /** column name */
- CollectionId = 'collection_id',
- /** column name */
- CreatedAt = 'created_at',
- /** column name */
- Id = 'id',
- /** column name */
- ProductId = 'product_id',
- /** column name */
- UpdatedAt = 'updated_at'
-}
-
-/** input type for updating data in table "collection_products" */
-export type Collection_Products_Set_Input = {
- collection_id?: InputMaybe;
- created_at?: InputMaybe;
- id?: InputMaybe;
- product_id?: InputMaybe;
- updated_at?: InputMaybe;
-};
-
-/** Streaming cursor of the table "collection_products" */
-export type Collection_Products_Stream_Cursor_Input = {
- /** Stream column input with initial value */
- initial_value: Collection_Products_Stream_Cursor_Value_Input;
- /** cursor ordering */
- ordering?: InputMaybe;
-};
-
-/** Initial value of the column from where the streaming should start */
-export type Collection_Products_Stream_Cursor_Value_Input = {
- collection_id?: InputMaybe;
- created_at?: InputMaybe;
- id?: InputMaybe;
- product_id?: InputMaybe;
- updated_at?: InputMaybe;
-};
-
-/** update columns of table "collection_products" */
-export enum Collection_Products_Update_Column {
- /** column name */
- CollectionId = 'collection_id',
- /** column name */
- CreatedAt = 'created_at',
- /** column name */
- Id = 'id',
- /** column name */
- ProductId = 'product_id',
- /** column name */
- UpdatedAt = 'updated_at'
-}
-
-export type Collection_Products_Updates = {
- /** sets the columns of the filtered rows to the given values */
- _set?: InputMaybe;
- /** filter the rows which have to be updated */
- where: Collection_Products_Bool_Exp;
-};
-
-/** columns and relationships of "collections" */
-export type Collections = {
- __typename?: 'collections';
- /** An array relationship */
- collection_products: Array;
- /** An aggregate relationship */
- collection_products_aggregate: Collection_Products_Aggregate;
- created_at: Scalars['timestamptz']['output'];
- description: Scalars['String']['output'];
- id: Scalars['uuid']['output'];
- name: Scalars['String']['output'];
- slug: Scalars['String']['output'];
- updated_at: Scalars['timestamptz']['output'];
-};
-
-
-/** columns and relationships of "collections" */
-export type CollectionsCollection_ProductsArgs = {
- distinct_on?: InputMaybe>;
- limit?: InputMaybe;
- offset?: InputMaybe;
- order_by?: InputMaybe>;
- where?: InputMaybe;
-};
-
-
-/** columns and relationships of "collections" */
-export type CollectionsCollection_Products_AggregateArgs = {
- distinct_on?: InputMaybe>;
- limit?: InputMaybe;
- offset?: InputMaybe;
- order_by?: InputMaybe>;
- where?: InputMaybe;
-};
-
-/** aggregated selection of "collections" */
-export type Collections_Aggregate = {
- __typename?: 'collections_aggregate';
- aggregate?: Maybe;
- nodes: Array;
-};
-
-export type Collections_Aggregate_Bool_Exp = {
- count?: InputMaybe;
-};
-
-export type Collections_Aggregate_Bool_Exp_Count = {
- arguments?: InputMaybe>;
- distinct?: InputMaybe;
- filter?: InputMaybe;
- predicate: Int_Comparison_Exp;
-};
-
-/** aggregate fields of "collections" */
-export type Collections_Aggregate_Fields = {
- __typename?: 'collections_aggregate_fields';
- count: Scalars['Int']['output'];
- max?: Maybe;
- min?: Maybe;
-};
-
-
-/** aggregate fields of "collections" */
-export type Collections_Aggregate_FieldsCountArgs = {
- columns?: InputMaybe>;
- distinct?: InputMaybe;
-};
-
-/** order by aggregate values of table "collections" */
-export type Collections_Aggregate_Order_By = {
- count?: InputMaybe;
- max?: InputMaybe;
- min?: InputMaybe;
-};
-
-/** input type for inserting array relation for remote table "collections" */
-export type Collections_Arr_Rel_Insert_Input = {
- data: Array;
- /** upsert condition */
- on_conflict?: InputMaybe;
-};
-
-/** Boolean expression to filter rows from the table "collections". All fields are combined with a logical 'AND'. */
-export type Collections_Bool_Exp = {
- _and?: InputMaybe>;
- _not?: InputMaybe;
- _or?: InputMaybe>;
- collection_products?: InputMaybe;
- collection_products_aggregate?: InputMaybe;
- created_at?: InputMaybe;
- description?: InputMaybe;
- id?: InputMaybe