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
diff --git a/README.md b/README.md
index 34eecc9f3..e1f74ef23 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
| Experimental (Nightly Build) |
-
+
jan.exe
|
-
+
Intel
|
-
+
M1/M2
|
-
+
jan.deb
|
-
+
jan.AppImage
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/docs/blog/authors.yml b/docs/blog/authors.yml
index 1a62bd7d7..728b0a7da 100644
--- a/docs/blog/authors.yml
+++ b/docs/blog/authors.yml
@@ -4,10 +4,18 @@ dan-jan:
url: https://github.com/dan-jan
image_url: https://avatars.githubusercontent.com/u/101145494?v=4
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
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
+ email: ashley@jan.ai
+
\ No newline at end of file
diff --git a/docs/docs/about/01-README.md b/docs/docs/about/01-README.md
index 3b2759513..d5d3b8dc2 100644
--- a/docs/docs/about/01-README.md
+++ b/docs/docs/about/01-README.md
@@ -110,9 +110,10 @@ 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.
+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.
#### Request for help
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..110f62e36
--- /dev/null
+++ b/docs/docs/developer/01-overview/04-install-and-prerequisites.md
@@ -0,0 +1,79 @@
+---
+title: Installation and Prerequisites
+slug: /developer/prereq
+description: Guide to install and setup Jan for development.
+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
+
+Ensure your system meets the following specifications to guarantee a smooth development experience:
+
+- [Hardware Requirements](../../guides/02-installation/06-hardware.md)
+
+### 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
+
+- [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:**
+
+```bash
+git clone https://github.com/janhq/jan
+cd jan
+git checkout -b DESIRED_BRANCH
+```
+
+2. **Install Dependencies**
+
+```bash
+yarn install
+```
+
+3. **Run Development and Use Jan Desktop**
+
+```bash
+make dev
+```
+
+This command starts the development server and opens 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 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.
diff --git a/docs/docs/guides/02-installation/01-mac.md b/docs/docs/guides/02-installation/01-mac.md
index 8e67b5bed..7a3961384 100644
--- a/docs/docs/guides/02-installation/01-mac.md
+++ b/docs/docs/guides/02-installation/01-mac.md
@@ -12,11 +12,16 @@ keywords:
conversational AI,
no-subscription fee,
large language model,
+ installation guide,
]
---
# Installing Jan on MacOS
+## System 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..d60ab86f7 100644
--- a/docs/docs/guides/02-installation/02-windows.md
+++ b/docs/docs/guides/02-installation/02-windows.md
@@ -12,11 +12,23 @@ keywords:
conversational AI,
no-subscription fee,
large language model,
+ installation guide,
]
---
# 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 +71,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..0ec7fea60 100644
--- a/docs/docs/guides/02-installation/03-linux.md
+++ b/docs/docs/guides/02-installation/03-linux.md
@@ -12,11 +12,24 @@ keywords:
conversational AI,
no-subscription fee,
large language model,
+ installation guide,
]
---
# Installing Jan on Linux
+## System 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 +79,6 @@ jan-linux-amd64-{version}.deb
# AppImage
jan-linux-x86_64-{version}.AppImage
```
-```
## Uninstall Jan
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.
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:
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 '<', "/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 share with us the [app logs](https://jan.ai/troubleshooting/how-to-get-error-logs/) via [Jan Discord](https://discord.gg/mY69SZaMaC).
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
diff --git a/docs/openapi/jan.yaml b/docs/openapi/jan.yaml
index bfff0ad73..864c80fdf 100644
--- a/docs/openapi/jan.yaml
+++ b/docs/openapi/jan.yaml
@@ -67,20 +67,31 @@ paths:
x-codeSamples:
- lang: cURL
source: |
- curl http://localhost:1337/v1/chat/completions \
- -H "Content-Type: application/json" \
+ curl -X 'POST' \
+ 'http://localhost: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."
+ "content": "You are a helpful assistant.",
+ "role": "system"
},
{
- "role": "user",
- "content": "Hello!"
+ "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:
@@ -103,7 +114,9 @@ paths:
x-codeSamples:
- lang: cURL
source: |
- curl http://localhost:1337/v1/models
+ curl -X 'GET' \
+ 'http://localhost: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://localhost: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://localhost: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://localhost:1337/v1/models/{model_id}' \
+ -H 'accept: application/json'
/threads:
post:
operationId: createThread
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..cfa391598 100644
--- a/docs/openapi/specs/chat.yaml
+++ b/docs/openapi/specs/chat.yaml
@@ -188,4 +188,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..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
@@ -309,4 +310,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 8113f3ab8..40e6abaaf 100644
--- a/docs/openapi/specs/models.yaml
+++ b/docs/openapi/specs/models.yaml
@@ -18,114 +18,82 @@ components:
Model:
type: object
properties:
- type:
+ source_url:
type: string
- default: model
- description: The type of the object.
- version:
- type: string
- default: "1"
- description: The version number of the model.
+ format: uri
+ description: URL to the source of the model.
+ 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
+ description:
+ Unique identifier used in chat-completions model_name, matches
folder name.
- example: zephyr-7b
+ example: trinity-v1.2-7b
+ object:
+ type: string
+ example: model
name:
type: string
description: Name of the model.
- example: Zephyr 7B
- owned_by:
+ example: Trinity-v1.2 7B Q4
+ version:
type: string
- description: Compatibility field for OpenAI.
- default: ""
- created:
- type: integer
- format: int64
- description: Unix timestamp representing the creation time.
+ default: "1.0"
+ description: The version number of the model.
description:
type: string
description: Description of the model.
- state:
- type: string
- enum:
- - null
- - downloading
- - ready
- - starting
- - stopping
- description: Current state 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: ggufv3
- source:
- type: array
- items:
- type: object
- properties:
- url:
- 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
- filename:
- type: string
- description: Filename of the model.
- example: zephyr-7b-beta.Q4_K_M.gguf
+ example: gguf
settings:
type: object
properties:
ctx_len:
- type: string
+ type: integer
description: Context length.
- example: "4096"
- 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: "4096"
- 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
+ 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:
@@ -133,7 +101,7 @@ components:
type: string
description: |
The identifier of the model.
- example: zephyr-7b
+ example: trinity-v1.2-7b
object:
type: string
description: |
@@ -153,197 +121,89 @@ components:
GetModelResponse:
type: object
properties:
+ source_url:
+ type: string
+ format: uri
+ description: URL to the source of the model.
+ 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: The identifier of the model.
- example: zephyr-7b
+ description:
+ Unique identifier used in chat-completions model_name, matches
+ folder name.
+ example: mistral-ins-7b-q4
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:
+ example: model
+ name:
type: string
- description: The entity that owns the model.
- example: _
- state:
+ description: Name of the model.
+ example: Mistral Instruct 7B Q4
+ version:
type: string
- enum:
- - not_downloaded
- - downloaded
- - running
- - stopped
- description: The current state of the model.
- source:
- type: array
- items:
- type: object
- properties:
- url:
- 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
- filename:
- type: string
- description: Filename of the model.
- example: 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: 4096
- example: 4096
- 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:
+ 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: 4096
+ description: Context length.
example: 4096
- 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
+ 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: 4096
- example: 4096
- 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
- - 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
+ example: mistral-ins-7b-q4
object:
type: string
description: Type of the object, indicating it's a model.
diff --git a/docs/openapi/specs/threads.yaml b/docs/openapi/specs/threads.yaml
index fe00f7588..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
@@ -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
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 })
-})
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/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 2b0021ba0..9e96ad93f 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);
@@ -251,7 +268,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/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 6e334b9ef..32dc70c70 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 = [
{
@@ -47,14 +48,22 @@ const menuLinks = [
const BottomBar = () => {
const { activeModel, stateModel } = useActiveModel()
- const { ram, cpu } = useGetSystemResources()
+ const { ram, cpu, gpus } = useGetSystemResources()
const progress = useAtomValue(appDownloadProgress)
- const { downloadedModels } = useGetDownloadedModels()
+ const downloadedModels = useAtomValue(downloadedModelsAtom)
+
const { setMainViewState } = useMainViewState()
const { downloadStates } = useDownloadState()
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 (
@@ -117,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/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..f22ed1bc7 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,16 +28,18 @@ import {
addNewMessageAtom,
updateMessageAtom,
} from '@/helpers/atoms/ChatMessage.atom'
+import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
import {
updateThreadWaitingForResponseAtom,
threadsAtom,
isGeneratingResponseAtom,
+ updateThreadAtom,
} from '@/helpers/atoms/Thread.atom'
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)
@@ -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({
@@ -143,7 +151,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/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"
>
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/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/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/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/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..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,17 +52,30 @@ 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(() => {
getSystemResources()
- }, 500)
+ }, 5000)
// clean up interval
return () => clearInterval(intervalId)
@@ -69,5 +86,6 @@ export default function useGetSystemResources() {
totalRamAtom,
ram,
cpu,
+ gpus,
}
}
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/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/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
+
+ 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/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}
+
+ ) : (
+
+ )}
>
diff --git a/web/screens/Chat/ThreadList/index.tsx b/web/screens/Chat/ThreadList/index.tsx
index b4a045b1d..2ad9a28c4 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,46 @@ 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
-
- 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
+ ? 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..17b897d51 100644
--- a/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx
+++ b/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx
@@ -27,15 +27,18 @@ 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 { totalRamAtom } from '@/helpers/atoms/SystemBar.atom'
+import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
+import {
+ nvidiaTotalVramAtom,
+ totalRamAtom,
+} from '@/helpers/atoms/SystemBar.atom'
type Props = {
model: Model
@@ -49,7 +52,14 @@ 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)
const downloadAtom = useMemo(
() => atom((get) => get(modelDownloadStateAtom)[model.id]),
@@ -60,17 +70,23 @@ 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
let downloadButton = (
-
+
)
const onUseModelClick = useCallback(async () => {
- const assistants = await getAssistants()
if (assistants.length === 0) {
alert('No assistant available')
return
@@ -107,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/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 (
{
} = 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 */}
diff --git a/web/screens/Settings/Models/index.tsx b/web/screens/Settings/Models/index.tsx
index 3c5a0c6e3..f8997e751 100644
--- a/web/screens/Settings/Models/index.tsx
+++ b/web/screens/Settings/Models/index.tsx
@@ -2,16 +2,17 @@ import { useState } from 'react'
import { Input } from '@janhq/uikit'
+import { useAtomValue } from 'jotai'
import { SearchIcon } from 'lucide-react'
-import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
-
import RowModel from './Row'
+import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
+
const Column = ['Name', 'Model ID', 'Size', 'Version', 'Status', '']
export default function Models() {
- const { downloadedModels } = useGetDownloadedModels()
+ const downloadedModels = useAtomValue(downloadedModelsAtom)
const [searchValue, setsearchValue] = useState('')
const filteredDownloadedModels = downloadedModels.filter((x) => {
|