From 0f8deddcdfac92913ac6a9682d16396e8b0a7692 Mon Sep 17 00:00:00 2001 From: hieu-jan <150573299+hieu-jan@users.noreply.github.com> Date: Tue, 23 Jan 2024 00:27:13 +0900 Subject: [PATCH 01/63] docs: add requirements to installation guides --- docs/docs/guides/02-installation/01-mac.md | 4 ++++ .../docs/guides/02-installation/02-windows.md | 21 ++++++++++--------- docs/docs/guides/02-installation/03-linux.md | 17 +++++++++++++-- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/docs/docs/guides/02-installation/01-mac.md b/docs/docs/guides/02-installation/01-mac.md index 8e67b5bed..a719cd913 100644 --- a/docs/docs/guides/02-installation/01-mac.md +++ b/docs/docs/guides/02-installation/01-mac.md @@ -17,6 +17,10 @@ keywords: # Installing Jan on MacOS +## Requirements + +Ensure that your MacOS version is 13 or higher to run Jan. + ## Installation Jan is available for download via our homepage, [https://jan.ai/](https://jan.ai/). diff --git a/docs/docs/guides/02-installation/02-windows.md b/docs/docs/guides/02-installation/02-windows.md index b200554d2..6623fba62 100644 --- a/docs/docs/guides/02-installation/02-windows.md +++ b/docs/docs/guides/02-installation/02-windows.md @@ -17,6 +17,17 @@ keywords: # Installing Jan on Windows +## System Requirements + +Ensure that your system meets the following requirements: + +- Windows 10 or higher is required to run Jan. + +To enable GPU support, you will need: + +- NVIDIA GPU with CUDA Toolkit 11.7 or higher +- NVIDIA driver 470.63.01 or higher + ## Installation Jan is available for download via our homepage, [https://jan.ai](https://jan.ai/). @@ -59,13 +70,3 @@ To remove all user data associated with Jan, you can delete the `/jan` directory cd C:\Users\%USERNAME%\AppData\Roaming rmdir /S jan ``` - -## Troubleshooting - -### Microsoft Defender - -**Error: "Microsoft Defender SmartScreen prevented an unrecognized app from starting"** - -Windows Defender may display the above warning when running the Jan Installer, as a standard security measure. - -To proceed, select the "More info" option and select the "Run Anyway" option to continue with the installation. diff --git a/docs/docs/guides/02-installation/03-linux.md b/docs/docs/guides/02-installation/03-linux.md index 21dfac1a9..bb93a8a3b 100644 --- a/docs/docs/guides/02-installation/03-linux.md +++ b/docs/docs/guides/02-installation/03-linux.md @@ -17,6 +17,18 @@ keywords: # Installing Jan on Linux +## Requirements + +Ensure that your system meets the following requirements: + +- glibc 2.27 or higher (check with `ldd --version`) +- gcc 11, g++ 11, cpp 11, or higher, refer to this [link](https://jan.ai/guides/troubleshooting/gpu-not-used/#specific-requirements-for-linux) for more information. + +To enable GPU support, you will need: + +- NVIDIA GPU with CUDA Toolkit 11.7 or higher +- NVIDIA driver 470.63.01 or higher + ## Installation Jan is available for download via our homepage, [https://jan.ai](https://jan.ai/). @@ -66,7 +78,8 @@ jan-linux-amd64-{version}.deb # AppImage jan-linux-x86_64-{version}.AppImage ``` -``` + +```` ## Uninstall Jan @@ -75,7 +88,7 @@ To uninstall Jan on Linux, you should use your package manager's uninstall or re ```bash sudo apt-get remove jan # where jan is the name of Jan package -``` +```` For other Linux distributions, if you installed Jan via the `.AppImage` file, you can uninstall Jan by deleting the `.AppImage` file. From cef149a11b3cd29c4285793b2e02ca60dfb47894 Mon Sep 17 00:00:00 2001 From: hieu-jan <150573299+hieu-jan@users.noreply.github.com> Date: Tue, 23 Jan 2024 00:33:18 +0900 Subject: [PATCH 02/63] docs: finalize installation guide --- docs/docs/guides/02-installation/01-mac.md | 3 ++- docs/docs/guides/02-installation/02-windows.md | 1 + docs/docs/guides/02-installation/03-linux.md | 7 +++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/docs/guides/02-installation/01-mac.md b/docs/docs/guides/02-installation/01-mac.md index a719cd913..7a3961384 100644 --- a/docs/docs/guides/02-installation/01-mac.md +++ b/docs/docs/guides/02-installation/01-mac.md @@ -12,12 +12,13 @@ keywords: conversational AI, no-subscription fee, large language model, + installation guide, ] --- # Installing Jan on MacOS -## Requirements +## System Requirements Ensure that your MacOS version is 13 or higher to run Jan. diff --git a/docs/docs/guides/02-installation/02-windows.md b/docs/docs/guides/02-installation/02-windows.md index 6623fba62..d60ab86f7 100644 --- a/docs/docs/guides/02-installation/02-windows.md +++ b/docs/docs/guides/02-installation/02-windows.md @@ -12,6 +12,7 @@ keywords: conversational AI, no-subscription fee, large language model, + installation guide, ] --- diff --git a/docs/docs/guides/02-installation/03-linux.md b/docs/docs/guides/02-installation/03-linux.md index bb93a8a3b..0ec7fea60 100644 --- a/docs/docs/guides/02-installation/03-linux.md +++ b/docs/docs/guides/02-installation/03-linux.md @@ -12,12 +12,13 @@ keywords: conversational AI, no-subscription fee, large language model, + installation guide, ] --- # Installing Jan on Linux -## Requirements +## System Requirements Ensure that your system meets the following requirements: @@ -79,8 +80,6 @@ jan-linux-amd64-{version}.deb jan-linux-x86_64-{version}.AppImage ``` -```` - ## Uninstall Jan To uninstall Jan on Linux, you should use your package manager's uninstall or remove option. For Debian/Ubuntu-based distributions, if you installed Jan via the `.deb` package, you can uninstall Jan using the following command: @@ -88,7 +87,7 @@ To uninstall Jan on Linux, you should use your package manager's uninstall or re ```bash sudo apt-get remove jan # where jan is the name of Jan package -```` +``` For other Linux distributions, if you installed Jan via the `.AppImage` file, you can uninstall Jan by deleting the `.AppImage` file. From f23e9115fc5cce5e2a85e2bbc5b5392bad876f84 Mon Sep 17 00:00:00 2001 From: hieu-jan <150573299+hieu-jan@users.noreply.github.com> Date: Tue, 23 Jan 2024 01:22:36 +0900 Subject: [PATCH 03/63] docs: add developer/install-and-prerequisites --- .../04-install-and-prerequisites.md | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 docs/docs/developer/01-overview/04-install-and-prerequisites.md diff --git a/docs/docs/developer/01-overview/04-install-and-prerequisites.md b/docs/docs/developer/01-overview/04-install-and-prerequisites.md new file mode 100644 index 000000000..dbc39fccd --- /dev/null +++ b/docs/docs/developer/01-overview/04-install-and-prerequisites.md @@ -0,0 +1,65 @@ +--- +title: Installation and Prerequisites +slug: /developer/install-and-prerequisites +description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +keywords: + [ + Jan AI, + Jan, + ChatGPT alternative, + local AI, + private AI, + conversational AI, + no-subscription fee, + large language model, + installation, + prerequisites, + developer setup, + ] +--- + +## Requirements: + +- [Hardware Requirements](../../guides/02-installation/06-hardware.md) + +- System Requirements: + - [Windows](../../install/windows/#system-requirements) + - [MacOS](../../install/mac/#system-requirements) + - [Linux](../../install/linux/#system-requirements) + +## Prerequisites + +Before installing Jan, make sure you have the following installed on your computer: + +- [Node.js](https://nodejs.org/en/) (version 20.0.0 or higher) +- [yarn](https://yarnpkg.com/) (version 1.22.0 or higher) +- [make](https://www.gnu.org/software/make/) (version 3.81 or higher) + +## Instructions + +1. Clone the repository and install dependencies + +```bash +git clone https://github.com/janhq/jan +cd jan +git checkout -b DESIRED_BRANCH +yarn install +``` + +2. Run development and use Jan Desktop + +```bash +make dev +``` + +This will start the development server and open the Jan Desktop app. + +## For Production Build + +```bash +# Do steps 1 and 2 in the previous section +# Build the app +make build +``` + +This will build the app MacOS (M1/M2/M3) for production (with code signing already done) and put the result in dist folder. From 7c0c6924d831bf6d070e8aa749ec8607dfeca6b9 Mon Sep 17 00:00:00 2001 From: hieu-jan <150573299+hieu-jan@users.noreply.github.com> Date: Tue, 23 Jan 2024 02:28:37 +0900 Subject: [PATCH 04/63] docs: finalize-installation-and-prerequisites --- .../01-overview/04-install-and-prerequisites.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/docs/developer/01-overview/04-install-and-prerequisites.md b/docs/docs/developer/01-overview/04-install-and-prerequisites.md index dbc39fccd..a5f7c985b 100644 --- a/docs/docs/developer/01-overview/04-install-and-prerequisites.md +++ b/docs/docs/developer/01-overview/04-install-and-prerequisites.md @@ -18,7 +18,7 @@ keywords: ] --- -## Requirements: +## Requirements - [Hardware Requirements](../../guides/02-installation/06-hardware.md) @@ -29,8 +29,6 @@ keywords: ## Prerequisites -Before installing Jan, make sure you have the following installed on your computer: - - [Node.js](https://nodejs.org/en/) (version 20.0.0 or higher) - [yarn](https://yarnpkg.com/) (version 1.22.0 or higher) - [make](https://www.gnu.org/software/make/) (version 3.81 or higher) @@ -43,10 +41,15 @@ Before installing Jan, make sure you have the following installed on your comput git clone https://github.com/janhq/jan cd jan git checkout -b DESIRED_BRANCH +``` + +2. Install dependencies + +```bash yarn install ``` -2. Run development and use Jan Desktop +3. Run development and use Jan Desktop ```bash make dev @@ -62,4 +65,4 @@ This will start the development server and open the Jan Desktop app. make build ``` -This will build the app MacOS (M1/M2/M3) for production (with code signing already done) and put the result in dist folder. +This will build the app MacOS (M1/M2/M3) for production (with code signing already done) and put the result in `dist` folder. From b14ff4cdb5fa35fa565397a5f8698c40d8bba24d Mon Sep 17 00:00:00 2001 From: hieu-jan <150573299+hieu-jan@users.noreply.github.com> Date: Tue, 23 Jan 2024 03:00:51 +0900 Subject: [PATCH 05/63] docs: finalize install and prerequisitites --- .../01-overview/04-install-and-prerequisites.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/docs/developer/01-overview/04-install-and-prerequisites.md b/docs/docs/developer/01-overview/04-install-and-prerequisites.md index a5f7c985b..b418609f6 100644 --- a/docs/docs/developer/01-overview/04-install-and-prerequisites.md +++ b/docs/docs/developer/01-overview/04-install-and-prerequisites.md @@ -35,7 +35,7 @@ keywords: ## Instructions -1. Clone the repository and install dependencies +1. **Clone the Repository:** ```bash git clone https://github.com/janhq/jan @@ -43,19 +43,19 @@ cd jan git checkout -b DESIRED_BRANCH ``` -2. Install dependencies +2. **Install Dependencie:s** ```bash yarn install ``` -3. Run development and use Jan Desktop +3. **Run Development and Use Jan Desktop** ```bash make dev ``` -This will start the development server and open the Jan Desktop app. +This command starts the development server and opens the Jan Desktop app. ## For Production Build @@ -65,4 +65,8 @@ This will start the development server and open the Jan Desktop app. make build ``` -This will build the app MacOS (M1/M2/M3) for production (with code signing already done) and put the result in `dist` folder. +This will build the app MacOS (M1/M2/M3) for production (with code signing already done) and place the result in `/electron/dist` folder. + +## Troubleshooting + +If you run into any issues due to a broken build, please check the [Stuck on a Broken Build](../../troubleshooting/stuck-on-broken-build) guide. From b946c5d3e029c88840c445930875b300989a00e7 Mon Sep 17 00:00:00 2001 From: hieu-jan <150573299+hieu-jan@users.noreply.github.com> Date: Tue, 23 Jan 2024 03:05:48 +0900 Subject: [PATCH 06/63] docs: correct slug --- .../developer/01-overview/04-install-and-prerequisites.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/developer/01-overview/04-install-and-prerequisites.md b/docs/docs/developer/01-overview/04-install-and-prerequisites.md index b418609f6..25fe2550e 100644 --- a/docs/docs/developer/01-overview/04-install-and-prerequisites.md +++ b/docs/docs/developer/01-overview/04-install-and-prerequisites.md @@ -1,7 +1,7 @@ --- title: Installation and Prerequisites -slug: /developer/install-and-prerequisites -description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server. +slug: /developer/prereq +description: Guide to install and setup Jan for development. keywords: [ Jan AI, From 0afdee4a9856d419499c74099b7dbe986d941c8b Mon Sep 17 00:00:00 2001 From: hieu-jan <150573299+hieu-jan@users.noreply.github.com> Date: Wed, 24 Jan 2024 01:40:08 +0900 Subject: [PATCH 07/63] docs: update API Reference page --- docs/openapi/jan.yaml | 55 +++-- docs/openapi/specs/models.yaml | 370 ++++++++++----------------------- 2 files changed, 150 insertions(+), 275 deletions(-) diff --git a/docs/openapi/jan.yaml b/docs/openapi/jan.yaml index bfff0ad73..9981f7308 100644 --- a/docs/openapi/jan.yaml +++ b/docs/openapi/jan.yaml @@ -67,21 +67,32 @@ paths: x-codeSamples: - lang: cURL source: | - curl http://localhost:1337/v1/chat/completions \ - -H "Content-Type: application/json" \ + curl -X 'POST' \ + 'http://127.0.0.1:1337/v1/chat/completions' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ -d '{ - "model": "tinyllama-1.1b", - "messages": [ - { - "role": "system", - "content": "You are a helpful assistant." - }, - { - "role": "user", - "content": "Hello!" - } - ] - }' + "messages": [ + { + "content": "You are a helpful assistant.", + "role": "system" + }, + { + "content": "Hello!", + "role": "user" + } + ], + "model": "tinyllama-1.1b", + "stream": true, + "max_tokens": 2048, + "stop": [ + "hello" + ], + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.7, + "top_p": 0.95 + }' /models: get: operationId: listModels @@ -103,7 +114,9 @@ paths: x-codeSamples: - lang: cURL source: | - curl http://localhost:1337/v1/models + curl -X 'GET' \ + 'http://127.0.0.1:1337/v1/models' \ + -H 'accept: application/json' "/models/download/{model_id}": get: operationId: downloadModel @@ -131,7 +144,9 @@ paths: x-codeSamples: - lang: cURL source: | - curl -X POST http://localhost:1337/v1/models/download/{model_id} + curl -X 'GET' \ + 'http://127.0.0.1:1337/v1/models/download/{model_id}' \ + -H 'accept: application/json' "/models/{model_id}": get: operationId: retrieveModel @@ -162,7 +177,9 @@ paths: x-codeSamples: - lang: cURL source: | - curl http://localhost:1337/v1/models/{model_id} + curl -X 'GET' \ + 'http://127.0.0.1:1337/v1/models/{model_id}' \ + -H 'accept: application/json' delete: operationId: deleteModel tags: @@ -191,7 +208,9 @@ paths: x-codeSamples: - lang: cURL source: | - curl -X DELETE http://localhost:1337/v1/models/{model_id} + curl -X 'DELETE' \ + 'http://127.0.0.1:1337/v1/models/{model_id}' \ + -H 'accept: application/json' /threads: post: operationId: createThread diff --git a/docs/openapi/specs/models.yaml b/docs/openapi/specs/models.yaml index 418be9563..791d14880 100644 --- a/docs/openapi/specs/models.yaml +++ b/docs/openapi/specs/models.yaml @@ -18,106 +18,77 @@ components: Model: type: object properties: - type: - type: string - default: model - description: The type of the object. - version: - type: string - default: "1" - description: The version number of the model. - id: - type: string - description: Unique identifier used in chat-completions model_name, matches - folder name. - example: zephyr-7b - name: - type: string - description: Name of the model. - example: Zephyr 7B - owned_by: - type: string - description: Compatibility field for OpenAI. - default: "" - created: - type: integer - format: int64 - description: Unix timestamp representing the creation time. - description: - type: string - description: Description of the model. - state: - type: string - enum: - - null - - downloading - - ready - - starting - - stopping - description: Current state of the model. - format: - type: string - description: State format of the model, distinct from the engine. - example: ggufv3 source_url: type: string format: uri description: URL to the source of the model. - example: https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF/blob/main/zephyr-7b-beta.Q4_K_M.gguf + example: https://huggingface.co/janhq/trinity-v1.2-GGUF/resolve/main/trinity-v1.2.Q4_K_M.gguf + id: + type: string + description: + Unique identifier used in chat-completions model_name, matches + folder name. + example: trinity-v1.2-7b + object: + type: string + example: model + name: + type: string + description: Name of the model. + example: Trinity-v1.2 7B Q4 + version: + type: string + default: "1.0" + description: The version number of the model. + description: + type: string + description: Description of the model. + example: Trinity is an experimental model merge using the Slerp method. Recommended for daily assistance purposes. + format: + type: string + description: State format of the model, distinct from the engine. + example: gguf settings: type: object properties: ctx_len: - type: string + type: integer description: Context length. - example: "2048" - ngl: + example: 4096 + prompt_template: type: string - description: Number of layers. - example: "100" - embedding: - type: string - description: Indicates if embedding is enabled. - example: "true" - n_parallel: - type: string - description: Number of parallel processes. - example: "4" + example: "<|im_start|>system\n{system_message}<|im_end|>\n<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant" additionalProperties: false parameters: type: object properties: temperature: - type: string - description: Temperature setting for the model. - example: "0.7" - token_limit: - type: string - description: Token limit for the model. - example: "2048" - top_k: - type: string - description: Top-k setting for the model. - example: "0" + example: 0.7 top_p: - type: string - description: Top-p setting for the model. - example: "1" + example: 0.95 stream: - type: string - description: Indicates if streaming is enabled. - example: "true" + example: true + max_tokens: + example: 4096 + stop: + example: [] + frequency_penalty: + example: 0 + presence_penalty: + example: 0 additionalProperties: false metadata: - type: object - description: Additional metadata. - assets: - type: array - items: + author: type: string - description: List of assets related to the model. - required: - - source_url + example: Jan + tags: + example: ["7B", "Merged", "Featured"] + size: + example: 4370000000, + cover: + example: "https://raw.githubusercontent.com/janhq/jan/main/models/trinity-v1.2-7b/cover.png" + engine: + example: nitro ModelObject: type: object properties: @@ -125,7 +96,7 @@ components: type: string description: | The identifier of the model. - example: zephyr-7b + example: ztrinity-v1.2-7b object: type: string description: | @@ -145,197 +116,82 @@ components: GetModelResponse: type: object properties: - id: - type: string - description: The identifier of the model. - example: zephyr-7b - object: - type: string - description: Type of the object, indicating it's a model. - default: model - created: - type: integer - format: int64 - description: Unix timestamp representing the creation time of the model. - owned_by: - type: string - description: The entity that owns the model. - example: _ - state: - type: string - enum: - - not_downloaded - - downloaded - - running - - stopped - description: The current state of the model. source_url: type: string format: uri description: URL to the source of the model. - example: https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF/blob/main/zephyr-7b-beta.Q4_K_M.gguf - engine_parameters: - type: object - properties: - pre_prompt: - type: string - description: Predefined prompt used for setting up internal configurations. - default: "" - example: Initial setup complete. - system_prompt: - type: string - description: Prefix used for system-level prompts. - default: "SYSTEM: " - user_prompt: - type: string - description: Prefix used for user prompts. - default: "USER: " - ai_prompt: - type: string - description: Prefix used for assistant prompts. - default: "ASSISTANT: " - ngl: - type: integer - description: Number of neural network layers loaded onto the GPU for - acceleration. - minimum: 0 - maximum: 100 - default: 100 - example: 100 - ctx_len: - type: integer - description: Context length for model operations, varies based on the specific - model. - minimum: 128 - maximum: 4096 - default: 2048 - example: 2048 - n_parallel: - type: integer - description: Number of parallel operations, relevant when continuous batching is - enabled. - minimum: 1 - maximum: 10 - default: 1 - example: 4 - cont_batching: - type: boolean - description: Indicates if continuous batching is used for processing. - default: false - example: false - cpu_threads: - type: integer - description: Number of threads allocated for CPU-based inference. - minimum: 1 - example: 8 - embedding: - type: boolean - description: Indicates if embedding layers are enabled in the model. - default: true - example: true - model_parameters: + example: https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf + id: + type: string + description: + Unique identifier used in chat-completions model_name, matches + folder name. + example: mistral-ins-7b-q4 + object: + type: string + example: model + name: + type: string + description: Name of the model. + example: Mistral Instruct 7B Q4 + version: + type: string + default: "1.0" + description: The version number of the model. + description: + type: string + description: Description of the model. + example: Trinity is an experimental model merge using the Slerp method. Recommended for daily assistance purposes. + format: + type: string + description: State format of the model, distinct from the engine. + example: gguf + settings: type: object properties: ctx_len: type: integer - description: Maximum context length the model can handle. - minimum: 0 - maximum: 4096 - default: 2048 - example: 2048 - ngl: - type: integer - description: Number of layers in the neural network. - minimum: 1 - maximum: 100 - default: 100 - example: 100 - embedding: - type: boolean - description: Indicates if embedding layers are used. - default: true - example: true - n_parallel: - type: integer - description: Number of parallel processes the model can run. - minimum: 1 - maximum: 10 - default: 1 - example: 4 + description: Context length. + example: 4096 + prompt_template: + type: string + example: "[INST] {prompt} [/INST]" + additionalProperties: false + parameters: + type: object + properties: temperature: - type: number - description: Controls randomness in model's responses. Higher values lead to - more random responses. - minimum: 0 - maximum: 2 - default: 0.7 example: 0.7 - token_limit: - type: integer - description: Maximum number of tokens the model can generate in a single - response. - minimum: 1 - maximum: 4096 - default: 2048 - example: 2048 - top_k: - type: integer - description: Limits the model to consider only the top k most likely next tokens - at each step. - minimum: 0 - maximum: 100 - default: 0 - example: 0 top_p: - type: number - description: Nucleus sampling parameter. The model considers the smallest set of - tokens whose cumulative probability exceeds the top_p value. - minimum: 0 - maximum: 1 - default: 1 - example: 1 + example: 0.95 + stream: + example: true + max_tokens: + example: 4096 + stop: + example: [] + frequency_penalty: + example: 0 + presence_penalty: + example: 0 + additionalProperties: false metadata: - type: object - properties: - engine: - type: string - description: The engine used by the model. - enum: - - nitro - - openai - - hf_inference - quantization: - type: string - description: Quantization parameter of the model. - example: Q3_K_L - size: - type: string - description: Size of the model. - example: 7B - required: - - id - - object - - created - - owned_by - - state - - source_url - - parameters - - metadata + author: + type: string + example: MistralAI + tags: + example: ["7B", "Featured", "Foundation Model"] + size: + example: 4370000000, + cover: + example: "https://raw.githubusercontent.com/janhq/jan/main/models/mistral-ins-7b-q4/cover.png" + engine: + example: nitro DeleteModelResponse: type: object properties: - id: - type: string - description: The identifier of the model that was deleted. - example: model-zephyr-7B - object: - type: string - description: Type of the object, indicating it's a model. - default: model - deleted: - type: boolean - description: Indicates whether the model was successfully deleted. - example: true + message: + example: Not found StartModelResponse: type: object properties: From ce68ccf178c776dbdcda68132b95539fad93c383 Mon Sep 17 00:00:00 2001 From: hieu-jan <150573299+hieu-jan@users.noreply.github.com> Date: Wed, 24 Jan 2024 01:47:49 +0900 Subject: [PATCH 08/63] docs: change 127.0.0.1 to localhost --- docs/openapi/jan.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/openapi/jan.yaml b/docs/openapi/jan.yaml index 9981f7308..90e6b9945 100644 --- a/docs/openapi/jan.yaml +++ b/docs/openapi/jan.yaml @@ -68,7 +68,7 @@ paths: - lang: cURL source: | curl -X 'POST' \ - 'http://127.0.0.1:1337/v1/chat/completions' \ + 'http://localhost:1337/v1/chat/completions' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ @@ -115,7 +115,7 @@ paths: - lang: cURL source: | curl -X 'GET' \ - 'http://127.0.0.1:1337/v1/models' \ + 'http://localhost:1337/v1/models' \ -H 'accept: application/json' "/models/download/{model_id}": get: @@ -145,7 +145,7 @@ paths: - lang: cURL source: | curl -X 'GET' \ - 'http://127.0.0.1:1337/v1/models/download/{model_id}' \ + 'http://localhost:1337/v1/models/download/{model_id}' \ -H 'accept: application/json' "/models/{model_id}": get: @@ -178,7 +178,7 @@ paths: - lang: cURL source: | curl -X 'GET' \ - 'http://127.0.0.1:1337/v1/models/{model_id}' \ + 'http://localhost:1337/v1/models/{model_id}' \ -H 'accept: application/json' delete: operationId: deleteModel @@ -209,7 +209,7 @@ paths: - lang: cURL source: | curl -X 'DELETE' \ - 'http://127.0.0.1:1337/v1/models/{model_id}' \ + 'http://localhost:1337/v1/models/{model_id}' \ -H 'accept: application/json' /threads: post: From b2d0add0060b29b61b1f3520a0a578d4c9c33471 Mon Sep 17 00:00:00 2001 From: hieu-jan <150573299+hieu-jan@users.noreply.github.com> Date: Wed, 24 Jan 2024 10:33:44 +0900 Subject: [PATCH 09/63] docs: add more details --- .../01-overview/04-install-and-prerequisites.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/docs/developer/01-overview/04-install-and-prerequisites.md b/docs/docs/developer/01-overview/04-install-and-prerequisites.md index 25fe2550e..6d9feeb14 100644 --- a/docs/docs/developer/01-overview/04-install-and-prerequisites.md +++ b/docs/docs/developer/01-overview/04-install-and-prerequisites.md @@ -20,12 +20,19 @@ keywords: ## Requirements +### Hardware Requirements + +Ensure your system meets the following specifications to guarantee a smooth development experience: + - [Hardware Requirements](../../guides/02-installation/06-hardware.md) -- System Requirements: - - [Windows](../../install/windows/#system-requirements) - - [MacOS](../../install/mac/#system-requirements) - - [Linux](../../install/linux/#system-requirements) +### System Requirements + +Make sure your operating system meets the specific requirements for Jan development: + +- [Windows](../../install/windows/#system-requirements) +- [MacOS](../../install/mac/#system-requirements) +- [Linux](../../install/linux/#system-requirements) ## Prerequisites From 89bec2a8a9d5703c065083c540306e7da2bb3831 Mon Sep 17 00:00:00 2001 From: hieu-jan <150573299+hieu-jan@users.noreply.github.com> Date: Wed, 24 Jan 2024 10:44:05 +0900 Subject: [PATCH 10/63] docs: fix typo --- docs/docs/developer/01-overview/04-install-and-prerequisites.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/developer/01-overview/04-install-and-prerequisites.md b/docs/docs/developer/01-overview/04-install-and-prerequisites.md index 6d9feeb14..110f62e36 100644 --- a/docs/docs/developer/01-overview/04-install-and-prerequisites.md +++ b/docs/docs/developer/01-overview/04-install-and-prerequisites.md @@ -50,7 +50,7 @@ cd jan git checkout -b DESIRED_BRANCH ``` -2. **Install Dependencie:s** +2. **Install Dependencies** ```bash yarn install From 45c08597fefadf35d76d187e05578b72a77288ea Mon Sep 17 00:00:00 2001 From: hieu-jan <150573299+hieu-jan@users.noreply.github.com> Date: Wed, 24 Jan 2024 10:52:48 +0900 Subject: [PATCH 11/63] docs: update DeleteModelResponse --- docs/openapi/specs/models.yaml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/openapi/specs/models.yaml b/docs/openapi/specs/models.yaml index 791d14880..776ffbd6d 100644 --- a/docs/openapi/specs/models.yaml +++ b/docs/openapi/specs/models.yaml @@ -190,8 +190,18 @@ components: DeleteModelResponse: type: object properties: - message: - example: Not found + id: + type: string + description: The identifier of the model that was deleted. + example: mistral-ins-7b-q4 + object: + type: string + description: Type of the object, indicating it's a model. + default: model + deleted: + type: boolean + description: Indicates whether the model was successfully deleted. + example: true StartModelResponse: type: object properties: From b4ffe006fed494f610c3248892c57ec711b6799f Mon Sep 17 00:00:00 2001 From: hieu-jan <150573299+hieu-jan@users.noreply.github.com> Date: Wed, 24 Jan 2024 10:55:24 +0900 Subject: [PATCH 12/63] docs: format .yaml --- docs/openapi/specs/assistants.yaml | 2 +- docs/openapi/specs/chat.yaml | 17 +++++++++++------ docs/openapi/specs/messages.yaml | 2 +- docs/openapi/specs/models.yaml | 2 +- docs/openapi/specs/threads.yaml | 2 +- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/docs/openapi/specs/assistants.yaml b/docs/openapi/specs/assistants.yaml index d784c315a..5db1f6a97 100644 --- a/docs/openapi/specs/assistants.yaml +++ b/docs/openapi/specs/assistants.yaml @@ -316,4 +316,4 @@ components: deleted: type: boolean description: Indicates whether the assistant was successfully deleted. - example: true \ No newline at end of file + example: true diff --git a/docs/openapi/specs/chat.yaml b/docs/openapi/specs/chat.yaml index b324501a8..3aeff380f 100644 --- a/docs/openapi/specs/chat.yaml +++ b/docs/openapi/specs/chat.yaml @@ -16,7 +16,8 @@ components: stream: type: boolean default: true - description: Enables continuous output generation, allowing for streaming of + description: + Enables continuous output generation, allowing for streaming of model responses. model: type: string @@ -25,23 +26,27 @@ components: max_tokens: type: number default: 2048 - description: The maximum number of tokens the model will generate in a single + description: + The maximum number of tokens the model will generate in a single response. stop: type: arrays example: - hello - description: Defines specific tokens or phrases at which the model will stop + description: + Defines specific tokens or phrases at which the model will stop generating further output/ frequency_penalty: type: number default: 0 - description: Adjusts the likelihood of the model repeating words or phrases in + description: + Adjusts the likelihood of the model repeating words or phrases in its output. presence_penalty: type: number default: 0 - description: Influences the generation of new and varied concepts in the model's + description: + Influences the generation of new and varied concepts in the model's output. temperature: type: number @@ -188,4 +193,4 @@ components: total_tokens: type: integer example: 533 - description: Total number of tokens used \ No newline at end of file + description: Total number of tokens used diff --git a/docs/openapi/specs/messages.yaml b/docs/openapi/specs/messages.yaml index d9d7d87a4..9a0799f6a 100644 --- a/docs/openapi/specs/messages.yaml +++ b/docs/openapi/specs/messages.yaml @@ -309,4 +309,4 @@ components: data: type: array items: - $ref: "#/components/schemas/MessageFileObject" \ No newline at end of file + $ref: "#/components/schemas/MessageFileObject" diff --git a/docs/openapi/specs/models.yaml b/docs/openapi/specs/models.yaml index 776ffbd6d..21276f899 100644 --- a/docs/openapi/specs/models.yaml +++ b/docs/openapi/specs/models.yaml @@ -96,7 +96,7 @@ components: type: string description: | The identifier of the model. - example: ztrinity-v1.2-7b + example: trinity-v1.2-7b object: type: string description: | diff --git a/docs/openapi/specs/threads.yaml b/docs/openapi/specs/threads.yaml index fe00f7588..825f166f1 100644 --- a/docs/openapi/specs/threads.yaml +++ b/docs/openapi/specs/threads.yaml @@ -224,4 +224,4 @@ components: deleted: type: boolean description: Indicates whether the thread was successfully deleted. - example: true \ No newline at end of file + example: true From 64cabe3d56c6759688cf71555a43b6d469d05818 Mon Sep 17 00:00:00 2001 From: hieu-jan <150573299+hieu-jan@users.noreply.github.com> Date: Wed, 24 Jan 2024 11:37:08 +0900 Subject: [PATCH 13/63] chore: lint .yaml --- docs/openapi/jan.yaml | 42 ++++++++++++++--------------- docs/openapi/specs/chat.yaml | 15 ++++------- docs/openapi/specs/messages.yaml | 45 ++++++++++++++++---------------- docs/openapi/specs/models.yaml | 22 +++++++++++----- docs/openapi/specs/threads.yaml | 2 +- 5 files changed, 66 insertions(+), 60 deletions(-) diff --git a/docs/openapi/jan.yaml b/docs/openapi/jan.yaml index 90e6b9945..864c80fdf 100644 --- a/docs/openapi/jan.yaml +++ b/docs/openapi/jan.yaml @@ -72,27 +72,27 @@ paths: -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ - "messages": [ - { - "content": "You are a helpful assistant.", - "role": "system" - }, - { - "content": "Hello!", - "role": "user" - } - ], - "model": "tinyllama-1.1b", - "stream": true, - "max_tokens": 2048, - "stop": [ - "hello" - ], - "frequency_penalty": 0, - "presence_penalty": 0, - "temperature": 0.7, - "top_p": 0.95 - }' + "messages": [ + { + "content": "You are a helpful assistant.", + "role": "system" + }, + { + "content": "Hello!", + "role": "user" + } + ], + "model": "tinyllama-1.1b", + "stream": true, + "max_tokens": 2048, + "stop": [ + "hello" + ], + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.7, + "top_p": 0.95 + }' /models: get: operationId: listModels diff --git a/docs/openapi/specs/chat.yaml b/docs/openapi/specs/chat.yaml index 3aeff380f..cfa391598 100644 --- a/docs/openapi/specs/chat.yaml +++ b/docs/openapi/specs/chat.yaml @@ -16,8 +16,7 @@ components: stream: type: boolean default: true - description: - Enables continuous output generation, allowing for streaming of + description: Enables continuous output generation, allowing for streaming of model responses. model: type: string @@ -26,27 +25,23 @@ components: max_tokens: type: number default: 2048 - description: - The maximum number of tokens the model will generate in a single + description: The maximum number of tokens the model will generate in a single response. stop: type: arrays example: - hello - description: - Defines specific tokens or phrases at which the model will stop + description: Defines specific tokens or phrases at which the model will stop generating further output/ frequency_penalty: type: number default: 0 - description: - Adjusts the likelihood of the model repeating words or phrases in + description: Adjusts the likelihood of the model repeating words or phrases in its output. presence_penalty: type: number default: 0 - description: - Influences the generation of new and varied concepts in the model's + description: Influences the generation of new and varied concepts in the model's output. temperature: type: number diff --git a/docs/openapi/specs/messages.yaml b/docs/openapi/specs/messages.yaml index 9a0799f6a..6f5fe1a58 100644 --- a/docs/openapi/specs/messages.yaml +++ b/docs/openapi/specs/messages.yaml @@ -1,3 +1,4 @@ +--- components: schemas: MessageObject: @@ -75,7 +76,7 @@ components: example: msg_abc123 object: type: string - description: "Type of the object, indicating it's a thread message." + description: Type of the object, indicating it's a thread message. default: thread.message created_at: type: integer @@ -88,7 +89,7 @@ components: example: thread_abc123 role: type: string - description: "Role of the sender, either 'user' or 'assistant'." + description: Role of the sender, either 'user' or 'assistant'. example: user content: type: array @@ -97,7 +98,7 @@ components: properties: type: type: string - description: "Type of content, e.g., 'text'." + description: Type of content, e.g., 'text'. example: text text: type: object @@ -110,21 +111,21 @@ components: type: array items: type: string - description: "Annotations for the text content, if any." + description: Annotations for the text content, if any. example: [] file_ids: type: array items: type: string - description: "Array of file IDs associated with the message, if any." + description: Array of file IDs associated with the message, if any. example: [] assistant_id: type: string - description: "Identifier of the assistant involved in the message, if applicable." + description: Identifier of the assistant involved in the message, if applicable. example: null run_id: type: string - description: "Run ID associated with the message, if applicable." + description: Run ID associated with the message, if applicable. example: null metadata: type: object @@ -139,7 +140,7 @@ components: example: msg_abc123 object: type: string - description: "Type of the object, indicating it's a thread message." + description: Type of the object, indicating it's a thread message. example: thread.message created_at: type: integer @@ -152,7 +153,7 @@ components: example: thread_abc123 role: type: string - description: "Role of the sender, either 'user' or 'assistant'." + description: Role of the sender, either 'user' or 'assistant'. example: user content: type: array @@ -161,7 +162,7 @@ components: properties: type: type: string - description: "Type of content, e.g., 'text'." + description: Type of content, e.g., 'text'. example: text text: type: object @@ -174,21 +175,21 @@ components: type: array items: type: string - description: "Annotations for the text content, if any." + description: Annotations for the text content, if any. example: [] file_ids: type: array items: type: string - description: "Array of file IDs associated with the message, if any." + description: Array of file IDs associated with the message, if any. example: [] assistant_id: type: string - description: "Identifier of the assistant involved in the message, if applicable." + description: Identifier of the assistant involved in the message, if applicable. example: null run_id: type: string - description: "Run ID associated with the message, if applicable." + description: Run ID associated with the message, if applicable. example: null metadata: type: object @@ -199,7 +200,7 @@ components: properties: object: type: string - description: "Type of the object, indicating it's a list." + description: Type of the object, indicating it's a list. default: list data: type: array @@ -226,7 +227,7 @@ components: example: msg_abc123 object: type: string - description: "Type of the object, indicating it's a thread message." + description: Type of the object, indicating it's a thread message. example: thread.message created_at: type: integer @@ -239,7 +240,7 @@ components: example: thread_abc123 role: type: string - description: "Role of the sender, either 'user' or 'assistant'." + description: Role of the sender, either 'user' or 'assistant'. example: user content: type: array @@ -248,7 +249,7 @@ components: properties: type: type: string - description: "Type of content, e.g., 'text'." + description: Type of content, e.g., 'text'. text: type: object properties: @@ -260,20 +261,20 @@ components: type: array items: type: string - description: "Annotations for the text content, if any." + description: Annotations for the text content, if any. file_ids: type: array items: type: string - description: "Array of file IDs associated with the message, if any." + description: Array of file IDs associated with the message, if any. example: [] assistant_id: type: string - description: "Identifier of the assistant involved in the message, if applicable." + description: Identifier of the assistant involved in the message, if applicable. example: null run_id: type: string - description: "Run ID associated with the message, if applicable." + description: Run ID associated with the message, if applicable. example: null metadata: type: object diff --git a/docs/openapi/specs/models.yaml b/docs/openapi/specs/models.yaml index 21276f899..40e6abaaf 100644 --- a/docs/openapi/specs/models.yaml +++ b/docs/openapi/specs/models.yaml @@ -43,7 +43,9 @@ components: description: type: string description: Description of the model. - example: Trinity is an experimental model merge using the Slerp method. Recommended for daily assistance purposes. + example: + Trinity is an experimental model merge using the Slerp method. + Recommended for daily assistance purposes. format: type: string description: State format of the model, distinct from the engine. @@ -82,11 +84,14 @@ components: type: string example: Jan tags: - example: ["7B", "Merged", "Featured"] + example: + - 7B + - Merged + - Featured size: example: 4370000000, cover: - example: "https://raw.githubusercontent.com/janhq/jan/main/models/trinity-v1.2-7b/cover.png" + example: https://raw.githubusercontent.com/janhq/jan/main/models/trinity-v1.2-7b/cover.png engine: example: nitro ModelObject: @@ -141,7 +146,9 @@ components: description: type: string description: Description of the model. - example: Trinity is an experimental model merge using the Slerp method. Recommended for daily assistance purposes. + example: + Trinity is an experimental model merge using the Slerp method. + Recommended for daily assistance purposes. format: type: string description: State format of the model, distinct from the engine. @@ -180,11 +187,14 @@ components: type: string example: MistralAI tags: - example: ["7B", "Featured", "Foundation Model"] + example: + - 7B + - Featured + - Foundation Model size: example: 4370000000, cover: - example: "https://raw.githubusercontent.com/janhq/jan/main/models/mistral-ins-7b-q4/cover.png" + example: https://raw.githubusercontent.com/janhq/jan/main/models/mistral-ins-7b-q4/cover.png engine: example: nitro DeleteModelResponse: diff --git a/docs/openapi/specs/threads.yaml b/docs/openapi/specs/threads.yaml index 825f166f1..40b2463fa 100644 --- a/docs/openapi/specs/threads.yaml +++ b/docs/openapi/specs/threads.yaml @@ -142,7 +142,7 @@ components: example: Jan instructions: type: string - description: | + description: > The instruction of assistant, defaults to "Be my grammar corrector" model: type: object From 49670a84c3d0e96fccb7abe2016f1665972d1231 Mon Sep 17 00:00:00 2001 From: hiento09 <136591877+hiento09@users.noreply.github.com> Date: Thu, 25 Jan 2024 21:49:44 +0700 Subject: [PATCH 14/63] Update 06-unexpected-token.mdx --- docs/docs/guides/08-troubleshooting/06-unexpected-token.mdx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/docs/guides/08-troubleshooting/06-unexpected-token.mdx b/docs/docs/guides/08-troubleshooting/06-unexpected-token.mdx index 973001f1b..1de609ffa 100644 --- a/docs/docs/guides/08-troubleshooting/06-unexpected-token.mdx +++ b/docs/docs/guides/08-troubleshooting/06-unexpected-token.mdx @@ -17,4 +17,8 @@ keywords: ] --- -1. You may receive an error response `Error occurred: Unexpected token '<', " Date: Fri, 26 Jan 2024 01:09:24 +0900 Subject: [PATCH 15/63] docs: add additional reasons to somethings amiss --- docs/docs/guides/08-troubleshooting/02-somethings-amiss.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/docs/guides/08-troubleshooting/02-somethings-amiss.mdx b/docs/docs/guides/08-troubleshooting/02-somethings-amiss.mdx index a5669e36d..4e16e362a 100644 --- a/docs/docs/guides/08-troubleshooting/02-somethings-amiss.mdx +++ b/docs/docs/guides/08-troubleshooting/02-somethings-amiss.mdx @@ -45,7 +45,9 @@ This may occur due to several reasons. Please follow these steps to resolve it: 5. If you are on Nvidia GPUs, please download [Cuda](https://developer.nvidia.com/cuda-downloads). -6. When [checking app logs](https://jan.ai/troubleshooting/how-to-get-error-logs/), if you encounter the error log `Bind address failed at 127.0.0.1:3928`, it indicates that the port used by Nitro might already be in use. Use the following commands to check the port status: +6. If you're using Linux, please ensure that your system meets the following requirements gcc 11, g++ 11, cpp 11, or higher, refer to this [link](https://jan.ai/guides/troubleshooting/gpu-not-used/#specific-requirements-for-linux) for more information. + +7. When [checking app logs](https://jan.ai/troubleshooting/how-to-get-error-logs/), if you encounter the error log `Bind address failed at 127.0.0.1:3928`, it indicates that the port used by Nitro might already be in use. Use the following commands to check the port status: From d26e97f4a7b13dde738074d41757c1f0dd0be431 Mon Sep 17 00:00:00 2001 From: hieu-jan <150573299+hieu-jan@users.noreply.github.com> Date: Fri, 26 Jan 2024 01:33:45 +0900 Subject: [PATCH 16/63] docs: add OpenAI model list --- .../04-using-models/03-integrate-with-remote-server.mdx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/docs/guides/04-using-models/03-integrate-with-remote-server.mdx b/docs/docs/guides/04-using-models/03-integrate-with-remote-server.mdx index 533797fca..f0db1bd55 100644 --- a/docs/docs/guides/04-using-models/03-integrate-with-remote-server.mdx +++ b/docs/docs/guides/04-using-models/03-integrate-with-remote-server.mdx @@ -65,6 +65,13 @@ Navigate to the `~/jan/models` folder. Create a folder named `gpt-3.5-turbo-16k` } ``` +:::tip + +- You can find the list of available models in the [OpenAI Platform](https://platform.openai.com/docs/models/overview). +- Please note that the `id` property need to match the model name in the list. For example, if you want to use the [GPT-4 Turbo](https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo), you need to set the `id` property as `gpt-4-1106-preview`. + +::: + ### 2. Configure OpenAI API Keys You can find your API keys in the [OpenAI Platform](https://platform.openai.com/api-keys) and set the OpenAI API keys in `~/jan/engines/openai.json` file. From fd872c3de07a1d070d7e5abd07af144d4dc6cbed Mon Sep 17 00:00:00 2001 From: Service Account Date: Thu, 1 Feb 2024 18:56:52 +0000 Subject: [PATCH 17/63] janhq/jan: Update README.md with nightly build artifact URL --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 34eecc9f3..ad672969c 100644 --- a/README.md +++ b/README.md @@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Experimental (Nightly Build) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage From ae23127ddb5a25ce5e55772481fde44e72585d3e Mon Sep 17 00:00:00 2001 From: hieu-jan <150573299+hieu-jan@users.noreply.github.com> Date: Fri, 2 Feb 2024 07:53:01 +0900 Subject: [PATCH 18/63] docs: add trouble 07-undefined-issue --- .../08-troubleshooting/07-undefined-issue.mdx | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/docs/guides/08-troubleshooting/07-undefined-issue.mdx diff --git a/docs/docs/guides/08-troubleshooting/07-undefined-issue.mdx b/docs/docs/guides/08-troubleshooting/07-undefined-issue.mdx new file mode 100644 index 000000000..b45bfd86e --- /dev/null +++ b/docs/docs/guides/08-troubleshooting/07-undefined-issue.mdx @@ -0,0 +1,26 @@ +--- +title: Undefined Issue +slug: /troubleshooting/undefined-issue +description: Undefined issue troubleshooting guide. +keywords: + [ + Jan AI, + Jan, + ChatGPT alternative, + local AI, + private AI, + conversational AI, + no-subscription fee, + large language model, + troubleshooting, + undefined issue, + ] +--- + +You may encounter an "undefined" issue when using Jan. Here are some troubleshooting steps to help you resolve the issue. + +1. Try wiping the Jan folder and reopening the Jan app and see if the issue persists. +2. If the issue persists, try to go `~/jan/extensions/@janhq/inference-nitro-extensions/dist/bin//nitro` and run the nitro manually and see if you get any error messages. +3. Resolve the error messages you get from the nitro and see if the issue persists. +4. Reopen the Jan app and see if the issue is resolved. +5. If the issue persists, please contact us at [Jan Discord](https://discord.gg/mY69SZaMaC). From 8ff04f4db96340546809af7e027a4105abc043c0 Mon Sep 17 00:00:00 2001 From: hieu-jan <150573299+hieu-jan@users.noreply.github.com> Date: Fri, 2 Feb 2024 10:03:39 +0900 Subject: [PATCH 19/63] docs: add app log --- docs/docs/guides/08-troubleshooting/07-undefined-issue.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/guides/08-troubleshooting/07-undefined-issue.mdx b/docs/docs/guides/08-troubleshooting/07-undefined-issue.mdx index b45bfd86e..4aba6438d 100644 --- a/docs/docs/guides/08-troubleshooting/07-undefined-issue.mdx +++ b/docs/docs/guides/08-troubleshooting/07-undefined-issue.mdx @@ -23,4 +23,4 @@ You may encounter an "undefined" issue when using Jan. Here are some troubleshoo 2. If the issue persists, try to go `~/jan/extensions/@janhq/inference-nitro-extensions/dist/bin//nitro` and run the nitro manually and see if you get any error messages. 3. Resolve the error messages you get from the nitro and see if the issue persists. 4. Reopen the Jan app and see if the issue is resolved. -5. If the issue persists, please contact us at [Jan Discord](https://discord.gg/mY69SZaMaC). +5. If the issue persists, please share with us the [app logs](https://jan.ai/troubleshooting/how-to-get-error-logs/) via [Jan Discord](https://discord.gg/mY69SZaMaC). From 9e1c41b8978769903b8342d084e926901d38d2c9 Mon Sep 17 00:00:00 2001 From: NamH Date: Fri, 2 Feb 2024 10:45:10 +0700 Subject: [PATCH 20/63] fix: chat using web interface (#1889) Signed-off-by: James Co-authored-by: James --- .../rollup.config.ts | 3 ++ .../src/@types/global.d.ts | 1 + .../src/helpers/sse.ts | 3 +- .../inference-nitro-extension/src/index.ts | 44 ++++++++++++++----- web/containers/Toast/index.tsx | 16 +++---- 5 files changed, 47 insertions(+), 20 deletions(-) diff --git a/extensions/inference-nitro-extension/rollup.config.ts b/extensions/inference-nitro-extension/rollup.config.ts index 374a054cd..77a9fb208 100644 --- a/extensions/inference-nitro-extension/rollup.config.ts +++ b/extensions/inference-nitro-extension/rollup.config.ts @@ -27,6 +27,9 @@ export default [ TROUBLESHOOTING_URL: JSON.stringify( "https://jan.ai/guides/troubleshooting" ), + JAN_SERVER_INFERENCE_URL: JSON.stringify( + "http://localhost:1337/v1/chat/completions" + ), }), // Allow json resolution json(), diff --git a/extensions/inference-nitro-extension/src/@types/global.d.ts b/extensions/inference-nitro-extension/src/@types/global.d.ts index bc126337f..7a4fb4805 100644 --- a/extensions/inference-nitro-extension/src/@types/global.d.ts +++ b/extensions/inference-nitro-extension/src/@types/global.d.ts @@ -1,6 +1,7 @@ declare const NODE: string; declare const INFERENCE_URL: string; declare const TROUBLESHOOTING_URL: string; +declare const JAN_SERVER_INFERENCE_URL: string; /** * The response from the initModel function. diff --git a/extensions/inference-nitro-extension/src/helpers/sse.ts b/extensions/inference-nitro-extension/src/helpers/sse.ts index c6352383d..aab260828 100644 --- a/extensions/inference-nitro-extension/src/helpers/sse.ts +++ b/extensions/inference-nitro-extension/src/helpers/sse.ts @@ -6,6 +6,7 @@ import { Observable } from "rxjs"; * @returns An Observable that emits the generated response as a string. */ export function requestInference( + inferenceUrl: string, recentMessages: any[], model: Model, controller?: AbortController @@ -17,7 +18,7 @@ export function requestInference( stream: true, ...model.parameters, }); - fetch(INFERENCE_URL, { + fetch(inferenceUrl, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/extensions/inference-nitro-extension/src/index.ts b/extensions/inference-nitro-extension/src/index.ts index 9f1f00263..81a0031ac 100644 --- a/extensions/inference-nitro-extension/src/index.ts +++ b/extensions/inference-nitro-extension/src/index.ts @@ -68,35 +68,48 @@ export default class JanInferenceNitroExtension extends InferenceExtension { */ private nitroProcessInfo: any = undefined; + private inferenceUrl = ""; + /** * Subscribes to events emitted by the @janhq/core package. */ async onLoad() { if (!(await fs.existsSync(JanInferenceNitroExtension._homeDir))) { - await fs - .mkdirSync(JanInferenceNitroExtension._homeDir) - .catch((err: Error) => console.debug(err)); + try { + await fs.mkdirSync(JanInferenceNitroExtension._homeDir); + } catch (e) { + console.debug(e); + } } + // init inference url + // @ts-ignore + const electronApi = window?.electronAPI; + this.inferenceUrl = INFERENCE_URL; + if (!electronApi) { + this.inferenceUrl = JAN_SERVER_INFERENCE_URL; + } + console.debug("Inference url: ", this.inferenceUrl); + if (!(await fs.existsSync(JanInferenceNitroExtension._settingsDir))) await fs.mkdirSync(JanInferenceNitroExtension._settingsDir); this.writeDefaultEngineSettings(); // Events subscription events.on(MessageEvent.OnMessageSent, (data: MessageRequest) => - this.onMessageRequest(data), + this.onMessageRequest(data) ); events.on(ModelEvent.OnModelInit, (model: Model) => - this.onModelInit(model), + this.onModelInit(model) ); events.on(ModelEvent.OnModelStop, (model: Model) => - this.onModelStop(model), + this.onModelStop(model) ); events.on(InferenceEvent.OnInferenceStopped, () => - this.onInferenceStopped(), + this.onInferenceStopped() ); // Attempt to fetch nvidia info @@ -121,7 +134,7 @@ export default class JanInferenceNitroExtension extends InferenceExtension { } else { await fs.writeFileSync( engineFile, - JSON.stringify(this._engineSettings, null, 2), + JSON.stringify(this._engineSettings, null, 2) ); } } catch (err) { @@ -149,7 +162,7 @@ export default class JanInferenceNitroExtension extends InferenceExtension { this.getNitroProcesHealthIntervalId = setInterval( () => this.periodicallyGetNitroHealth(), - JanInferenceNitroExtension._intervalHealthCheck, + JanInferenceNitroExtension._intervalHealthCheck ); } @@ -206,7 +219,11 @@ export default class JanInferenceNitroExtension extends InferenceExtension { return new Promise(async (resolve, reject) => { if (!this._currentModel) return Promise.reject("No model loaded"); - requestInference(data.messages ?? [], this._currentModel).subscribe({ + requestInference( + this.inferenceUrl, + data.messages ?? [], + this._currentModel + ).subscribe({ next: (_content: any) => {}, complete: async () => { resolve(message); @@ -254,7 +271,12 @@ export default class JanInferenceNitroExtension extends InferenceExtension { ...(this._currentModel || {}), ...(data.model || {}), }; - requestInference(data.messages ?? [], model, this.controller).subscribe({ + requestInference( + this.inferenceUrl, + data.messages ?? [], + model, + this.controller + ).subscribe({ next: (content: any) => { const messageContent: ThreadContent = { type: ContentType.Text, diff --git a/web/containers/Toast/index.tsx b/web/containers/Toast/index.tsx index 7cffa89b9..eae340fee 100644 --- a/web/containers/Toast/index.tsx +++ b/web/containers/Toast/index.tsx @@ -19,8 +19,8 @@ const ErrorIcon = () => { xmlns="http://www.w3.org/2000/svg" > @@ -38,8 +38,8 @@ const WarningIcon = () => { xmlns="http://www.w3.org/2000/svg" > @@ -57,8 +57,8 @@ const SuccessIcon = () => { xmlns="http://www.w3.org/2000/svg" > @@ -76,8 +76,8 @@ const DefaultIcon = () => { xmlns="http://www.w3.org/2000/svg" > From ce63e1805e403d83f746e60912455e1f5c9b33e2 Mon Sep 17 00:00:00 2001 From: hieu-jan <150573299+hieu-jan@users.noreply.github.com> Date: Fri, 2 Feb 2024 15:03:18 +0900 Subject: [PATCH 21/63] docs: update what is tracked --- docs/docs/about/01-README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/docs/about/01-README.md b/docs/docs/about/01-README.md index 3b2759513..c69a35751 100644 --- a/docs/docs/about/01-README.md +++ b/docs/docs/about/01-README.md @@ -111,8 +111,9 @@ Adhering to Jan's privacy preserving philosophy, our analytics philosophy is to #### What is tracked 1. By default, Github tracks downloads and device metadata for all public Github repos. This helps us troubleshoot & ensure cross platform support. -1. We use Posthog to track a single `app.opened` event without additional user metadata, in order to understand retention. -1. Additionally, we plan to enable a `Settings` feature for users to turn off all tracking. +2. We use [Umami](https://umami.is/) to collect, analyze, and understand application data while maintaining visitor privacy and data ownership. We are using the Umami Cloud in Europe to ensure GDPR compliant. Please see [Umami Privacy Policy](https://umami.is/privacy) for more details. +3. We use Umami to track a single `app.opened` event without additional user metadata, in order to understand retention. In addition, we track `app.event` to understand app version usage. +4. Additionally, we plan to enable a `Settings` feature for users to turn off all tracking. #### Request for help From f7c591eca994d0719e0c94bfc0fffdfc7d494722 Mon Sep 17 00:00:00 2001 From: hieu-jan <150573299+hieu-jan@users.noreply.github.com> Date: Fri, 2 Feb 2024 15:08:39 +0900 Subject: [PATCH 22/63] docs: fix typo --- docs/docs/about/01-README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/about/01-README.md b/docs/docs/about/01-README.md index c69a35751..d5d3b8dc2 100644 --- a/docs/docs/about/01-README.md +++ b/docs/docs/about/01-README.md @@ -110,8 +110,8 @@ Adhering to Jan's privacy preserving philosophy, our analytics philosophy is to #### What is tracked -1. By default, Github tracks downloads and device metadata for all public Github repos. This helps us troubleshoot & ensure cross platform support. -2. We use [Umami](https://umami.is/) to collect, analyze, and understand application data while maintaining visitor privacy and data ownership. We are using the Umami Cloud in Europe to ensure GDPR compliant. Please see [Umami Privacy Policy](https://umami.is/privacy) for more details. +1. By default, Github tracks downloads and device metadata for all public GitHub repositories. This helps us troubleshoot & ensure cross-platform support. +2. We use [Umami](https://umami.is/) to collect, analyze, and understand application data while maintaining visitor privacy and data ownership. We are using the Umami Cloud in Europe to ensure GDPR compliance. Please see [Umami Privacy Policy](https://umami.is/privacy) for more details. 3. We use Umami to track a single `app.opened` event without additional user metadata, in order to understand retention. In addition, we track `app.event` to understand app version usage. 4. Additionally, we plan to enable a `Settings` feature for users to turn off all tracking. From 071c9286c42f5976214cd739aecd463fbc56b78c Mon Sep 17 00:00:00 2001 From: Service Account Date: Fri, 2 Feb 2024 09:12:13 +0000 Subject: [PATCH 23/63] janhq/jan: Update README.md with nightly build artifact URL --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ad672969c..ec35170f6 100644 --- a/README.md +++ b/README.md @@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Experimental (Nightly Build) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage From 81bbd838a77c7934ff63077544d475e32ea14868 Mon Sep 17 00:00:00 2001 From: Service Account Date: Sat, 3 Feb 2024 08:41:33 +0000 Subject: [PATCH 24/63] janhq/jan: Update README.md with nightly build artifact URL --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ec35170f6..783c37393 100644 --- a/README.md +++ b/README.md @@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Experimental (Nightly Build) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage From 135a7773082f2719b98896aa7d200fde86126c34 Mon Sep 17 00:00:00 2001 From: Service Account Date: Sat, 3 Feb 2024 10:05:06 +0000 Subject: [PATCH 25/63] janhq/jan: Update README.md with nightly build artifact URL --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 783c37393..6a7b280ec 100644 --- a/README.md +++ b/README.md @@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Experimental (Nightly Build) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage From 4472bb1b9b6d264fbc89a7c9206637e88047e6dd Mon Sep 17 00:00:00 2001 From: Service Account Date: Sun, 4 Feb 2024 04:29:58 +0000 Subject: [PATCH 26/63] janhq/jan: Update README.md with nightly build artifact URL --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6a7b280ec..8939ed598 100644 --- a/README.md +++ b/README.md @@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Experimental (Nightly Build) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage From 0b44649553070770ed4ccb43c0cf352ce3e91152 Mon Sep 17 00:00:00 2001 From: Service Account Date: Sun, 4 Feb 2024 06:41:07 +0000 Subject: [PATCH 27/63] janhq/jan: Update README.md with nightly build artifact URL --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8939ed598..16574ad41 100644 --- a/README.md +++ b/README.md @@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Experimental (Nightly Build) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage From 3fecdc3c332af1fe9da40a9828a85c24664125e4 Mon Sep 17 00:00:00 2001 From: Service Account Date: Sun, 4 Feb 2024 09:02:09 +0000 Subject: [PATCH 28/63] janhq/jan: Update README.md with nightly build artifact URL --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 16574ad41..02a5aa6e9 100644 --- a/README.md +++ b/README.md @@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Experimental (Nightly Build) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage From 480a1d9cc1e7b86e669f64eb9691902a9e394cc4 Mon Sep 17 00:00:00 2001 From: hiento09 <136591877+hiento09@users.noreply.github.com> Date: Sun, 4 Feb 2024 21:02:40 +0700 Subject: [PATCH 29/63] Cloudflare R2 clean every 10 days (#1917) Co-authored-by: Hien To --- .../workflows/clean-cloudflare-page-preview-url-and-r2.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/clean-cloudflare-page-preview-url-and-r2.yml b/.github/workflows/clean-cloudflare-page-preview-url-and-r2.yml index 620f74714..de761ca69 100644 --- a/.github/workflows/clean-cloudflare-page-preview-url-and-r2.yml +++ b/.github/workflows/clean-cloudflare-page-preview-url-and-r2.yml @@ -55,10 +55,10 @@ jobs: steps: - name: install-aws-cli-action uses: unfor19/install-aws-cli-action@v1 - - name: Delete object older than 7 days + - name: Delete object older than 10 days run: | # Get the list of objects in the 'latest' folder - OBJECTS=$(aws s3api list-objects --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --query 'Contents[?LastModified<`'$(date -d "$current_date -30 days" -u +"%Y-%m-%dT%H:%M:%SZ")'`].{Key: Key}' --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com | jq -c .) + OBJECTS=$(aws s3api list-objects --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --query 'Contents[?LastModified<`'$(date -d "$current_date -10 days" -u +"%Y-%m-%dT%H:%M:%SZ")'`].{Key: Key}' --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com | jq -c .) # Create a JSON file for the delete operation echo "{\"Objects\": $OBJECTS, \"Quiet\": false}" > delete.json From ccbe18e5b8417fb44dc8e130782c7da60c348ee8 Mon Sep 17 00:00:00 2001 From: Service Account Date: Mon, 5 Feb 2024 03:29:06 +0000 Subject: [PATCH 30/63] Update README.md with Stable Download URLs --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 02a5aa6e9..e1a06820e 100644 --- a/README.md +++ b/README.md @@ -43,31 +43,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Stable (Recommended) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage From 0b7e634855a7d7820860026a64bde0af2d841210 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Mon, 5 Feb 2024 11:04:50 +0700 Subject: [PATCH 31/63] fix: download model will close panel item hub --- .../ExploreModels/ExploreModelItemHeader/index.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx b/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx index 3ffe2cbac..378b9ffa8 100644 --- a/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx +++ b/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx @@ -66,7 +66,15 @@ const ExploreModelItemHeader: React.FC = ({ model, onClick, open }) => { const isDownloaded = downloadedModels.find((md) => md.id === model.id) != null let downloadButton = ( - + ) const onUseModelClick = useCallback(async () => { From 01fec497988f10b25f8f4ea18deb2d875eeb57a5 Mon Sep 17 00:00:00 2001 From: NamH Date: Mon, 5 Feb 2024 13:13:39 +0700 Subject: [PATCH 32/63] fix: reduce the number of api call (#1896) Signed-off-by: James Co-authored-by: James --- core/src/node/api/routes/common.ts | 11 +- core/src/node/api/routes/download.ts | 68 ++--- .../conversational-extension/src/index.ts | 43 ++-- web/containers/Layout/BottomBar/index.tsx | 6 +- .../CommandListDownloadedModel/index.tsx | 6 +- web/containers/Layout/TopBar/index.tsx | 11 +- web/containers/Providers/DataLoader.tsx | 21 ++ web/containers/Providers/EventHandler.tsx | 6 +- web/containers/Providers/EventListener.tsx | 10 +- web/containers/Providers/index.tsx | 6 +- web/helpers/atoms/Assistant.atom.ts | 4 + web/helpers/atoms/Model.atom.ts | 4 + web/hooks/useActiveModel.ts | 4 +- web/hooks/useAssistants.ts | 28 +++ web/hooks/useDeleteModel.ts | 7 +- web/hooks/useGetAssistants.ts | 27 -- web/hooks/useGetConfiguredModels.ts | 30 --- web/hooks/useGetDownloadedModels.ts | 27 -- web/hooks/useGetSystemResources.ts | 2 +- web/hooks/useModels.ts | 46 ++++ web/hooks/useRecommendedModel.ts | 15 +- web/hooks/useSetActiveThread.ts | 55 ++-- web/hooks/useThreads.ts | 26 +- web/screens/Chat/ChatBody/index.tsx | 6 +- web/screens/Chat/CleanThreadModal/index.tsx | 65 +++++ web/screens/Chat/DeleteThreadModal/index.tsx | 68 +++++ .../Chat/RequestDownloadModel/index.tsx | 7 +- web/screens/Chat/ThreadList/index.tsx | 234 ++++-------------- .../ExploreModelItemHeader/index.tsx | 8 +- .../ExploreModels/ModelVersionItem/index.tsx | 6 +- web/screens/ExploreModels/index.tsx | 26 +- web/screens/Settings/Models/index.tsx | 7 +- 32 files changed, 469 insertions(+), 421 deletions(-) create mode 100644 web/containers/Providers/DataLoader.tsx create mode 100644 web/helpers/atoms/Assistant.atom.ts create mode 100644 web/hooks/useAssistants.ts delete mode 100644 web/hooks/useGetAssistants.ts delete mode 100644 web/hooks/useGetConfiguredModels.ts delete mode 100644 web/hooks/useGetDownloadedModels.ts create mode 100644 web/hooks/useModels.ts create mode 100644 web/screens/Chat/CleanThreadModal/index.tsx create mode 100644 web/screens/Chat/DeleteThreadModal/index.tsx diff --git a/core/src/node/api/routes/common.ts b/core/src/node/api/routes/common.ts index 27385e561..8887755fe 100644 --- a/core/src/node/api/routes/common.ts +++ b/core/src/node/api/routes/common.ts @@ -12,6 +12,8 @@ import { import { JanApiRouteConfiguration } from '../common/configuration' import { startModel, stopModel } from '../common/startStopModel' import { ModelSettingParams } from '../../../types' +import { getJanDataFolderPath } from '../../utils' +import { normalizeFilePath } from '../../path' export const commonRouter = async (app: HttpServer) => { // Common Routes @@ -52,7 +54,14 @@ export const commonRouter = async (app: HttpServer) => { // App Routes app.post(`/app/${AppRoute.joinPath}`, async (request: any, reply: any) => { const args = JSON.parse(request.body) as any[] - reply.send(JSON.stringify(join(...args[0]))) + + const paths = args[0].map((arg: string) => + typeof arg === 'string' && (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`)) + ? join(getJanDataFolderPath(), normalizeFilePath(arg)) + : arg + ) + + reply.send(JSON.stringify(join(...paths))) }) app.post(`/app/${AppRoute.baseName}`, async (request: any, reply: any) => { diff --git a/core/src/node/api/routes/download.ts b/core/src/node/api/routes/download.ts index b4e11f957..ab8c0bd37 100644 --- a/core/src/node/api/routes/download.ts +++ b/core/src/node/api/routes/download.ts @@ -4,55 +4,55 @@ import { DownloadManager } from '../../download' import { HttpServer } from '../HttpServer' import { createWriteStream } from 'fs' import { getJanDataFolderPath } from '../../utils' -import { normalizeFilePath } from "../../path"; +import { normalizeFilePath } from '../../path' export const downloadRouter = async (app: HttpServer) => { app.post(`/${DownloadRoute.downloadFile}`, async (req, res) => { - const strictSSL = !(req.query.ignoreSSL === "true"); - const proxy = req.query.proxy?.startsWith("http") ? req.query.proxy : undefined; - const body = JSON.parse(req.body as any); + const strictSSL = !(req.query.ignoreSSL === 'true') + const proxy = req.query.proxy?.startsWith('http') ? req.query.proxy : undefined + const body = JSON.parse(req.body as any) const normalizedArgs = body.map((arg: any) => { - if (typeof arg === "string") { - return join(getJanDataFolderPath(), normalizeFilePath(arg)); + if (typeof arg === 'string' && arg.startsWith('file:')) { + return join(getJanDataFolderPath(), normalizeFilePath(arg)) } - return arg; - }); + return arg + }) - const localPath = normalizedArgs[1]; - const fileName = localPath.split("/").pop() ?? ""; + const localPath = normalizedArgs[1] + const fileName = localPath.split('/').pop() ?? '' - const request = require("request"); - const progress = require("request-progress"); + const request = require('request') + const progress = require('request-progress') - const rq = request({ url: normalizedArgs[0], strictSSL, proxy }); + const rq = request({ url: normalizedArgs[0], strictSSL, proxy }) progress(rq, {}) - .on("progress", function (state: any) { - console.log("download onProgress", state); + .on('progress', function (state: any) { + console.log('download onProgress', state) }) - .on("error", function (err: Error) { - console.log("download onError", err); + .on('error', function (err: Error) { + console.log('download onError', err) }) - .on("end", function () { - console.log("download onEnd"); + .on('end', function () { + console.log('download onEnd') }) - .pipe(createWriteStream(normalizedArgs[1])); + .pipe(createWriteStream(normalizedArgs[1])) - DownloadManager.instance.setRequest(fileName, rq); - }); + DownloadManager.instance.setRequest(fileName, rq) + }) app.post(`/${DownloadRoute.abortDownload}`, async (req, res) => { - const body = JSON.parse(req.body as any); + const body = JSON.parse(req.body as any) const normalizedArgs = body.map((arg: any) => { - if (typeof arg === "string") { - return join(getJanDataFolderPath(), normalizeFilePath(arg)); + if (typeof arg === 'string' && arg.startsWith('file:')) { + return join(getJanDataFolderPath(), normalizeFilePath(arg)) } - return arg; - }); + return arg + }) - const localPath = normalizedArgs[0]; - const fileName = localPath.split("/").pop() ?? ""; - const rq = DownloadManager.instance.networkRequests[fileName]; - DownloadManager.instance.networkRequests[fileName] = undefined; - rq?.abort(); - }); -}; + const localPath = normalizedArgs[0] + const fileName = localPath.split('/').pop() ?? '' + const rq = DownloadManager.instance.networkRequests[fileName] + DownloadManager.instance.networkRequests[fileName] = undefined + rq?.abort() + }) +} diff --git a/extensions/conversational-extension/src/index.ts b/extensions/conversational-extension/src/index.ts index 3d28a9c1d..bf8c213ad 100644 --- a/extensions/conversational-extension/src/index.ts +++ b/extensions/conversational-extension/src/index.ts @@ -12,7 +12,7 @@ import { * functionality for managing threads. */ export default class JSONConversationalExtension extends ConversationalExtension { - private static readonly _homeDir = 'file://threads' + private static readonly _threadFolder = 'file://threads' private static readonly _threadInfoFileName = 'thread.json' private static readonly _threadMessagesFileName = 'messages.jsonl' @@ -20,8 +20,8 @@ export default class JSONConversationalExtension extends ConversationalExtension * Called when the extension is loaded. */ async onLoad() { - if (!(await fs.existsSync(JSONConversationalExtension._homeDir))) - await fs.mkdirSync(JSONConversationalExtension._homeDir) + if (!(await fs.existsSync(JSONConversationalExtension._threadFolder))) + await fs.mkdirSync(JSONConversationalExtension._threadFolder) console.debug('JSONConversationalExtension loaded') } @@ -68,7 +68,7 @@ export default class JSONConversationalExtension extends ConversationalExtension async saveThread(thread: Thread): Promise { try { const threadDirPath = await joinPath([ - JSONConversationalExtension._homeDir, + JSONConversationalExtension._threadFolder, thread.id, ]) const threadJsonPath = await joinPath([ @@ -92,7 +92,7 @@ export default class JSONConversationalExtension extends ConversationalExtension */ async deleteThread(threadId: string): Promise { const path = await joinPath([ - JSONConversationalExtension._homeDir, + JSONConversationalExtension._threadFolder, `${threadId}`, ]) try { @@ -109,7 +109,7 @@ export default class JSONConversationalExtension extends ConversationalExtension async addNewMessage(message: ThreadMessage): Promise { try { const threadDirPath = await joinPath([ - JSONConversationalExtension._homeDir, + JSONConversationalExtension._threadFolder, message.thread_id, ]) const threadMessagePath = await joinPath([ @@ -177,7 +177,7 @@ export default class JSONConversationalExtension extends ConversationalExtension ): Promise { try { const threadDirPath = await joinPath([ - JSONConversationalExtension._homeDir, + JSONConversationalExtension._threadFolder, threadId, ]) const threadMessagePath = await joinPath([ @@ -205,7 +205,7 @@ export default class JSONConversationalExtension extends ConversationalExtension private async readThread(threadDirName: string): Promise { return fs.readFileSync( await joinPath([ - JSONConversationalExtension._homeDir, + JSONConversationalExtension._threadFolder, threadDirName, JSONConversationalExtension._threadInfoFileName, ]), @@ -219,14 +219,14 @@ export default class JSONConversationalExtension extends ConversationalExtension */ private async getValidThreadDirs(): Promise { const fileInsideThread: string[] = await fs.readdirSync( - JSONConversationalExtension._homeDir + JSONConversationalExtension._threadFolder ) const threadDirs: string[] = [] for (let i = 0; i < fileInsideThread.length; i++) { if (fileInsideThread[i].includes('.DS_Store')) continue const path = await joinPath([ - JSONConversationalExtension._homeDir, + JSONConversationalExtension._threadFolder, fileInsideThread[i], ]) @@ -246,7 +246,7 @@ export default class JSONConversationalExtension extends ConversationalExtension async getAllMessages(threadId: string): Promise { try { const threadDirPath = await joinPath([ - JSONConversationalExtension._homeDir, + JSONConversationalExtension._threadFolder, threadId, ]) @@ -263,22 +263,17 @@ export default class JSONConversationalExtension extends ConversationalExtension JSONConversationalExtension._threadMessagesFileName, ]) - const result = await fs - .readFileSync(messageFilePath, 'utf-8') - .then((content) => - content - .toString() - .split('\n') - .filter((line) => line !== '') - ) + let readResult = await fs.readFileSync(messageFilePath, 'utf-8') + + if (typeof readResult === 'object') { + readResult = JSON.stringify(readResult) + } + + const result = readResult.split('\n').filter((line) => line !== '') const messages: ThreadMessage[] = [] result.forEach((line: string) => { - try { - messages.push(JSON.parse(line) as ThreadMessage) - } catch (err) { - console.error(err) - } + messages.push(JSON.parse(line)) }) return messages } catch (err) { diff --git a/web/containers/Layout/BottomBar/index.tsx b/web/containers/Layout/BottomBar/index.tsx index 6e334b9ef..7dc5a9444 100644 --- a/web/containers/Layout/BottomBar/index.tsx +++ b/web/containers/Layout/BottomBar/index.tsx @@ -26,11 +26,12 @@ import { MainViewState } from '@/constants/screens' import { useActiveModel } from '@/hooks/useActiveModel' import { useDownloadState } from '@/hooks/useDownloadState' -import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' + import useGetSystemResources from '@/hooks/useGetSystemResources' import { useMainViewState } from '@/hooks/useMainViewState' import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom' +import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom' const menuLinks = [ { @@ -49,7 +50,8 @@ const BottomBar = () => { const { activeModel, stateModel } = useActiveModel() const { ram, cpu } = useGetSystemResources() const progress = useAtomValue(appDownloadProgress) - const { downloadedModels } = useGetDownloadedModels() + const downloadedModels = useAtomValue(downloadedModelsAtom) + const { setMainViewState } = useMainViewState() const { downloadStates } = useDownloadState() const setShowSelectModelModal = useSetAtom(showSelectModelModalAtom) diff --git a/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx b/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx index 3edce06eb..ac5756e9f 100644 --- a/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx +++ b/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx @@ -11,7 +11,7 @@ import { Badge, } from '@janhq/uikit' -import { useAtom } from 'jotai' +import { useAtom, useAtomValue } from 'jotai' import { DatabaseIcon, CpuIcon } from 'lucide-react' import { showSelectModelModalAtom } from '@/containers/Providers/KeyListener' @@ -19,14 +19,14 @@ import { showSelectModelModalAtom } from '@/containers/Providers/KeyListener' import { MainViewState } from '@/constants/screens' import { useActiveModel } from '@/hooks/useActiveModel' -import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' import { useMainViewState } from '@/hooks/useMainViewState' import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom' +import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom' export default function CommandListDownloadedModel() { const { setMainViewState } = useMainViewState() - const { downloadedModels } = useGetDownloadedModels() + const downloadedModels = useAtomValue(downloadedModelsAtom) const { activeModel, startModel, stopModel } = useActiveModel() const [serverEnabled] = useAtom(serverEnabledAtom) const [showSelectModelModal, setShowSelectModelModal] = useAtom( diff --git a/web/containers/Layout/TopBar/index.tsx b/web/containers/Layout/TopBar/index.tsx index f72f5f066..206a9013d 100644 --- a/web/containers/Layout/TopBar/index.tsx +++ b/web/containers/Layout/TopBar/index.tsx @@ -20,7 +20,6 @@ import { MainViewState } from '@/constants/screens' import { useClickOutside } from '@/hooks/useClickOutside' import { useCreateNewThread } from '@/hooks/useCreateNewThread' -import useGetAssistants, { getAssistants } from '@/hooks/useGetAssistants' import { useMainViewState } from '@/hooks/useMainViewState' import { usePath } from '@/hooks/usePath' @@ -29,13 +28,14 @@ import { showRightSideBarAtom } from '@/screens/Chat/Sidebar' import { openFileTitle } from '@/utils/titleUtils' +import { assistantsAtom } from '@/helpers/atoms/Assistant.atom' import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' const TopBar = () => { const activeThread = useAtomValue(activeThreadAtom) const { mainViewState } = useMainViewState() const { requestCreateNewThread } = useCreateNewThread() - const { assistants } = useGetAssistants() + const assistants = useAtomValue(assistantsAtom) const [showRightSideBar, setShowRightSideBar] = useAtom(showRightSideBarAtom) const [showLeftSideBar, setShowLeftSideBar] = useAtom(showLeftSideBarAtom) const showing = useAtomValue(showRightSideBarAtom) @@ -61,12 +61,7 @@ const TopBar = () => { const onCreateConversationClick = async () => { if (assistants.length === 0) { - const res = await getAssistants() - if (res.length === 0) { - alert('No assistant available') - return - } - requestCreateNewThread(res[0]) + alert('No assistant available') } else { requestCreateNewThread(assistants[0]) } diff --git a/web/containers/Providers/DataLoader.tsx b/web/containers/Providers/DataLoader.tsx new file mode 100644 index 000000000..2b6675d98 --- /dev/null +++ b/web/containers/Providers/DataLoader.tsx @@ -0,0 +1,21 @@ +'use client' + +import { Fragment, ReactNode } from 'react' + +import useAssistants from '@/hooks/useAssistants' +import useModels from '@/hooks/useModels' +import useThreads from '@/hooks/useThreads' + +type Props = { + children: ReactNode +} + +const DataLoader: React.FC = ({ children }) => { + useModels() + useThreads() + useAssistants() + + return {children} +} + +export default DataLoader diff --git a/web/containers/Providers/EventHandler.tsx b/web/containers/Providers/EventHandler.tsx index ec0fbfc90..e9d70d5d2 100644 --- a/web/containers/Providers/EventHandler.tsx +++ b/web/containers/Providers/EventHandler.tsx @@ -18,7 +18,6 @@ import { loadModelErrorAtom, stateModelAtom, } from '@/hooks/useActiveModel' -import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' import { queuedMessageAtom } from '@/hooks/useSendChatMessage' @@ -29,6 +28,7 @@ import { addNewMessageAtom, updateMessageAtom, } from '@/helpers/atoms/ChatMessage.atom' +import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom' import { updateThreadWaitingForResponseAtom, threadsAtom, @@ -38,7 +38,7 @@ import { export default function EventHandler({ children }: { children: ReactNode }) { const addNewMessage = useSetAtom(addNewMessageAtom) const updateMessage = useSetAtom(updateMessageAtom) - const { downloadedModels } = useGetDownloadedModels() + const downloadedModels = useAtomValue(downloadedModelsAtom) const setActiveModel = useSetAtom(activeModelAtom) const setStateModel = useSetAtom(stateModelAtom) const setQueuedMessage = useSetAtom(queuedMessageAtom) @@ -143,7 +143,7 @@ export default function EventHandler({ children }: { children: ReactNode }) { ?.addNewMessage(message) } }, - [updateMessage, updateThreadWaiting] + [updateMessage, updateThreadWaiting, setIsGeneratingResponse] ) useEffect(() => { diff --git a/web/containers/Providers/EventListener.tsx b/web/containers/Providers/EventListener.tsx index 62d4cacb6..5e8556f33 100644 --- a/web/containers/Providers/EventListener.tsx +++ b/web/containers/Providers/EventListener.tsx @@ -3,10 +3,9 @@ import { PropsWithChildren, useEffect, useRef } from 'react' import { baseName } from '@janhq/core' -import { useAtomValue, useSetAtom } from 'jotai' +import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { useDownloadState } from '@/hooks/useDownloadState' -import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' import { modelBinFileName } from '@/utils/model' @@ -14,14 +13,17 @@ import EventHandler from './EventHandler' import { appDownloadProgress } from './Jotai' -import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom' +import { + downloadedModelsAtom, + downloadingModelsAtom, +} from '@/helpers/atoms/Model.atom' export default function EventListenerWrapper({ children }: PropsWithChildren) { const setProgress = useSetAtom(appDownloadProgress) const models = useAtomValue(downloadingModelsAtom) const modelsRef = useRef(models) - const { setDownloadedModels, downloadedModels } = useGetDownloadedModels() + const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelsAtom) const { setDownloadState, setDownloadStateSuccess, diff --git a/web/containers/Providers/index.tsx b/web/containers/Providers/index.tsx index c8a20bca7..e7a179ec4 100644 --- a/web/containers/Providers/index.tsx +++ b/web/containers/Providers/index.tsx @@ -23,6 +23,8 @@ import Umami from '@/utils/umami' import Loader from '../Loader' +import DataLoader from './DataLoader' + import KeyListener from './KeyListener' import { extensionManager } from '@/extension' @@ -81,7 +83,9 @@ const Providers = (props: PropsWithChildren) => { - {children} + + {children} + {!isMac && } diff --git a/web/helpers/atoms/Assistant.atom.ts b/web/helpers/atoms/Assistant.atom.ts new file mode 100644 index 000000000..e90923d3d --- /dev/null +++ b/web/helpers/atoms/Assistant.atom.ts @@ -0,0 +1,4 @@ +import { Assistant } from '@janhq/core/.' +import { atom } from 'jotai' + +export const assistantsAtom = atom([]) diff --git a/web/helpers/atoms/Model.atom.ts b/web/helpers/atoms/Model.atom.ts index 6eb7f2ad6..5c9188ad7 100644 --- a/web/helpers/atoms/Model.atom.ts +++ b/web/helpers/atoms/Model.atom.ts @@ -24,3 +24,7 @@ export const removeDownloadingModelAtom = atom( ) } ) + +export const downloadedModelsAtom = atom([]) + +export const configuredModelsAtom = atom([]) diff --git a/web/hooks/useActiveModel.ts b/web/hooks/useActiveModel.ts index 54a1fdbe0..1b61a0dd1 100644 --- a/web/hooks/useActiveModel.ts +++ b/web/hooks/useActiveModel.ts @@ -3,9 +3,9 @@ import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai' import { toaster } from '@/containers/Toast' -import { useGetDownloadedModels } from './useGetDownloadedModels' import { LAST_USED_MODEL_ID } from './useRecommendedModel' +import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom' import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' export const activeModelAtom = atom(undefined) @@ -21,7 +21,7 @@ export function useActiveModel() { const [activeModel, setActiveModel] = useAtom(activeModelAtom) const activeThread = useAtomValue(activeThreadAtom) const [stateModel, setStateModel] = useAtom(stateModelAtom) - const { downloadedModels } = useGetDownloadedModels() + const downloadedModels = useAtomValue(downloadedModelsAtom) const setLoadModelError = useSetAtom(loadModelErrorAtom) const startModel = async (modelId: string) => { diff --git a/web/hooks/useAssistants.ts b/web/hooks/useAssistants.ts new file mode 100644 index 000000000..8f2c4a92c --- /dev/null +++ b/web/hooks/useAssistants.ts @@ -0,0 +1,28 @@ +import { useEffect } from 'react' + +import { Assistant, AssistantExtension, ExtensionTypeEnum } from '@janhq/core' + +import { useSetAtom } from 'jotai' + +import { extensionManager } from '@/extension' +import { assistantsAtom } from '@/helpers/atoms/Assistant.atom' + +const useAssistants = () => { + const setAssistants = useSetAtom(assistantsAtom) + + useEffect(() => { + const getAssistants = async () => { + const assistants = await getLocalAssistants() + setAssistants(assistants) + } + + getAssistants() + }, [setAssistants]) +} + +const getLocalAssistants = async (): Promise => + extensionManager + .get(ExtensionTypeEnum.Assistant) + ?.getAssistants() ?? [] + +export default useAssistants diff --git a/web/hooks/useDeleteModel.ts b/web/hooks/useDeleteModel.ts index fa0cfb45e..d9f2b94be 100644 --- a/web/hooks/useDeleteModel.ts +++ b/web/hooks/useDeleteModel.ts @@ -1,13 +1,14 @@ import { ExtensionTypeEnum, ModelExtension, Model } from '@janhq/core' +import { useAtom } from 'jotai' + import { toaster } from '@/containers/Toast' -import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' - import { extensionManager } from '@/extension/ExtensionManager' +import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom' export default function useDeleteModel() { - const { setDownloadedModels, downloadedModels } = useGetDownloadedModels() + const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelsAtom) const deleteModel = async (model: Model) => { await extensionManager diff --git a/web/hooks/useGetAssistants.ts b/web/hooks/useGetAssistants.ts deleted file mode 100644 index 2b34bfbd1..000000000 --- a/web/hooks/useGetAssistants.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect, useState } from 'react' - -import { Assistant, ExtensionTypeEnum, AssistantExtension } from '@janhq/core' - -import { extensionManager } from '@/extension/ExtensionManager' - -export const getAssistants = async (): Promise => - extensionManager - .get(ExtensionTypeEnum.Assistant) - ?.getAssistants() ?? [] - -/** - * Hooks for get assistants - * - * @returns assistants - */ -export default function useGetAssistants() { - const [assistants, setAssistants] = useState([]) - - useEffect(() => { - getAssistants() - .then((data) => setAssistants(data)) - .catch((err) => console.error(err)) - }, []) - - return { assistants } -} diff --git a/web/hooks/useGetConfiguredModels.ts b/web/hooks/useGetConfiguredModels.ts deleted file mode 100644 index 8be052ae2..000000000 --- a/web/hooks/useGetConfiguredModels.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useCallback, useEffect, useState } from 'react' - -import { ExtensionTypeEnum, ModelExtension, Model } from '@janhq/core' - -import { extensionManager } from '@/extension/ExtensionManager' - -export function useGetConfiguredModels() { - const [loading, setLoading] = useState(false) - const [models, setModels] = useState([]) - - const fetchModels = useCallback(async () => { - setLoading(true) - const models = await getConfiguredModels() - setLoading(false) - setModels(models) - }, []) - - useEffect(() => { - fetchModels() - }, [fetchModels]) - - return { loading, models } -} - -const getConfiguredModels = async (): Promise => { - const models = await extensionManager - .get(ExtensionTypeEnum.Model) - ?.getConfiguredModels() - return models ?? [] -} diff --git a/web/hooks/useGetDownloadedModels.ts b/web/hooks/useGetDownloadedModels.ts deleted file mode 100644 index bba420858..000000000 --- a/web/hooks/useGetDownloadedModels.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect } from 'react' - -import { ExtensionTypeEnum, ModelExtension, Model } from '@janhq/core' - -import { atom, useAtom } from 'jotai' - -import { extensionManager } from '@/extension/ExtensionManager' - -export const downloadedModelsAtom = atom([]) - -export function useGetDownloadedModels() { - const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelsAtom) - - useEffect(() => { - getDownloadedModels().then((downloadedModels) => { - setDownloadedModels(downloadedModels) - }) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - return { downloadedModels, setDownloadedModels } -} - -export const getDownloadedModels = async (): Promise => - extensionManager - .get(ExtensionTypeEnum.Model) - ?.getDownloadedModels() ?? [] diff --git a/web/hooks/useGetSystemResources.ts b/web/hooks/useGetSystemResources.ts index de595ad7b..3429a93aa 100644 --- a/web/hooks/useGetSystemResources.ts +++ b/web/hooks/useGetSystemResources.ts @@ -58,7 +58,7 @@ export default function useGetSystemResources() { // There is a possibility that this will be removed and replaced by the process event hook? const intervalId = setInterval(() => { getSystemResources() - }, 500) + }, 5000) // clean up interval return () => clearInterval(intervalId) diff --git a/web/hooks/useModels.ts b/web/hooks/useModels.ts new file mode 100644 index 000000000..23e098007 --- /dev/null +++ b/web/hooks/useModels.ts @@ -0,0 +1,46 @@ +import { useEffect } from 'react' + +import { ExtensionTypeEnum, Model, ModelExtension } from '@janhq/core' + +import { useSetAtom } from 'jotai' + +import { extensionManager } from '@/extension' +import { + configuredModelsAtom, + downloadedModelsAtom, +} from '@/helpers/atoms/Model.atom' + +const useModels = () => { + const setDownloadedModels = useSetAtom(downloadedModelsAtom) + const setConfiguredModels = useSetAtom(configuredModelsAtom) + + useEffect(() => { + const getDownloadedModels = async () => { + const models = await getLocalDownloadedModels() + setDownloadedModels(models) + } + + getDownloadedModels() + }, [setDownloadedModels]) + + useEffect(() => { + const getConfiguredModels = async () => { + const models = await getLocalConfiguredModels() + setConfiguredModels(models) + } + + getConfiguredModels() + }, [setConfiguredModels]) +} + +const getLocalConfiguredModels = async (): Promise => + extensionManager + .get(ExtensionTypeEnum.Model) + ?.getConfiguredModels() ?? [] + +const getLocalDownloadedModels = async (): Promise => + extensionManager + .get(ExtensionTypeEnum.Model) + ?.getDownloadedModels() ?? [] + +export default useModels diff --git a/web/hooks/useRecommendedModel.ts b/web/hooks/useRecommendedModel.ts index 427d2bf73..8122e2b77 100644 --- a/web/hooks/useRecommendedModel.ts +++ b/web/hooks/useRecommendedModel.ts @@ -5,9 +5,9 @@ import { Model, InferenceEngine } from '@janhq/core' import { atom, useAtomValue } from 'jotai' import { activeModelAtom } from './useActiveModel' -import { getDownloadedModels } from './useGetDownloadedModels' -import { activeThreadAtom, threadStatesAtom } from '@/helpers/atoms/Thread.atom' +import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom' +import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' export const lastUsedModel = atom(undefined) @@ -24,19 +24,20 @@ export const LAST_USED_MODEL_ID = 'last-used-model-id' */ export default function useRecommendedModel() { const activeModel = useAtomValue(activeModelAtom) - const [downloadedModels, setDownloadedModels] = useState([]) + const [sortedModels, setSortedModels] = useState([]) const [recommendedModel, setRecommendedModel] = useState() const activeThread = useAtomValue(activeThreadAtom) + const downloadedModels = useAtomValue(downloadedModelsAtom) const getAndSortDownloadedModels = useCallback(async (): Promise => { - const models = (await getDownloadedModels()).sort((a, b) => + const models = downloadedModels.sort((a, b) => a.engine !== InferenceEngine.nitro && b.engine === InferenceEngine.nitro ? 1 : -1 ) - setDownloadedModels(models) + setSortedModels(models) return models - }, []) + }, [downloadedModels]) const getRecommendedModel = useCallback(async (): Promise< Model | undefined @@ -98,5 +99,5 @@ export default function useRecommendedModel() { getRecommendedModel() }, [getRecommendedModel]) - return { recommendedModel, downloadedModels } + return { recommendedModel, downloadedModels: sortedModels } } diff --git a/web/hooks/useSetActiveThread.ts b/web/hooks/useSetActiveThread.ts index f5649ccaf..6cf94d45d 100644 --- a/web/hooks/useSetActiveThread.ts +++ b/web/hooks/useSetActiveThread.ts @@ -1,3 +1,5 @@ +import { useCallback } from 'react' + import { InferenceEvent, ExtensionTypeEnum, @@ -6,7 +8,7 @@ import { ConversationalExtension, } from '@janhq/core' -import { useAtomValue, useSetAtom } from 'jotai' +import { useSetAtom } from 'jotai' import { loadModelErrorAtom } from './useActiveModel' @@ -14,43 +16,46 @@ import { extensionManager } from '@/extension' import { setConvoMessagesAtom } from '@/helpers/atoms/ChatMessage.atom' import { ModelParams, - getActiveThreadIdAtom, isGeneratingResponseAtom, setActiveThreadIdAtom, setThreadModelParamsAtom, } from '@/helpers/atoms/Thread.atom' export default function useSetActiveThread() { - const activeThreadId = useAtomValue(getActiveThreadIdAtom) const setActiveThreadId = useSetAtom(setActiveThreadIdAtom) const setThreadMessage = useSetAtom(setConvoMessagesAtom) const setThreadModelParams = useSetAtom(setThreadModelParamsAtom) const setIsGeneratingResponse = useSetAtom(isGeneratingResponseAtom) const setLoadModelError = useSetAtom(loadModelErrorAtom) - const setActiveThread = async (thread: Thread) => { - if (activeThreadId === thread.id) { - console.debug('Thread already active') - return - } + const setActiveThread = useCallback( + async (thread: Thread) => { + setIsGeneratingResponse(false) + events.emit(InferenceEvent.OnInferenceStopped, thread.id) - setIsGeneratingResponse(false) - setLoadModelError(undefined) - events.emit(InferenceEvent.OnInferenceStopped, thread.id) + // load the corresponding messages + const messages = await getLocalThreadMessage(thread.id) + setThreadMessage(thread.id, messages) - // load the corresponding messages - const messages = await extensionManager - .get(ExtensionTypeEnum.Conversational) - ?.getAllMessages(thread.id) - setThreadMessage(thread.id, messages ?? []) + setActiveThreadId(thread.id) + const modelParams: ModelParams = { + ...thread.assistants[0]?.model?.parameters, + ...thread.assistants[0]?.model?.settings, + } + setThreadModelParams(thread.id, modelParams) + }, + [ + setActiveThreadId, + setThreadMessage, + setThreadModelParams, + setIsGeneratingResponse, + ] + ) - setActiveThreadId(thread.id) - const modelParams: ModelParams = { - ...thread.assistants[0]?.model?.parameters, - ...thread.assistants[0]?.model?.settings, - } - setThreadModelParams(thread.id, modelParams) - } - - return { activeThreadId, setActiveThread } + return { setActiveThread } } + +const getLocalThreadMessage = async (threadId: string) => + extensionManager + .get(ExtensionTypeEnum.Conversational) + ?.getAllMessages(threadId) ?? [] diff --git a/web/hooks/useThreads.ts b/web/hooks/useThreads.ts index b7de014cc..1ac038b26 100644 --- a/web/hooks/useThreads.ts +++ b/web/hooks/useThreads.ts @@ -1,3 +1,5 @@ +import { useEffect } from 'react' + import { ExtensionTypeEnum, Thread, @@ -5,14 +7,13 @@ import { ConversationalExtension, } from '@janhq/core' -import { useAtomValue, useSetAtom } from 'jotai' +import { useSetAtom } from 'jotai' import useSetActiveThread from './useSetActiveThread' import { extensionManager } from '@/extension/ExtensionManager' import { ModelParams, - activeThreadAtom, threadModelParamsAtom, threadStatesAtom, threadsAtom, @@ -22,11 +23,10 @@ const useThreads = () => { const setThreadStates = useSetAtom(threadStatesAtom) const setThreads = useSetAtom(threadsAtom) const setThreadModelRuntimeParams = useSetAtom(threadModelParamsAtom) - const activeThread = useAtomValue(activeThreadAtom) const { setActiveThread } = useSetActiveThread() - const getThreads = async () => { - try { + useEffect(() => { + const getThreads = async () => { const localThreads = await getLocalThreads() const localThreadStates: Record = {} const threadModelParams: Record = {} @@ -54,17 +54,19 @@ const useThreads = () => { setThreadStates(localThreadStates) setThreads(localThreads) setThreadModelRuntimeParams(threadModelParams) - if (localThreads.length && !activeThread) { + + if (localThreads.length > 0) { setActiveThread(localThreads[0]) } - } catch (error) { - console.error(error) } - } - return { - getThreads, - } + getThreads() + }, [ + setActiveThread, + setThreadModelRuntimeParams, + setThreadStates, + setThreads, + ]) } const getLocalThreads = async (): Promise => diff --git a/web/screens/Chat/ChatBody/index.tsx b/web/screens/Chat/ChatBody/index.tsx index 66f14d076..c67d6a538 100644 --- a/web/screens/Chat/ChatBody/index.tsx +++ b/web/screens/Chat/ChatBody/index.tsx @@ -11,7 +11,6 @@ import LogoMark from '@/containers/Brand/Logo/Mark' import { MainViewState } from '@/constants/screens' import { loadModelErrorAtom } from '@/hooks/useActiveModel' -import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' import { useMainViewState } from '@/hooks/useMainViewState' @@ -20,10 +19,13 @@ import ChatItem from '../ChatItem' import ErrorMessage from '../ErrorMessage' import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom' +import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom' const ChatBody: React.FC = () => { const messages = useAtomValue(getCurrentChatMessagesAtom) - const { downloadedModels } = useGetDownloadedModels() + + const downloadedModels = useAtomValue(downloadedModelsAtom) + const { setMainViewState } = useMainViewState() if (downloadedModels.length === 0) diff --git a/web/screens/Chat/CleanThreadModal/index.tsx b/web/screens/Chat/CleanThreadModal/index.tsx new file mode 100644 index 000000000..6ef505e6f --- /dev/null +++ b/web/screens/Chat/CleanThreadModal/index.tsx @@ -0,0 +1,65 @@ +import React, { useCallback } from 'react' + +import { + Button, + Modal, + ModalClose, + ModalContent, + ModalFooter, + ModalHeader, + ModalPortal, + ModalTitle, + ModalTrigger, +} from '@janhq/uikit' +import { Paintbrush } from 'lucide-react' + +import useDeleteThread from '@/hooks/useDeleteThread' + +type Props = { + threadId: string +} + +const CleanThreadModal: React.FC = ({ threadId }) => { + const { cleanThread } = useDeleteThread() + const onCleanThreadClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation() + cleanThread(threadId) + }, + [cleanThread, threadId] + ) + + return ( + + e.stopPropagation()}> +
+ + + Clean thread + +
+
+ + + + Clean Thread + +

Are you sure you want to clean this thread?

+ +
+ e.stopPropagation()}> + + + + + +
+
+
+
+ ) +} + +export default React.memo(CleanThreadModal) diff --git a/web/screens/Chat/DeleteThreadModal/index.tsx b/web/screens/Chat/DeleteThreadModal/index.tsx new file mode 100644 index 000000000..edbdb09b4 --- /dev/null +++ b/web/screens/Chat/DeleteThreadModal/index.tsx @@ -0,0 +1,68 @@ +import React, { useCallback } from 'react' + +import { + Modal, + ModalTrigger, + ModalPortal, + ModalContent, + ModalHeader, + ModalTitle, + ModalFooter, + ModalClose, + Button, +} from '@janhq/uikit' +import { Trash2Icon } from 'lucide-react' + +import useDeleteThread from '@/hooks/useDeleteThread' + +type Props = { + threadId: string +} + +const DeleteThreadModal: React.FC = ({ threadId }) => { + const { deleteThread } = useDeleteThread() + const onDeleteThreadClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation() + deleteThread(threadId) + }, + [deleteThread, threadId] + ) + + return ( + + e.stopPropagation()}> +
+ + + Delete thread + +
+
+ + + + Delete Thread + +

+ Are you sure you want to delete this thread? This action cannot be + undone. +

+ +
+ e.stopPropagation()}> + + + + + +
+
+
+
+ ) +} + +export default React.memo(DeleteThreadModal) diff --git a/web/screens/Chat/RequestDownloadModel/index.tsx b/web/screens/Chat/RequestDownloadModel/index.tsx index e62dc562d..88fdadd57 100644 --- a/web/screens/Chat/RequestDownloadModel/index.tsx +++ b/web/screens/Chat/RequestDownloadModel/index.tsx @@ -2,15 +2,18 @@ import React, { Fragment, useCallback } from 'react' 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 { downloadedModelsAtom } from '@/helpers/atoms/Model.atom' + const RequestDownloadModel: React.FC = () => { - const { downloadedModels } = useGetDownloadedModels() + const downloadedModels = useAtomValue(downloadedModelsAtom) const { setMainViewState } = useMainViewState() const onClick = useCallback(() => { diff --git a/web/screens/Chat/ThreadList/index.tsx b/web/screens/Chat/ThreadList/index.tsx index b4a045b1d..8f5bfb8f2 100644 --- a/web/screens/Chat/ThreadList/index.tsx +++ b/web/screens/Chat/ThreadList/index.tsx @@ -1,76 +1,39 @@ -import { useEffect, useState } from 'react' +import { useCallback } from 'react' -import { - Modal, - ModalTrigger, - ModalClose, - ModalFooter, - ModalPortal, - ModalContent, - ModalHeader, - ModalTitle, - Button, -} from '@janhq/uikit' +import { Thread } from '@janhq/core/' import { motion as m } from 'framer-motion' import { useAtomValue } from 'jotai' -import { - GalleryHorizontalEndIcon, - MoreVerticalIcon, - Trash2Icon, - Paintbrush, -} from 'lucide-react' +import { GalleryHorizontalEndIcon, MoreVerticalIcon } from 'lucide-react' import { twMerge } from 'tailwind-merge' -import { useCreateNewThread } from '@/hooks/useCreateNewThread' -import useDeleteThread from '@/hooks/useDeleteThread' - -import useGetAssistants from '@/hooks/useGetAssistants' -import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' import useSetActiveThread from '@/hooks/useSetActiveThread' -import useThreads from '@/hooks/useThreads' - import { displayDate } from '@/utils/datetime' +import CleanThreadModal from '../CleanThreadModal' + +import DeleteThreadModal from '../DeleteThreadModal' + import { - activeThreadAtom, + getActiveThreadIdAtom, threadStatesAtom, threadsAtom, } from '@/helpers/atoms/Thread.atom' export default function ThreadList() { - const threads = useAtomValue(threadsAtom) const threadStates = useAtomValue(threadStatesAtom) - const { getThreads } = useThreads() - const { assistants } = useGetAssistants() - const { requestCreateNewThread } = useCreateNewThread() - const activeThread = useAtomValue(activeThreadAtom) - const { deleteThread, cleanThread } = useDeleteThread() - const { downloadedModels } = useGetDownloadedModels() - const [isThreadsReady, setIsThreadsReady] = useState(false) + const threads = useAtomValue(threadsAtom) + const activeThreadId = useAtomValue(getActiveThreadIdAtom) + const { setActiveThread } = useSetActiveThread() - const { activeThreadId, setActiveThread: onThreadClick } = - useSetActiveThread() - - useEffect(() => { - getThreads().then(() => setIsThreadsReady(true)) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - useEffect(() => { - if ( - isThreadsReady && - downloadedModels.length !== 0 && - threads.length === 0 && - assistants.length !== 0 && - !activeThread - ) { - requestCreateNewThread(assistants[0]) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [assistants, threads, downloadedModels, activeThread, isThreadsReady]) + const onThreadClick = useCallback( + (thread: Thread) => { + setActiveThread(thread) + }, + [setActiveThread] + ) return (
@@ -83,133 +46,44 @@ export default function ThreadList() {

No Thread History

) : ( - threads.map((thread, i) => { - const lastMessage = - threadStates[thread.id]?.lastMessage ?? 'No new message' - return ( -
{ - onThreadClick(thread) - }} - > -
-

- {thread.updated && displayDate(thread.updated)} -

-

{thread.title}

-

- {lastMessage || 'No new message'} -

-
-
- -
- - e.stopPropagation()}> -
- - - Clean thread - -
-
- - - - Clean Thread - -

Are you sure you want to clean this thread?

- -
- e.stopPropagation()} - > - - - - - -
-
-
-
- - e.stopPropagation()}> -
- - - Delete thread - -
-
- - - - Delete Thread - -

- Are you sure you want to delete this thread? This action - cannot be undone. -

- -
- e.stopPropagation()} - > - - - - - -
-
-
-
-
-
- {activeThreadId === thread.id && ( - - )} + threads.map((thread) => ( +
{ + onThreadClick(thread) + }} + > +
+

+ {thread.updated && displayDate(thread.updated)} +

+

{thread.title}

+

+ {threadStates[thread.id]?.lastMessage ?? 'No new message'} +

- ) - }) +
+ +
+ + +
+
+ {activeThreadId === thread.id && ( + + )} +
+ )) )}
) diff --git a/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx b/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx index 3ffe2cbac..755494ee3 100644 --- a/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx +++ b/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx @@ -27,14 +27,14 @@ import useDownloadModel from '@/hooks/useDownloadModel' import { useDownloadState } from '@/hooks/useDownloadState' -import { getAssistants } from '@/hooks/useGetAssistants' -import { downloadedModelsAtom } from '@/hooks/useGetDownloadedModels' import { useMainViewState } from '@/hooks/useMainViewState' import { toGibibytes } from '@/utils/converter' +import { assistantsAtom } from '@/helpers/atoms/Assistant.atom' import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom' +import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom' import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom' type Props = { @@ -49,7 +49,9 @@ const ExploreModelItemHeader: React.FC = ({ model, onClick, open }) => { const { modelDownloadStateAtom } = useDownloadState() const { requestCreateNewThread } = useCreateNewThread() const totalRam = useAtomValue(totalRamAtom) + const serverEnabled = useAtomValue(serverEnabledAtom) + const assistants = useAtomValue(assistantsAtom) const downloadAtom = useMemo( () => atom((get) => get(modelDownloadStateAtom)[model.id]), @@ -60,7 +62,6 @@ const ExploreModelItemHeader: React.FC = ({ model, onClick, open }) => { const onDownloadClick = useCallback(() => { downloadModel(model) - // eslint-disable-next-line react-hooks/exhaustive-deps }, [model]) const isDownloaded = downloadedModels.find((md) => md.id === model.id) != null @@ -70,7 +71,6 @@ const ExploreModelItemHeader: React.FC = ({ model, onClick, open }) => { ) const onUseModelClick = useCallback(async () => { - const assistants = await getAssistants() if (assistants.length === 0) { alert('No assistant available') return diff --git a/web/screens/ExploreModels/ModelVersionItem/index.tsx b/web/screens/ExploreModels/ModelVersionItem/index.tsx index 50d71b161..3a9385670 100644 --- a/web/screens/ExploreModels/ModelVersionItem/index.tsx +++ b/web/screens/ExploreModels/ModelVersionItem/index.tsx @@ -10,9 +10,11 @@ import { MainViewState } from '@/constants/screens' import useDownloadModel from '@/hooks/useDownloadModel' import { useDownloadState } from '@/hooks/useDownloadState' -import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' + import { useMainViewState } from '@/hooks/useMainViewState' +import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom' + type Props = { model: Model isRecommended: boolean @@ -20,7 +22,7 @@ type Props = { const ModelVersionItem: React.FC = ({ model }) => { const { downloadModel } = useDownloadModel() - const { downloadedModels } = useGetDownloadedModels() + const downloadedModels = useAtomValue(downloadedModelsAtom) const { setMainViewState } = useMainViewState() const isDownloaded = downloadedModels.find( diff --git a/web/screens/ExploreModels/index.tsx b/web/screens/ExploreModels/index.tsx index 398b2db08..7002c60b7 100644 --- a/web/screens/ExploreModels/index.tsx +++ b/web/screens/ExploreModels/index.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useCallback, useState } from 'react' import { openExternalUrl } from '@janhq/core' import { @@ -12,24 +12,24 @@ import { SelectItem, } from '@janhq/uikit' +import { useAtomValue } from 'jotai' import { SearchIcon } from 'lucide-react' -import Loader from '@/containers/Loader' - -import { useGetConfiguredModels } from '@/hooks/useGetConfiguredModels' - -import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' - import ExploreModelList from './ExploreModelList' +import { + configuredModelsAtom, + downloadedModelsAtom, +} from '@/helpers/atoms/Model.atom' + const ExploreModelsScreen = () => { - const { loading, models } = useGetConfiguredModels() + const configuredModels = useAtomValue(configuredModelsAtom) + const downloadedModels = useAtomValue(downloadedModelsAtom) const [searchValue, setsearchValue] = useState('') - const { downloadedModels } = useGetDownloadedModels() const [sortSelected, setSortSelected] = useState('All Models') const sortMenu = ['All Models', 'Recommended', 'Downloaded'] - const filteredModels = models.filter((x) => { + const filteredModels = configuredModels.filter((x) => { if (sortSelected === 'Downloaded') { return ( x.name.toLowerCase().includes(searchValue.toLowerCase()) && @@ -45,11 +45,9 @@ const ExploreModelsScreen = () => { } }) - const onHowToImportModelClick = () => { + const onHowToImportModelClick = useCallback(() => { openExternalUrl('https://jan.ai/guides/using-models/import-manually/') - } - - if (loading) return + }, []) return (
{ From d5830b3fbffab8f6b017f12ea2b4c2442d216659 Mon Sep 17 00:00:00 2001 From: Van Pham <64197333+Van-QA@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:08:14 +0700 Subject: [PATCH 33/63] adding new feature for v0.4.6 (#1927) Move Jan Data App: https://github.com/janhq/jan/issues/1010 Factory Settings: https://github.com/janhq/jan/issues/1620 --- docs/docs/template/QA_script.md | 59 +++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/docs/docs/template/QA_script.md b/docs/docs/template/QA_script.md index 05dbed2b4..bba667bcd 100644 --- a/docs/docs/template/QA_script.md +++ b/docs/docs/template/QA_script.md @@ -1,6 +1,6 @@ # [Release Version] QA Script -**Release Version:** +**Release Version:** v0.4.6 **Operating System:** @@ -25,10 +25,10 @@ ### 3. Users uninstall app -- [ ] :key: Check that the uninstallation process removes all components of the app from the system. +- [ ] :key::warning: Check that the uninstallation process removes the app successfully from the system. - [ ] Clean the Jan root directory and open the app to check if it creates all the necessary folders, especially models and extensions. - [ ] When updating the app, check if the `/models` directory has any JSON files that change according to the update. -- [ ] Verify if updating the app also updates extensions correctly (test functionality changes; support notifications for necessary tests with each version related to extensions update). +- [ ] Verify if updating the app also updates extensions correctly (test functionality changes, support notifications for necessary tests with each version related to extensions update). ### 4. Users close app @@ -60,49 +60,45 @@ - [ ] :key: Ensure that the conversation thread is maintained without any loss of data upon sending multiple messages. - [ ] Test for the ability to send different types of messages (e.g., text, emojis, code blocks). - [ ] :key: Validate the scroll functionality in the chat window for lengthy conversations. -- [ ] Check if the user can renew responses multiple times. - [ ] Check if the user can copy the response. - [ ] Check if the user can delete responses. -- [ ] :warning: Test if the user deletes the message midway, then the assistant stops that response. - [ ] :key: Check the `clear message` button works. - [ ] :key: Check the `delete entire chat` works. -- [ ] :warning: Check if deleting all the chat retains the system prompt. +- [ ] Check if deleting all the chat retains the system prompt. - [ ] Check the output format of the AI (code blocks, JSON, markdown, ...). - [ ] :key: Validate that there is appropriate error handling and messaging if the assistant fails to respond. - [ ] Test assistant's ability to maintain context over multiple exchanges. - [ ] :key: Check the `create new chat` button works correctly - [ ] Confirm that by changing `models` mid-thread the app can still handle it. -- [ ] Check that by changing `instructions` mid-thread the app can still handle it. -- [ ] Check the `regenerate` button renews the response. -- [ ] Check the `Instructions` update correctly after the user updates it midway. +- [ ] Check the `regenerate` button renews the response (single / multiple times). +- [ ] Check the `Instructions` update correctly after the user updates it midway (mid-thread). ### 2. Users can customize chat settings like model parameters via both the GUI & thread.json -- [ ] :key: Confirm that the chat settings options are accessible via the GUI. +- [ ] :key: Confirm that the Threads settings options are accessible. - [ ] Test the functionality to adjust model parameters (e.g., Temperature, Top K, Top P) from the GUI and verify they are reflected in the chat behavior. - [ ] :key: Ensure that changes can be saved and persisted between sessions. - [ ] Validate that users can access and modify the thread.json file. - [ ] :key: Check that changes made in thread.json are correctly applied to the chat session upon reload or restart. -- [ ] Verify if there is a revert option to go back to previous settings after changes are made. -- [ ] Test for user feedback or confirmation after saving changes to settings. - [ ] Check the maximum and minimum limits of the adjustable parameters and how they affect the assistant's responses. - [ ] :key: Validate user permissions for those who can change settings and persist them. - [ ] :key: Ensure that users switch between threads with different models, the app can handle it. -### 3. Users can click on a history thread +### 3. Model dropdown +- [ ] :key: Model list should highlight recommended based on user RAM +- [ ] Model size should display (for both installed and imported models) +### 4. Users can click on a history thread - [ ] Test the ability to click on any thread in the history panel. - [ ] :key: Verify that clicking a thread brings up the past conversation in the main chat window. - [ ] :key: Ensure that the selected thread is highlighted or otherwise indicated in the history panel. - [ ] Confirm that the chat window displays the entire conversation from the selected history thread without any missing messages. - [ ] :key: Check the performance and accuracy of the history feature when dealing with a large number of threads. - [ ] Validate that historical threads reflect the exact state of the chat at that time, including settings. -- [ ] :key: :warning: Test the search functionality within the history panel for quick navigation. - [ ] :key: Verify the ability to delete or clean old threads. - [ ] :key: Confirm that changing the title of the thread updates correctly. -### 4. Users can config instructions for the assistant. - +### 5. Users can config instructions for the assistant. - [ ] Ensure there is a clear interface to input or change instructions for the assistant. - [ ] Test if the instructions set by the user are being followed by the assistant in subsequent conversations. - [ ] :key: Validate that changes to instructions are updated in real time and do not require a restart of the application or session. @@ -112,6 +108,8 @@ - [ ] Validate that instructions can be saved with descriptive names for easy retrieval. - [ ] :key: Check if the assistant can handle conflicting instructions and how it resolves them. - [ ] Ensure that instruction configurations are documented for user reference. +- [ ] :key: RAG - Users can import documents and the system should process queries about the uploaded file, providing accurate and appropriate responses in the conversation thread. + ## D. Hub @@ -125,8 +123,7 @@ - [ ] Display the best model for their RAM at the top. - [ ] :key: Ensure that models are labeled with RAM requirements and compatibility. -- [ ] :key: Validate that the download function is disabled for models that exceed the user's system capabilities. -- [ ] Test that the platform provides alternative recommendations for models not suitable due to RAM limitations. +- [ ] :warning: Test that the platform provides alternative recommendations for models not suitable due to RAM limitations. - [ ] :key: Check the download model functionality and validate if the cancel download feature works correctly. ### 3. Users can download models via a HuggingFace URL (coming soon) @@ -139,7 +136,7 @@ - [ ] :key: Have clear instructions so users can do their own. - [ ] :key: Ensure the new model updates after restarting the app. -- [ ] Ensure it raises clear errors for users to fix the problem while adding a new model. +- [ ] :warning:Ensure it raises clear errors for users to fix the problem while adding a new model. ### 5. Users can use the model as they want @@ -149,9 +146,13 @@ - [ ] Check if starting another model stops the other model entirely. - [ ] Check the `Explore models` navigate correctly to the model panel. - [ ] :key: Check when deleting a model it will delete all the files on the user's computer. -- [ ] The recommended tags should present right for the user's hardware. +- [ ] :warning:The recommended tags should present right for the user's hardware. - [ ] Assess that the descriptions of models are accurate and informative. +### 6. Users can Integrate With a Remote Server +- [ ] :key: Import openAI GPT model https://jan.ai/guides/using-models/integrate-with-remote-server/ and the model displayed in Hub / Thread dropdown +- [ ] Users can use the remote model properly + ## E. System Monitor ### 1. Users can see disk and RAM utilization @@ -181,7 +182,7 @@ - [ ] Confirm that the application saves the theme preference and persists it across sessions. - [ ] Validate that all elements of the UI are compatible with the theme changes and maintain legibility and contrast. -### 2. Users change the extensions +### 2. Users change the extensions [TBU] - [ ] Confirm that the `Extensions` tab lists all available plugins. - [ ] :key: Test the toggle switch for each plugin to ensure it enables or disables the plugin correctly. @@ -208,3 +209,19 @@ - [ ] :key: Test that the application prevents the installation of incompatible or corrupt plugin files. - [ ] :key: Check that the user can uninstall or disable custom plugins as easily as pre-installed ones. - [ ] Verify that the application's performance remains stable after the installation of custom plugins. + +### 5. Advanced Settings +- [ ] Attemp to test downloading model from hub using **HTTP Proxy** [guideline](https://github.com/janhq/jan/pull/1562) +- [ ] Users can move **Jan data folder** +- [ ] Users can click on Reset button to **factory reset** app settings to its original state & delete all usage data. + +## G. Local API server + +### 1. Local Server Usage with Server Options +- [ ] :key: Explore API Reference: Swagger API for sending/receiving requests + - [ ] Use default server option + - [ ] Configure and use custom server options +- [ ] Test starting/stopping the local API server with different Model/Model settings +- [ ] Server logs captured with correct Server Options provided +- [ ] Verify functionality of Open logs/Clear feature +- [ ] Ensure that threads and other functions impacting the model are disabled while the local server is running From 0d152a25f08d8defb1d2d8b2058330ccfbeacfe9 Mon Sep 17 00:00:00 2001 From: Service Account Date: Mon, 5 Feb 2024 09:16:42 +0000 Subject: [PATCH 34/63] janhq/jan: Update README.md with nightly build artifact URL --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e1a06820e..007d81cc5 100644 --- a/README.md +++ b/README.md @@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Experimental (Nightly Build) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage From 61419e5c0205618b5b4774aa3ba03b9802b8461e Mon Sep 17 00:00:00 2001 From: Service Account Date: Mon, 5 Feb 2024 20:20:35 +0000 Subject: [PATCH 35/63] janhq/jan: Update README.md with nightly build artifact URL --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 007d81cc5..b84e03e7f 100644 --- a/README.md +++ b/README.md @@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Experimental (Nightly Build) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage From 29a7fb8c93cffb32c2f89b0775f408ec3fe64789 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Tue, 6 Feb 2024 16:00:29 +0700 Subject: [PATCH 36/63] fix: avoid users to create so many threads at the same time (#1930) * fix: avoid allow users to create so many threads at the same time * fix missing last message * remove console * update last message metadata thread * update conditional statement --- web/containers/Providers/EventHandler.tsx | 8 ++++++++ web/hooks/useCreateNewThread.ts | 12 +++++++++++- web/screens/Chat/ThreadList/index.tsx | 4 +++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/web/containers/Providers/EventHandler.tsx b/web/containers/Providers/EventHandler.tsx index e9d70d5d2..f22ed1bc7 100644 --- a/web/containers/Providers/EventHandler.tsx +++ b/web/containers/Providers/EventHandler.tsx @@ -33,6 +33,7 @@ import { updateThreadWaitingForResponseAtom, threadsAtom, isGeneratingResponseAtom, + updateThreadAtom, } from '@/helpers/atoms/Thread.atom' export default function EventHandler({ children }: { children: ReactNode }) { @@ -49,6 +50,7 @@ export default function EventHandler({ children }: { children: ReactNode }) { const modelsRef = useRef(downloadedModels) const threadsRef = useRef(threads) const setIsGeneratingResponse = useSetAtom(isGeneratingResponseAtom) + const updateThread = useSetAtom(updateThreadAtom) useEffect(() => { threadsRef.current = threads @@ -131,6 +133,12 @@ export default function EventHandler({ children }: { children: ReactNode }) { ...thread.metadata, lastMessage: messageContent, } + + updateThread({ + ...thread, + metadata, + }) + extensionManager .get(ExtensionTypeEnum.Conversational) ?.saveThread({ diff --git a/web/hooks/useCreateNewThread.ts b/web/hooks/useCreateNewThread.ts index ee8df22df..12a5e04ca 100644 --- a/web/hooks/useCreateNewThread.ts +++ b/web/hooks/useCreateNewThread.ts @@ -7,7 +7,7 @@ import { ThreadState, Model, } from '@janhq/core' -import { atom, useSetAtom } from 'jotai' +import { atom, useAtomValue, useSetAtom } from 'jotai' import { selectedModelAtom } from '@/containers/DropdownListSidebar' import { fileUploadAtom } from '@/containers/Providers/Jotai' @@ -19,6 +19,7 @@ import useRecommendedModel from './useRecommendedModel' import useSetActiveThread from './useSetActiveThread' import { extensionManager } from '@/extension' + import { threadsAtom, threadStatesAtom, @@ -53,12 +54,21 @@ export const useCreateNewThread = () => { const { recommendedModel, downloadedModels } = useRecommendedModel() + const threads = useAtomValue(threadsAtom) + const requestCreateNewThread = async ( assistant: Assistant, model?: Model | undefined ) => { const defaultModel = model ?? recommendedModel ?? downloadedModels[0] + // check last thread message, if there empty last message use can not create thread + const lastMessage = threads[0]?.metadata?.lastMessage + + if (!lastMessage && threads.length) { + return null + } + const createdAt = Date.now() const assistantInfo: ThreadAssistantInfo = { assistant_id: assistant.id, diff --git a/web/screens/Chat/ThreadList/index.tsx b/web/screens/Chat/ThreadList/index.tsx index 8f5bfb8f2..2ad9a28c4 100644 --- a/web/screens/Chat/ThreadList/index.tsx +++ b/web/screens/Chat/ThreadList/index.tsx @@ -62,7 +62,9 @@ export default function ThreadList() {

{thread.title}

- {threadStates[thread.id]?.lastMessage ?? 'No new message'} + {threadStates[thread.id]?.lastMessage + ? threadStates[thread.id]?.lastMessage + : 'No new message'}

Date: Tue, 6 Feb 2024 17:31:46 +0700 Subject: [PATCH 37/63] feat: User Selectable GPUs and GPU-based Model Recommendations (#1730) --- .../src/node/execute.ts | 16 ++--- .../src/node/nvidia.ts | 63 +++++++++---------- extensions/monitoring-extension/package.json | 5 +- extensions/monitoring-extension/src/module.ts | 54 +++++++++++++--- web/containers/Layout/BottomBar/index.tsx | 20 +++++- web/helpers/atoms/SystemBar.atom.ts | 2 + web/hooks/useGetSystemResources.ts | 20 +++++- web/hooks/useSettings.ts | 3 + .../ExploreModelItemHeader/index.tsx | 14 ++++- web/screens/Settings/Advanced/index.tsx | 57 ++++++++++++++++- 10 files changed, 196 insertions(+), 58 deletions(-) diff --git a/extensions/inference-nitro-extension/src/node/execute.ts b/extensions/inference-nitro-extension/src/node/execute.ts index ca266639c..83b5226d4 100644 --- a/extensions/inference-nitro-extension/src/node/execute.ts +++ b/extensions/inference-nitro-extension/src/node/execute.ts @@ -25,12 +25,12 @@ export const executableNitroFile = (): NitroExecutableOptions => { if (nvidiaInfo["run_mode"] === "cpu") { binaryFolder = path.join(binaryFolder, "win-cpu"); } else { - if (nvidiaInfo["cuda"].version === "12") { - binaryFolder = path.join(binaryFolder, "win-cuda-12-0"); - } else { + if (nvidiaInfo["cuda"].version === "11") { binaryFolder = path.join(binaryFolder, "win-cuda-11-7"); + } else { + binaryFolder = path.join(binaryFolder, "win-cuda-12-0"); } - cudaVisibleDevices = nvidiaInfo["gpu_highest_vram"]; + cudaVisibleDevices = nvidiaInfo["gpus_in_use"].join(","); } binaryName = "nitro.exe"; } else if (process.platform === "darwin") { @@ -50,12 +50,12 @@ export const executableNitroFile = (): NitroExecutableOptions => { if (nvidiaInfo["run_mode"] === "cpu") { binaryFolder = path.join(binaryFolder, "linux-cpu"); } else { - if (nvidiaInfo["cuda"].version === "12") { - binaryFolder = path.join(binaryFolder, "linux-cuda-12-0"); - } else { + if (nvidiaInfo["cuda"].version === "11") { binaryFolder = path.join(binaryFolder, "linux-cuda-11-7"); + } else { + binaryFolder = path.join(binaryFolder, "linux-cuda-12-0"); } - cudaVisibleDevices = nvidiaInfo["gpu_highest_vram"]; + cudaVisibleDevices = nvidiaInfo["gpus_in_use"].join(","); } } return { diff --git a/extensions/inference-nitro-extension/src/node/nvidia.ts b/extensions/inference-nitro-extension/src/node/nvidia.ts index 13e43290b..bed2856a1 100644 --- a/extensions/inference-nitro-extension/src/node/nvidia.ts +++ b/extensions/inference-nitro-extension/src/node/nvidia.ts @@ -19,6 +19,8 @@ const DEFALT_SETTINGS = { }, gpus: [], gpu_highest_vram: "", + gpus_in_use: [], + is_initial: true, }; /** @@ -48,11 +50,15 @@ export interface NitroProcessInfo { */ export async function updateNvidiaInfo() { if (process.platform !== "darwin") { - await Promise.all([ - updateNvidiaDriverInfo(), - updateCudaExistence(), - updateGpuInfo(), - ]); + let data; + try { + data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8")); + } catch (error) { + data = DEFALT_SETTINGS; + writeFileSync(NVIDIA_INFO_FILE, JSON.stringify(data, null, 2)); + } + updateNvidiaDriverInfo(); + updateGpuInfo(); } } @@ -73,12 +79,7 @@ export async function updateNvidiaDriverInfo(): Promise { exec( "nvidia-smi --query-gpu=driver_version --format=csv,noheader", (error, stdout) => { - let data; - try { - data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8")); - } catch (error) { - data = DEFALT_SETTINGS; - } + let data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8")); if (!error) { const firstLine = stdout.split("\n")[0].trim(); @@ -107,7 +108,7 @@ export function checkFileExistenceInPaths( /** * Validate cuda for linux and windows */ -export function updateCudaExistence() { +export function updateCudaExistence(data: Record = DEFALT_SETTINGS): Record { let filesCuda12: string[]; let filesCuda11: string[]; let paths: string[]; @@ -141,19 +142,14 @@ export function updateCudaExistence() { cudaVersion = "12"; } - let data; - try { - data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8")); - } catch (error) { - data = DEFALT_SETTINGS; - } - data["cuda"].exist = cudaExists; data["cuda"].version = cudaVersion; - if (cudaExists) { + console.log(data["is_initial"], data["gpus_in_use"]); + if (cudaExists && data["is_initial"] && data["gpus_in_use"].length > 0) { data.run_mode = "gpu"; } - writeFileSync(NVIDIA_INFO_FILE, JSON.stringify(data, null, 2)); + data.is_initial = false; + return data; } /** @@ -161,14 +157,9 @@ export function updateCudaExistence() { */ export async function updateGpuInfo(): Promise { exec( - "nvidia-smi --query-gpu=index,memory.total --format=csv,noheader,nounits", + "nvidia-smi --query-gpu=index,memory.total,name --format=csv,noheader,nounits", (error, stdout) => { - let data; - try { - data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8")); - } catch (error) { - data = DEFALT_SETTINGS; - } + let data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8")); if (!error) { // Get GPU info and gpu has higher memory first @@ -178,21 +169,27 @@ export async function updateGpuInfo(): Promise { .trim() .split("\n") .map((line) => { - let [id, vram] = line.split(", "); + let [id, vram, name] = line.split(", "); vram = vram.replace(/\r/g, ""); if (parseFloat(vram) > highestVram) { highestVram = parseFloat(vram); highestVramId = id; } - return { id, vram }; + return { id, vram, name }; }); - data["gpus"] = gpus; - data["gpu_highest_vram"] = highestVramId; + data.gpus = gpus; + data.gpu_highest_vram = highestVramId; } else { - data["gpus"] = []; + data.gpus = []; + data.gpu_highest_vram = ""; } + if (!data["gpus_in_use"] || data["gpus_in_use"].length === 0) { + data.gpus_in_use = [data["gpu_highest_vram"]]; + } + + data = updateCudaExistence(data); writeFileSync(NVIDIA_INFO_FILE, JSON.stringify(data, null, 2)); Promise.resolve(); } diff --git a/extensions/monitoring-extension/package.json b/extensions/monitoring-extension/package.json index 9935e536e..20d3c485f 100644 --- a/extensions/monitoring-extension/package.json +++ b/extensions/monitoring-extension/package.json @@ -1,6 +1,6 @@ { "name": "@janhq/monitoring-extension", - "version": "1.0.9", + "version": "1.0.10", "description": "This extension provides system health and OS level data", "main": "dist/index.js", "module": "dist/module.js", @@ -26,6 +26,7 @@ "README.md" ], "bundleDependencies": [ - "node-os-utils" + "node-os-utils", + "@janhq/core" ] } diff --git a/extensions/monitoring-extension/src/module.ts b/extensions/monitoring-extension/src/module.ts index 86b553d52..2c1b14343 100644 --- a/extensions/monitoring-extension/src/module.ts +++ b/extensions/monitoring-extension/src/module.ts @@ -1,4 +1,14 @@ const nodeOsUtils = require("node-os-utils"); +const getJanDataFolderPath = require("@janhq/core/node").getJanDataFolderPath; +const path = require("path"); +const { readFileSync } = require("fs"); +const exec = require("child_process").exec; + +const NVIDIA_INFO_FILE = path.join( + getJanDataFolderPath(), + "settings", + "settings.json" +); const getResourcesInfo = () => new Promise((resolve) => { @@ -16,18 +26,48 @@ const getResourcesInfo = () => }); const getCurrentLoad = () => - new Promise((resolve) => { + new Promise((resolve, reject) => { nodeOsUtils.cpu.usage().then((cpuPercentage) => { - const response = { - cpu: { - usage: cpuPercentage, - }, + let data = { + run_mode: "cpu", + gpus_in_use: [], }; - resolve(response); + if (process.platform !== "darwin") { + data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8")); + } + if (data.run_mode === "gpu" && data.gpus_in_use.length > 0) { + const gpuIds = data["gpus_in_use"].join(","); + if (gpuIds !== "") { + exec( + `nvidia-smi --query-gpu=index,name,temperature.gpu,utilization.gpu,memory.total,memory.free,utilization.memory --format=csv,noheader,nounits --id=${gpuIds}`, + (error, stdout, stderr) => { + if (error) { + console.error(`exec error: ${error}`); + reject(error); + return; + } + const gpuInfo = stdout.trim().split("\n").map((line) => { + const [id, name, temperature, utilization, memoryTotal, memoryFree, memoryUtilization] = line.split(", ").map(item => item.replace(/\r/g, "")); + return { id, name, temperature, utilization, memoryTotal, memoryFree, memoryUtilization }; + }); + resolve({ + cpu: { usage: cpuPercentage }, + gpu: gpuInfo + }); + } + ); + } else { + // Handle the case where gpuIds is empty + resolve({ cpu: { usage: cpuPercentage }, gpu: [] }); + } + } else { + // Handle the case where run_mode is not 'gpu' or no GPUs are in use + resolve({ cpu: { usage: cpuPercentage }, gpu: [] }); + } }); }); module.exports = { getResourcesInfo, getCurrentLoad, -}; +}; \ No newline at end of file diff --git a/web/containers/Layout/BottomBar/index.tsx b/web/containers/Layout/BottomBar/index.tsx index 7dc5a9444..32dc70c70 100644 --- a/web/containers/Layout/BottomBar/index.tsx +++ b/web/containers/Layout/BottomBar/index.tsx @@ -48,7 +48,7 @@ const menuLinks = [ const BottomBar = () => { const { activeModel, stateModel } = useActiveModel() - const { ram, cpu } = useGetSystemResources() + const { ram, cpu, gpus } = useGetSystemResources() const progress = useAtomValue(appDownloadProgress) const downloadedModels = useAtomValue(downloadedModelsAtom) @@ -57,6 +57,13 @@ const BottomBar = () => { const setShowSelectModelModal = useSetAtom(showSelectModelModalAtom) const [serverEnabled] = useAtom(serverEnabledAtom) + const calculateGpuMemoryUsage = (gpu: Record) => { + const total = parseInt(gpu.memoryTotal) + const free = parseInt(gpu.memoryFree) + if (!total || !free) return 0 + return Math.round(((total - free) / total) * 100) + } + return (
@@ -119,6 +126,17 @@ const BottomBar = () => {
+ {gpus.length > 0 && ( +
+ {gpus.map((gpu, index) => ( + + ))} +
+ )} {/* VERSION is defined by webpack, please see next.config.js */} Jan v{VERSION ?? ''} diff --git a/web/helpers/atoms/SystemBar.atom.ts b/web/helpers/atoms/SystemBar.atom.ts index 42ef7b29f..22a7573ec 100644 --- a/web/helpers/atoms/SystemBar.atom.ts +++ b/web/helpers/atoms/SystemBar.atom.ts @@ -5,3 +5,5 @@ export const usedRamAtom = atom(0) export const availableRamAtom = atom(0) export const cpuUsageAtom = atom(0) + +export const nvidiaTotalVramAtom = atom(0) diff --git a/web/hooks/useGetSystemResources.ts b/web/hooks/useGetSystemResources.ts index 3429a93aa..3f71040d7 100644 --- a/web/hooks/useGetSystemResources.ts +++ b/web/hooks/useGetSystemResources.ts @@ -10,15 +10,19 @@ import { cpuUsageAtom, totalRamAtom, usedRamAtom, + nvidiaTotalVramAtom, } from '@/helpers/atoms/SystemBar.atom' export default function useGetSystemResources() { const [ram, setRam] = useState(0) const [cpu, setCPU] = useState(0) + + const [gpus, setGPUs] = useState[]>([]) const setTotalRam = useSetAtom(totalRamAtom) const setUsedRam = useSetAtom(usedRamAtom) const setAvailableRam = useSetAtom(availableRamAtom) const setCpuUsage = useSetAtom(cpuUsageAtom) + const setTotalNvidiaVram = useSetAtom(nvidiaTotalVramAtom) const getSystemResources = async () => { if ( @@ -48,12 +52,25 @@ export default function useGetSystemResources() { ) setCPU(Math.round(currentLoadInfor?.cpu?.usage ?? 0)) setCpuUsage(Math.round(currentLoadInfor?.cpu?.usage ?? 0)) + + const gpus = currentLoadInfor?.gpu ?? [] + setGPUs(gpus) + + let totalNvidiaVram = 0 + if (gpus.length > 0) { + totalNvidiaVram = gpus.reduce( + (total: number, gpu: { memoryTotal: string }) => + total + Number(gpu.memoryTotal), + 0 + ) + } + setTotalNvidiaVram(totalNvidiaVram) } useEffect(() => { getSystemResources() - // Fetch interval - every 0.5s + // Fetch interval - every 2s // TODO: Will we really need this? // There is a possibility that this will be removed and replaced by the process event hook? const intervalId = setInterval(() => { @@ -69,5 +86,6 @@ export default function useGetSystemResources() { totalRamAtom, ram, cpu, + gpus, } } diff --git a/web/hooks/useSettings.ts b/web/hooks/useSettings.ts index 168e72489..289355b36 100644 --- a/web/hooks/useSettings.ts +++ b/web/hooks/useSettings.ts @@ -47,14 +47,17 @@ export const useSettings = () => { const saveSettings = async ({ runMode, notify, + gpusInUse, }: { runMode?: string | undefined notify?: boolean | undefined + gpusInUse?: string[] | undefined }) => { const settingsFile = await joinPath(['file://settings', 'settings.json']) const settings = await readSettings() if (runMode != null) settings.run_mode = runMode if (notify != null) settings.notify = notify + if (gpusInUse != null) settings.gpus_in_use = gpusInUse await fs.writeFileSync(settingsFile, JSON.stringify(settings)) } diff --git a/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx b/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx index 5faa772c7..17b897d51 100644 --- a/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx +++ b/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx @@ -35,7 +35,10 @@ import { assistantsAtom } from '@/helpers/atoms/Assistant.atom' import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom' import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom' -import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom' +import { + nvidiaTotalVramAtom, + totalRamAtom, +} from '@/helpers/atoms/SystemBar.atom' type Props = { model: Model @@ -49,7 +52,12 @@ const ExploreModelItemHeader: React.FC = ({ model, onClick, open }) => { const { modelDownloadStateAtom } = useDownloadState() const { requestCreateNewThread } = useCreateNewThread() const totalRam = useAtomValue(totalRamAtom) - + const nvidiaTotalVram = useAtomValue(nvidiaTotalVramAtom) + // Default nvidia returns vram in MB, need to convert to bytes to match the unit of totalRamW + let ram = nvidiaTotalVram * 1024 * 1024 + if (ram === 0) { + ram = totalRam + } const serverEnabled = useAtomValue(serverEnabledAtom) const assistants = useAtomValue(assistantsAtom) @@ -115,7 +123,7 @@ const ExploreModelItemHeader: React.FC = ({ model, onClick, open }) => { } const getLabel = (size: number) => { - if (size * 1.25 >= totalRam) { + if (size * 1.25 >= ram) { return ( Not enough RAM diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx index d2f7d81ee..f6c8fb4d8 100644 --- a/web/screens/Settings/Advanced/index.tsx +++ b/web/screens/Settings/Advanced/index.tsx @@ -33,7 +33,10 @@ const Advanced = () => { } = useContext(FeatureToggleContext) const [partialProxy, setPartialProxy] = useState(proxy) const [gpuEnabled, setGpuEnabled] = useState(false) - + const [gpuList, setGpuList] = useState([ + { id: 'none', vram: null, name: 'none' }, + ]) + const [gpusInUse, setGpusInUse] = useState([]) const { readSettings, saveSettings, validateSettings, setShowNotification } = useSettings() @@ -54,6 +57,10 @@ const Advanced = () => { const setUseGpuIfPossible = async () => { const settings = await readSettings() setGpuEnabled(settings.run_mode === 'gpu') + setGpusInUse(settings.gpus_in_use || []) + if (settings.gpus) { + setGpuList(settings.gpus) + } } setUseGpuIfPossible() }, [readSettings]) @@ -69,6 +76,20 @@ const Advanced = () => { }) } + const handleGPUChange = (gpuId: string) => { + let updatedGpusInUse = [...gpusInUse] + if (updatedGpusInUse.includes(gpuId)) { + updatedGpusInUse = updatedGpusInUse.filter((id) => id !== gpuId) + if (gpuEnabled && updatedGpusInUse.length === 0) { + updatedGpusInUse.push(gpuId) + } + } else { + updatedGpusInUse.push(gpuId) + } + setGpusInUse(updatedGpusInUse) + saveSettings({ gpusInUse: updatedGpusInUse }) + } + return (
{/* Keyboard shortcut */} @@ -133,10 +154,40 @@ const Advanced = () => { />
)} - {/* Directory */} + {gpuEnabled && ( +
+ +
+ {gpuList.map((gpu) => ( +
+ handleGPUChange(gpu.id)} + /> + +
+ ))} +
+
+ )} + {/* Warning message */} + {gpuEnabled && gpusInUse.length > 1 && ( +

+ If enabling multi-GPU without the same GPU model or without NVLink, it + may affect token speed. +

+ )} - {/* Proxy */}
From a8cd9724ef07f9add359d60906f6f7ca4f76c6f4 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Tue, 6 Feb 2024 22:46:48 +0700 Subject: [PATCH 38/63] fix: markdown render for chat completion role user (#1944) --- web/screens/Chat/SimpleTextMessage/index.tsx | 56 +++++++++++++++----- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/web/screens/Chat/SimpleTextMessage/index.tsx b/web/screens/Chat/SimpleTextMessage/index.tsx index 261bb3497..9be45e7e6 100644 --- a/web/screens/Chat/SimpleTextMessage/index.tsx +++ b/web/screens/Chat/SimpleTextMessage/index.tsx @@ -18,7 +18,7 @@ import hljs from 'highlight.js' import { useAtomValue } from 'jotai' import { FolderOpenIcon } from 'lucide-react' -import { Marked, Renderer } from 'marked' +import { Marked, Renderer, marked as markedDefault } from 'marked' import { markedHighlight } from 'marked-highlight' @@ -37,13 +37,29 @@ import MessageToolbar from '../MessageToolbar' import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom' +function isMarkdownValue(value: string): boolean { + const tokenTypes: string[] = [] + markedDefault(value, { + walkTokens: (token) => { + tokenTypes.push(token.type) + }, + }) + const isMarkdown = ['code', 'codespan'].some((tokenType) => { + return tokenTypes.includes(tokenType) + }) + return isMarkdown +} + const SimpleTextMessage: React.FC = (props) => { let text = '' + const isUser = props.role === ChatCompletionRole.User + const isSystem = props.role === ChatCompletionRole.System + if (props.content && props.content.length > 0) { text = props.content[0]?.text?.value ?? '' } + const clipboard = useClipboard({ timeout: 1000 }) - const { onViewFile, onViewFileContainer } = usePath() const marked: Marked = new Marked( markedHighlight({ @@ -88,9 +104,8 @@ const SimpleTextMessage: React.FC = (props) => { } ) + const { onViewFile, onViewFileContainer } = usePath() const parsedText = marked.parse(text) - const isUser = props.role === ChatCompletionRole.User - const isSystem = props.role === ChatCompletionRole.System const [tokenCount, setTokenCount] = useState(0) const [lastTimestamp, setLastTimestamp] = useState() const [tokenSpeed, setTokenSpeed] = useState(0) @@ -260,16 +275,29 @@ const SimpleTextMessage: React.FC = (props) => {
)} -
+ {isUser && !isMarkdownValue(text) ? ( +
+ {text} +
+ ) : ( +
+ )}
From 823f8e09973c997735bbbe9c7631faba1e987678 Mon Sep 17 00:00:00 2001 From: Service Account Date: Tue, 6 Feb 2024 20:20:11 +0000 Subject: [PATCH 39/63] janhq/jan: Update README.md with nightly build artifact URL --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b84e03e7f..e1f74ef23 100644 --- a/README.md +++ b/README.md @@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Experimental (Nightly Build) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage From 2f961d7cabcfa025c6da3cb2b4597edb01ceed43 Mon Sep 17 00:00:00 2001 From: Van Pham <64197333+Van-QA@users.noreply.github.com> Date: Wed, 7 Feb 2024 09:41:35 +0700 Subject: [PATCH 40/63] feat: Playwright capture screenshot of Electron desktop app (Jan) on failures (#1934) * feat: Apply Screenshot on failures * feat: set timeout by default * chore: clean up import --- electron/playwright.config.ts | 9 ++- electron/tests/e2e/hub.e2e.spec.ts | 34 ++++++++++++ electron/tests/e2e/navigation.e2e.spec.ts | 38 +++++++++++++ electron/tests/e2e/settings.e2e.spec.ts | 23 ++++++++ electron/tests/hub.e2e.spec.ts | 48 ---------------- electron/tests/navigation.e2e.spec.ts | 61 --------------------- electron/tests/pages/basePage.ts | 67 +++++++++++++++++++++++ electron/tests/settings.e2e.spec.ts | 45 --------------- 8 files changed, 170 insertions(+), 155 deletions(-) create mode 100644 electron/tests/e2e/hub.e2e.spec.ts create mode 100644 electron/tests/e2e/navigation.e2e.spec.ts create mode 100644 electron/tests/e2e/settings.e2e.spec.ts delete mode 100644 electron/tests/hub.e2e.spec.ts delete mode 100644 electron/tests/navigation.e2e.spec.ts create mode 100644 electron/tests/pages/basePage.ts delete mode 100644 electron/tests/settings.e2e.spec.ts diff --git a/electron/playwright.config.ts b/electron/playwright.config.ts index 1fa3313f2..8047b7513 100644 --- a/electron/playwright.config.ts +++ b/electron/playwright.config.ts @@ -1,9 +1,16 @@ import { PlaywrightTestConfig } from '@playwright/test' const config: PlaywrightTestConfig = { - testDir: './tests', + testDir: './tests/e2e', retries: 0, globalTimeout: 300000, + use: { + screenshot: 'only-on-failure', + video: 'retain-on-failure', + trace: 'retain-on-failure', + }, + + reporter: [['html', { outputFolder: './playwright-report' }]], } export default config diff --git a/electron/tests/e2e/hub.e2e.spec.ts b/electron/tests/e2e/hub.e2e.spec.ts new file mode 100644 index 000000000..68632058e --- /dev/null +++ b/electron/tests/e2e/hub.e2e.spec.ts @@ -0,0 +1,34 @@ +import { + page, + test, + setupElectron, + teardownElectron, + TIMEOUT, +} from '../pages/basePage' +import { expect } from '@playwright/test' + +test.beforeAll(async () => { + const appInfo = await setupElectron() + 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() +}) + +test.afterAll(async () => { + await teardownElectron() +}) + +test('explores hub', async () => { + await page.getByTestId('Hub').first().click({ + timeout: TIMEOUT, + }) + await page.getByTestId('hub-container-test-id').isVisible({ + timeout: TIMEOUT, + }) +}) diff --git a/electron/tests/e2e/navigation.e2e.spec.ts b/electron/tests/e2e/navigation.e2e.spec.ts new file mode 100644 index 000000000..2da59953c --- /dev/null +++ b/electron/tests/e2e/navigation.e2e.spec.ts @@ -0,0 +1,38 @@ +import { expect } from '@playwright/test' +import { + page, + setupElectron, + TIMEOUT, + test, + teardownElectron, +} from '../pages/basePage' + +test.beforeAll(async () => { + await setupElectron() +}) + +test.afterAll(async () => { + await teardownElectron() +}) + +test('renders left navigation panel', async () => { + const systemMonitorBtn = await page + .getByTestId('System Monitor') + .first() + .isEnabled({ + timeout: TIMEOUT, + }) + const settingsBtn = await page + .getByTestId('Thread') + .first() + .isEnabled({ timeout: TIMEOUT }) + expect([systemMonitorBtn, settingsBtn].filter((e) => !e).length).toBe(0) + // Chat section should be there + await page.getByTestId('Local API Server').first().click({ + timeout: TIMEOUT, + }) + const localServer = page.getByTestId('local-server-testid').first() + await expect(localServer).toBeVisible({ + timeout: TIMEOUT, + }) +}) diff --git a/electron/tests/e2e/settings.e2e.spec.ts b/electron/tests/e2e/settings.e2e.spec.ts new file mode 100644 index 000000000..54215d9b1 --- /dev/null +++ b/electron/tests/e2e/settings.e2e.spec.ts @@ -0,0 +1,23 @@ +import { expect } from '@playwright/test' + +import { + setupElectron, + teardownElectron, + test, + page, + TIMEOUT, +} from '../pages/basePage' + +test.beforeAll(async () => { + await setupElectron() +}) + +test.afterAll(async () => { + await teardownElectron() +}) + +test('shows settings', async () => { + await page.getByTestId('Settings').first().click({ timeout: TIMEOUT }) + const settingDescription = page.getByTestId('testid-setting-description') + await expect(settingDescription).toBeVisible({ timeout: TIMEOUT }) +}) diff --git a/electron/tests/hub.e2e.spec.ts b/electron/tests/hub.e2e.spec.ts deleted file mode 100644 index cc72e037e..000000000 --- a/electron/tests/hub.e2e.spec.ts +++ /dev/null @@ -1,48 +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 -const TIMEOUT: number = parseInt(process.env.TEST_TIMEOUT || '300000') - -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({ - timeout: TIMEOUT, - }) -}) - -test.afterAll(async () => { - await electronApp.close() - await page.close() -}) - -test('explores hub', async () => { - test.setTimeout(TIMEOUT) - await page.getByTestId('Hub').first().click({ - timeout: TIMEOUT, - }) - await page.getByTestId('hub-container-test-id').isVisible({ - timeout: TIMEOUT, - }) -}) diff --git a/electron/tests/navigation.e2e.spec.ts b/electron/tests/navigation.e2e.spec.ts deleted file mode 100644 index 5c8721c2f..000000000 --- a/electron/tests/navigation.e2e.spec.ts +++ /dev/null @@ -1,61 +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 -const TIMEOUT: number = parseInt(process.env.TEST_TIMEOUT || '300000') - -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({ - timeout: TIMEOUT, - }) -}) - -test.afterAll(async () => { - await electronApp.close() - await page.close() -}) - -test('renders left navigation panel', async () => { - test.setTimeout(TIMEOUT) - const systemMonitorBtn = await page - .getByTestId('System Monitor') - .first() - .isEnabled({ - timeout: TIMEOUT, - }) - const settingsBtn = await page - .getByTestId('Thread') - .first() - .isEnabled({ timeout: TIMEOUT }) - expect([systemMonitorBtn, settingsBtn].filter((e) => !e).length).toBe(0) - // Chat section should be there - await page.getByTestId('Local API Server').first().click({ - timeout: TIMEOUT, - }) - const localServer = await page.getByTestId('local-server-testid').first() - await expect(localServer).toBeVisible({ - timeout: TIMEOUT, - }) -}) diff --git a/electron/tests/pages/basePage.ts b/electron/tests/pages/basePage.ts new file mode 100644 index 000000000..5f1a6fca1 --- /dev/null +++ b/electron/tests/pages/basePage.ts @@ -0,0 +1,67 @@ +import { + expect, + test as base, + _electron as electron, + ElectronApplication, + Page, +} from '@playwright/test' +import { + findLatestBuild, + parseElectronApp, + stubDialog, +} from 'electron-playwright-helpers' + +export const TIMEOUT: number = parseInt(process.env.TEST_TIMEOUT || '300000') + +export let electronApp: ElectronApplication +export let page: Page + +export async function setupElectron() { + 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({ + timeout: TIMEOUT, + }) + // Return appInfo for future use + return appInfo +} + +export async function teardownElectron() { + await page.close() + await electronApp.close() +} + +export const test = base.extend<{ + attachScreenshotsToReport: void +}>({ + attachScreenshotsToReport: [ + async ({ request }, use, testInfo) => { + await use() + + // After the test, we can check whether the test passed or failed. + if (testInfo.status !== testInfo.expectedStatus) { + const screenshot = await page.screenshot() + await testInfo.attach('screenshot', { + body: screenshot, + contentType: 'image/png', + }) + } + }, + { auto: true }, + ], +}) + +test.setTimeout(TIMEOUT) diff --git a/electron/tests/settings.e2e.spec.ts b/electron/tests/settings.e2e.spec.ts deleted file mode 100644 index ad2d7b4a4..000000000 --- a/electron/tests/settings.e2e.spec.ts +++ /dev/null @@ -1,45 +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 -const TIMEOUT: number = parseInt(process.env.TEST_TIMEOUT || '300000') - -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({ - timeout: TIMEOUT, - }) -}) - -test.afterAll(async () => { - await electronApp.close() - await page.close() -}) - -test('shows settings', async () => { - test.setTimeout(TIMEOUT) - await page.getByTestId('Settings').first().click({ timeout: TIMEOUT }) - const settingDescription = page.getByTestId('testid-setting-description') - await expect(settingDescription).toBeVisible({ timeout: TIMEOUT }) -}) From e6ea11e860a192839616a2de8293eb8edec7291e Mon Sep 17 00:00:00 2001 From: hiro Date: Wed, 7 Feb 2024 11:55:32 +0700 Subject: [PATCH 41/63] chore: Add Author - Hiro --- docs/blog/authors.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/blog/authors.yml b/docs/blog/authors.yml index f30d4610d..c0ed02fae 100644 --- a/docs/blog/authors.yml +++ b/docs/blog/authors.yml @@ -3,4 +3,11 @@ dan-jan: title: Co-Founder url: https://github.com/dan-jan image_url: https://avatars.githubusercontent.com/u/101145494?v=4 - email: daniel@jan.ai \ No newline at end of file + email: daniel@jan.ai + +hiro-v: + name: Hiro Vuong + title: MLE + url: https://github.com/hiro-v + image_url: https://avatars.githubusercontent.com/u/22463238?v=4 + email: hiro@jan.ai \ No newline at end of file From 9dae95a222daa95b369fa3a1e3409127c92c9f62 Mon Sep 17 00:00:00 2001 From: Ashley Date: Wed, 7 Feb 2024 12:17:57 +0700 Subject: [PATCH 42/63] add author --- docs/blog/authors.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/blog/authors.yml b/docs/blog/authors.yml index f30d4610d..1a62bd7d7 100644 --- a/docs/blog/authors.yml +++ b/docs/blog/authors.yml @@ -3,4 +3,11 @@ dan-jan: title: Co-Founder url: https://github.com/dan-jan image_url: https://avatars.githubusercontent.com/u/101145494?v=4 - email: daniel@jan.ai \ No newline at end of file + email: daniel@jan.ai + +ashley-jan: + name: Ashley Tran + title: Product Designer + url: https://github.com/imtuyethan + image_url: https://avatars.githubusercontent.com/u/89722390?v=4 + email: ashley@jan.ai \ No newline at end of file From 893d73596ba5d13fa0d08e8991a93f323b1363e1 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 7 Feb 2024 13:21:02 +0700 Subject: [PATCH 43/63] chore: add author james Signed-off-by: James --- docs/blog/authors.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/blog/authors.yml b/docs/blog/authors.yml index f30d4610d..3bd5f9472 100644 --- a/docs/blog/authors.yml +++ b/docs/blog/authors.yml @@ -1,6 +1,13 @@ dan-jan: name: Daniel Onggunhao title: Co-Founder - url: https://github.com/dan-jan + url: https://github.com/dan-jan image_url: https://avatars.githubusercontent.com/u/101145494?v=4 - email: daniel@jan.ai \ No newline at end of file + email: daniel@jan.ai + +namchuai: + name: Nam Nguyen + title: Developer + url: https://github.com/namchuai + image_url: https://avatars.githubusercontent.com/u/10397206?v=4 + email: james@jan.ai From 2fc8f22c0039f6c1c042a4cddffddc172f5a8c1b Mon Sep 17 00:00:00 2001 From: hiento09 <136591877+hiento09@users.noreply.github.com> Date: Wed, 7 Feb 2024 13:57:21 +0700 Subject: [PATCH 44/63] Update authors.yml --- docs/blog/authors.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/blog/authors.yml b/docs/blog/authors.yml index d15982e8c..044708811 100644 --- a/docs/blog/authors.yml +++ b/docs/blog/authors.yml @@ -25,4 +25,10 @@ ashley-jan: url: https://github.com/imtuyethan image_url: https://avatars.githubusercontent.com/u/89722390?v=4 email: ashley@jan.ai - + +hientominh: + name: Hien To + title: DevOps Engineer + url: https://github.com/hientominh + image_url: https://avatars.githubusercontent.com/u/37921427?v=4 + email: hien@jan.ai From fe29265ed6cd2dc08d37eef402b8f64ba5674076 Mon Sep 17 00:00:00 2001 From: Van Pham <64197333+Van-QA@users.noreply.github.com> Date: Wed, 7 Feb 2024 14:02:25 +0700 Subject: [PATCH 45/63] Update authors.yml for Van Pham --- docs/blog/authors.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/blog/authors.yml b/docs/blog/authors.yml index d15982e8c..6ac49607a 100644 --- a/docs/blog/authors.yml +++ b/docs/blog/authors.yml @@ -25,4 +25,11 @@ ashley-jan: url: https://github.com/imtuyethan image_url: https://avatars.githubusercontent.com/u/89722390?v=4 email: ashley@jan.ai + +Van-QA: + name: Van Pham + title: QA & Release Manager + url: https://github.com/Van-QA + image_url: https://avatars.githubusercontent.com/u/64197333?v=4 + email: van@jan.ai From 60d4fd1f040b0085a5c4b5bae48e687a0c1398c8 Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 7 Feb 2024 15:52:29 +0700 Subject: [PATCH 46/63] Update authors.yml Louis --- docs/blog/authors.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/blog/authors.yml b/docs/blog/authors.yml index 6ac49607a..9c9c02e2f 100644 --- a/docs/blog/authors.yml +++ b/docs/blog/authors.yml @@ -33,3 +33,9 @@ Van-QA: image_url: https://avatars.githubusercontent.com/u/64197333?v=4 email: van@jan.ai +louis-jan: + name: Louis Le + title: Software Engineer + url: https://github.com/louis-jan + image_url: https://avatars.githubusercontent.com/u/133622055?v=4 + email: louis@jan.ai From 41888650a8904cb8f26d747d53a527113016716c Mon Sep 17 00:00:00 2001 From: Hoang Ha <64120343+hahuyhoang411@users.noreply.github.com> Date: Wed, 7 Feb 2024 16:11:43 +0700 Subject: [PATCH 47/63] Update authors.yml Rex --- docs/blog/authors.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/blog/authors.yml b/docs/blog/authors.yml index 6ac49607a..fe344cacb 100644 --- a/docs/blog/authors.yml +++ b/docs/blog/authors.yml @@ -32,4 +32,11 @@ Van-QA: url: https://github.com/Van-QA image_url: https://avatars.githubusercontent.com/u/64197333?v=4 email: van@jan.ai + +hahuyhoang411: + name: Rex Ha + title: LLM Researcher & Content writer + url: https://github.com/hahuyhoang411 + image_url: https://avatars.githubusercontent.com/u/64120343?v=4 + email: rex@jan.ai From 5890ade451da894eb4b778cf7491eebb99f69ae1 Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 7 Feb 2024 17:54:35 +0700 Subject: [PATCH 48/63] chore: server download progress + S3 (#1925) * fix: reduce the number of api call Signed-off-by: James * fix: download progress Signed-off-by: James * chore: save blob * fix: server boot up * fix: download state not updating Signed-off-by: James * fix: copy assets * Add Dockerfile CPU for Jan Server and Jan Web * Add Dockerfile GPU for Jan Server and Jan Web * feat: S3 adapter * Update check find count from ./pre-install and correct copy:asserts command * server add bundleDependencies @janhq/core * server add bundleDependencies @janhq/core * fix: update success/failed download state (#1945) * fix: update success/failed download state Signed-off-by: James * fix: download model progress and state handling for both Desktop and Web --------- Signed-off-by: James Co-authored-by: James Co-authored-by: Louis * chore: refactor * fix: load models empty first time open * Add Docker compose * fix: assistants onUpdate --------- Signed-off-by: James Co-authored-by: James Co-authored-by: Hien To Co-authored-by: NamH --- Dockerfile | 67 +++++---- Dockerfile.gpu | 65 +++++++++ Makefile | 4 +- README.md | 25 ++++ core/package.json | 3 +- core/src/api/index.ts | 1 + core/src/node/api/routes/download.ts | 64 ++++++++- core/src/node/api/routes/fileManager.ts | 27 +++- core/src/node/api/routes/fs.ts | 13 +- core/src/node/api/routes/v1.ts | 3 + core/src/node/download.ts | 11 +- core/src/node/extension/index.ts | 10 +- core/src/types/assistant/assistantEvent.ts | 8 ++ core/src/types/assistant/index.ts | 1 + core/src/types/file/index.ts | 23 +++ core/src/types/model/modelEvent.ts | 2 + docker-compose.yml | 110 +++++++++++++++ electron/handlers/download.ts | 29 ++-- electron/handlers/fileManager.ts | 1 + electron/handlers/fs.ts | 3 +- electron/package.json | 13 +- extensions/assistant-extension/package.json | 6 +- extensions/assistant-extension/src/index.ts | 25 ++-- .../conversational-extension/package.json | 2 +- .../inference-nitro-extension/package.json | 6 +- .../inference-openai-extension/package.json | 2 +- .../package.json | 2 +- extensions/model-extension/package.json | 2 +- .../model-extension/src/@types/global.d.ts | 18 ++- .../model-extension/src/helpers/path.ts | 11 ++ extensions/model-extension/src/index.ts | 93 ++++++++++-- extensions/model-extension/tsconfig.json | 4 +- extensions/model-extension/webpack.config.js | 2 +- extensions/monitoring-extension/package.json | 2 +- package.json | 13 +- pre-install/.gitkeep | 0 server/helpers/setup.ts | 47 +++++++ server/index.ts | 6 +- server/main.ts | 10 +- server/middleware/s3.ts | 70 ++++++++++ server/nodemon.json | 5 - server/package.json | 13 +- server/tsconfig.json | 2 +- .../BottomBar/DownloadingState/index.tsx | 80 +++++------ web/containers/Layout/BottomBar/index.tsx | 7 +- web/containers/ModalCancelDownload/index.tsx | 13 +- web/containers/Providers/EventListener.tsx | 128 ++++++----------- web/helpers/atoms/Model.atom.ts | 23 +-- web/hooks/useAssistants.ts | 29 ++-- web/hooks/useDownloadModel.ts | 77 +++++----- web/hooks/useDownloadState.ts | 132 +++++++----------- web/hooks/useModels.ts | 33 +++-- web/hooks/useSetActiveThread.ts | 3 - web/next.config.js | 4 +- web/screens/Chat/ChatBody/index.tsx | 2 - web/screens/Chat/ErrorMessage/index.tsx | 1 - .../ExploreModelItemHeader/index.tsx | 3 +- .../ExploreModels/ModelVersionItem/index.tsx | 84 ----------- .../ExploreModels/ModelVersionList/index.tsx | 25 ---- web/services/restService.ts | 4 + web/types/downloadState.d.ts | 3 +- 61 files changed, 957 insertions(+), 518 deletions(-) create mode 100644 Dockerfile.gpu create mode 100644 core/src/types/assistant/assistantEvent.ts create mode 100644 docker-compose.yml create mode 100644 extensions/model-extension/src/helpers/path.ts create mode 100644 pre-install/.gitkeep create mode 100644 server/helpers/setup.ts create mode 100644 server/middleware/s3.ts delete mode 100644 server/nodemon.json delete mode 100644 web/screens/ExploreModels/ModelVersionItem/index.tsx delete mode 100644 web/screens/ExploreModels/ModelVersionList/index.tsx diff --git a/Dockerfile b/Dockerfile index 949a92673..82c657604 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,39 +1,58 @@ -FROM node:20-bullseye AS base +FROM node:20-bookworm AS base # 1. Install dependencies only when needed -FROM base AS deps +FROM base AS builder + +# Install g++ 11 +RUN apt update && apt install -y gcc-11 g++-11 cpp-11 jq xsel && rm -rf /var/lib/apt/lists/* + WORKDIR /app # Install dependencies based on the preferred package manager -COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ -RUN yarn install +COPY . ./ + +RUN export NITRO_VERSION=$(cat extensions/inference-nitro-extension/bin/version.txt) && \ + jq --arg nitroVersion $NITRO_VERSION '(.scripts."downloadnitro:linux" | gsub("\\${NITRO_VERSION}"; $nitroVersion)) | gsub("\r"; "")' extensions/inference-nitro-extension/package.json > /tmp/newcommand.txt && export NEW_COMMAND=$(sed 's/^"//;s/"$//' /tmp/newcommand.txt) && jq --arg newCommand "$NEW_COMMAND" '.scripts."downloadnitro:linux" = $newCommand' extensions/inference-nitro-extension/package.json > /tmp/package.json && mv /tmp/package.json extensions/inference-nitro-extension/package.json +RUN make install-and-build +RUN yarn workspace jan-web install + +RUN export NODE_ENV=production && yarn workspace jan-web build # # 2. Rebuild the source code only when needed -FROM base AS builder -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules -COPY . . -# This will do the trick, use the corresponding env file for each environment. -RUN yarn workspace server install -RUN yarn server:prod - -# 3. Production image, copy all the files and run next FROM base AS runner + +# Install g++ 11 +RUN apt update && apt install -y gcc-11 g++-11 cpp-11 jq xsel && rm -rf /var/lib/apt/lists/* + WORKDIR /app -ENV NODE_ENV=production +# Copy the package.json and yarn.lock of root yarn space to leverage Docker cache +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/node_modules ./node_modules/ +COPY --from=builder /app/yarn.lock ./yarn.lock -# RUN addgroup -g 1001 -S nodejs; -COPY --from=builder /app/server/build ./ +# Copy the package.json, yarn.lock, and build output of server yarn space to leverage Docker cache +COPY --from=builder /app/server ./server/ +COPY --from=builder /app/docs/openapi ./docs/openapi/ -# Automatically leverage output traces to reduce image size -# https://nextjs.org/docs/advanced-features/output-file-tracing -COPY --from=builder /app/server/node_modules ./node_modules -COPY --from=builder /app/server/package.json ./package.json +# Copy pre-install dependencies +COPY --from=builder /app/pre-install ./pre-install/ -EXPOSE 4000 3928 +# Copy the package.json, yarn.lock, and output of web yarn space to leverage Docker cache +COPY --from=builder /app/web/out ./web/out/ +COPY --from=builder /app/web/.next ./web/.next/ +COPY --from=builder /app/web/package.json ./web/package.json +COPY --from=builder /app/web/yarn.lock ./web/yarn.lock +COPY --from=builder /app/models ./models/ -ENV PORT 4000 -ENV APPDATA /app/data +RUN npm install -g serve@latest -CMD ["node", "main.js"] \ No newline at end of file +EXPOSE 1337 3000 3928 + +ENV JAN_API_HOST 0.0.0.0 +ENV JAN_API_PORT 1337 + +CMD ["sh", "-c", "cd server && node build/main.js & cd web && npx serve out"] + +# docker build -t jan . +# docker run -p 1337:1337 -p 3000:3000 -p 3928:3928 jan diff --git a/Dockerfile.gpu b/Dockerfile.gpu new file mode 100644 index 000000000..7b00e91d5 --- /dev/null +++ b/Dockerfile.gpu @@ -0,0 +1,65 @@ +FROM nvidia/cuda:12.0.0-devel-ubuntu22.04 AS base + +# 1. Install dependencies only when needed +FROM base AS builder + +# Install g++ 11 +RUN apt update && apt install -y gcc-11 g++-11 cpp-11 jq xsel curl gnupg && curl -sL https://deb.nodesource.com/setup_20.x | bash - && apt install nodejs -y && rm -rf /var/lib/apt/lists/* + +RUN npm install -g yarn + +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY . ./ + +RUN export NITRO_VERSION=$(cat extensions/inference-nitro-extension/bin/version.txt) && \ + jq --arg nitroVersion $NITRO_VERSION '(.scripts."downloadnitro:linux" | gsub("\\${NITRO_VERSION}"; $nitroVersion)) | gsub("\r"; "")' extensions/inference-nitro-extension/package.json > /tmp/newcommand.txt && export NEW_COMMAND=$(sed 's/^"//;s/"$//' /tmp/newcommand.txt) && jq --arg newCommand "$NEW_COMMAND" '.scripts."downloadnitro:linux" = $newCommand' extensions/inference-nitro-extension/package.json > /tmp/package.json && mv /tmp/package.json extensions/inference-nitro-extension/package.json +RUN make install-and-build +RUN yarn workspace jan-web install + +RUN export NODE_ENV=production && yarn workspace jan-web build + +# # 2. Rebuild the source code only when needed +FROM base AS runner + +# Install g++ 11 +RUN apt update && apt install -y gcc-11 g++-11 cpp-11 jq xsel curl gnupg && curl -sL https://deb.nodesource.com/setup_20.x | bash - && apt-get install nodejs -y && rm -rf /var/lib/apt/lists/* + +RUN npm install -g yarn + +WORKDIR /app + +# Copy the package.json and yarn.lock of root yarn space to leverage Docker cache +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/node_modules ./node_modules/ +COPY --from=builder /app/yarn.lock ./yarn.lock + +# Copy the package.json, yarn.lock, and build output of server yarn space to leverage Docker cache +COPY --from=builder /app/server ./server/ +COPY --from=builder /app/docs/openapi ./docs/openapi/ + +# Copy pre-install dependencies +COPY --from=builder /app/pre-install ./pre-install/ + +# Copy the package.json, yarn.lock, and output of web yarn space to leverage Docker cache +COPY --from=builder /app/web/out ./web/out/ +COPY --from=builder /app/web/.next ./web/.next/ +COPY --from=builder /app/web/package.json ./web/package.json +COPY --from=builder /app/web/yarn.lock ./web/yarn.lock +COPY --from=builder /app/models ./models/ + +RUN npm install -g serve@latest + +EXPOSE 1337 3000 3928 + +ENV LD_LIBRARY_PATH=/usr/local/cuda-12.0/targets/x86_64-linux/lib:/usr/local/cuda-12.0/compat${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}} + +ENV JAN_API_HOST 0.0.0.0 +ENV JAN_API_PORT 1337 + +CMD ["sh", "-c", "cd server && node build/main.js & cd web && npx serve out"] + +# pre-requisites: nvidia-docker +# docker build -t jan-gpu . -f Dockerfile.gpu +# docker run -p 1337:1337 -p 3000:3000 -p 3928:3928 --gpus all jan-gpu diff --git a/Makefile b/Makefile index 905a68321..ffb1abee2 100644 --- a/Makefile +++ b/Makefile @@ -24,9 +24,9 @@ endif check-file-counts: install-and-build ifeq ($(OS),Windows_NT) - powershell -Command "if ((Get-ChildItem -Path electron/pre-install -Filter *.tgz | Measure-Object | Select-Object -ExpandProperty Count) -ne (Get-ChildItem -Path extensions -Directory | Measure-Object | Select-Object -ExpandProperty Count)) { Write-Host 'Number of .tgz files in electron/pre-install does not match the number of subdirectories in extension'; exit 1 } else { Write-Host 'Extension build successful' }" + powershell -Command "if ((Get-ChildItem -Path pre-install -Filter *.tgz | Measure-Object | Select-Object -ExpandProperty Count) -ne (Get-ChildItem -Path extensions -Directory | Measure-Object | Select-Object -ExpandProperty Count)) { Write-Host 'Number of .tgz files in pre-install does not match the number of subdirectories in extension'; exit 1 } else { Write-Host 'Extension build successful' }" else - @tgz_count=$$(find electron/pre-install -type f -name "*.tgz" | wc -l); dir_count=$$(find extensions -mindepth 1 -maxdepth 1 -type d | wc -l); if [ $$tgz_count -ne $$dir_count ]; then echo "Number of .tgz files in electron/pre-install ($$tgz_count) does not match the number of subdirectories in extension ($$dir_count)"; exit 1; else echo "Extension build successful"; fi + @tgz_count=$$(find pre-install -type f -name "*.tgz" | wc -l); dir_count=$$(find extensions -mindepth 1 -maxdepth 1 -type d | wc -l); if [ $$tgz_count -ne $$dir_count ]; then echo "Number of .tgz files in pre-install ($$tgz_count) does not match the number of subdirectories in extension ($$dir_count)"; exit 1; else echo "Extension build successful"; fi endif dev: check-file-counts diff --git a/README.md b/README.md index e1f74ef23..14437498b 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,31 @@ make build This will build the app MacOS m1/m2 for production (with code signing already done) and put the result in `dist` folder. +### Docker mode + +- Supported OS: Linux, WSL2 Docker +- Pre-requisites: + - `docker` and `docker compose`, follow instruction [here](https://docs.docker.com/engine/install/ubuntu/) + + ```bash + curl -fsSL https://get.docker.com -o get-docker.sh + sudo sh ./get-docker.sh --dry-run + ``` + + - `nvidia docker`, follow instruction [here](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) (If you want to run with GPU mode) + +- Run Jan in Docker mode + + ```bash + # CPU mode + docker compose --profile cpu up + + # GPU mode + docker compose --profile gpu up + ``` + + This will start the web server and you can access Jan at `http://localhost:3000`. + ## Acknowledgements Jan builds on top of other open-source projects: diff --git a/core/package.json b/core/package.json index 437e6d0a6..c3abe2d56 100644 --- a/core/package.json +++ b/core/package.json @@ -57,6 +57,7 @@ "rollup-plugin-typescript2": "^0.36.0", "ts-jest": "^26.1.1", "tslib": "^2.6.2", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "rimraf": "^3.0.2" } } diff --git a/core/src/api/index.ts b/core/src/api/index.ts index 0d7cc51f7..f4ec3cd7e 100644 --- a/core/src/api/index.ts +++ b/core/src/api/index.ts @@ -30,6 +30,7 @@ export enum DownloadRoute { downloadFile = 'downloadFile', pauseDownload = 'pauseDownload', resumeDownload = 'resumeDownload', + getDownloadProgress = 'getDownloadProgress', } export enum DownloadEvent { diff --git a/core/src/node/api/routes/download.ts b/core/src/node/api/routes/download.ts index ab8c0bd37..7fb05daee 100644 --- a/core/src/node/api/routes/download.ts +++ b/core/src/node/api/routes/download.ts @@ -5,8 +5,27 @@ import { HttpServer } from '../HttpServer' import { createWriteStream } from 'fs' import { getJanDataFolderPath } from '../../utils' import { normalizeFilePath } from '../../path' +import { DownloadState } from '../../../types' export const downloadRouter = async (app: HttpServer) => { + app.get(`/${DownloadRoute.getDownloadProgress}/:modelId`, async (req, res) => { + const modelId = req.params.modelId + + console.debug(`Getting download progress for model ${modelId}`) + console.debug( + `All Download progress: ${JSON.stringify(DownloadManager.instance.downloadProgressMap)}` + ) + + // check if null DownloadManager.instance.downloadProgressMap + if (!DownloadManager.instance.downloadProgressMap[modelId]) { + return res.status(404).send({ + message: 'Download progress not found', + }) + } else { + return res.status(200).send(DownloadManager.instance.downloadProgressMap[modelId]) + } + }) + app.post(`/${DownloadRoute.downloadFile}`, async (req, res) => { const strictSSL = !(req.query.ignoreSSL === 'true') const proxy = req.query.proxy?.startsWith('http') ? req.query.proxy : undefined @@ -19,7 +38,10 @@ export const downloadRouter = async (app: HttpServer) => { }) const localPath = normalizedArgs[1] - const fileName = localPath.split('/').pop() ?? '' + const array = localPath.split('/') + const fileName = array.pop() ?? '' + const modelId = array.pop() ?? '' + console.debug('downloadFile', normalizedArgs, fileName, modelId) const request = require('request') const progress = require('request-progress') @@ -27,17 +49,44 @@ export const downloadRouter = async (app: HttpServer) => { const rq = request({ url: normalizedArgs[0], strictSSL, proxy }) progress(rq, {}) .on('progress', function (state: any) { - console.log('download onProgress', state) + const downloadProps: DownloadState = { + ...state, + modelId, + fileName, + downloadState: 'downloading', + } + console.debug(`Download ${modelId} onProgress`, downloadProps) + DownloadManager.instance.downloadProgressMap[modelId] = downloadProps }) .on('error', function (err: Error) { - console.log('download onError', err) + console.debug(`Download ${modelId} onError`, err.message) + + const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId] + if (currentDownloadState) { + DownloadManager.instance.downloadProgressMap[modelId] = { + ...currentDownloadState, + downloadState: 'error', + } + } }) .on('end', function () { - console.log('download onEnd') + console.debug(`Download ${modelId} onEnd`) + + const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId] + if (currentDownloadState) { + if (currentDownloadState.downloadState === 'downloading') { + // if the previous state is downloading, then set the state to end (success) + DownloadManager.instance.downloadProgressMap[modelId] = { + ...currentDownloadState, + downloadState: 'end', + } + } + } }) .pipe(createWriteStream(normalizedArgs[1])) - DownloadManager.instance.setRequest(fileName, rq) + DownloadManager.instance.setRequest(localPath, rq) + res.status(200).send({ message: 'Download started' }) }) app.post(`/${DownloadRoute.abortDownload}`, async (req, res) => { @@ -54,5 +103,10 @@ export const downloadRouter = async (app: HttpServer) => { const rq = DownloadManager.instance.networkRequests[fileName] DownloadManager.instance.networkRequests[fileName] = undefined rq?.abort() + if (rq) { + res.status(200).send({ message: 'Download aborted' }) + } else { + res.status(404).send({ message: 'Download not found' }) + } }) } diff --git a/core/src/node/api/routes/fileManager.ts b/core/src/node/api/routes/fileManager.ts index 66056444e..b4c73dda1 100644 --- a/core/src/node/api/routes/fileManager.ts +++ b/core/src/node/api/routes/fileManager.ts @@ -1,14 +1,29 @@ import { FileManagerRoute } from '../../../api' import { HttpServer } from '../../index' +import { join } from 'path' -export const fsRouter = async (app: HttpServer) => { - app.post(`/app/${FileManagerRoute.syncFile}`, async (request: any, reply: any) => {}) +export const fileManagerRouter = async (app: HttpServer) => { + app.post(`/fs/${FileManagerRoute.syncFile}`, async (request: any, reply: any) => { + const reflect = require('@alumna/reflect') + const args = JSON.parse(request.body) + return reflect({ + src: args[0], + dest: args[1], + recursive: true, + delete: false, + overwrite: true, + errorOnExist: false, + }) + }) - app.post(`/app/${FileManagerRoute.getJanDataFolderPath}`, async (request: any, reply: any) => {}) + app.post(`/fs/${FileManagerRoute.getJanDataFolderPath}`, async (request: any, reply: any) => + global.core.appPath() + ) - app.post(`/app/${FileManagerRoute.getResourcePath}`, async (request: any, reply: any) => {}) + app.post(`/fs/${FileManagerRoute.getResourcePath}`, async (request: any, reply: any) => + join(global.core.appPath(), '../../..') + ) app.post(`/app/${FileManagerRoute.getUserHomePath}`, async (request: any, reply: any) => {}) - - app.post(`/app/${FileManagerRoute.fileStat}`, async (request: any, reply: any) => {}) + app.post(`/fs/${FileManagerRoute.fileStat}`, async (request: any, reply: any) => {}) } diff --git a/core/src/node/api/routes/fs.ts b/core/src/node/api/routes/fs.ts index c5404ccce..9535418a0 100644 --- a/core/src/node/api/routes/fs.ts +++ b/core/src/node/api/routes/fs.ts @@ -1,8 +1,9 @@ -import { FileSystemRoute } from '../../../api' +import { FileManagerRoute, FileSystemRoute } from '../../../api' import { join } from 'path' import { HttpServer } from '../HttpServer' import { getJanDataFolderPath } from '../../utils' import { normalizeFilePath } from '../../path' +import { writeFileSync } from 'fs' export const fsRouter = async (app: HttpServer) => { const moduleName = 'fs' @@ -26,4 +27,14 @@ export const fsRouter = async (app: HttpServer) => { } }) }) + app.post(`/${FileManagerRoute.writeBlob}`, async (request: any, reply: any) => { + try { + const args = JSON.parse(request.body) as any[] + console.log('writeBlob:', args[0]) + const dataBuffer = Buffer.from(args[1], 'base64') + writeFileSync(args[0], dataBuffer) + } catch (err) { + console.error(`writeFile ${request.body} result: ${err}`) + } + }) } diff --git a/core/src/node/api/routes/v1.ts b/core/src/node/api/routes/v1.ts index a2a48cd8b..301c41ac0 100644 --- a/core/src/node/api/routes/v1.ts +++ b/core/src/node/api/routes/v1.ts @@ -4,6 +4,7 @@ import { threadRouter } from './thread' import { fsRouter } from './fs' import { extensionRouter } from './extension' import { downloadRouter } from './download' +import { fileManagerRouter } from './fileManager' export const v1Router = async (app: HttpServer) => { // MARK: External Routes @@ -16,6 +17,8 @@ export const v1Router = async (app: HttpServer) => { app.register(fsRouter, { prefix: '/fs', }) + app.register(fileManagerRouter) + app.register(extensionRouter, { prefix: '/extension', }) diff --git a/core/src/node/download.ts b/core/src/node/download.ts index 6d15fc344..b3f284440 100644 --- a/core/src/node/download.ts +++ b/core/src/node/download.ts @@ -1,15 +1,18 @@ +import { DownloadState } from '../types' /** * Manages file downloads and network requests. */ export class DownloadManager { - public networkRequests: Record = {}; + public networkRequests: Record = {} - public static instance: DownloadManager = new DownloadManager(); + public static instance: DownloadManager = new DownloadManager() + + public downloadProgressMap: Record = {} constructor() { if (DownloadManager.instance) { - return DownloadManager.instance; + return DownloadManager.instance } } /** @@ -18,6 +21,6 @@ export class DownloadManager { * @param {Request | undefined} request - The network request to set, or undefined to clear the request. */ setRequest(fileName: string, request: any | undefined) { - this.networkRequests[fileName] = request; + this.networkRequests[fileName] = request } } diff --git a/core/src/node/extension/index.ts b/core/src/node/extension/index.ts index ed8544773..994fc97f2 100644 --- a/core/src/node/extension/index.ts +++ b/core/src/node/extension/index.ts @@ -41,8 +41,8 @@ async function registerExtensionProtocol() { console.error('Electron is not available') } const extensionPath = ExtensionManager.instance.getExtensionsPath() - if (electron) { - return electron.protocol.registerFileProtocol('extension', (request: any, callback: any) => { + if (electron && electron.protocol) { + return electron.protocol?.registerFileProtocol('extension', (request: any, callback: any) => { const entry = request.url.substr('extension://'.length - 1) const url = normalize(extensionPath + entry) @@ -69,7 +69,7 @@ export function useExtensions(extensionsPath: string) { // Read extension list from extensions folder const extensions = JSON.parse( - readFileSync(ExtensionManager.instance.getExtensionsFile(), 'utf-8'), + readFileSync(ExtensionManager.instance.getExtensionsFile(), 'utf-8') ) try { // Create and store a Extension instance for each extension in list @@ -82,7 +82,7 @@ export function useExtensions(extensionsPath: string) { throw new Error( 'Could not successfully rebuild list of installed extensions.\n' + error + - '\nPlease check the extensions.json file in the extensions folder.', + '\nPlease check the extensions.json file in the extensions folder.' ) } @@ -122,7 +122,7 @@ function loadExtension(ext: any) { export function getStore() { if (!ExtensionManager.instance.getExtensionsFile()) { throw new Error( - 'The extension path has not yet been set up. Please run useExtensions before accessing the store', + 'The extension path has not yet been set up. Please run useExtensions before accessing the store' ) } diff --git a/core/src/types/assistant/assistantEvent.ts b/core/src/types/assistant/assistantEvent.ts new file mode 100644 index 000000000..f8f3e6ad0 --- /dev/null +++ b/core/src/types/assistant/assistantEvent.ts @@ -0,0 +1,8 @@ +/** + * The `EventName` enumeration contains the names of all the available events in the Jan platform. + */ +export enum AssistantEvent { + /** The `OnAssistantsUpdate` event is emitted when the assistant list is updated. */ + OnAssistantsUpdate = 'OnAssistantsUpdate', + } + \ No newline at end of file diff --git a/core/src/types/assistant/index.ts b/core/src/types/assistant/index.ts index 83ea73f85..e18589551 100644 --- a/core/src/types/assistant/index.ts +++ b/core/src/types/assistant/index.ts @@ -1,2 +1,3 @@ export * from './assistantEntity' +export * from './assistantEvent' export * from './assistantInterface' diff --git a/core/src/types/file/index.ts b/core/src/types/file/index.ts index 6526cfc6d..57d687d2f 100644 --- a/core/src/types/file/index.ts +++ b/core/src/types/file/index.ts @@ -2,3 +2,26 @@ export type FileStat = { isDirectory: boolean size: number } + +export type DownloadState = { + modelId: string + filename: string + time: DownloadTime + speed: number + percent: number + + size: DownloadSize + children?: DownloadState[] + error?: string + downloadState: 'downloading' | 'error' | 'end' +} + +type DownloadTime = { + elapsed: number + remaining: number +} + +type DownloadSize = { + total: number + transferred: number +} diff --git a/core/src/types/model/modelEvent.ts b/core/src/types/model/modelEvent.ts index 978a48724..443f3a34f 100644 --- a/core/src/types/model/modelEvent.ts +++ b/core/src/types/model/modelEvent.ts @@ -12,4 +12,6 @@ export enum ModelEvent { OnModelStop = 'OnModelStop', /** The `OnModelStopped` event is emitted when a model stopped ok. */ OnModelStopped = 'OnModelStopped', + /** The `OnModelUpdate` event is emitted when the model list is updated. */ + OnModelsUpdate = 'OnModelsUpdate', } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..fd0f44096 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,110 @@ +version: '3.7' + +services: + minio: + image: minio/minio + volumes: + - minio_data:/data + ports: + - "9000:9000" + - "9001:9001" + environment: + MINIO_ROOT_USER: minioadmin # This acts as AWS_ACCESS_KEY + MINIO_ROOT_PASSWORD: minioadmin # This acts as AWS_SECRET_ACCESS_KEY + command: server --console-address ":9001" /data + restart: always + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + interval: 30s + timeout: 20s + retries: 3 + networks: + vpcbr: + ipv4_address: 10.5.0.2 + + createbuckets: + image: minio/mc + depends_on: + - minio + entrypoint: > + /bin/sh -c " + /usr/bin/mc alias set myminio http://minio:9000 minioadmin minioadmin; + /usr/bin/mc mb myminio/mybucket; + /usr/bin/mc policy set public myminio/mybucket; + exit 0; + " + networks: + vpcbr: + + + app_cpu: + image: jan:latest + volumes: + - app_data:/app/server/build/jan + build: + context: . + dockerfile: Dockerfile + environment: + AWS_ACCESS_KEY_ID: minioadmin + AWS_SECRET_ACCESS_KEY: minioadmin + S3_BUCKET_NAME: mybucket + AWS_ENDPOINT: http://10.5.0.2:9000 + AWS_REGION: us-east-1 + restart: always + profiles: + - cpu + ports: + - "3000:3000" + - "1337:1337" + - "3928:3928" + networks: + vpcbr: + ipv4_address: 10.5.0.3 + + + app_gpu: + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + image: jan-gpu:latest + volumes: + - app_data:/app/server/build/jan + build: + context: . + dockerfile: Dockerfile.gpu + restart: always + environment: + AWS_ACCESS_KEY_ID: minioadmin + AWS_SECRET_ACCESS_KEY: minioadmin + S3_BUCKET_NAME: mybucket + AWS_ENDPOINT: http://10.5.0.2:9000 + AWS_REGION: us-east-1 + + profiles: + - gpu + ports: + - "3000:3000" + - "1337:1337" + - "3928:3928" + networks: + vpcbr: + ipv4_address: 10.5.0.4 + +volumes: + minio_data: + app_data: + +networks: + vpcbr: + driver: bridge + ipam: + config: + - subnet: 10.5.0.0/16 + gateway: 10.5.0.1 + +# docker compose --profile cpu up +# docker compose --profile gpu up diff --git a/electron/handlers/download.ts b/electron/handlers/download.ts index f63e56f6b..5f1d8371e 100644 --- a/electron/handlers/download.ts +++ b/electron/handlers/download.ts @@ -5,7 +5,11 @@ import request from 'request' import { createWriteStream, renameSync } from 'fs' import { DownloadEvent, DownloadRoute } from '@janhq/core' const progress = require('request-progress') -import { DownloadManager, getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node' +import { + DownloadManager, + getJanDataFolderPath, + normalizeFilePath, +} from '@janhq/core/node' export function handleDownloaderIPCs() { /** @@ -56,20 +60,23 @@ export function handleDownloaderIPCs() { */ ipcMain.handle( DownloadRoute.downloadFile, - async (_event, url, fileName, network) => { + async (_event, url, localPath, network) => { const strictSSL = !network?.ignoreSSL const proxy = network?.proxy?.startsWith('http') ? network.proxy : undefined - - if (typeof fileName === 'string') { - fileName = normalizeFilePath(fileName) + if (typeof localPath === 'string') { + localPath = normalizeFilePath(localPath) } - const destination = resolve(getJanDataFolderPath(), fileName) + const array = localPath.split('/') + const fileName = array.pop() ?? '' + const modelId = array.pop() ?? '' + + const destination = resolve(getJanDataFolderPath(), localPath) const rq = request({ url, strictSSL, proxy }) // Put request to download manager instance - DownloadManager.instance.setRequest(fileName, rq) + DownloadManager.instance.setRequest(localPath, rq) // Downloading file to a temp file first const downloadingTempFile = `${destination}.download` @@ -81,6 +88,7 @@ export function handleDownloaderIPCs() { { ...state, fileName, + modelId, } ) }) @@ -90,11 +98,12 @@ export function handleDownloaderIPCs() { { fileName, err, + modelId, } ) }) .on('end', function () { - if (DownloadManager.instance.networkRequests[fileName]) { + if (DownloadManager.instance.networkRequests[localPath]) { // Finished downloading, rename temp file to actual file renameSync(downloadingTempFile, destination) @@ -102,14 +111,16 @@ export function handleDownloaderIPCs() { DownloadEvent.onFileDownloadSuccess, { fileName, + modelId, } ) - DownloadManager.instance.setRequest(fileName, undefined) + DownloadManager.instance.setRequest(localPath, undefined) } else { WindowManager?.instance.currentWindow?.webContents.send( DownloadEvent.onFileDownloadError, { fileName, + modelId, err: { message: 'aborted' }, } ) diff --git a/electron/handlers/fileManager.ts b/electron/handlers/fileManager.ts index e328cb53b..15c371d34 100644 --- a/electron/handlers/fileManager.ts +++ b/electron/handlers/fileManager.ts @@ -38,6 +38,7 @@ export function handleFileMangerIPCs() { getResourcePath() ) + // Handles the 'getUserHomePath' IPC event. This event is triggered to get the user home path. ipcMain.handle(FileManagerRoute.getUserHomePath, async (_event) => app.getPath('home') ) diff --git a/electron/handlers/fs.ts b/electron/handlers/fs.ts index 34026b940..8ac575cb2 100644 --- a/electron/handlers/fs.ts +++ b/electron/handlers/fs.ts @@ -1,8 +1,7 @@ import { ipcMain } from 'electron' import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node' -import fs from 'fs' -import { FileManagerRoute, FileSystemRoute } from '@janhq/core' +import { FileSystemRoute } from '@janhq/core' import { join } from 'path' /** * Handles file system operations. diff --git a/electron/package.json b/electron/package.json index 08f15b262..229979b41 100644 --- a/electron/package.json +++ b/electron/package.json @@ -57,17 +57,18 @@ "scripts": { "lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"", "test:e2e": "playwright test --workers=1", - "dev": "tsc -p . && electron .", - "build": "run-script-os", - "build:test": "run-script-os", + "copy:assets": "rimraf --glob \"./pre-install/*.tgz\" && cpx \"../pre-install/*.tgz\" \"./pre-install\"", + "dev": "yarn copy:assets && tsc -p . && electron .", + "build": "yarn copy:assets && run-script-os", + "build:test": "yarn copy:assets && run-script-os", "build:test:darwin": "tsc -p . && electron-builder -p never -m --dir", "build:test:win32": "tsc -p . && electron-builder -p never -w --dir", "build:test:linux": "tsc -p . && electron-builder -p never -l --dir", - "build:darwin": "tsc -p . && electron-builder -p never -m", + "build:darwin": "tsc -p . && electron-builder -p never -m --x64 --arm64", "build:win32": "tsc -p . && electron-builder -p never -w", "build:linux": "tsc -p . && electron-builder -p never -l deb -l AppImage", - "build:publish": "run-script-os", - "build:publish:darwin": "tsc -p . && electron-builder -p always -m", + "build:publish": "yarn copy:assets && run-script-os", + "build:publish:darwin": "tsc -p . && electron-builder -p always -m --x64 --arm64", "build:publish:win32": "tsc -p . && electron-builder -p always -w", "build:publish:linux": "tsc -p . && electron-builder -p always -l deb -l AppImage" }, diff --git a/extensions/assistant-extension/package.json b/extensions/assistant-extension/package.json index 84bcdf47e..5f45ecabe 100644 --- a/extensions/assistant-extension/package.json +++ b/extensions/assistant-extension/package.json @@ -8,9 +8,9 @@ "license": "AGPL-3.0", "scripts": { "build": "tsc --module commonjs && rollup -c rollup.config.ts", - "build:publish:linux": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install", - "build:publish:darwin": "rimraf *.tgz --glob && npm run build && ../../.github/scripts/auto-sign.sh && npm pack && cpx *.tgz ../../electron/pre-install", - "build:publish:win32": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install", + "build:publish:linux": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install", + "build:publish:darwin": "rimraf *.tgz --glob && npm run build && ../../.github/scripts/auto-sign.sh && npm pack && cpx *.tgz ../../pre-install", + "build:publish:win32": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install", "build:publish": "run-script-os" }, "devDependencies": { diff --git a/extensions/assistant-extension/src/index.ts b/extensions/assistant-extension/src/index.ts index 6495ea786..8bc8cafdc 100644 --- a/extensions/assistant-extension/src/index.ts +++ b/extensions/assistant-extension/src/index.ts @@ -9,6 +9,7 @@ import { joinPath, executeOnMain, AssistantExtension, + AssistantEvent, } from "@janhq/core"; export default class JanAssistantExtension extends AssistantExtension { @@ -21,7 +22,7 @@ export default class JanAssistantExtension extends AssistantExtension { async onLoad() { // making the assistant directory const assistantDirExist = await fs.existsSync( - JanAssistantExtension._homeDir, + JanAssistantExtension._homeDir ); if ( localStorage.getItem(`${EXTENSION_NAME}-version`) !== VERSION || @@ -31,14 +32,16 @@ export default class JanAssistantExtension extends AssistantExtension { await fs.mkdirSync(JanAssistantExtension._homeDir); // Write assistant metadata - this.createJanAssistant(); + await this.createJanAssistant(); // Finished migration localStorage.setItem(`${EXTENSION_NAME}-version`, VERSION); + // Update the assistant list + events.emit(AssistantEvent.OnAssistantsUpdate, {}); } // Events subscription events.on(MessageEvent.OnMessageSent, (data: MessageRequest) => - JanAssistantExtension.handleMessageRequest(data, this), + JanAssistantExtension.handleMessageRequest(data, this) ); events.on(InferenceEvent.OnInferenceStopped, () => { @@ -53,7 +56,7 @@ export default class JanAssistantExtension extends AssistantExtension { private static async handleMessageRequest( data: MessageRequest, - instance: JanAssistantExtension, + instance: JanAssistantExtension ) { instance.isCancelled = false; instance.controller = new AbortController(); @@ -80,7 +83,7 @@ export default class JanAssistantExtension extends AssistantExtension { NODE, "toolRetrievalIngestNewDocument", docFile, - data.model?.proxyEngine, + data.model?.proxyEngine ); } } @@ -96,7 +99,7 @@ export default class JanAssistantExtension extends AssistantExtension { NODE, "toolRetrievalUpdateTextSplitter", data.thread.assistants[0].tools[0]?.settings?.chunk_size ?? 4000, - data.thread.assistants[0].tools[0]?.settings?.chunk_overlap ?? 200, + data.thread.assistants[0].tools[0]?.settings?.chunk_overlap ?? 200 ); } @@ -110,7 +113,7 @@ export default class JanAssistantExtension extends AssistantExtension { const retrievalResult = await executeOnMain( NODE, "toolRetrievalQueryResult", - prompt, + prompt ); // Update the message content @@ -168,7 +171,7 @@ export default class JanAssistantExtension extends AssistantExtension { try { await fs.writeFileSync( assistantMetadataPath, - JSON.stringify(assistant, null, 2), + JSON.stringify(assistant, null, 2) ); } catch (err) { console.error(err); @@ -180,7 +183,7 @@ export default class JanAssistantExtension extends AssistantExtension { // get all the assistant metadata json const results: Assistant[] = []; const allFileName: string[] = await fs.readdirSync( - JanAssistantExtension._homeDir, + JanAssistantExtension._homeDir ); for (const fileName of allFileName) { const filePath = await joinPath([ @@ -190,7 +193,7 @@ export default class JanAssistantExtension extends AssistantExtension { if (filePath.includes(".DS_Store")) continue; const jsonFiles: string[] = (await fs.readdirSync(filePath)).filter( - (file: string) => file === "assistant.json", + (file: string) => file === "assistant.json" ); if (jsonFiles.length !== 1) { @@ -200,7 +203,7 @@ export default class JanAssistantExtension extends AssistantExtension { const content = await fs.readFileSync( await joinPath([filePath, jsonFiles[0]]), - "utf-8", + "utf-8" ); const assistant: Assistant = typeof content === "object" ? content : JSON.parse(content); diff --git a/extensions/conversational-extension/package.json b/extensions/conversational-extension/package.json index a60c12339..b84c75d3d 100644 --- a/extensions/conversational-extension/package.json +++ b/extensions/conversational-extension/package.json @@ -7,7 +7,7 @@ "license": "MIT", "scripts": { "build": "tsc -b . && webpack --config webpack.config.js", - "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install" + "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install" }, "exports": { ".": "./dist/index.js", diff --git a/extensions/inference-nitro-extension/package.json b/extensions/inference-nitro-extension/package.json index 8ad516ad9..cccfbefd0 100644 --- a/extensions/inference-nitro-extension/package.json +++ b/extensions/inference-nitro-extension/package.json @@ -12,9 +12,9 @@ "downloadnitro:darwin": "NITRO_VERSION=$(cat ./bin/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-arm64.tar.gz -e --strip 1 -o ./bin/mac-arm64 && chmod +x ./bin/mac-arm64/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-amd64.tar.gz -e --strip 1 -o ./bin/mac-x64 && chmod +x ./bin/mac-x64/nitro", "downloadnitro:win32": "download.bat", "downloadnitro": "run-script-os", - "build:publish:darwin": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && ../../.github/scripts/auto-sign.sh && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../electron/pre-install", - "build:publish:win32": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../electron/pre-install", - "build:publish:linux": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../electron/pre-install", + "build:publish:darwin": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && ../../.github/scripts/auto-sign.sh && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../pre-install", + "build:publish:win32": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../pre-install", + "build:publish:linux": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../pre-install", "build:publish": "run-script-os" }, "exports": { diff --git a/extensions/inference-openai-extension/package.json b/extensions/inference-openai-extension/package.json index 5fa0ce974..0ba6f18db 100644 --- a/extensions/inference-openai-extension/package.json +++ b/extensions/inference-openai-extension/package.json @@ -8,7 +8,7 @@ "license": "AGPL-3.0", "scripts": { "build": "tsc -b . && webpack --config webpack.config.js", - "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install" + "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install" }, "exports": { ".": "./dist/index.js", diff --git a/extensions/inference-triton-trtllm-extension/package.json b/extensions/inference-triton-trtllm-extension/package.json index 1d27f9f18..0f4c2de23 100644 --- a/extensions/inference-triton-trtllm-extension/package.json +++ b/extensions/inference-triton-trtllm-extension/package.json @@ -8,7 +8,7 @@ "license": "AGPL-3.0", "scripts": { "build": "tsc -b . && webpack --config webpack.config.js", - "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install" + "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install" }, "exports": { ".": "./dist/index.js", diff --git a/extensions/model-extension/package.json b/extensions/model-extension/package.json index 86f177d14..1af5d38cb 100644 --- a/extensions/model-extension/package.json +++ b/extensions/model-extension/package.json @@ -8,7 +8,7 @@ "license": "AGPL-3.0", "scripts": { "build": "tsc -b . && webpack --config webpack.config.js", - "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install" + "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install" }, "devDependencies": { "cpx": "^1.5.0", diff --git a/extensions/model-extension/src/@types/global.d.ts b/extensions/model-extension/src/@types/global.d.ts index e998455f2..7a9202a62 100644 --- a/extensions/model-extension/src/@types/global.d.ts +++ b/extensions/model-extension/src/@types/global.d.ts @@ -1,3 +1,15 @@ -declare const EXTENSION_NAME: string -declare const MODULE_PATH: string -declare const VERSION: stringå +export {} +declare global { + declare const EXTENSION_NAME: string + declare const MODULE_PATH: string + declare const VERSION: string + + interface Core { + api: APIFunctions + events: EventEmitter + } + interface Window { + core?: Core | undefined + electronAPI?: any | undefined + } +} diff --git a/extensions/model-extension/src/helpers/path.ts b/extensions/model-extension/src/helpers/path.ts new file mode 100644 index 000000000..cbb151aa6 --- /dev/null +++ b/extensions/model-extension/src/helpers/path.ts @@ -0,0 +1,11 @@ +/** + * try to retrieve the download file name from the source url + */ + +export function extractFileName(url: string, fileExtension: string): string { + const extractedFileName = url.split('/').pop() + const fileName = extractedFileName.toLowerCase().endsWith(fileExtension) + ? extractedFileName + : extractedFileName + fileExtension + return fileName +} diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts index b9fa7731e..e26fd4929 100644 --- a/extensions/model-extension/src/index.ts +++ b/extensions/model-extension/src/index.ts @@ -8,7 +8,13 @@ import { ModelExtension, Model, getJanDataFolderPath, + events, + DownloadEvent, + DownloadRoute, + ModelEvent, } from '@janhq/core' +import { DownloadState } from '@janhq/core/.' +import { extractFileName } from './helpers/path' /** * A extension for models @@ -29,6 +35,8 @@ export default class JanModelExtension extends ModelExtension { */ async onLoad() { this.copyModelsToHomeDir() + // Handle Desktop Events + this.handleDesktopEvents() } /** @@ -61,6 +69,8 @@ export default class JanModelExtension extends ModelExtension { // Finished migration localStorage.setItem(`${EXTENSION_NAME}-version`, VERSION) + + events.emit(ModelEvent.OnModelsUpdate, {}) } catch (err) { console.error(err) } @@ -83,31 +93,66 @@ export default class JanModelExtension extends ModelExtension { if (model.sources.length > 1) { // path to model binaries for (const source of model.sources) { - let path = this.extractFileName(source.url) + let path = extractFileName( + source.url, + JanModelExtension._supportedModelFormat + ) if (source.filename) { path = await joinPath([modelDirPath, source.filename]) } downloadFile(source.url, path, network) } + // TODO: handle multiple binaries for web later } else { - const fileName = this.extractFileName(model.sources[0]?.url) + const fileName = extractFileName( + model.sources[0]?.url, + JanModelExtension._supportedModelFormat + ) const path = await joinPath([modelDirPath, fileName]) downloadFile(model.sources[0]?.url, path, network) + + if (window && window.core?.api && window.core.api.baseApiUrl) { + this.startPollingDownloadProgress(model.id) + } } } /** - * try to retrieve the download file name from the source url + * Specifically for Jan server. */ - private extractFileName(url: string): string { - const extractedFileName = url.split('/').pop() - const fileName = extractedFileName - .toLowerCase() - .endsWith(JanModelExtension._supportedModelFormat) - ? extractedFileName - : extractedFileName + JanModelExtension._supportedModelFormat - return fileName + private async startPollingDownloadProgress(modelId: string): Promise { + // wait for some seconds before polling + await new Promise((resolve) => setTimeout(resolve, 3000)) + + return new Promise((resolve) => { + const interval = setInterval(async () => { + fetch( + `${window.core.api.baseApiUrl}/v1/download/${DownloadRoute.getDownloadProgress}/${modelId}`, + { + method: 'GET', + headers: { contentType: 'application/json' }, + } + ).then(async (res) => { + const state: DownloadState = await res.json() + if (state.downloadState === 'end') { + events.emit(DownloadEvent.onFileDownloadSuccess, state) + clearInterval(interval) + resolve() + return + } + + if (state.downloadState === 'error') { + events.emit(DownloadEvent.onFileDownloadError, state) + clearInterval(interval) + resolve() + return + } + + events.emit(DownloadEvent.onFileDownloadUpdate, state) + }) + }, 1000) + }) } /** @@ -318,7 +363,7 @@ export default class JanModelExtension extends ModelExtension { return } - const defaultModel = await this.getDefaultModel() as Model + const defaultModel = (await this.getDefaultModel()) as Model if (!defaultModel) { console.error('Unable to find default model') return @@ -382,4 +427,28 @@ export default class JanModelExtension extends ModelExtension { async getConfiguredModels(): Promise { return this.getModelsMetadata() } + + handleDesktopEvents() { + if (window && window.electronAPI) { + window.electronAPI.onFileDownloadUpdate( + async (_event: string, state: any | undefined) => { + if (!state) return + state.downloadState = 'update' + events.emit(DownloadEvent.onFileDownloadUpdate, state) + } + ) + window.electronAPI.onFileDownloadError( + async (_event: string, state: any) => { + state.downloadState = 'error' + events.emit(DownloadEvent.onFileDownloadError, state) + } + ) + window.electronAPI.onFileDownloadSuccess( + async (_event: string, state: any) => { + state.downloadState = 'end' + events.emit(DownloadEvent.onFileDownloadSuccess, state) + } + ) + } + } } diff --git a/extensions/model-extension/tsconfig.json b/extensions/model-extension/tsconfig.json index addd8e127..c175d9437 100644 --- a/extensions/model-extension/tsconfig.json +++ b/extensions/model-extension/tsconfig.json @@ -8,7 +8,7 @@ "forceConsistentCasingInFileNames": true, "strict": false, "skipLibCheck": true, - "rootDir": "./src" + "rootDir": "./src", }, - "include": ["./src"] + "include": ["./src"], } diff --git a/extensions/model-extension/webpack.config.js b/extensions/model-extension/webpack.config.js index 347719f91..c67bf8dc0 100644 --- a/extensions/model-extension/webpack.config.js +++ b/extensions/model-extension/webpack.config.js @@ -19,7 +19,7 @@ module.exports = { new webpack.DefinePlugin({ EXTENSION_NAME: JSON.stringify(packageJson.name), MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`), - VERSION: JSON.stringify(packageJson.version), + VERSION: JSON.stringify(packageJson.version) }), ], output: { diff --git a/extensions/monitoring-extension/package.json b/extensions/monitoring-extension/package.json index 20d3c485f..538f6bdee 100644 --- a/extensions/monitoring-extension/package.json +++ b/extensions/monitoring-extension/package.json @@ -8,7 +8,7 @@ "license": "AGPL-3.0", "scripts": { "build": "tsc -b . && webpack --config webpack.config.js", - "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install" + "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install" }, "devDependencies": { "rimraf": "^3.0.2", diff --git a/package.json b/package.json index 4b8bc4af0..957934fda 100644 --- a/package.json +++ b/package.json @@ -21,22 +21,23 @@ "lint": "yarn workspace jan lint && yarn workspace jan-web lint", "test:unit": "yarn workspace @janhq/core test", "test": "yarn workspace jan test:e2e", - "copy:assets": "cpx \"models/**\" \"electron/models/\" && cpx \"docs/openapi/**\" \"electron/docs/openapi\"", + "copy:assets": "cpx \"models/**\" \"electron/models/\" && cpx \"pre-install/*.tgz\" \"electron/pre-install/\" && cpx \"docs/openapi/**\" \"electron/docs/openapi\"", "dev:electron": "yarn copy:assets && yarn workspace jan dev", "dev:web": "yarn workspace jan-web dev", - "dev:server": "yarn workspace @janhq/server dev", + "dev:server": "yarn copy:assets && yarn workspace @janhq/server dev", "dev": "concurrently --kill-others \"yarn dev:web\" \"wait-on http://localhost:3000 && yarn dev:electron\"", "test-local": "yarn lint && yarn build:test && yarn test", "dev:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit dev", "build:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit build", - "build:server": "cd server && yarn install && yarn run build", + "build:server": "yarn copy:assets && cd server && yarn install && yarn run 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 copy:assets && yarn workspace jan build", "build:electron:test": "yarn workspace jan build:test", - "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:windows": "rimraf ./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 ./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 ./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:server": "yarn workspace build:extensions ", "build:extensions": "run-script-os", "build:test": "yarn copy:assets && yarn build:web && yarn workspace jan build:test", "build": "yarn build:web && yarn build:electron", diff --git a/pre-install/.gitkeep b/pre-install/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/server/helpers/setup.ts b/server/helpers/setup.ts new file mode 100644 index 000000000..51d8eebe5 --- /dev/null +++ b/server/helpers/setup.ts @@ -0,0 +1,47 @@ +import { join, extname } from "path"; +import { existsSync, readdirSync, writeFileSync, mkdirSync } from "fs"; +import { init, installExtensions } from "@janhq/core/node"; + +export async function setup() { + /** + * Setup Jan Data Directory + */ + const appDir = process.env.JAN_DATA_DIRECTORY ?? join(__dirname, "..", "jan"); + + console.debug(`Create app data directory at ${appDir}...`); + if (!existsSync(appDir)) mkdirSync(appDir); + //@ts-ignore + global.core = { + // Define appPath function for app to retrieve app path globaly + appPath: () => appDir, + }; + init({ + extensionsPath: join(appDir, "extensions"), + }); + + /** + * Write app configurations. See #1619 + */ + console.debug("Writing config file..."); + writeFileSync( + join(appDir, "settings.json"), + JSON.stringify({ + data_folder: appDir, + }), + "utf-8" + ); + + /** + * Install extensions + */ + + console.debug("Installing extensions..."); + + const baseExtensionPath = join(__dirname, "../../..", "pre-install"); + const extensions = readdirSync(baseExtensionPath) + .filter((file) => extname(file) === ".tgz") + .map((file) => join(baseExtensionPath, file)); + + await installExtensions(extensions); + console.debug("Extensions installed"); +} diff --git a/server/index.ts b/server/index.ts index 05bfdca96..91349a81f 100644 --- a/server/index.ts +++ b/server/index.ts @@ -38,6 +38,7 @@ export interface ServerConfig { isVerboseEnabled?: boolean; schemaPath?: string; baseDir?: string; + storageAdataper?: any; } /** @@ -103,9 +104,12 @@ export const startServer = async (configs?: ServerConfig) => { { prefix: "extensions" } ); + // Register proxy middleware + if (configs?.storageAdataper) + server.addHook("preHandler", configs.storageAdataper); + // Register API routes await server.register(v1Router, { prefix: "/v1" }); - // Start listening for requests await server .listen({ diff --git a/server/main.ts b/server/main.ts index c3eb69135..3be397e6f 100644 --- a/server/main.ts +++ b/server/main.ts @@ -1,3 +1,7 @@ -import { startServer } from "./index"; - -startServer(); +import { s3 } from "./middleware/s3"; +import { setup } from "./helpers/setup"; +import { startServer as start } from "./index"; +/** + * Setup extensions and start the server + */ +setup().then(() => start({ storageAdataper: s3 })); diff --git a/server/middleware/s3.ts b/server/middleware/s3.ts new file mode 100644 index 000000000..624865222 --- /dev/null +++ b/server/middleware/s3.ts @@ -0,0 +1,70 @@ +import { join } from "path"; + +// Middleware to intercept requests and proxy if certain conditions are met +const config = { + endpoint: process.env.AWS_ENDPOINT, + region: process.env.AWS_REGION, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + }, +}; + +const S3_BUCKET_NAME = process.env.S3_BUCKET_NAME; + +const fs = require("@cyclic.sh/s3fs")(S3_BUCKET_NAME, config); +const PROXY_PREFIX = "/v1/fs"; +const PROXY_ROUTES = ["/threads", "/messages"]; + +export const s3 = (req: any, reply: any, done: any) => { + // Proxy FS requests to S3 using S3FS + if (req.url.startsWith(PROXY_PREFIX)) { + const route = req.url.split("/").pop(); + const args = parseRequestArgs(req); + + // Proxy matched requests to the s3fs module + if (args.length && PROXY_ROUTES.some((route) => args[0].includes(route))) { + try { + // Handle customized route + // S3FS does not handle appendFileSync + if (route === "appendFileSync") { + let result = handAppendFileSync(args); + + reply.status(200).send(result); + return; + } + // Reroute the other requests to the s3fs module + const result = fs[route](...args); + reply.status(200).send(result); + return; + } catch (ex) { + console.log(ex); + } + } + } + // Let other requests go through + done(); +}; + +const parseRequestArgs = (req: Request) => { + const { + getJanDataFolderPath, + normalizeFilePath, + } = require("@janhq/core/node"); + + return JSON.parse(req.body as any).map((arg: any) => + typeof arg === "string" && + (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`)) + ? join(getJanDataFolderPath(), normalizeFilePath(arg)) + : arg + ); +}; + +const handAppendFileSync = (args: any[]) => { + if (fs.existsSync(args[0])) { + const data = fs.readFileSync(args[0], "utf-8"); + return fs.writeFileSync(args[0], data + args[1]); + } else { + return fs.writeFileSync(args[0], args[1]); + } +}; diff --git a/server/nodemon.json b/server/nodemon.json deleted file mode 100644 index 0ea41ca96..000000000 --- a/server/nodemon.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "watch": ["main.ts", "v1"], - "ext": "ts, json", - "exec": "tsc && node ./build/main.js" -} \ No newline at end of file diff --git a/server/package.json b/server/package.json index f61730da4..c1a104506 100644 --- a/server/package.json +++ b/server/package.json @@ -13,16 +13,18 @@ "scripts": { "lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"", "test:e2e": "playwright test --workers=1", - "dev": "tsc --watch & node --watch build/main.js", - "build": "tsc" + "build:core": "cd node_modules/@janhq/core && yarn install && yarn build", + "dev": "yarn build:core && tsc --watch & node --watch build/main.js", + "build": "yarn build:core && tsc" }, "dependencies": { "@alumna/reflect": "^1.1.3", + "@cyclic.sh/s3fs": "^1.2.9", "@fastify/cors": "^8.4.2", "@fastify/static": "^6.12.0", "@fastify/swagger": "^8.13.0", "@fastify/swagger-ui": "2.0.1", - "@janhq/core": "link:./core", + "@janhq/core": "file:../core", "dotenv": "^16.3.1", "fastify": "^4.24.3", "request": "^2.88.2", @@ -39,5 +41,8 @@ "run-script-os": "^1.1.6", "@types/tcp-port-used": "^1.0.4", "typescript": "^5.2.2" - } + }, + "bundleDependencies": [ + "@janhq/core" + ] } diff --git a/server/tsconfig.json b/server/tsconfig.json index 2c4fc4a64..dd27b8932 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -20,5 +20,5 @@ // "sourceMap": true, "include": ["./**/*.ts"], - "exclude": ["core", "build", "dist", "tests", "node_modules"] + "exclude": ["core", "build", "dist", "tests", "node_modules", "extensions"] } diff --git a/web/containers/Layout/BottomBar/DownloadingState/index.tsx b/web/containers/Layout/BottomBar/DownloadingState/index.tsx index 7aef36caf..c7191d0b9 100644 --- a/web/containers/Layout/BottomBar/DownloadingState/index.tsx +++ b/web/containers/Layout/BottomBar/DownloadingState/index.tsx @@ -13,22 +13,22 @@ import { import { useAtomValue } from 'jotai' import useDownloadModel from '@/hooks/useDownloadModel' -import { useDownloadState } from '@/hooks/useDownloadState' +import { modelDownloadStateAtom } from '@/hooks/useDownloadState' import { formatDownloadPercentage } from '@/utils/converter' -import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom' +import { getDownloadingModelAtom } from '@/helpers/atoms/Model.atom' export default function DownloadingState() { - const { downloadStates } = useDownloadState() - const downloadingModels = useAtomValue(downloadingModelsAtom) + const downloadStates = useAtomValue(modelDownloadStateAtom) + const downloadingModels = useAtomValue(getDownloadingModelAtom) const { abortModelDownload } = useDownloadModel() - const totalCurrentProgress = downloadStates + const totalCurrentProgress = Object.values(downloadStates) .map((a) => a.size.transferred + a.size.transferred) .reduce((partialSum, a) => partialSum + a, 0) - const totalSize = downloadStates + const totalSize = Object.values(downloadStates) .map((a) => a.size.total + a.size.total) .reduce((partialSum, a) => partialSum + a, 0) @@ -36,12 +36,14 @@ export default function DownloadingState() { return ( - {downloadStates?.length > 0 && ( + {Object.values(downloadStates)?.length > 0 && (
Downloading model - {downloadStates.map((item, i) => { - return ( -
- -
-
-

{item?.modelId}

- {formatDownloadPercentage(item?.percent)} -
- + {Object.values(downloadStates).map((item, i) => ( +
+ +
+
+

{item?.modelId}

+ {formatDownloadPercentage(item?.percent)}
+
- ) - })} +
+ ))} )} diff --git a/web/containers/Layout/BottomBar/index.tsx b/web/containers/Layout/BottomBar/index.tsx index 32dc70c70..5021b821c 100644 --- a/web/containers/Layout/BottomBar/index.tsx +++ b/web/containers/Layout/BottomBar/index.tsx @@ -25,8 +25,7 @@ import { MainViewState } from '@/constants/screens' import { useActiveModel } from '@/hooks/useActiveModel' -import { useDownloadState } from '@/hooks/useDownloadState' - +import { modelDownloadStateAtom } from '@/hooks/useDownloadState' import useGetSystemResources from '@/hooks/useGetSystemResources' import { useMainViewState } from '@/hooks/useMainViewState' @@ -53,7 +52,7 @@ const BottomBar = () => { const downloadedModels = useAtomValue(downloadedModelsAtom) const { setMainViewState } = useMainViewState() - const { downloadStates } = useDownloadState() + const downloadStates = useAtomValue(modelDownloadStateAtom) const setShowSelectModelModal = useSetAtom(showSelectModelModalAtom) const [serverEnabled] = useAtom(serverEnabledAtom) @@ -109,7 +108,7 @@ const BottomBar = () => { )} {downloadedModels.length === 0 && !stateModel.loading && - downloadStates.length === 0 && ( + Object.values(downloadStates).length === 0 && ( - ) - - if (isDownloaded) { - downloadButton = ( - - ) - } - - if (downloadState != null && downloadStates.length > 0) { - downloadButton = - } - - return ( -
-
- - {model.name} - -
-
-
- {downloadButton} -
-
- ) -} - -export default ModelVersionItem diff --git a/web/screens/ExploreModels/ModelVersionList/index.tsx b/web/screens/ExploreModels/ModelVersionList/index.tsx deleted file mode 100644 index 7992b7a51..000000000 --- a/web/screens/ExploreModels/ModelVersionList/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Model } from '@janhq/core' - -import ModelVersionItem from '../ModelVersionItem' - -type Props = { - models: Model[] - recommendedVersion: string -} - -export default function ModelVersionList({ - models, - recommendedVersion, -}: Props) { - return ( -
- {models.map((model) => ( - - ))} -
- ) -} diff --git a/web/services/restService.ts b/web/services/restService.ts index 25488ae15..6b749fd71 100644 --- a/web/services/restService.ts +++ b/web/services/restService.ts @@ -3,6 +3,7 @@ import { AppRoute, DownloadRoute, ExtensionRoute, + FileManagerRoute, FileSystemRoute, } from '@janhq/core' @@ -22,6 +23,7 @@ export const APIRoutes = [ route: r, })), ...Object.values(FileSystemRoute).map((r) => ({ path: `fs`, route: r })), + ...Object.values(FileManagerRoute).map((r) => ({ path: `fs`, route: r })), ] // Define the restAPI object with methods for each API route @@ -50,4 +52,6 @@ export const restAPI = { } }, {}), openExternalUrl, + // Jan Server URL + baseApiUrl: API_BASE_URL, } diff --git a/web/types/downloadState.d.ts b/web/types/downloadState.d.ts index cca526bf1..766a0bde5 100644 --- a/web/types/downloadState.d.ts +++ b/web/types/downloadState.d.ts @@ -1,12 +1,13 @@ type DownloadState = { modelId: string + filename: string time: DownloadTime speed: number percent: number size: DownloadSize - isFinished?: boolean children?: DownloadState[] error?: string + downloadState: 'downloading' | 'error' | 'end' } type DownloadTime = { From 9a1b1adc72961b37fe6d23b6c26de60b91fb2924 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Wed, 7 Feb 2024 18:07:12 +0700 Subject: [PATCH 49/63] fix: update conditional check last status message (#1951) * fix: update conditional check last status message * fix: no new message on failed message --------- Co-authored-by: Louis --- web/containers/Providers/EventHandler.tsx | 6 +++--- web/helpers/atoms/ChatMessage.atom.ts | 14 ++++++++------ web/hooks/useCreateNewThread.ts | 7 +++++-- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/web/containers/Providers/EventHandler.tsx b/web/containers/Providers/EventHandler.tsx index f22ed1bc7..cfd2c5629 100644 --- a/web/containers/Providers/EventHandler.tsx +++ b/web/containers/Providers/EventHandler.tsx @@ -128,10 +128,10 @@ export default function EventHandler({ children }: { children: ReactNode }) { const thread = threadsRef.current?.find((e) => e.id == message.thread_id) if (thread) { - const messageContent = message.content[0]?.text.value ?? '' + const messageContent = message.content[0]?.text?.value const metadata = { ...thread.metadata, - lastMessage: messageContent, + ...(messageContent && { lastMessage: messageContent }), } updateThread({ @@ -151,7 +151,7 @@ export default function EventHandler({ children }: { children: ReactNode }) { ?.addNewMessage(message) } }, - [updateMessage, updateThreadWaiting, setIsGeneratingResponse] + [updateMessage, updateThreadWaiting, setIsGeneratingResponse, updateThread] ) useEffect(() => { diff --git a/web/helpers/atoms/ChatMessage.atom.ts b/web/helpers/atoms/ChatMessage.atom.ts index b11e8f3be..45cc773e6 100644 --- a/web/helpers/atoms/ChatMessage.atom.ts +++ b/web/helpers/atoms/ChatMessage.atom.ts @@ -70,11 +70,12 @@ export const addNewMessageAtom = atom( set(chatMessages, newData) // Update thread last message - set( - updateThreadStateLastMessageAtom, - newMessage.thread_id, - newMessage.content - ) + if (newMessage.content.length) + set( + updateThreadStateLastMessageAtom, + newMessage.thread_id, + newMessage.content + ) } ) @@ -131,7 +132,8 @@ export const updateMessageAtom = atom( newData[conversationId] = updatedMessages set(chatMessages, newData) // Update thread last message - set(updateThreadStateLastMessageAtom, conversationId, text) + if (text.length) + set(updateThreadStateLastMessageAtom, conversationId, text) } } ) diff --git a/web/hooks/useCreateNewThread.ts b/web/hooks/useCreateNewThread.ts index 12a5e04ca..406bf8f74 100644 --- a/web/hooks/useCreateNewThread.ts +++ b/web/hooks/useCreateNewThread.ts @@ -6,6 +6,7 @@ import { ThreadAssistantInfo, ThreadState, Model, + MessageStatus, } from '@janhq/core' import { atom, useAtomValue, useSetAtom } from 'jotai' @@ -20,6 +21,7 @@ import useSetActiveThread from './useSetActiveThread' import { extensionManager } from '@/extension' +import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom' import { threadsAtom, threadStatesAtom, @@ -51,6 +53,7 @@ export const useCreateNewThread = () => { const setFileUpload = useSetAtom(fileUploadAtom) const setSelectedModel = useSetAtom(selectedModelAtom) const setThreadModelParams = useSetAtom(setThreadModelParamsAtom) + const messages = useAtomValue(getCurrentChatMessagesAtom) const { recommendedModel, downloadedModels } = useRecommendedModel() @@ -63,9 +66,9 @@ export const useCreateNewThread = () => { const defaultModel = model ?? recommendedModel ?? downloadedModels[0] // check last thread message, if there empty last message use can not create thread - const lastMessage = threads[0]?.metadata?.lastMessage + const lastMessage = threads[threads.length - 1]?.metadata?.lastMessage - if (!lastMessage && threads.length) { + if (!lastMessage && threads.length && !messages.length) { return null } From 44a7f0590b11ef00acd90c1d7db5bc987cc026d7 Mon Sep 17 00:00:00 2001 From: Hoang Ha <64120343+hahuyhoang411@users.noreply.github.com> Date: Wed, 7 Feb 2024 18:12:32 +0700 Subject: [PATCH 50/63] fix: authors.yml --- docs/blog/authors.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/blog/authors.yml b/docs/blog/authors.yml index fe344cacb..482ef0788 100644 --- a/docs/blog/authors.yml +++ b/docs/blog/authors.yml @@ -35,8 +35,14 @@ Van-QA: hahuyhoang411: name: Rex Ha - title: LLM Researcher & Content writer + title: LLM Researcher & Content Writer url: https://github.com/hahuyhoang411 image_url: https://avatars.githubusercontent.com/u/64120343?v=4 email: rex@jan.ai - + +automaticcat: + name: Alan Dao + title: AI Engineer + url: https://github.com/tikikun + image_url: https://avatars.githubusercontent.com/u/22268502?v=4 + email: alan@jan.ai From fd36310bb3a17d27c857125479857b200d343b34 Mon Sep 17 00:00:00 2001 From: hieu-jan <150573299+hieu-jan@users.noreply.github.com> Date: Wed, 7 Feb 2024 18:23:35 +0700 Subject: [PATCH 51/63] feat: integrate umami script locally --- web/public/umami_script.js | 148 +++++++++++++++++++++++++++++++++++++ web/utils/umami.tsx | 86 ++++++++++++++------- 2 files changed, 209 insertions(+), 25 deletions(-) create mode 100644 web/public/umami_script.js diff --git a/web/public/umami_script.js b/web/public/umami_script.js new file mode 100644 index 000000000..f9b5b7fcf --- /dev/null +++ b/web/public/umami_script.js @@ -0,0 +1,148 @@ +! function() { + "use strict"; + ! function(t) { + var e = t.screen, + n = e.width, + r = e.height, + a = t.navigator.language, + i = t.location, + o = t.localStorage, + u = t.document, + c = t.history, + f = "jan.ai", + s = "main page", + l = i.search, + d = u.currentScript; + if (d) { + var m = "data-", + h = d.getAttribute.bind(d), + v = h(m + "website-id"), + p = h(m + "host-url"), + g = "false" !== h(m + "auto-track"), + y = h(m + "do-not-track"), + b = h(m + "domains") || "", + S = b.split(",").map((function(t) { + return t.trim() + })), + k = (p ? p.replace(/\/$/, "") : d.src.split("/").slice(0, -1).join("/")) + "/api/send", + w = n + "x" + r, + N = /data-umami-event-([\w-_]+)/, + T = m + "umami-event", + j = 300, + A = function(t, e, n) { + var r = t[e]; + return function() { + for (var e = [], a = arguments.length; a--;) e[a] = arguments[a]; + return n.apply(null, e), r.apply(t, e) + } + }, + x = function() { + return { + website: v, + hostname: f, + screen: w, + language: a, + title: M, + url: I, + referrer: J + } + }, + E = function() { + return o && o.getItem("umami.disabled") || y && function() { + var e = t.doNotTrack, + n = t.navigator, + r = t.external, + a = "msTrackingProtectionEnabled", + i = e || n.doNotTrack || n.msDoNotTrack || r && a in r && r[a](); + return "1" == i || "yes" === i + }() || b && !S.includes(f) + }, + O = function(t, e, n) { + n && (J = I, (I = function(t) { + try { + return new URL(t).pathname + } catch (e) { + return t + } + }(n.toString())) !== J && setTimeout(D, j)) + }, + L = function(t, e) { + if (void 0 === e && (e = "event"), !E()) { + var n = { + "Content-Type": "application/json" + }; + return void 0 !== K && (n["x-umami-cache"] = K), fetch(k, { + method: "POST", + body: JSON.stringify({ + type: e, + payload: t + }), + headers: n + }).then((function(t) { + return t.text() + })).then((function(t) { + return K = t + })).catch((function() {})) + } + }, + D = function(t, e) { + return L("string" == typeof t ? Object.assign({}, x(), { + name: t, + data: "object" == typeof e ? e : void 0 + }) : "object" == typeof t ? t : "function" == typeof t ? t(x()) : x()) + }; + t.umami || (t.umami = { + track: D, + identify: function(t) { + return L(Object.assign({}, x(), { + data: t + }), "identify") + } + }); + var K, P, _, q, C, I = "" + s + l, + J = u.referrer, + M = u.title; + if (g && !E()) { + c.pushState = A(c, "pushState", O), c.replaceState = A(c, "replaceState", O), C = function(t) { + var e = t.getAttribute.bind(t), + n = e(T); + if (n) { + var r = {}; + return t.getAttributeNames().forEach((function(t) { + var n = t.match(N); + n && (r[n[1]] = e(t)) + })), D(n, r) + } + return Promise.resolve() + }, u.addEventListener("click", (function(t) { + var e = t.target, + n = "A" === e.tagName ? e : function(t, e) { + for (var n = t, r = 0; r < e; r++) { + if ("A" === n.tagName) return n; + if (!(n = n.parentElement)) return null + } + return null + }(e, 10); + if (n) { + var r = n.href, + a = "_blank" === n.target || t.ctrlKey || t.shiftKey || t.metaKey || t.button && 1 === t.button; + if (n.getAttribute(T) && r) return a || t.preventDefault(), C(n).then((function() { + a || (i.href = r) + })) + } else C(e) + }), !0), _ = new MutationObserver((function(t) { + var e = t[0]; + M = e && e.target ? e.target.text : void 0 + })), (q = u.querySelector("head > title")) && _.observe(q, { + subtree: !0, + characterData: !0, + childList: !0 + }); + var R = function() { + "complete" !== u.readyState || P || (D(), P = !0) + }; + u.addEventListener("readystatechange", R, !0), R() + } + } + }(window) +}(); \ No newline at end of file diff --git a/web/utils/umami.tsx b/web/utils/umami.tsx index 277ae1223..9e678d77f 100644 --- a/web/utils/umami.tsx +++ b/web/utils/umami.tsx @@ -1,31 +1,67 @@ import { useEffect } from 'react' -const Umami = () => { - useEffect(() => { - if (!VERSION || !ANALYTICS_HOST || !ANALYTICS_ID) return - fetch(ANALYTICS_HOST, { - method: 'POST', - // eslint-disable-next-line @typescript-eslint/naming-convention - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - payload: { - website: ANALYTICS_ID, - hostname: 'jan.ai', - screen: `${screen.width}x${screen.height}`, - language: navigator.language, - referrer: 'index.html', - data: { version: VERSION }, - type: 'event', - title: document.title, - url: 'index.html', - name: VERSION, - }, - type: 'event', - }), - }) - }, []) +import Script from 'next/script' - return <> +// Define the type for the umami data object +interface UmamiData { + version: string +} + +declare global { + interface Window { + umami: + | { + track: (event: string, data?: UmamiData) => void + } + | undefined + } +} + +const Umami = () => { + const appVersion = VERSION + const analyticsScriptPath = './umami_script.js' + const analyticsId = ANALYTICS_ID + + useEffect(() => { + if (!appVersion || !analyticsScriptPath || !analyticsId) return + + const ping = () => { + // Check if umami is defined before ping + if (window.umami !== null && typeof window.umami !== 'undefined') { + window.umami.track(appVersion, { + version: appVersion, + }) + } + } + + // Wait for umami to be defined before ping + if (window.umami !== null && typeof window.umami !== 'undefined') { + ping() + } else { + // Listen for umami script load event + document.addEventListener('umami:loaded', ping) + } + + // Cleanup function to remove event listener if the component unmounts + return () => { + document.removeEventListener('umami:loaded', ping) + } + }, [appVersion, analyticsScriptPath, analyticsId]) + + return ( + <> + {appVersion && analyticsScriptPath && analyticsId && ( +