diff --git a/.github/workflows/jan-electron-build-nightly.yml b/.github/workflows/jan-electron-build-nightly.yml index 1a34fee4d..d0c2adb0c 100644 --- a/.github/workflows/jan-electron-build-nightly.yml +++ b/.github/workflows/jan-electron-build-nightly.yml @@ -1,4 +1,4 @@ -name: Jan Build Electron App Nightly +name: Jan Build Electron App Nightly or Manual on: schedule: @@ -173,8 +173,9 @@ jobs: name: jan-linux-amd64-${{ steps.version_update.outputs.new_version }}.deb path: ./electron/dist/*.deb - noti-discord: + noti-discord-nightly: needs: [build-macos, build-windows-x64, build-linux-x64] + if: github.event_name == 'schedule' runs-on: ubuntu-latest steps: - name: Notify Discord @@ -183,3 +184,15 @@ jobs: args: "Nightly build artifact: https://github.com/janhq/jan/actions/runs/{{ GITHUB_RUN_ID }}" env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + + noti-discord-manual: + needs: [build-macos, build-windows-x64, build-linux-x64] + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - name: Notify Discord + uses: Ilshidur/action-discord@master + with: + args: "Manual build artifact: https://github.com/janhq/jan/actions/runs/{{ GITHUB_RUN_ID }}" + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} diff --git a/README.md b/README.md index 7a1daacdd..9c686c4c2 100644 --- a/README.md +++ b/README.md @@ -55,23 +55,17 @@ As Jan is development mode, you might get stuck on a broken build. To reset your installation: -1. Delete Jan from your `/Applications` folder +1. **Remove Jan from your Applications folder and Cache folder** -1. Delete Application data: - ```sh - # Newer versions - rm -rf /Users/$(whoami)/Library/Application\ Support/jan - - # Versions 0.2.0 and older - rm -rf /Users/$(whoami)/Library/Application\ Support/jan-electron - ``` - -1. Clear Application cache: - ```sh - rm -rf /Users/$(whoami)/Library/Caches/jan* + ```bash + make clean ``` -1. Use the following commands to remove any dangling backend processes: + This will remove all build artifacts and cached files: + - Delete Jan from your `/Applications` folder + - Clear Application cache in `/Users/$(whoami)/Library/Caches/jan` + +2. Use the following commands to remove any dangling backend processes: ```sh ps aux | grep nitro @@ -124,6 +118,22 @@ make build This will build the app MacOS m1/m2 for production (with code signing already done) and put the result in `dist` folder. +## Nightly Build + +Nightly build is a process where the software is built automatically every night. This helps in detecting and fixing bugs early in the development cycle. The process for this project is defined in [`.github/workflows/jan-electron-build-nightly.yml`](.github/workflows/jan-electron-build-nightly.yml) + +You can join our Discord server [here](https://discord.gg/FTk2MvZwJH) and go to channel [github-jan](https://discordapp.com/channels/1107178041848909847/1148534730359308298) to monitor the build process. + +The nightly build is triggered at 2:00 AM UTC every day. + +The nightly build can be downloaded from the url notified in the Discord channel. Please access the url from the browser and download the build artifacts from there. + +## Manual Build + +Manual build is a process where the software is built manually by the developers. This is usually done when a new feature is implemented or a bug is fixed. The process for this project is defined in [`.github/workflows/jan-electron-build-nightly.yml`](.github/workflows/jan-electron-build-nightly.yml) + +It is similar to the nightly build process, except that it is triggered manually by the developers. + ## Acknowledgements Jan builds on top of other open-source projects: diff --git a/core/src/types/index.ts b/core/src/types/index.ts index bbd1e98de..7580c2432 100644 --- a/core/src/types/index.ts +++ b/core/src/types/index.ts @@ -143,6 +143,7 @@ export type ThreadAssistantInfo = { assistant_id: string; assistant_name: string; model: ModelInfo; + instructions?: string; }; /** @@ -288,13 +289,13 @@ export type Assistant = { /** Represents the name of the object. */ name: string; /** Represents the description of the object. */ - description: string; + description?: string; /** Represents the model of the object. */ model: string; /** Represents the instructions for the object. */ - instructions: string; + instructions?: string; /** Represents the tools associated with the object. */ - tools: any; + tools?: any; /** Represents the file identifiers associated with the object. */ file_ids: string[]; /** Represents the metadata of the object. */ diff --git a/docs/docs/about/about.md b/docs/docs/about/about.md index 4a82d93d6..5fabb707e 100644 --- a/docs/docs/about/about.md +++ b/docs/docs/about/about.md @@ -1,5 +1,7 @@ --- title: About Jan +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- Jan believes in the need for an open source AI ecosystem, and are building the infra and tooling to allow open source AIs to compete on a level playing field with proprietary ones. diff --git a/docs/docs/community/community.md b/docs/docs/community/community.md index d6807f38a..623cea8e8 100644 --- a/docs/docs/community/community.md +++ b/docs/docs/community/community.md @@ -1,5 +1,7 @@ --- title: Community +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- - [ ] Social media links \ No newline at end of file diff --git a/docs/docs/docs/assistants.md b/docs/docs/docs/assistants.md index 0edc163ba..2f4b1f99f 100644 --- a/docs/docs/docs/assistants.md +++ b/docs/docs/docs/assistants.md @@ -1,3 +1,5 @@ --- title: Build an Assistant +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- \ No newline at end of file diff --git a/docs/docs/docs/extensions.md b/docs/docs/docs/extensions.md index 87edbf863..56cfdfe51 100644 --- a/docs/docs/docs/extensions.md +++ b/docs/docs/docs/extensions.md @@ -1,5 +1,7 @@ --- title: Extending Jan +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- ## Overview diff --git a/docs/docs/docs/models.md b/docs/docs/docs/models.md index 9e929b76b..4e123e746 100644 --- a/docs/docs/docs/models.md +++ b/docs/docs/docs/models.md @@ -1,3 +1,5 @@ --- title: Model Management +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- \ No newline at end of file diff --git a/docs/docs/docs/modules.md b/docs/docs/docs/modules.md index 41a112417..cb7888f67 100644 --- a/docs/docs/docs/modules.md +++ b/docs/docs/docs/modules.md @@ -1,3 +1,5 @@ --- title: Build a Module +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- \ No newline at end of file diff --git a/docs/docs/docs/server.md b/docs/docs/docs/server.md index 05a715932..d309d8817 100644 --- a/docs/docs/docs/server.md +++ b/docs/docs/docs/server.md @@ -1,5 +1,7 @@ --- title: API Server +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- :::warning diff --git a/docs/docs/docs/themes.md b/docs/docs/docs/themes.md index 2d07b30e1..3edfaf490 100644 --- a/docs/docs/docs/themes.md +++ b/docs/docs/docs/themes.md @@ -1,3 +1,5 @@ --- title: Build a Theme +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- \ No newline at end of file diff --git a/docs/docs/docs/tools.md b/docs/docs/docs/tools.md index 3c6c721e4..d8dd132a8 100644 --- a/docs/docs/docs/tools.md +++ b/docs/docs/docs/tools.md @@ -1,3 +1,5 @@ --- title: Build a Tool +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- \ No newline at end of file diff --git a/docs/docs/handbook/engineering/engineering.md b/docs/docs/handbook/engineering/engineering.md index e320b25fe..3ca9952c4 100644 --- a/docs/docs/handbook/engineering/engineering.md +++ b/docs/docs/handbook/engineering/engineering.md @@ -1,5 +1,7 @@ --- title: Engineering +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- ## Connecting to Rigs diff --git a/docs/docs/handbook/handbook.md b/docs/docs/handbook/handbook.md index 674b96c76..a0485da61 100644 --- a/docs/docs/handbook/handbook.md +++ b/docs/docs/handbook/handbook.md @@ -1,6 +1,8 @@ --- title: Onboarding Checklist slug: /handbook +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- # Welcome diff --git a/docs/docs/hardware/community.md b/docs/docs/hardware/community.md index 5d9bfcc16..a8c3ffee9 100644 --- a/docs/docs/hardware/community.md +++ b/docs/docs/hardware/community.md @@ -1,5 +1,7 @@ --- title: Hardware Examples +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- ## Add your own example diff --git a/docs/docs/install/from-source.md b/docs/docs/install/from-source.md index 961e7fc85..5377e831c 100644 --- a/docs/docs/install/from-source.md +++ b/docs/docs/install/from-source.md @@ -1,5 +1,7 @@ --- title: From Source +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- # Install Jan from Source diff --git a/docs/docs/install/linux.md b/docs/docs/install/linux.md index a7af581c4..0b61f96d8 100644 --- a/docs/docs/install/linux.md +++ b/docs/docs/install/linux.md @@ -1,5 +1,7 @@ --- title: Linux +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- # Installing Jan on Linux diff --git a/docs/docs/install/mac.md b/docs/docs/install/mac.md index 21ecdb54c..a618d05e3 100644 --- a/docs/docs/install/mac.md +++ b/docs/docs/install/mac.md @@ -1,5 +1,7 @@ --- title: Mac +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- # Installing Jan on MacOS diff --git a/docs/docs/install/overview.md b/docs/docs/install/overview.md index 067eb55f2..b41db64d7 100644 --- a/docs/docs/install/overview.md +++ b/docs/docs/install/overview.md @@ -1,5 +1,7 @@ --- title: Overview +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- Getting up and running open-source AI models on your own computer with Jan is quick and easy. Jan is lightweight and can run on a variety of hardware and platform versions. Specific requirements tailored to your platform are outlined below. diff --git a/docs/docs/install/windows.md b/docs/docs/install/windows.md index ecf57f51f..f3de435ec 100644 --- a/docs/docs/install/windows.md +++ b/docs/docs/install/windows.md @@ -1,5 +1,7 @@ --- title: Windows +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- # Installing Jan on Windows diff --git a/docs/docs/intro/how-jan-works.md b/docs/docs/intro/how-jan-works.md index fdfd12a10..b8202224d 100644 --- a/docs/docs/intro/how-jan-works.md +++ b/docs/docs/intro/how-jan-works.md @@ -1,5 +1,7 @@ --- title: How Jan Works +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- - Local Filesystem diff --git a/docs/docs/intro/introduction.md b/docs/docs/intro/introduction.md index a483b10cc..1501cfc4b 100644 --- a/docs/docs/intro/introduction.md +++ b/docs/docs/intro/introduction.md @@ -1,6 +1,8 @@ --- title: Introduction slug: /docs +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- Jan is a ChatGPT-alternative that runs on your own computer, with a [local API server](/api). diff --git a/docs/docs/intro/quickstart.md b/docs/docs/intro/quickstart.md index e417838ea..606003be1 100644 --- a/docs/docs/intro/quickstart.md +++ b/docs/docs/intro/quickstart.md @@ -1,5 +1,7 @@ --- title: Quickstart +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- - Write in the style of comics, explanation diff --git a/docs/docs/specs/architecture.md b/docs/docs/specs/architecture.md index 39b7fa833..2557f6203 100644 --- a/docs/docs/specs/architecture.md +++ b/docs/docs/specs/architecture.md @@ -1,6 +1,8 @@ --- title: Architecture slug: /specs +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- :::warning diff --git a/docs/docs/specs/engineering/assistants.md b/docs/docs/specs/engineering/assistants.md index ea0ec0955..8a96f6408 100644 --- a/docs/docs/specs/engineering/assistants.md +++ b/docs/docs/specs/engineering/assistants.md @@ -1,6 +1,8 @@ --- title: "Assistants" slug: /specs/assistants +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- :::caution diff --git a/docs/docs/specs/engineering/chats.md b/docs/docs/specs/engineering/chats.md index 7bb96faf0..7daac57b0 100644 --- a/docs/docs/specs/engineering/chats.md +++ b/docs/docs/specs/engineering/chats.md @@ -1,6 +1,8 @@ --- title: Chats slug: /specs/chats +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- :::caution diff --git a/docs/docs/specs/engineering/engine.md b/docs/docs/specs/engineering/engine.md new file mode 100644 index 000000000..d25fdfc04 --- /dev/null +++ b/docs/docs/specs/engineering/engine.md @@ -0,0 +1,60 @@ +--- +title: Engine +slug: /specs/engine +--- + +:::caution + +Currently Under Development + +::: + +## Overview + +In the Jan application, engines serve as primary entities with the following capabilities: + +- Engine will be installed through `inference-extensions`. +- Models will depend on engines to do [inference](https://en.wikipedia.org/wiki/Inference_engine). +- Engine configuration and required metadata will be stored in a json file. + +## Folder Structure + +- Default parameters for engines are stored in JSON files located in the `/engines` folder. +- These parameter files are named uniquely with `engine_id`. +- Engines are referenced directly using `engine_id` in the `model.json` file. + +```yaml +jan/ + engines/ + nitro.json + openai.json + ..... +``` + +## Engine Default Parameter Files + +- Each inference engine requires default parameters to function in cases where user-provided parameters are absent. +- These parameters are stored in JSON files, structured as simple key-value pairs. + +### Example + +Here is an example of an engine file for `engine_id` `nitro`: + +```js +{ + "ctx_len": 512, + "ngl": 100, + "embedding": false, + "n_parallel": 1, + "cont_batching": false + "prompt_template": "<|im_start|>system\n{system_message}<|im_end|>\n<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant" +} +``` + +For detailed engine parameters, refer to: [Nitro's Model Settings](https://nitro.jan.ai/features/load-unload#table-of-parameters) + +## Adding an Engine + +- Engine parameter files are automatically generated upon installing an `inference-extension` in the Jan application. + +--- diff --git a/docs/docs/specs/engineering/files.md b/docs/docs/specs/engineering/files.md index 0becbf6d6..b93054ef1 100644 --- a/docs/docs/specs/engineering/files.md +++ b/docs/docs/specs/engineering/files.md @@ -1,6 +1,8 @@ --- title: "Files" slug: /specs/files +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- :::warning diff --git a/docs/docs/specs/engineering/fine-tuning.md b/docs/docs/specs/engineering/fine-tuning.md index f2d4153d2..97c45d85b 100644 --- a/docs/docs/specs/engineering/fine-tuning.md +++ b/docs/docs/specs/engineering/fine-tuning.md @@ -1,6 +1,8 @@ --- title: "Fine-tuning" slug: /specs/finetuning +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- Todo: @hiro diff --git a/docs/docs/specs/engineering/messages.md b/docs/docs/specs/engineering/messages.md index 62a721fa8..4032e61d4 100644 --- a/docs/docs/specs/engineering/messages.md +++ b/docs/docs/specs/engineering/messages.md @@ -1,6 +1,8 @@ --- title: Messages slug: /specs/messages +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- :::caution diff --git a/docs/docs/specs/engineering/models.md b/docs/docs/specs/engineering/models.md index c47a62bab..e10fbd088 100644 --- a/docs/docs/specs/engineering/models.md +++ b/docs/docs/specs/engineering/models.md @@ -1,6 +1,8 @@ --- title: Models slug: /specs/models +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- :::caution @@ -51,9 +53,9 @@ jan/ # Jan root folder Here's a standard example `model.json` for a GGUF model. -- `source_url`: https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF/. ```js +{ "id": "zephyr-7b", // Defaults to foldername "object": "model", // Defaults to "model" "source_url": "https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF/blob/main/zephyr-7b-beta.Q4_K_M.gguf", @@ -62,15 +64,16 @@ Here's a standard example `model.json` for a GGUF model. "version": "1", // Defaults to 1 "created": 1231231, // Defaults to file creation time "description": null, // Defaults to null -"state": enum[null, "downloading", "ready", "starting", "stopping", ...] +"state": enum[null, "ready"] "format": "ggufv3", // Defaults to "ggufv3" -"settings": { // Models are initialized with settings - "ctx_len": 2048, +"engine": "nitro", // engine_id specified in jan/engine folder +"engine_parameters": { // Engine parameters inside model.json can override + "ctx_len": 2048, // the value inside the base engine.json "ngl": 100, "embedding": true, "n_parallel": 4, }, -"parameters": { // Models are called parameters +"model_parameters": { // Models are called parameters "stream": true, "max_tokens": 2048, "stop": [""], // This usually can be left blank, only used with specific need from model author @@ -83,9 +86,10 @@ Here's a standard example `model.json` for a GGUF model. "assets": [ // Defaults to current dir "file://.../zephyr-7b-q4_k_m.bin", ] +} ``` -The model settings in the example can be found at: [Nitro's model settings](https://nitro.jan.ai/features/load-unload#table-of-parameters) +The engine parameters in the example can be found at: [Nitro's model settings](https://nitro.jan.ai/features/load-unload#table-of-parameters) The model parameters in the example can be found at: [Nitro's model parameters](https://nitro.jan.ai/api-reference#tag/Chat-Completion) diff --git a/docs/docs/specs/engineering/prompts.md b/docs/docs/specs/engineering/prompts.md index 37422b517..9d4fa4fd6 100644 --- a/docs/docs/specs/engineering/prompts.md +++ b/docs/docs/specs/engineering/prompts.md @@ -1,6 +1,8 @@ --- title: Prompts slug: /specs/prompts +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- - [ ] /prompts folder diff --git a/docs/docs/specs/engineering/threads.md b/docs/docs/specs/engineering/threads.md index 982c4f8cb..c1421e4ae 100644 --- a/docs/docs/specs/engineering/threads.md +++ b/docs/docs/specs/engineering/threads.md @@ -1,6 +1,8 @@ --- title: Threads slug: /specs/threads +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- :::caution diff --git a/docs/docs/specs/file-based.md b/docs/docs/specs/file-based.md index 3b38bb06b..26f3d8efb 100644 --- a/docs/docs/specs/file-based.md +++ b/docs/docs/specs/file-based.md @@ -1,5 +1,7 @@ --- title: File-based Approach +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- :::warning diff --git a/docs/docs/specs/jan.md b/docs/docs/specs/jan.md index 9a97c29c2..e92dddf7a 100644 --- a/docs/docs/specs/jan.md +++ b/docs/docs/specs/jan.md @@ -1,5 +1,7 @@ --- title: Jan (Assistant) +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- ## Jan: a "global" assistant diff --git a/docs/docs/specs/product/chat.md b/docs/docs/specs/product/chat.md index 28969f348..acbf57487 100644 --- a/docs/docs/specs/product/chat.md +++ b/docs/docs/specs/product/chat.md @@ -1,6 +1,8 @@ --- title: Chat slug: /specs/chat +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- ## Overview diff --git a/docs/docs/specs/product/hub.md b/docs/docs/specs/product/hub.md index c2523b0fb..1a9f6064a 100644 --- a/docs/docs/specs/product/hub.md +++ b/docs/docs/specs/product/hub.md @@ -1,6 +1,8 @@ --- title: Hub slug: /specs/hub +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- ## Overview diff --git a/docs/docs/specs/product/settings.md b/docs/docs/specs/product/settings.md index a80c50034..d7e60e943 100644 --- a/docs/docs/specs/product/settings.md +++ b/docs/docs/specs/product/settings.md @@ -1,6 +1,8 @@ --- title: Settings slug: /specs/settings +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- ## Overview diff --git a/docs/docs/specs/product/system-monitor.md b/docs/docs/specs/product/system-monitor.md index 52d11a272..f4c77c38c 100644 --- a/docs/docs/specs/product/system-monitor.md +++ b/docs/docs/specs/product/system-monitor.md @@ -1,6 +1,8 @@ --- title: System Monitor slug: /specs/system-monitor +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- ## Overview diff --git a/docs/docs/specs/user-interface.md b/docs/docs/specs/user-interface.md index c540a6973..156eac5a6 100644 --- a/docs/docs/specs/user-interface.md +++ b/docs/docs/specs/user-interface.md @@ -1,5 +1,7 @@ --- title: User Interface +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: [Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee] --- :::warning diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 7b07016d2..da62e3399 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -38,6 +38,8 @@ const config = { mermaid: true, }, + noIndex: false, + // Plugins we added plugins: [ "docusaurus-plugin-sass", @@ -140,15 +142,44 @@ const config = { metadata: [ { name: 'description', content: 'Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.' }, { name: 'keywords', content: 'Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee' }, + { name: 'robots', content: 'index, follow' }, { property: 'og:title', content: 'Run your own AI | Jan' }, { property: 'og:description', content: 'Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.' }, { property: 'og:image', content: 'https://jan.ai/img/jan-social-card.png' }, + { property: 'og:type', content: 'website' }, { property: 'twitter:card', content: 'summary_large_image' }, { property: 'twitter:site', content: '@janhq_' }, { property: 'twitter:title', content: 'Run your own AI | Jan' }, { property: 'twitter:description', content: 'Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.' }, { property: 'twitter:image', content: 'https://jan.ai/img/jan-social-card.png' }, ], + headTags: [ + // Declare a preconnect tag + { + tagName: 'link', + attributes: { + rel: 'preconnect', + href: 'https://jan.ai/', + }, + }, + // Declare some json-ld structured data + { + tagName: 'script', + attributes: { + type: 'application/ld+json', + }, + innerHTML: JSON.stringify({ + '@context': 'https://schema.org/', + '@type': 'localAI', + name: 'Jan', + description: "Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.", + keywords: "Jan, ChatGPT alternative, on-premises AI, local API server, local AI, llm, conversational AI, no-subscription fee", + applicationCategory: "BusinessApplication", + operatingSystem: "Multiple", + url: 'https://jan.ai/', + }), + }, + ], navbar: { title: "Jan", logo: { diff --git a/docs/sidebars.js b/docs/sidebars.js index edef458cd..384f47e9d 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -81,6 +81,7 @@ const sidebars = { items: [ "specs/engineering/chats", "specs/engineering/models", + "specs/engineering/engine", "specs/engineering/threads", "specs/engineering/messages", "specs/engineering/assistants", diff --git a/docs/src/pages/index.js b/docs/src/pages/index.js index d87e00498..986fff9a2 100644 --- a/docs/src/pages/index.js +++ b/docs/src/pages/index.js @@ -19,7 +19,7 @@ export default function Home() {
diff --git a/docs/static/robots.txt b/docs/static/robots.txt new file mode 100644 index 000000000..f6e6d1d41 --- /dev/null +++ b/docs/static/robots.txt @@ -0,0 +1,2 @@ +User-Agent: * +Allow: / diff --git a/electron/managers/window.ts b/electron/managers/window.ts index c930dd5ec..0d5a0eaf4 100644 --- a/electron/managers/window.ts +++ b/electron/managers/window.ts @@ -1,15 +1,15 @@ -import { BrowserWindow } from "electron"; +import { BrowserWindow } from 'electron' /** * Manages the current window instance. */ export class WindowManager { - public static instance: WindowManager = new WindowManager(); - public currentWindow?: BrowserWindow; + public static instance: WindowManager = new WindowManager() + public currentWindow?: BrowserWindow constructor() { if (WindowManager.instance) { - return WindowManager.instance; + return WindowManager.instance } } @@ -21,17 +21,17 @@ export class WindowManager { createWindow(options?: Electron.BrowserWindowConstructorOptions | undefined) { this.currentWindow = new BrowserWindow({ width: 1200, - minWidth: 800, + minWidth: 1200, height: 800, show: false, trafficLightPosition: { x: 10, y: 15, }, - titleBarStyle: "hidden", - vibrancy: "sidebar", + titleBarStyle: 'hidden', + vibrancy: 'sidebar', ...options, - }); - return this.currentWindow; + }) + return this.currentWindow } } diff --git a/electron/tests/explore.e2e.spec.ts b/electron/tests/explore.e2e.spec.ts index 5a4412cb3..77eb3dbda 100644 --- a/electron/tests/explore.e2e.spec.ts +++ b/electron/tests/explore.e2e.spec.ts @@ -1,41 +1,41 @@ -import { _electron as electron } from "playwright"; -import { ElectronApplication, Page, expect, test } from "@playwright/test"; +import { _electron as electron } from 'playwright' +import { ElectronApplication, Page, expect, test } from '@playwright/test' import { findLatestBuild, parseElectronApp, stubDialog, -} from "electron-playwright-helpers"; +} from 'electron-playwright-helpers' -let electronApp: ElectronApplication; -let page: Page; +let electronApp: ElectronApplication +let page: Page test.beforeAll(async () => { - process.env.CI = "e2e"; + process.env.CI = 'e2e' - const latestBuild = findLatestBuild("dist"); - expect(latestBuild).toBeTruthy(); + const latestBuild = findLatestBuild('dist') + expect(latestBuild).toBeTruthy() // parse the packaged Electron app and find paths and other info - const appInfo = parseElectronApp(latestBuild); - expect(appInfo).toBeTruthy(); + const appInfo = parseElectronApp(latestBuild) + expect(appInfo).toBeTruthy() electronApp = await electron.launch({ args: [appInfo.main], // main file from package.json executablePath: appInfo.executable, // path to the Electron executable - }); - await stubDialog(electronApp, "showMessageBox", { response: 1 }); + }) + await stubDialog(electronApp, 'showMessageBox', { response: 1 }) - page = await electronApp.firstWindow(); -}); + page = await electronApp.firstWindow() +}) test.afterAll(async () => { - await electronApp.close(); - await page.close(); -}); + await electronApp.close() + await page.close() +}) -test("explores models", async () => { - await page.getByTestId("Explore Models").first().click(); - await page.getByTestId("testid-explore-models").isVisible(); +test('explores models', async () => { + await page.getByTestId('Hub').first().click() + await page.getByTestId('testid-explore-models').isVisible() // More test cases here... -}); +}) diff --git a/electron/tests/main.e2e.spec.ts b/electron/tests/main.e2e.spec.ts index d6df31ca4..1a5bfe696 100644 --- a/electron/tests/main.e2e.spec.ts +++ b/electron/tests/main.e2e.spec.ts @@ -1,55 +1,55 @@ -import { _electron as electron } from "playwright"; -import { ElectronApplication, Page, expect, test } from "@playwright/test"; +import { _electron as electron } from 'playwright' +import { ElectronApplication, Page, expect, test } from '@playwright/test' import { findLatestBuild, parseElectronApp, stubDialog, -} from "electron-playwright-helpers"; +} from 'electron-playwright-helpers' -let electronApp: ElectronApplication; -let page: Page; +let electronApp: ElectronApplication +let page: Page test.beforeAll(async () => { - process.env.CI = "e2e"; + process.env.CI = 'e2e' - const latestBuild = findLatestBuild("dist"); - expect(latestBuild).toBeTruthy(); + const latestBuild = findLatestBuild('dist') + expect(latestBuild).toBeTruthy() // parse the packaged Electron app and find paths and other info - const appInfo = parseElectronApp(latestBuild); - expect(appInfo).toBeTruthy(); - expect(appInfo.asar).toBe(true); - expect(appInfo.executable).toBeTruthy(); - expect(appInfo.main).toBeTruthy(); - expect(appInfo.name).toBe("jan"); - expect(appInfo.packageJson).toBeTruthy(); - expect(appInfo.packageJson.name).toBe("jan"); - expect(appInfo.platform).toBeTruthy(); - expect(appInfo.platform).toBe(process.platform); - expect(appInfo.resourcesDir).toBeTruthy(); + const appInfo = parseElectronApp(latestBuild) + expect(appInfo).toBeTruthy() + expect(appInfo.asar).toBe(true) + expect(appInfo.executable).toBeTruthy() + expect(appInfo.main).toBeTruthy() + expect(appInfo.name).toBe('jan') + expect(appInfo.packageJson).toBeTruthy() + expect(appInfo.packageJson.name).toBe('jan') + expect(appInfo.platform).toBeTruthy() + expect(appInfo.platform).toBe(process.platform) + expect(appInfo.resourcesDir).toBeTruthy() electronApp = await electron.launch({ args: [appInfo.main], // main file from package.json executablePath: appInfo.executable, // path to the Electron executable - }); - await stubDialog(electronApp, "showMessageBox", { response: 1 }); + }) + await stubDialog(electronApp, 'showMessageBox', { response: 1 }) - page = await electronApp.firstWindow(); -}); + page = await electronApp.firstWindow() +}) test.afterAll(async () => { - await electronApp.close(); - await page.close(); -}); + await electronApp.close() + await page.close() +}) -test("renders the home page", async () => { - expect(page).toBeDefined(); +test('renders the home page', async () => { + expect(page).toBeDefined() // Welcome text is available const welcomeText = await page - .getByTestId("testid-welcome-title") + .getByTestId('testid-welcome-title') .first() - .isVisible(); - expect(welcomeText).toBe(false); -}); + .isVisible() + expect(welcomeText).toBe(false) +}) diff --git a/electron/tests/my-models.e2e.spec.ts b/electron/tests/my-models.e2e.spec.ts deleted file mode 100644 index a3355fb33..000000000 --- a/electron/tests/my-models.e2e.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { _electron as electron } from "playwright"; -import { ElectronApplication, Page, expect, test } from "@playwright/test"; - -import { - findLatestBuild, - parseElectronApp, - stubDialog, -} from "electron-playwright-helpers"; - -let electronApp: ElectronApplication; -let page: Page; - -test.beforeAll(async () => { - process.env.CI = "e2e"; - - const latestBuild = findLatestBuild("dist"); - expect(latestBuild).toBeTruthy(); - - // parse the packaged Electron app and find paths and other info - const appInfo = parseElectronApp(latestBuild); - expect(appInfo).toBeTruthy(); - - electronApp = await electron.launch({ - args: [appInfo.main], // main file from package.json - executablePath: appInfo.executable, // path to the Electron executable - }); - await stubDialog(electronApp, "showMessageBox", { response: 1 }); - - page = await electronApp.firstWindow(); -}); - -test.afterAll(async () => { - await electronApp.close(); - await page.close(); -}); - -test("shows my models", async () => { - await page.getByTestId("My Models").first().click(); - await page.getByTestId("testid-my-models").isVisible(); - // More test cases here... -}); diff --git a/electron/tests/navigation.e2e.spec.ts b/electron/tests/navigation.e2e.spec.ts index 104333650..2f4f7b767 100644 --- a/electron/tests/navigation.e2e.spec.ts +++ b/electron/tests/navigation.e2e.spec.ts @@ -1,43 +1,43 @@ -import { _electron as electron } from "playwright"; -import { ElectronApplication, Page, expect, test } from "@playwright/test"; +import { _electron as electron } from 'playwright' +import { ElectronApplication, Page, expect, test } from '@playwright/test' import { findLatestBuild, parseElectronApp, stubDialog, -} from "electron-playwright-helpers"; +} from 'electron-playwright-helpers' -let electronApp: ElectronApplication; -let page: Page; +let electronApp: ElectronApplication +let page: Page test.beforeAll(async () => { - process.env.CI = "e2e"; + process.env.CI = 'e2e' - const latestBuild = findLatestBuild("dist"); - expect(latestBuild).toBeTruthy(); + const latestBuild = findLatestBuild('dist') + expect(latestBuild).toBeTruthy() // parse the packaged Electron app and find paths and other info - const appInfo = parseElectronApp(latestBuild); - expect(appInfo).toBeTruthy(); + const appInfo = parseElectronApp(latestBuild) + expect(appInfo).toBeTruthy() electronApp = await electron.launch({ args: [appInfo.main], // main file from package.json executablePath: appInfo.executable, // path to the Electron executable - }); - await stubDialog(electronApp, "showMessageBox", { response: 1 }); + }) + await stubDialog(electronApp, 'showMessageBox', { response: 1 }) - page = await electronApp.firstWindow(); -}); + page = await electronApp.firstWindow() +}) test.afterAll(async () => { - await electronApp.close(); - await page.close(); -}); + await electronApp.close() + await page.close() +}) -test("renders left navigation panel", async () => { +test('renders left navigation panel', async () => { // Chat section should be there - const chatSection = await page.getByTestId("Chat").first().isVisible(); - expect(chatSection).toBe(false); + const chatSection = await page.getByTestId('Chat').first().isVisible() + expect(chatSection).toBe(false) // Home actions /* Disable unstable feature tests @@ -45,7 +45,10 @@ test("renders left navigation panel", async () => { ** Enable back when it is whitelisted */ - const myModelsBtn = await page.getByTestId("My Models").first().isEnabled(); - const settingsBtn = await page.getByTestId("Settings").first().isEnabled(); - expect([myModelsBtn, settingsBtn].filter((e) => !e).length).toBe(0); -}); + const systemMonitorBtn = await page + .getByTestId('System Monitor') + .first() + .isEnabled() + const settingsBtn = await page.getByTestId('Settings').first().isEnabled() + expect([systemMonitorBtn, settingsBtn].filter((e) => !e).length).toBe(0) +}) diff --git a/electron/tests/settings.e2e.spec.ts b/electron/tests/settings.e2e.spec.ts index 2f8d6465b..798504c70 100644 --- a/electron/tests/settings.e2e.spec.ts +++ b/electron/tests/settings.e2e.spec.ts @@ -1,40 +1,40 @@ -import { _electron as electron } from "playwright"; -import { ElectronApplication, Page, expect, test } from "@playwright/test"; +import { _electron as electron } from 'playwright' +import { ElectronApplication, Page, expect, test } from '@playwright/test' import { findLatestBuild, parseElectronApp, stubDialog, -} from "electron-playwright-helpers"; +} from 'electron-playwright-helpers' -let electronApp: ElectronApplication; -let page: Page; +let electronApp: ElectronApplication +let page: Page test.beforeAll(async () => { - process.env.CI = "e2e"; + process.env.CI = 'e2e' - const latestBuild = findLatestBuild("dist"); - expect(latestBuild).toBeTruthy(); + const latestBuild = findLatestBuild('dist') + expect(latestBuild).toBeTruthy() // parse the packaged Electron app and find paths and other info - const appInfo = parseElectronApp(latestBuild); - expect(appInfo).toBeTruthy(); + const appInfo = parseElectronApp(latestBuild) + expect(appInfo).toBeTruthy() electronApp = await electron.launch({ args: [appInfo.main], // main file from package.json executablePath: appInfo.executable, // path to the Electron executable - }); - await stubDialog(electronApp, "showMessageBox", { response: 1 }); + }) + await stubDialog(electronApp, 'showMessageBox', { response: 1 }) - page = await electronApp.firstWindow(); -}); + page = await electronApp.firstWindow() +}) test.afterAll(async () => { - await electronApp.close(); - await page.close(); -}); + await electronApp.close() + await page.close() +}) -test("shows settings", async () => { - await page.getByTestId("Settings").first().click(); - await page.getByTestId("testid-setting-description").isVisible(); -}); +test('shows settings', async () => { + await page.getByTestId('Settings').first().click() + await page.getByTestId('testid-setting-description').isVisible() +}) diff --git a/electron/tests/system-monitor.e2e.spec.ts b/electron/tests/system-monitor.e2e.spec.ts new file mode 100644 index 000000000..747a8ae18 --- /dev/null +++ b/electron/tests/system-monitor.e2e.spec.ts @@ -0,0 +1,41 @@ +import { _electron as electron } from 'playwright' +import { ElectronApplication, Page, expect, test } from '@playwright/test' + +import { + findLatestBuild, + parseElectronApp, + stubDialog, +} from 'electron-playwright-helpers' + +let electronApp: ElectronApplication +let page: Page + +test.beforeAll(async () => { + process.env.CI = 'e2e' + + const latestBuild = findLatestBuild('dist') + expect(latestBuild).toBeTruthy() + + // parse the packaged Electron app and find paths and other info + const appInfo = parseElectronApp(latestBuild) + expect(appInfo).toBeTruthy() + + electronApp = await electron.launch({ + args: [appInfo.main], // main file from package.json + executablePath: appInfo.executable, // path to the Electron executable + }) + await stubDialog(electronApp, 'showMessageBox', { response: 1 }) + + page = await electronApp.firstWindow() +}) + +test.afterAll(async () => { + await electronApp.close() + await page.close() +}) + +test('shows system monitor', async () => { + await page.getByTestId('System Monitor').first().click() + await page.getByTestId('testid-system-monitor').isVisible() + // More test cases here... +}) diff --git a/extensions/assistant-extension/src/index.ts b/extensions/assistant-extension/src/index.ts index 7321a0660..8d01021b7 100644 --- a/extensions/assistant-extension/src/index.ts +++ b/extensions/assistant-extension/src/index.ts @@ -89,12 +89,12 @@ export default class JanAssistantExtension implements AssistantExtension { private async createJanAssistant(): Promise { const janAssistant: Assistant = { avatar: "", - thread_location: undefined, // TODO: make this property ? + thread_location: undefined, id: "jan", object: "assistant", // TODO: maybe we can set default value for this? created_at: Date.now(), - name: "Jan Assistant", - description: "Just Jan Assistant", + name: "Jan", + description: "A default assistant that can use all downloaded models", model: "*", instructions: "Your name is Jan.", tools: undefined, diff --git a/extensions/inference-extension/download.bat b/extensions/inference-extension/download.bat index 3dfe34218..723268919 100644 --- a/extensions/inference-extension/download.bat +++ b/extensions/inference-extension/download.bat @@ -1,4 +1,3 @@ @echo off set /p NITRO_VERSION=<./nitro/version.txt -.\node_modules\.bin\download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-cuda.zip -e --strip 1 -o ./nitro/win-cuda -.\node_modules\.bin\download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64.zip -e --strip 1 -o ./nitro/win-cpu +.\node_modules\.bin\download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-cuda.tar.gz -e --strip 1 -o ./nitro/win-cuda && .\node_modules\.bin\download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64.tar.gz -e --strip 1 -o ./nitro/win-cpu diff --git a/extensions/inference-extension/nitro/version.txt b/extensions/inference-extension/nitro/version.txt index 44a7df273..964f548b5 100644 --- a/extensions/inference-extension/nitro/version.txt +++ b/extensions/inference-extension/nitro/version.txt @@ -1 +1 @@ -0.1.17 \ No newline at end of file +0.1.20 \ No newline at end of file diff --git a/package.json b/package.json index 9192a0238..2a4a7fa85 100644 --- a/package.json +++ b/package.json @@ -30,11 +30,14 @@ "build:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit build", "build:core": "cd core && yarn install && yarn run build", "build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"", - "build:electron": "yarn workspace jan build && cpx \"models/**\" \"electron/models/\"", + "build:electron": "cpx \"models/**\" \"electron/models/\" && yarn workspace jan build", "build:electron:test": "yarn workspace jan build:test", - "build:extensions": "rimraf ./electron/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./extensions/conversational-extension && npm install && npm run build:publish\" \"cd ./extensions/inference-extension && npm install && npm run build:publish\" \"cd ./extensions/model-extension && npm install && npm run build:publish\" \"cd ./extensions/monitoring-extension && npm install && npm run build:publish\" \"cd ./extensions/assistant-extension && npm install && npm run build:publish\"", + "build:extensions:windows": "rimraf ./electron/pre-install/*.tgz && powershell -command \"$jobs = Get-ChildItem -Path './extensions' -Directory | ForEach-Object { Start-Job -Name ($_.Name) -ScriptBlock { param($_dir); try { Set-Location $_dir; npm install; npm run build:publish; Write-Output 'Build successful in ' + $_dir } catch { Write-Error 'Error in ' + $_dir; throw } } -ArgumentList $_.FullName }; $jobs | Wait-Job; $jobs | ForEach-Object { Receive-Job -Job $_ -Keep } | ForEach-Object { Write-Host $_ }; $failed = $jobs | Where-Object { $_.State -ne 'Completed' -or $_.ChildJobs[0].JobStateInfo.State -ne 'Completed' }; if ($failed) { Exit 1 }\"", + "build:extensions:linux": "rimraf ./electron/pre-install/*.tgz && find ./extensions -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -n 1 -P 4 -I {} sh -c 'cd {} && npm install && npm run build:publish'", + "build:extensions:darwin": "rimraf ./electron/pre-install/*.tgz && find ./extensions -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -n 1 -P 4 -I {} sh -c 'cd {} && npm install && npm run build:publish'", + "build:extensions": "run-script-os", "build:test": "yarn build:web && yarn workspace jan build:test", - "build": "yarn build:web && yarn workspace jan build", + "build": "yarn build:web && yarn build:electron", "build:publish": "yarn build:web && yarn workspace jan build:publish" }, "devDependencies": { diff --git a/uikit/package.json b/uikit/package.json index dd67be599..a96b5d37e 100644 --- a/uikit/package.json +++ b/uikit/package.json @@ -20,9 +20,11 @@ "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-context": "^1.0.1", "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-scroll-area": "^1.0.5", + "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-toast": "^1.1.5", diff --git a/uikit/src/badge/styles.scss b/uikit/src/badge/styles.scss index e5a783d88..cf8e52c8b 100644 --- a/uikit/src/badge/styles.scss +++ b/uikit/src/badge/styles.scss @@ -6,7 +6,7 @@ } &-success { - @apply border-transparent bg-green-500 text-green-900 hover:bg-green-500/80; + @apply border-transparent bg-green-100 text-green-600; } &-secondary { diff --git a/uikit/src/command/styles.scss b/uikit/src/command/styles.scss index 80171ef50..a832792d6 100644 --- a/uikit/src/command/styles.scss +++ b/uikit/src/command/styles.scss @@ -25,7 +25,7 @@ } &-list-item { - @apply text-foreground aria-selected:bg-primary relative flex cursor-pointer select-none items-center rounded-md px-2 py-2 text-sm outline-none; + @apply text-foreground aria-selected:bg-secondary relative flex cursor-pointer select-none items-center rounded-md px-2 py-2 text-sm outline-none; } &-empty { diff --git a/uikit/src/index.ts b/uikit/src/index.ts index 67c3af93f..067752de0 100644 --- a/uikit/src/index.ts +++ b/uikit/src/index.ts @@ -10,3 +10,4 @@ export * from './tooltip' export * from './modal' export * from './command' export * from './textarea' +export * from './select' diff --git a/uikit/src/input/index.tsx b/uikit/src/input/index.tsx index 8d90ab232..9b7808055 100644 --- a/uikit/src/input/index.tsx +++ b/uikit/src/input/index.tsx @@ -9,7 +9,7 @@ const Input = forwardRef( return ( diff --git a/uikit/src/main.scss b/uikit/src/main.scss index 562e09532..1eca363b4 100644 --- a/uikit/src/main.scss +++ b/uikit/src/main.scss @@ -14,6 +14,7 @@ @import './modal/styles.scss'; @import './command/styles.scss'; @import './textarea/styles.scss'; +@import './select/styles.scss'; .animate-spin { animation: spin 1s linear infinite; @@ -104,7 +105,3 @@ --secondary-foreground: 210 20% 98%; } } - -:is(p) { - @apply text-muted-foreground; -} diff --git a/uikit/src/select/index.tsx b/uikit/src/select/index.tsx new file mode 100644 index 000000000..9bee7a153 --- /dev/null +++ b/uikit/src/select/index.tsx @@ -0,0 +1,139 @@ +'use client' + +import * as React from 'react' +import { + CaretSortIcon, + // CheckIcon, + ChevronDownIcon, + ChevronUpIcon, +} from '@radix-ui/react-icons' + +import * as SelectPrimitive from '@radix-ui/react-select' + +import { twMerge } from 'tailwind-merge' + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = 'popper', ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {/* + + + + */} + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/uikit/src/select/styles.scss b/uikit/src/select/styles.scss new file mode 100644 index 000000000..a0bf625f0 --- /dev/null +++ b/uikit/src/select/styles.scss @@ -0,0 +1,31 @@ +.select { + @apply ring-offset-background placeholder:text-muted-foreground border-border flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1; + + &-caret { + @apply h-4 w-4 opacity-50; + } + + &-scroll-up-button { + @apply flex cursor-default items-center justify-center py-1; + } + + &-scroll-down-button { + @apply flex cursor-default items-center justify-center py-1; + } + + &-label { + @apply px-2 py-1.5 text-sm font-semibold; + } + + &-item { + @apply hover:bg-secondary relative my-1 block w-full cursor-pointer select-none items-center rounded-sm px-4 py-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50; + } + + &-trigger-viewport { + @apply w-full py-1; + } + + &-content { + @apply bg-background border-border relative z-50 mt-1 block max-h-96 w-full min-w-[8rem] overflow-hidden rounded-md border shadow-md; + } +} diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 38dee2056..c62390ba5 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -15,7 +15,7 @@ export const metadata: Metadata = { export default function RootLayout({ children }: PropsWithChildren) { return ( - +
{children} diff --git a/web/app/page.tsx b/web/app/page.tsx index 20abda6f9..cae3262a7 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -8,29 +8,25 @@ import { useMainViewState } from '@/hooks/useMainViewState' import ChatScreen from '@/screens/Chat' import ExploreModelsScreen from '@/screens/ExploreModels' -import MyModelsScreen from '@/screens/MyModels' + import SettingsScreen from '@/screens/Settings' -import WelcomeScreen from '@/screens/Welcome' +import SystemMonitorScreen from '@/screens/SystemMonitor' export default function Page() { const { mainViewState } = useMainViewState() let children = null switch (mainViewState) { - case MainViewState.Welcome: - children = - break - - case MainViewState.ExploreModels: + case MainViewState.Hub: children = break - case MainViewState.MyModels: - children = + case MainViewState.Settings: + children = break - case MainViewState.Setting: - children = + case MainViewState.SystemMonitor: + children = break default: diff --git a/web/constants/screens.ts b/web/constants/screens.ts index 76ad6fab5..19f82aaac 100644 --- a/web/constants/screens.ts +++ b/web/constants/screens.ts @@ -1,7 +1,7 @@ export enum MainViewState { - Welcome, - ExploreModels, + Hub, MyModels, - Setting, - Chat, + Settings, + Thread, + SystemMonitor, } diff --git a/web/containers/CardSidebar/index.tsx b/web/containers/CardSidebar/index.tsx index 42f975aaf..38264e457 100644 --- a/web/containers/CardSidebar/index.tsx +++ b/web/containers/CardSidebar/index.tsx @@ -1,13 +1,15 @@ -import { ReactNode, useState } from 'react' -import { Fragment } from 'react' +import { ReactNode, useState, useRef } from 'react' -import { Menu, Transition } from '@headlessui/react' import { ChevronDownIcon, - EllipsisVerticalIcon, -} from '@heroicons/react/20/solid' + MoreVerticalIcon, + FolderOpenIcon, + Code2Icon, +} from 'lucide-react' import { twMerge } from 'tailwind-merge' +import { useClickOutside } from '@/hooks/useClickOutside' + interface Props { children: ReactNode title: string @@ -21,65 +23,75 @@ export default function CardSidebar({ onViewJsonClick, }: Props) { const [show, setShow] = useState(true) + const [more, setMore] = useState(false) + const [menu, setMenu] = useState(null) + const [toggle, setToggle] = useState(null) + + useClickOutside(() => setMore(false), null, [menu, toggle]) return ( -
-
+
+
- - - Open options - - setMore(!more)} + > + +
+ {more && ( +
- - - {({ active }) => ( - onRevealInFinderClick(title)} - className={twMerge( - active ? 'bg-gray-50' : '', - 'block cursor-pointer px-3 py-1 text-xs leading-6 text-gray-900' - )} - > - Reveal in finder - - )} - - - {({ active }) => ( - onViewJsonClick(title)} - className={twMerge( - active ? 'bg-gray-50' : '', - 'block cursor-pointer px-3 py-1 text-xs leading-6 text-gray-900' - )} - > - View a JSON - - )} - - - - +
{ + onRevealInFinderClick(title) + setMore(false) + }} + > + + + Reveal in Finder + +
+
{ + onViewJsonClick(title) + setMore(false) + }} + > + + + View as JSON + +
+
+ )}
{show &&
{children}
}
diff --git a/web/containers/DropdownListSidebar/index.tsx b/web/containers/DropdownListSidebar/index.tsx index b159a131e..589847fdf 100644 --- a/web/containers/DropdownListSidebar/index.tsx +++ b/web/containers/DropdownListSidebar/index.tsx @@ -1,104 +1,114 @@ -import { Fragment, useEffect, useState } from 'react' - -import { Listbox, Transition } from '@headlessui/react' -import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid' +import { useEffect, useState } from 'react' import { Model } from '@janhq/core' -import { atom, useSetAtom } from 'jotai' +import { + Button, + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from '@janhq/uikit' + +import { atom, useAtomValue, useSetAtom } from 'jotai' + +import { MonitorIcon } from 'lucide-react' + import { twMerge } from 'tailwind-merge' +import { MainViewState } from '@/constants/screens' + import { getDownloadedModels } from '@/hooks/useGetDownloadedModels' +import { useMainViewState } from '@/hooks/useMainViewState' + +import { toGigabytes } from '@/utils/converter' + +import { activeThreadAtom } from '@/helpers/atoms/Conversation.atom' + export const selectedModelAtom = atom(undefined) export default function DropdownListSidebar() { const [downloadedModels, setDownloadedModels] = useState([]) - const [selected, setSelected] = useState() const setSelectedModel = useSetAtom(selectedModelAtom) + const activeThread = useAtomValue(activeThreadAtom) + const [selected, setSelected] = useState() + const { setMainViewState } = useMainViewState() useEffect(() => { getDownloadedModels().then((downloadedModels) => { setDownloadedModels(downloadedModels) - if (downloadedModels.length > 0) { - setSelected(downloadedModels[0]) - setSelectedModel(downloadedModels[0]) + setSelected( + downloadedModels.filter( + (x) => x.id === activeThread?.assistants[0].model.id + )[0] || downloadedModels[0] + ) + setSelectedModel( + downloadedModels.filter( + (x) => x.id === activeThread?.assistants[0].model.id + )[0] || downloadedModels[0] + ) } }) - }, []) - - if (!selected) return null + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeThread]) return ( - { - setSelected(model) - setSelectedModel(model) + ) } diff --git a/web/containers/Layout/BottomBar/DownloadingState/index.tsx b/web/containers/Layout/BottomBar/DownloadingState/index.tsx index 1aad0fb1c..0648508d0 100644 --- a/web/containers/Layout/BottomBar/DownloadingState/index.tsx +++ b/web/containers/Layout/BottomBar/DownloadingState/index.tsx @@ -12,18 +12,14 @@ import { ModalTrigger, } from '@janhq/uikit' -import { useAtomValue } from 'jotai' - import { useDownloadState } from '@/hooks/useDownloadState' import { formatDownloadPercentage } from '@/utils/converter' import { extensionManager } from '@/extension' -import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom' export default function DownloadingState() { const { downloadStates } = useDownloadState() - const models = useAtomValue(downloadingModelsAtom) const totalCurrentProgress = downloadStates .map((a) => a.size.transferred + a.size.transferred) diff --git a/web/containers/Layout/BottomBar/index.tsx b/web/containers/Layout/BottomBar/index.tsx index 1a264da02..fb0ef5ed6 100644 --- a/web/containers/Layout/BottomBar/index.tsx +++ b/web/containers/Layout/BottomBar/index.tsx @@ -30,7 +30,7 @@ const BottomBar = () => { const { downloadStates } = useDownloadState() return ( -
+
{progress && progress > 0 ? ( @@ -49,7 +49,7 @@ const BottomBar = () => { name="Active model:" value={ activeModel?.id || ( - +   to show your model @@ -63,7 +63,7 @@ const BottomBar = () => { diff --git a/web/containers/Layout/Ribbon/index.tsx b/web/containers/Layout/Ribbon/index.tsx index 6babadb9d..fa6d53193 100644 --- a/web/containers/Layout/Ribbon/index.tsx +++ b/web/containers/Layout/Ribbon/index.tsx @@ -1,5 +1,3 @@ -import { useContext } from 'react' - import { Tooltip, TooltipContent, @@ -11,9 +9,8 @@ import { motion as m } from 'framer-motion' import { MessageCircleIcon, SettingsIcon, - DatabaseIcon, - CpuIcon, - BookOpenIcon, + MonitorIcon, + LayoutGridIcon, } from 'lucide-react' import { twMerge } from 'tailwind-merge' @@ -34,36 +31,51 @@ export default function RibbonNav() { const primaryMenus = [ { - name: 'Getting Started', - icon: , - state: MainViewState.Welcome, + name: 'Thread', + icon: ( + + ), + state: MainViewState.Thread, }, { - name: 'Chat', - icon: , - state: MainViewState.Chat, + name: 'Hub', + icon: ( + + ), + state: MainViewState.Hub, }, ] const secondaryMenus = [ { - name: 'Explore Models', - icon: , - state: MainViewState.ExploreModels, - }, - { - name: 'My Models', - icon: , - state: MainViewState.MyModels, + name: 'System Monitor', + icon: ( + + ), + state: MainViewState.SystemMonitor, }, { name: 'Settings', - icon: , - state: MainViewState.Setting, + icon: ( + + ), + state: MainViewState.Settings, }, ] return ( -
+
@@ -90,7 +102,7 @@ export default function RibbonNav() {
{isActive && ( )} @@ -126,7 +138,7 @@ export default function RibbonNav() {
{isActive && ( )} diff --git a/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx b/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx index 0fb278080..d0ea6b26b 100644 --- a/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx +++ b/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx @@ -85,12 +85,12 @@ export default function CommandListDownloadedModel() { { - setMainViewState(MainViewState.ExploreModels) + setMainViewState(MainViewState.Hub) setOpen(false) }} > - Explore Models + Explore The Hub diff --git a/web/containers/Layout/TopBar/CommandSearch/index.tsx b/web/containers/Layout/TopBar/CommandSearch/index.tsx index 2e20ff583..d83feb22e 100644 --- a/web/containers/Layout/TopBar/CommandSearch/index.tsx +++ b/web/containers/Layout/TopBar/CommandSearch/index.tsx @@ -1,7 +1,6 @@ import { Fragment, useState, useEffect } from 'react' import { - Button, CommandModal, CommandEmpty, CommandGroup, @@ -11,14 +10,7 @@ import { CommandList, } from '@janhq/uikit' -import { useAtomValue, useSetAtom } from 'jotai' -import { - MessageCircleIcon, - SettingsIcon, - DatabaseIcon, - CpuIcon, - BookOpenIcon, -} from 'lucide-react' +import { MessageCircleIcon, SettingsIcon, LayoutGridIcon } from 'lucide-react' import ShortCut from '@/containers/Shortcut' @@ -26,43 +18,27 @@ import { MainViewState } from '@/constants/screens' import { useMainViewState } from '@/hooks/useMainViewState' -import { showRightSideBarAtom } from '@/screens/Chat/Sidebar' - -import { activeThreadAtom } from '@/helpers/atoms/Conversation.atom' - export default function CommandSearch() { const { setMainViewState } = useMainViewState() const [open, setOpen] = useState(false) - const setShowRightSideBar = useSetAtom(showRightSideBarAtom) - const activeThread = useAtomValue(activeThreadAtom) const menus = [ - { - name: 'Getting Started', - icon: , - state: MainViewState.Welcome, - }, { name: 'Chat', icon: ( ), - state: MainViewState.Chat, + state: MainViewState.Thread, }, { - name: 'Explore Models', - icon: , - state: MainViewState.ExploreModels, - }, - { - name: 'My Models', - icon: , - state: MainViewState.MyModels, + name: 'Hub', + icon: , + state: MainViewState.Hub, }, { name: 'Settings', icon: , - state: MainViewState.Setting, + state: MainViewState.Settings, shortcut: , }, ] @@ -75,7 +51,7 @@ export default function CommandSearch() { } if (e.key === ',' && (e.metaKey || e.ctrlKey)) { e.preventDefault() - setMainViewState(MainViewState.Setting) + setMainViewState(MainViewState.Settings) } } document.addEventListener('keydown', down) @@ -85,7 +61,8 @@ export default function CommandSearch() { return ( -
+ {/* Temporary disable view search input until we have proper UI placement, but we keep function cmd + K for showing list page */} + {/*
-
- +
*/} @@ -124,15 +100,6 @@ export default function CommandSearch() { - {activeThread && ( - - )} ) } diff --git a/web/containers/Layout/TopBar/index.tsx b/web/containers/Layout/TopBar/index.tsx index 5ab4ebc84..aa7912bd3 100644 --- a/web/containers/Layout/TopBar/index.tsx +++ b/web/containers/Layout/TopBar/index.tsx @@ -1,21 +1,86 @@ +import { useAtomValue, useSetAtom } from 'jotai' +import { PanelLeftIcon, PenSquareIcon, PanelRightIcon } from 'lucide-react' + import CommandListDownloadedModel from '@/containers/Layout/TopBar/CommandListDownloadedModel' import CommandSearch from '@/containers/Layout/TopBar/CommandSearch' +import { MainViewState } from '@/constants/screens' + +import { useCreateNewThread } from '@/hooks/useCreateNewThread' +import useGetAssistants from '@/hooks/useGetAssistants' import { useMainViewState } from '@/hooks/useMainViewState' +import { showRightSideBarAtom } from '@/screens/Chat/Sidebar' + +import { activeThreadAtom } from '@/helpers/atoms/Conversation.atom' + const TopBar = () => { - const { viewStateName } = useMainViewState() + const activeThread = useAtomValue(activeThreadAtom) + const { mainViewState } = useMainViewState() + const { requestCreateNewThread } = useCreateNewThread() + const { assistants } = useGetAssistants() + const setShowRightSideBar = useSetAtom(showRightSideBarAtom) + + const titleScreen = (viewStateName: MainViewState) => { + switch (viewStateName) { + case MainViewState.Thread: + return activeThread ? activeThread?.title : 'New Thread' + + default: + return MainViewState[viewStateName]?.replace(/([A-Z])/g, ' $1').trim() + } + } + + const onCreateConversationClick = async () => { + if (assistants.length === 0) { + alert('No assistant available') + return + } + requestCreateNewThread(assistants[0]) + } return ( -
+
+ {mainViewState === MainViewState.Thread && ( +
+ )}
-
- - {viewStateName.replace(/([A-Z])/g, ' $1').trim()} - -
+ {mainViewState === MainViewState.Thread ? ( +
+
+
+ +
+
+ +
+
+ + {titleScreen(mainViewState)} + + {activeThread && ( +
setShowRightSideBar((show) => !show)} + > + +
+ )} +
+ ) : ( +
+ + {titleScreen(mainViewState)} + +
+ )} - {/* Command without trigger interface */}
diff --git a/web/containers/ModalCancelDownload/index.tsx b/web/containers/ModalCancelDownload/index.tsx index 8619c543c..4153b89ee 100644 --- a/web/containers/ModalCancelDownload/index.tsx +++ b/web/containers/ModalCancelDownload/index.tsx @@ -35,7 +35,6 @@ export default function ModalCancelDownload({ model, isFromList }: Props) { // eslint-disable-next-line react-hooks/exhaustive-deps [model.id] ) - const models = useAtomValue(downloadingModelsAtom) const downloadState = useAtomValue(downloadAtom) const cancelText = `Cancel ${formatDownloadPercentage(downloadState.percent)}` diff --git a/web/containers/Shortcut/index.tsx b/web/containers/Shortcut/index.tsx index 67a5f8d0c..ae93a827e 100644 --- a/web/containers/Shortcut/index.tsx +++ b/web/containers/Shortcut/index.tsx @@ -14,7 +14,7 @@ export default function ShortCut(props: { menu: string }) { } return ( -
+

{getSymbol(os) + ' + ' + menu}

) diff --git a/web/containers/Toast/index.tsx b/web/containers/Toast/index.tsx index 50f1f0f29..c5e5f03da 100644 --- a/web/containers/Toast/index.tsx +++ b/web/containers/Toast/index.tsx @@ -16,7 +16,7 @@ export function toaster(props: Props) { return (
{ const newData: Record = { ...get(chatMessages), } - newData[id] = newData[id].filter((e) => e.role === ChatCompletionRole.System) + newData[id] = newData[id]?.filter((e) => e.role === ChatCompletionRole.System) set(chatMessages, newData) }) diff --git a/web/helpers/atoms/SystemBar.atom.ts b/web/helpers/atoms/SystemBar.atom.ts index 9b44c2e92..aa5e77d58 100644 --- a/web/helpers/atoms/SystemBar.atom.ts +++ b/web/helpers/atoms/SystemBar.atom.ts @@ -1,3 +1,6 @@ import { atom } from 'jotai' export const totalRamAtom = atom(0) +export const usedRamAtom = atom(0) + +export const cpuUsageAtom = atom(0) diff --git a/web/hooks/useClickOutside.ts b/web/hooks/useClickOutside.ts new file mode 100644 index 000000000..4e8e5d2c3 --- /dev/null +++ b/web/hooks/useClickOutside.ts @@ -0,0 +1,42 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useEffect, useRef } from 'react' + +const DEFAULT_EVENTS = ['mousedown', 'touchstart'] + +export function useClickOutside( + handler: () => void, + events?: string[] | null, + nodes?: (HTMLElement | null)[] +) { + const ref = useRef() + + useEffect(() => { + const listener = (event: any) => { + const { target } = event ?? {} + if (Array.isArray(nodes)) { + const shouldIgnore = + target?.hasAttribute('data-ignore-outside-clicks') || + (!document.body.contains(target) && target.tagName !== 'HTML') + const shouldTrigger = nodes.every( + (node) => !!node && !event.composedPath().includes(node) + ) + shouldTrigger && !shouldIgnore && handler() + } else if (ref.current && !ref.current.contains(target)) { + handler() + } + } + + ;(events || DEFAULT_EVENTS).forEach((fn) => + document.addEventListener(fn, listener) + ) + + return () => { + ;(events || DEFAULT_EVENTS).forEach((fn) => + document.removeEventListener(fn, listener) + ) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ref, handler, nodes]) + + return ref +} diff --git a/web/hooks/useCreateNewThread.ts b/web/hooks/useCreateNewThread.ts index 9ccecee7a..7526feb49 100644 --- a/web/hooks/useCreateNewThread.ts +++ b/web/hooks/useCreateNewThread.ts @@ -40,7 +40,6 @@ export const useCreateNewThread = () => { const setActiveThreadId = useSetAtom(setActiveThreadIdAtom) const [threadStates, setThreadStates] = useAtom(threadStatesAtom) const threads = useAtomValue(threadsAtom) - const activeThread = useAtomValue(activeThreadAtom) const updateThread = useSetAtom(updateThreadAtom) const requestCreateNewThread = async (assistant: Assistant) => { @@ -69,6 +68,7 @@ export const useCreateNewThread = () => { stream: false, }, }, + instructions: assistant.instructions, } const threadId = generateThreadId(assistant.id) const thread: Thread = { @@ -93,20 +93,18 @@ export const useCreateNewThread = () => { setActiveThreadId(thread.id) } - function updateThreadTitle(title: string) { - if (!activeThread) return - const updatedConv: Thread = { - ...activeThread, - title, + function updateThreadMetadata(thread: Thread) { + const updatedThread: Thread = { + ...thread, } - updateThread(updatedConv) + updateThread(updatedThread) extensionManager .get(ExtensionType.Conversational) - ?.saveThread(updatedConv) + ?.saveThread(updatedThread) } return { requestCreateNewThread, - updateThreadTitle, + updateThreadMetadata, } } diff --git a/web/hooks/useDeleteConversation.ts b/web/hooks/useDeleteConversation.ts index 1cfceebcf..b02796b10 100644 --- a/web/hooks/useDeleteConversation.ts +++ b/web/hooks/useDeleteConversation.ts @@ -17,7 +17,6 @@ import { } from '@/helpers/atoms/ChatMessage.atom' import { threadsAtom, - getActiveThreadIdAtom, setActiveThreadIdAtom, } from '@/helpers/atoms/Conversation.atom' @@ -25,14 +24,13 @@ export default function useDeleteThread() { const { activeModel } = useActiveModel() const [threads, setThreads] = useAtom(threadsAtom) const setCurrentPrompt = useSetAtom(currentPromptAtom) - const activeThreadId = useAtomValue(getActiveThreadIdAtom) const messages = useAtomValue(getCurrentChatMessagesAtom) const setActiveConvoId = useSetAtom(setActiveThreadIdAtom) const deleteMessages = useSetAtom(deleteConversationMessage) const cleanMessages = useSetAtom(cleanConversationMessages) - const cleanThread = async () => { + const cleanThread = async (activeThreadId: string) => { if (activeThreadId) { const thread = threads.filter((c) => c.id === activeThreadId)[0] cleanMessages(activeThreadId) @@ -46,7 +44,7 @@ export default function useDeleteThread() { } } - const deleteThread = async () => { + const deleteThread = async (activeThreadId: string) => { if (!activeThreadId) { alert('No active thread') return @@ -60,8 +58,8 @@ export default function useDeleteThread() { deleteMessages(activeThreadId) setCurrentPrompt('') toaster({ - title: 'Chat successfully deleted.', - description: `Chat with ${activeModel?.name} has been successfully deleted.`, + title: 'Thread successfully deleted.', + description: `Thread with ${activeModel?.name} has been successfully deleted.`, }) if (availableThreads.length > 0) { setActiveConvoId(availableThreads[0].id) diff --git a/web/hooks/useDownloadModel.ts b/web/hooks/useDownloadModel.ts index 6bcffdaed..b91ac2a57 100644 --- a/web/hooks/useDownloadModel.ts +++ b/web/hooks/useDownloadModel.ts @@ -1,6 +1,6 @@ import { Model, ExtensionType, ModelExtension } from '@janhq/core' -import { useAtom, useAtomValue } from 'jotai' +import { useAtom } from 'jotai' import { useDownloadState } from './useDownloadState' diff --git a/web/hooks/useGetSystemResources.ts b/web/hooks/useGetSystemResources.ts index ef4b2ef08..e2de61519 100644 --- a/web/hooks/useGetSystemResources.ts +++ b/web/hooks/useGetSystemResources.ts @@ -6,12 +6,18 @@ import { MonitoringExtension } from '@janhq/core' import { useSetAtom } from 'jotai' import { extensionManager } from '@/extension/ExtensionManager' -import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom' +import { + cpuUsageAtom, + totalRamAtom, + usedRamAtom, +} from '@/helpers/atoms/SystemBar.atom' export default function useGetSystemResources() { const [ram, setRam] = useState(0) const [cpu, setCPU] = useState(0) const setTotalRam = useSetAtom(totalRamAtom) + const setUsedRam = useSetAtom(usedRamAtom) + const setCpuUsage = useSetAtom(cpuUsageAtom) const getSystemResources = async () => { if ( @@ -27,10 +33,12 @@ export default function useGetSystemResources() { const ram = (resourceInfor?.mem?.active ?? 0) / (resourceInfor?.mem?.total ?? 1) + if (resourceInfor?.mem?.active) setUsedRam(resourceInfor.mem.active) if (resourceInfor?.mem?.total) setTotalRam(resourceInfor.mem.total) setRam(Math.round(ram * 100)) setCPU(Math.round(currentLoadInfor?.currentLoad ?? 0)) + setCpuUsage(Math.round(currentLoadInfor?.currentLoad ?? 0)) } useEffect(() => { @@ -45,6 +53,7 @@ export default function useGetSystemResources() { // clean up interval return () => clearInterval(intervalId) + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return { diff --git a/web/hooks/useMainViewState.ts b/web/hooks/useMainViewState.ts index 3dccbb704..91c1a1c4d 100644 --- a/web/hooks/useMainViewState.ts +++ b/web/hooks/useMainViewState.ts @@ -2,7 +2,7 @@ import { atom, useAtom } from 'jotai' import { MainViewState } from '@/constants/screens' -const currentMainViewState = atom(MainViewState.Welcome) +const currentMainViewState = atom(MainViewState.Thread) export function useMainViewState() { const [mainViewState, setMainViewState] = useAtom(currentMainViewState) diff --git a/web/hooks/useSendChatMessage.ts b/web/hooks/useSendChatMessage.ts index 6b60a0e04..8b9a1bada 100644 --- a/web/hooks/useSendChatMessage.ts +++ b/web/hooks/useSendChatMessage.ts @@ -1,3 +1,5 @@ +import { useState } from 'react' + import { ChatCompletionMessage, ChatCompletionRole, @@ -10,7 +12,7 @@ import { ThreadMessage, events, } from '@janhq/core' -import { ConversationalExtension, InferenceExtension } from '@janhq/core' +import { ConversationalExtension } from '@janhq/core' import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { ulid } from 'ulid' @@ -44,6 +46,48 @@ export default function useSendChatMessage() { const { activeModel } = useActiveModel() const selectedModel = useAtomValue(selectedModelAtom) const { startModel } = useActiveModel() + const [queuedMessage, setQueuedMessage] = useState(false) + + const resendChatMessage = async () => { + if (!activeThread) { + console.error('No active thread') + return + } + + updateThreadWaiting(activeThread.id, true) + + const messages: ChatCompletionMessage[] = [ + activeThread.assistants[0]?.instructions, + ] + .map((instructions) => { + const systemMessage: ChatCompletionMessage = { + role: ChatCompletionRole.System, + content: instructions, + } + return systemMessage + }) + .concat( + currentMessages.map((msg) => ({ + role: msg.role, + content: msg.content[0]?.text.value ?? '', + })) + ) + + const messageRequest: MessageRequest = { + id: ulid(), + messages: messages, + threadId: activeThread.id, + } + + const modelId = selectedModel?.id ?? activeThread.assistants[0].model.id + + if (activeModel?.id !== modelId) { + setQueuedMessage(true) + await startModel(modelId) + setQueuedMessage(false) + } + events.emit(EventName.OnMessageSent, messageRequest) + } const sendChatMessage = async () => { if (!currentPrompt || currentPrompt.trim().length === 0) { @@ -61,14 +105,15 @@ export default function useSendChatMessage() { } const assistantId = activeThread.assistants[0].assistant_id ?? '' const assistantName = activeThread.assistants[0].assistant_name ?? '' + const instructions = activeThread.assistants[0].instructions ?? '' const updatedThread: Thread = { ...activeThread, isFinishInit: true, - title: `${activeThread.assistants[0].assistant_name} with ${selectedModel.name}`, assistants: [ { assistant_id: assistantId, assistant_name: assistantName, + instructions: instructions, model: { id: selectedModel.id, settings: selectedModel.settings, @@ -90,18 +135,29 @@ export default function useSendChatMessage() { const prompt = currentPrompt.trim() setCurrentPrompt('') - const messages: ChatCompletionMessage[] = currentMessages - .map((msg) => ({ - role: msg.role, - content: msg.content[0]?.text.value ?? '', - })) - .concat([ - { - role: ChatCompletionRole.User, - content: prompt, - } as ChatCompletionMessage, - ]) - console.debug(`Sending messages: ${JSON.stringify(messages, null, 2)}`) + const messages: ChatCompletionMessage[] = [ + activeThread.assistants[0]?.instructions, + ] + .map((instructions) => { + const systemMessage: ChatCompletionMessage = { + role: ChatCompletionRole.System, + content: instructions, + } + return systemMessage + }) + .concat( + currentMessages + .map((msg) => ({ + role: msg.role, + content: msg.content[0]?.text.value ?? '', + })) + .concat([ + { + role: ChatCompletionRole.User, + content: prompt, + } as ChatCompletionMessage, + ]) + ) const msgId = ulid() const messageRequest: MessageRequest = { id: msgId, @@ -136,17 +192,18 @@ export default function useSendChatMessage() { ?.addNewMessage(threadMessage) const modelId = selectedModel?.id ?? activeThread.assistants[0].model.id + if (activeModel?.id !== modelId) { - toaster({ - title: 'Message queued.', - description: 'It will be sent once the model is done loading', - }) + setQueuedMessage(true) await startModel(modelId) + setQueuedMessage(false) } events.emit(EventName.OnMessageSent, messageRequest) } return { sendChatMessage, + resendChatMessage, + queuedMessage, } } diff --git a/web/package.json b/web/package.json index 16522cace..922bc556a 100644 --- a/web/package.json +++ b/web/package.json @@ -33,6 +33,7 @@ "react-dom": "18.2.0", "react-hook-form": "^7.47.0", "react-hot-toast": "^2.4.1", + "react-scroll-to-bottom": "^4.2.0", "react-toastify": "^9.1.3", "sass": "^1.69.4", "tailwind-merge": "^2.0.0", @@ -48,6 +49,7 @@ "@types/node": "20.8.10", "@types/react": "18.2.34", "@types/react-dom": "18.2.14", + "@types/react-scroll-to-bottom": "^4.2.4", "@types/uuid": "^9.0.6", "@typescript-eslint/eslint-plugin": "^6.8.0", "@typescript-eslint/parser": "^6.8.0", diff --git a/web/screens/Chat/ChatBody/index.tsx b/web/screens/Chat/ChatBody/index.tsx index 10d008661..0a92b7a6c 100644 --- a/web/screens/Chat/ChatBody/index.tsx +++ b/web/screens/Chat/ChatBody/index.tsx @@ -1,17 +1,65 @@ +import { Fragment } from 'react' + +import ScrollToBottom from 'react-scroll-to-bottom' + +import { Button } from '@janhq/uikit' import { useAtomValue } from 'jotai' +import LogoMark from '@/containers/Brand/Logo/Mark' + +import { MainViewState } from '@/constants/screens' + +import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' + +import { useMainViewState } from '@/hooks/useMainViewState' + import ChatItem from '../ChatItem' import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom' const ChatBody: React.FC = () => { const messages = useAtomValue(getCurrentChatMessagesAtom) + const { downloadedModels } = useGetDownloadedModels() + const { setMainViewState } = useMainViewState() + + if (downloadedModels.length === 0) + return ( +
+ +

Welcome!

+

You need to download your first model

+ +
+ ) + return ( -
- {messages.map((message) => ( - - ))} -
+ + {messages.length === 0 ? ( +
+ +

How can I help you?

+
+ ) : ( + + {messages.map((message) => ( + + ))} + + )} +
) } diff --git a/web/screens/Chat/ChatItem/index.tsx b/web/screens/Chat/ChatItem/index.tsx index 5f192d436..fcc6cbab5 100644 --- a/web/screens/Chat/ChatItem/index.tsx +++ b/web/screens/Chat/ChatItem/index.tsx @@ -7,10 +7,7 @@ import SimpleTextMessage from '../SimpleTextMessage' type Ref = HTMLDivElement const ChatItem = forwardRef((message, ref) => ( -
+
)) diff --git a/web/screens/Chat/MessageToolbar/index.tsx b/web/screens/Chat/MessageToolbar/index.tsx index 5fe432e62..a0929336c 100644 --- a/web/screens/Chat/MessageToolbar/index.tsx +++ b/web/screens/Chat/MessageToolbar/index.tsx @@ -1,18 +1,13 @@ -import { useMemo } from 'react' - import { - ChatCompletionRole, - ChatCompletionMessage, EventName, - MessageRequest, MessageStatus, ExtensionType, ThreadMessage, events, } from '@janhq/core' import { ConversationalExtension, InferenceExtension } from '@janhq/core' -import { atom, useAtomValue, useSetAtom } from 'jotai' -import { RefreshCcw, ClipboardCopy, Trash2Icon, StopCircle } from 'lucide-react' +import { useAtomValue, useSetAtom } from 'jotai' +import { RefreshCcw, Copy, Trash2Icon, StopCircle } from 'lucide-react' import { twMerge } from 'tailwind-merge' @@ -23,22 +18,16 @@ import { deleteMessageAtom, getCurrentChatMessagesAtom, } from '@/helpers/atoms/ChatMessage.atom' -import { - activeThreadAtom, - threadStatesAtom, -} from '@/helpers/atoms/Conversation.atom' +import { activeThreadAtom } from '@/helpers/atoms/Conversation.atom' +import useSendChatMessage from '@/hooks/useSendChatMessage' const MessageToolbar = ({ message }: { message: ThreadMessage }) => { const deleteMessage = useSetAtom(deleteMessageAtom) const thread = useAtomValue(activeThreadAtom) const messages = useAtomValue(getCurrentChatMessagesAtom) - const threadStateAtom = useMemo( - () => atom((get) => get(threadStatesAtom)[thread?.id ?? '']), - [thread?.id] - ) - const threadState = useAtomValue(threadStateAtom) + const { resendChatMessage } = useSendChatMessage() - const stopInference = async () => { + const onStopInferenceClick = async () => { await extensionManager .get(ExtensionType.Inference) ?.stopInference() @@ -50,18 +39,25 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => { }, 300) } + const onDeleteClick = async () => { + deleteMessage(message.id ?? '') + if (thread) { + await extensionManager + .get(ExtensionType.Conversational) + ?.writeMessages( + thread.id, + messages.filter((msg) => msg.id !== message.id) + ) + } + } + return ( -
+
{message.status === MessageStatus.Pending && (
stopInference()} + onClick={onStopInferenceClick} >
@@ -70,20 +66,7 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => { message.id === messages[messages.length - 1]?.id && (
{ - const messageRequest: MessageRequest = { - id: message.id ?? '', - messages: messages.slice(0, -1).map((e) => { - const msg: ChatCompletionMessage = { - role: e.role, - content: e.content[0].text.value, - } - return msg - }), - threadId: message.thread_id ?? '', - } - events.emit(EventName.OnMessageSent, messageRequest) - }} + onClick={resendChatMessage} >
@@ -97,20 +80,11 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => { }) }} > - +
{ - deleteMessage(message.id ?? '') - if (thread) - await extensionManager - .get(ExtensionType.Conversational) - ?.writeMessages( - thread.id, - messages.filter((msg) => msg.id !== message.id) - ) - }} + onClick={onDeleteClick} >
diff --git a/web/screens/Chat/Sidebar/index.tsx b/web/screens/Chat/Sidebar/index.tsx index bda987a1c..cf8c46b48 100644 --- a/web/screens/Chat/Sidebar/index.tsx +++ b/web/screens/Chat/Sidebar/index.tsx @@ -1,30 +1,35 @@ import { join } from 'path' import { getUserSpace, openFileExplorer } from '@janhq/core' + +import { Input, Textarea } from '@janhq/uikit' + import { atom, useAtomValue } from 'jotai' +import { twMerge } from 'tailwind-merge' + +import LogoMark from '@/containers/Brand/Logo/Mark' import CardSidebar from '@/containers/CardSidebar' import DropdownListSidebar, { selectedModelAtom, } from '@/containers/DropdownListSidebar' -import ItemCardSidebar from '@/containers/ItemCardSidebar' import { useCreateNewThread } from '@/hooks/useCreateNewThread' import { activeThreadAtom } from '@/helpers/atoms/Conversation.atom' -export const showRightSideBarAtom = atom(false) +export const showRightSideBarAtom = atom(true) export default function Sidebar() { const showing = useAtomValue(showRightSideBarAtom) const activeThread = useAtomValue(activeThreadAtom) const selectedModel = useAtomValue(selectedModelAtom) - const { updateThreadTitle } = useCreateNewThread() + const { updateThreadMetadata } = useCreateNewThread() const onReviewInFinderClick = async (type: string) => { if (!activeThread) return if (!activeThread.isFinishInit) { - alert('Thread is not ready') + alert('Thread is not started yet') return } @@ -56,7 +61,7 @@ export default function Sidebar() { const onViewJsonClick = async (type: string) => { if (!activeThread) return if (!activeThread.isFinishInit) { - alert('Thread is not ready') + alert('Thread is not started yet') return } @@ -87,44 +92,104 @@ export default function Sidebar() { return (
-
+
- - updateThreadTitle(title ?? '')} - /> +
+
+ + { + if (activeThread) + updateThreadMetadata({ + ...activeThread, + title: e.target.value || '', + }) + }} + /> +
+
+ + + {activeThread?.id || '-'} + +
+
- +
+
+ + + {activeThread?.assistants[0].assistant_name ?? '-'} + +
+
+ +