Merge branch 'dev' into chore/get-to-3.5-performance

This commit is contained in:
Henry 2024-03-01 22:46:02 +09:00 committed by GitHub
commit 1e4474df01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
75 changed files with 562 additions and 560 deletions

View File

@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
<tr style="text-align:center"> <tr style="text-align:center">
<td style="text-align:center"><b>Experimental (Nightly Build)</b></td> <td style="text-align:center"><b>Experimental (Nightly Build)</b></td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-win-x64-0.4.7-289.exe'> <a href='https://delta.jan.ai/latest/jan-win-x64-0.4.7-293.exe'>
<img src='./docs/static/img/windows.png' style="height:14px; width: 14px" /> <img src='./docs/static/img/windows.png' style="height:14px; width: 14px" />
<b>jan.exe</b> <b>jan.exe</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-mac-x64-0.4.7-289.dmg'> <a href='https://delta.jan.ai/latest/jan-mac-x64-0.4.7-293.dmg'>
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" /> <img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
<b>Intel</b> <b>Intel</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-mac-arm64-0.4.7-289.dmg'> <a href='https://delta.jan.ai/latest/jan-mac-arm64-0.4.7-293.dmg'>
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" /> <img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
<b>M1/M2</b> <b>M1/M2</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-linux-amd64-0.4.7-289.deb'> <a href='https://delta.jan.ai/latest/jan-linux-amd64-0.4.7-293.deb'>
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" /> <img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.deb</b> <b>jan.deb</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-linux-x86_64-0.4.7-289.AppImage'> <a href='https://delta.jan.ai/latest/jan-linux-x86_64-0.4.7-293.AppImage'>
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" /> <img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.AppImage</b> <b>jan.AppImage</b>
</a> </a>

View File

@ -49,7 +49,7 @@ export enum DownloadEvent {
export enum LocalImportModelEvent { export enum LocalImportModelEvent {
onLocalImportModelUpdate = 'onLocalImportModelUpdate', onLocalImportModelUpdate = 'onLocalImportModelUpdate',
onLocalImportModelError = 'onLocalImportModelError', onLocalImportModelFailed = 'onLocalImportModelFailed',
onLocalImportModelSuccess = 'onLocalImportModelSuccess', onLocalImportModelSuccess = 'onLocalImportModelSuccess',
onLocalImportModelFinished = 'onLocalImportModelFinished', onLocalImportModelFinished = 'onLocalImportModelFinished',
} }

View File

@ -65,7 +65,7 @@ const joinPath: (paths: string[]) => Promise<string> = (paths) => global.core.ap
* @param path - The path to retrieve. * @param path - The path to retrieve.
* @returns {Promise<string>} A promise that resolves with the basename. * @returns {Promise<string>} A promise that resolves with the basename.
*/ */
const baseName: (paths: string[]) => Promise<string> = (path) => global.core.api?.baseName(path) const baseName: (paths: string) => Promise<string> = (path) => global.core.api?.baseName(path)
/** /**
* Opens an external URL in the default web browser. * Opens an external URL in the default web browser.

View File

@ -42,6 +42,24 @@ export class Downloader implements Processor {
// Downloading file to a temp file first // Downloading file to a temp file first
const downloadingTempFile = `${destination}.download` const downloadingTempFile = `${destination}.download`
// adding initial download state
const initialDownloadState: DownloadState = {
modelId,
fileName,
time: {
elapsed: 0,
remaining: 0,
},
speed: 0,
percent: 0,
size: {
total: 0,
transferred: 0,
},
downloadState: 'downloading',
}
DownloadManager.instance.downloadProgressMap[modelId] = initialDownloadState
progress(rq, {}) progress(rq, {})
.on('progress', (state: any) => { .on('progress', (state: any) => {
const downloadState: DownloadState = { const downloadState: DownloadState = {

View File

@ -19,4 +19,5 @@ export type ImportingModel = {
status: ImportingModelStatus status: ImportingModelStatus
format: string format: string
percentage?: number percentage?: number
error?: string
} }

View File

@ -29,3 +29,18 @@ keywords:
## Careers ## Careers
- [Jobs](https://janai.bamboohr.com/careers) - [Jobs](https://janai.bamboohr.com/careers)
## Newsletter
<iframe
width="100%"
height="600px"
src="https://c0c7c086.sibforms.com/serve/MUIFAEWm49nC1OONIibGnlV44yxPMw6Fu1Yc8pK7nP3jp7rZ6rvrb5uOmCD8IIhrRj6-h-_AYrw-sz7JNpcUZ8LAAZoUIOjGmSvNWHwoFhxX5lb-38-fxXj933yIdGzEMBZJv4Nu2BqC2A4uThDGmjM-n_DZBV1v_mKbTcVUWVUE7VutWhRqrDr69IWI4SgbuIMACkcTiWX8ZNLw"
frameborder="0"
scrolling="auto"
allowfullscreen
style={{
margin: 'auto',
maxWidth: '100%',
}}
></iframe>

View File

@ -108,7 +108,7 @@ sudo sh ./get-docker.sh --dry-run
```bash ```bash
# GPU mode with default file system # GPU mode with default file system
docker compose --profile gpu up -d docker compose --profile gpu-fs up -d
# GPU mode with S3 file system # GPU mode with S3 file system
docker compose --profile gpu-s3fs up -d docker compose --profile gpu-s3fs up -d

View File

@ -1,7 +1,7 @@
--- ---
title: Antivirus Compatibility Testing title: Antivirus Testing
slug: /guides/install/antivirus-compatibility-testing slug: /guides/install/antivirus-compatibility-testing
description: Antivirus compatibility testing documentation for the Jan App v0.4.4 release. description: Antivirus compatibility testing documentation
keywords: keywords:
[ [
Jan AI, Jan AI,
@ -16,18 +16,16 @@ keywords:
] ]
--- ---
This documentation outlines the antivirus compatibility testing conducted for the Jan App v0.4.4 release. This documentation includes a matrix that correlates the Jan App version with the tested antivirus versions. As a part of our release process, we run antivirus compatibility tests for Jan v0.4.4 and onwards. This documentation includes a matrix that correlates the Jan App version with the tested antivirus versions.
## Tested Antivirus Versions ## Antivirus Software Tested
The Jan App v0.4.4 release has undergone automatic testing through CI with a selection of popular antivirus software to ensure compatibility and safety. The following summarizes the testing results: The following summarizes ongoing testing targets:
| Antivirus | Version | Result | | Antivirus | Version | Target Result |
| ------------------ | ------------ | -------------------------------- | | ------------------ | ------------ | -------------------------------- |
| Bitdefender | 27.0.27.125 | Scanned and 0 threat(s) detected | | Bitdefender | 27.0.27.125 | Scanned and 0 threat(s) detected |
| McAfee | 4.21.0.0 | Scanned and 0 threat(s) detected | | McAfee | 4.21.0.0 | Scanned and 0 threat(s) detected |
| Microsoft Defender | 1.403.2259.0 | Scanned and 0 threat(s) detected | | Microsoft Defender | 1.403.2259.0 | Scanned and 0 threat(s) detected |
## Conclusion To report issues, false positives, or to request additional testing, please email devops@jan.ai
The testing indicates that Jan App v0.4.4 is compatible with Bitdefender, Microsoft Defender, and McAfee. Any updates or changes to compatibility status will be promptly documented.

View File

@ -85,26 +85,28 @@ Edit the `config.json` file and include the following configuration.
``` ```
- Ensure that the `provider` is `openai`. - Ensure that the `provider` is `openai`.
- Ensure that the `model` is the same as the one you enabled in the Jan API Server. - Ensure that the `model` is the ID of the running model. You can check for the respective ID in System Monitor.
- Ensure that the `apiBase` is `http://localhost:1337/v1`. - Ensure that the `apiBase` is `http://localhost:1337/v1`.
- Ensure that the `apiKey` is `EMPTY`. - Ensure that the `apiKey` is `EMPTY`.
### 4. Ensure the Using Model Is Activated in Jan ### 4. Double Check the Model is Running
Navigate to `Settings` > `Models`. Activate the model that you want to use in Jan by clicking the **three dots (⋮)** and **start model**. Open up the `System Monitor` to check that your model is currently running.
If there are not active models, go to `Settings` > `My Models`. Click on the **three dots (⋮)** and **start model**.
![Active Models](assets/01-start-model.png) ![Active Models](assets/01-start-model.png)
### 5. Try Out the Integration of Jan and Continue in VS Code ### 5. Use Continue in VS Code
#### Asking questions about the code #### Asking questions about the code
- Highlight a code snippet and press `Command + Shift + M` to open the **Left Panel**. - Highlight a code snippet and press `Command + M` to open the Continue Extension in VSCode.
- Select Jan at the bottom and ask a question about the code, for example, `Explain this code`. - Select Jan at the bottom and ask a question about the code, for example, `Explain this code`.
![Continue Interactions](assets/01-continue-ask.png) ![Continue Interactions](assets/01-continue-ask.png)
#### Editing the code with the help of a large language model #### Editing the code directly
- Highlight a code snippet and press `Command + Shift + L` and input your edit request, for example, `Write comments for this code`. - Highlight a code snippet and press `Command + Shift + L` and input your edit request, for example, `Write comments for this code`.

View File

@ -1,8 +1,8 @@
# [Release Version] QA Script # Regression test
**Release Version:** v0.4.6 **Release Version:** v0.4.7
**Operating System:** **Operating System:** MacOS
--- ---
@ -10,78 +10,64 @@
### 1. Users install app ### 1. Users install app
- [ ] :key: Test for clear user installation instructions.
- [ ] :key: Verify that the installation path is correct for each OS.
- [ ] Check that the installation package is not corrupted and passes all security checks. - [ ] Check that the installation package is not corrupted and passes all security checks.
- [ ] Validate that the app is correctly installed in the default or user-specified directory. - [ ] :key: Confirm that the app launches successfully after installation.
- [ ] Ensure that all necessary dependencies are installed along with the app.
- [ ] :key: :rocket: Confirm that the app launches successfully after installation.
### 2. Users update app ### 2. Users update app
- [ ] :key: Test that the updated version includes the new features or fixes outlined in the update notes.
- [ ] :key: Validate that the update does not corrupt user data or settings. - [ ] :key: Validate that the update does not corrupt user data or settings.
- [ ] :key: Confirm that the app restarts or prompts the user to restart after an update. - [ ] :key: Confirm that the app restarts or prompts the user to restart after an update.
### 3. Users uninstall app
- [ ] :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. - [ ] 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 ### 3. Users uninstall / close app
- [ ] :key: Ensure that after closing the app, all models are unloaded. - [ ] :key: Ensure that after closing the app, all models are unloaded.
- [ ] :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.
## B. Overview ## B. Overview
### 1. Users use shortcut keys ### 1. Shortcut key, memory usage / CPU usage
- [ ] :key: Test each shortcut key to confirm it works as described (My models, navigating, opening, closing, etc.). - [ ] :key: Test each shortcut key to confirm it works as described (My models, navigating, opening, closing, etc.).
### 2. Users check the memory usage and CPU usage
- [ ] :key: Ensure that the interface presents the correct numbers for memory and CPU usage. - [ ] :key: Ensure that the interface presents the correct numbers for memory and CPU usage.
### 3. Users check the `active model` ### 2. Users check the `active model`
- [ ] :key: Verify that the app correctly displays the state of the loading model (e.g., loading, ready, error). - [ ] :key: Verify that the app correctly displays the state of the loading model (e.g., loading, ready, error).
- [ ] :key: Confirm that the app allows users to switch between models if multiple are available. - [ ] :key: Confirm that the app allows users to switch between models if multiple are available.
- [ ] Check that the app provides feedback or instructions if the model fails to load. - [ ] Check that the app provides feedback or instructions if the model fails to load.
- [ ] Verify the troubleshooting assistant correctly capture hardware / log info #1784
## C. Thread ## C. Thread
### 1. Users can chat with Jan, the default assistant ### 1. Users can chat with Jan, the default assistant
- [ ] Verify that the input box for messages is present and functional. - [ ] :key: Verify sending a message enables users to receive responses from model.
- [ ] :key: Check if typing a message and hitting `Send` results in the message appearing in the chat window.
- [ ] :key: Confirm that Jan, the default assistant, replies to user inputs.
- [ ] :key: Ensure that the conversation thread is maintained without any loss of data upon sending multiple messages. - [ ] :key: Ensure that the conversation thread is maintained without any loss of data upon sending multiple messages.
- [ ] Users should be able to edit msg and the assistant will re-generate the answer based on the edited version of the message.
- [ ] Test for the ability to send different types of messages (e.g., text, emojis, code blocks). - [ ] 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 copy the response.
- [ ] Check if the user can delete responses.
- [ ] :key: Check the `clear message` button works.
- [ ] :key: Check the `delete entire chat` works.
- [ ] Check if deleting all the chat retains the system prompt.
- [ ] Check the output format of the AI (code blocks, JSON, markdown, ...). - [ ] Check the output format of the AI (code blocks, JSON, markdown, ...).
- [ ] :key: Validate the scroll functionality in the chat window for lengthy conversations.
- [ ] Check if the user can copy / delete the response.
- [ ] :key: Check the `clear message` / `delete entire chat` button works.
- [ ] Check if deleting all the chat retains the system prompt.
- [ ] :key: Validate that there is appropriate error handling and messaging if the assistant fails to respond. - [ ] :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. - [ ] Test assistant's ability to maintain context over multiple exchanges.
- [ ] :key: Check the `create new chat` button works correctly - [ ] :key: Check the `create new chat` button, and new conversation will have an automatically generated thread title based on users msg.
- [ ] Confirm that by changing `models` mid-thread the app can still handle it. - [ ] Confirm that by changing `models` mid-thread the app can still handle it.
- [ ] Check the `regenerate` button renews the response (single / multiple times). - [ ] Check the `regenerate` button renews the response (single / multiple times).
- [ ] Check the `Instructions` update correctly after the user updates it midway (mid-thread). - [ ] 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 ### 2. Users can customize chat settings like model parameters via both the GUI & thread.json
- [ ] :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. - [ ] 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. - [ ] :key: Ensure that changes can be saved and persisted between sessions.
- [ ] Validate that users can access and modify the thread.json file. - [ ] 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. - [ ] :key: Check that changes made in thread.json are correctly applied to the chat session upon reload or restart.
- [ ] Check the maximum and minimum limits of the adjustable parameters and how they affect the assistant's responses. - [ ] 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. - [ ] :key: Ensure that users switch between threads with different models, the app can handle it.
### 3. Model dropdown ### 3. Model dropdown
@ -89,25 +75,16 @@
- [ ] Model size should display (for both installed and imported models) - [ ] Model size should display (for both installed and imported models)
### 4. Users can click on a history thread ### 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. - [ ] 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. - [ ] :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. - [ ] Validate that historical threads reflect the exact state of the chat at that time, including settings.
- [ ] :key: Verify the ability to delete or clean old threads. - [ ] :key: Verify the ability to delete or clean old threads.
- [ ] :key: Confirm that changing the title of the thread updates correctly. - [ ] Confirm that changing the title of the thread updates correctly.
### 5. 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. - [ ] 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. - [ ] :key: Validate that changes to instructions are updated in real time and do not require a restart of the application or session.
- [ ] :key: Confirm that the assistant's behavior changes in accordance with the new instructions provided.
- [ ] :key: Check for the ability to reset instructions to default or clear them completely. - [ ] :key: Check for the ability to reset instructions to default or clear them completely.
- [ ] :key: Test the feature that allows users to save custom sets of instructions for different scenarios.
- [ ] 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. - [ ] :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.
@ -115,7 +92,6 @@
### 1. Users can discover recommended models (Jan ships with a few preconfigured model.json files) ### 1. Users can discover recommended models (Jan ships with a few preconfigured model.json files)
- [ ] :key: Verify that recommended models are displayed prominently on the main page.
- [ ] :key: Ensure that each model's recommendations are consistent with the users activity and preferences. - [ ] :key: Ensure that each model's recommendations are consistent with the users activity and preferences.
- [ ] Test the functionality of any filters that refine model recommendations. - [ ] Test the functionality of any filters that refine model recommendations.
@ -123,7 +99,6 @@
- [ ] Display the best model for their RAM at the top. - [ ] Display the best model for their RAM at the top.
- [ ] :key: Ensure that models are labeled with RAM requirements and compatibility. - [ ] :key: Ensure that models are labeled with RAM requirements and compatibility.
- [ ] :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. - [ ] :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) ### 3. Users can download models via a HuggingFace URL (coming soon)
@ -132,22 +107,21 @@
- [ ] :key: Check the progress bar reflects the right process. - [ ] :key: Check the progress bar reflects the right process.
- [ ] Validate the error handling for invalid or inaccessible URLs. - [ ] Validate the error handling for invalid or inaccessible URLs.
### 4. Users can add a new model to the Hub ### 4. Users can import new models to the Hub
- [ ] :key: Have clear instructions so users can do their own. - [ ] :key: Ensure import successfully via drag / drop or upload GGUF.
- [ ] :key: Verify Move model binary file / Keep Original Files & Symlink option are working
- [ ] :warning: Ensure it raises clear errors for users to fix the problem while adding a new model.
- [ ] Users can add more info to the imported model / edit name
- [ ] :key: Ensure the new model updates after restarting the app. - [ ] :key: Ensure the new model updates after restarting the app.
- [ ] :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 ### 5. Users can use the model as they want
- [ ] :key: Check `start` button response exactly what it does. - [ ] :key: Check `start` / `stop` / `delete` button response exactly what it does.
- [ ] :key: Check `stop` button response exactly what it does.
- [ ] :key: Check `delete` button response exactly what it does.
- [ ] Check if starting another model stops the other model entirely. - [ ] Check if starting another model stops the other model entirely.
- [ ] Check the `Explore models` navigate correctly to the model panel. - [x] :rocket: 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. - [ ] :key: Check when deleting a model it will delete all the files on the user's computer.
- [ ] :warning: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 ### 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 - [ ] :key: Import openAI GPT model https://jan.ai/guides/using-models/integrate-with-remote-server/ and the model displayed in Hub / Thread dropdown
@ -166,53 +140,45 @@
- [ ] :key: Test the 'Start' action for a model to ensure it initiates and the system resource usage reflects this change. - [ ] :key: Test the 'Start' action for a model to ensure it initiates and the system resource usage reflects this change.
- [ ] :key: Verify the 'Stop' action for a model to confirm it ceases operation and frees up the system resources accordingly. - [ ] :key: Verify the 'Stop' action for a model to confirm it ceases operation and frees up the system resources accordingly.
- [ ] :key: Check the functionality that allows starting a model based on available system resources.
- [ ] :key: Validate that the system prevents starting a new model if it exceeds safe resource utilization thresholds.
- [ ] Ensure that the system provides warnings or recommendations when resource utilization is high before starting new models.
- [ ] Test the ease of accessing model settings from the system monitor for resource management.
- [ ] Confirm that any changes in model status (start/stop) are logged or reported to the user for transparency. - [ ] Confirm that any changes in model status (start/stop) are logged or reported to the user for transparency.
## F. Settings ## F. Settings
### 1. Users can set color themes and dark/ light modes ### 1. Appearance
- [ ] Verify that the theme setting is easily accessible in the `Appearance` tab.
- [ ] :key: Check that the theme change is reflected immediately upon selection.
- [ ] :key: Test the `Light`, `Dark`, and `System` theme settings to ensure they are functioning as expected. - [ ] :key: Test the `Light`, `Dark`, and `System` theme settings to ensure they are functioning as expected.
- [ ] Confirm that the application saves the theme preference and persists it across sessions. - [ ] 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. - [ ] Validate that all elements of the UI are compatible with the theme changes and maintain legibility and contrast.
### 2. Users change the extensions [TBU] ### 2. Extensions [TBU]
- [ ] Confirm that the `Extensions` tab lists all available plugins. - [ ] 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. - [x] :key: Test the toggle switch for each plugin to ensure it enables or disables the plugin correctly.
- [ ] Verify that plugin changes take effect without needing to restart the application unless specified. - [x] Verify that plugin changes take effect without needing to restart the application unless specified.
- [ ] :key: Check that the plugin's status (`Installed the latest version`) updates accurately after any changes. - [x] :key: Check that the plugin's status (`Installed the latest version`) updates accurately after any changes.
- [ ] Validate the `Manual Installation` process by selecting and installing a plugin file. - [x] Validate the `Manual Installation` process by selecting and installing a plugin file.
- [ ] Test for proper error handling and user feedback when a plugin installation fails. - [x] Test for proper error handling and user feedback when a plugin installation fails.
### 3. Users change the advanced settings ### 3. Users can add custom plugins via manual installation [TBU]
- [x] Verify that the `Manual Installation` option is clearly visible and accessible in the `Extensions` section.
- [x] Test the functionality of the `Select` button within the `Manual Installation` area.
- [x] :warning: Check that the file picker dialog allows for the correct plugin file types (e.g., .tgz).
- [x] :key: Validate that the selected plugin file installs correctly and the plugin becomes functional.
- [x] Ensure that there is a progress indicator or confirmation message once the installation is complete.
- [x] Confirm that if the installation is interrupted or fails, the user is given a clear error message.
- [x] :key: Test that the application prevents the installation of incompatible or corrupt plugin files.
- [x] :key: Check that the user can uninstall or disable custom plugins as easily as pre-installed ones.
- [x] Verify that the application's performance remains stable after the installation of custom plugins.
### 4. Advanced settings
- [ ] :key: Test the `Experimental Mode` toggle to confirm it enables or disables experimental features as intended. - [ ] :key: Test the `Experimental Mode` toggle to confirm it enables or disables experimental features as intended.
- [ ] :key: Check the functionality of `Open App Directory` to ensure it opens the correct folder in the system file explorer. - [ ] :key: Check the functionality of `Open App Directory` to ensure it opens the correct folder in the system file explorer.
- [ ] Validate that changes in advanced settings are applied immediately or provide appropriate instructions if a restart is needed.
- [ ] Test the application's stability when experimental features are enabled.
### 4. Users can add custom plugins via manual installation [TBU]
- [ ] Verify that the `Manual Installation` option is clearly visible and accessible in the `Extensions` section.
- [ ] Test the functionality of the `Select` button within the `Manual Installation` area.
- [ ] :warning: Check that the file picker dialog allows for the correct plugin file types (e.g., .tgz).
- [ ] :key: Validate that the selected plugin file installs correctly and the plugin becomes functional.
- [ ] Ensure that there is a progress indicator or confirmation message once the installation is complete.
- [ ] Confirm that if the installation is interrupted or fails, the user is given a clear error message.
- [ ] :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 move **Jan data folder**
- [ ] Validate that changes in advanced settings are applied immediately or provide appropriate instructions if a restart is needed.
- [ ] Attemp to test downloading model from hub using **HTTP Proxy** [guideline](https://github.com/janhq/jan/pull/1562)
- [ ] Logs that are older than 7 days or exceed 1MB in size will be automatically cleared upon starting the application.
- [ ] Users can click on Reset button to **factory reset** app settings to its original state & delete all usage data. - [ ] Users can click on Reset button to **factory reset** app settings to its original state & delete all usage data.
## G. Local API server ## G. Local API server

View File

@ -1,3 +1,95 @@
--- ---
title: Wall of Love ❤️ title: Wall of Love ❤️
--- ---
## Twitter
Check out our amazing users and what they are saying about Jan!
<div>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">I can confirm <a href="https://t.co/Hvrfp0iaf9">https://t.co/Hvrfp0iaf9</a> is awesome 👌</p>&mdash; Cristian (@cristianmoreno) <a href="https://twitter.com/cristianmoreno/status/1757504717519749292?ref_src=twsrc%5Etfw">February 13, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8" loading="lazy"></script>
</div>
<div>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">downloaded this a few weeks ago. amazed by the speed and quality</p>&mdash; siddharth (@siddharthd01) <a href="https://twitter.com/siddharthd01/status/1757500111629025788?ref_src=twsrc%5Etfw">February 13, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8" loading="lazy"></script>
</div>
<div>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Anyone else out there running LLMs on steam deck? <a href="https://twitter.com/janframework?ref_src=twsrc%5Etfw">@janframework</a> bringing nerd dreams to life! <a href="https://t.co/7XpnBmc8MN">pic.twitter.com/7XpnBmc8MN</a></p>&mdash; crossdefault (@crossdefault) <a href="https://twitter.com/crossdefault/status/1750801065132384302?ref_src=twsrc%5Etfw">January 26, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8" loading="lazy"></script>
</div>
<div>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">If you are like me, always wanting your own ChatGPT and have sufficient coding knowledge, you would watch open sourced <a href="https://twitter.com/janframework?ref_src=twsrc%5Etfw">@janframework</a> by <a href="https://twitter.com/0xSage?ref_src=twsrc%5Etfw">@0xSage</a> like a &quot;my-own-ai&quot; hawk<br></br>Still under development, the architecture is really futuristic. The desktop app for Windows, Mac, Linux are… <a href="https://t.co/0HrNquhBsL">pic.twitter.com/0HrNquhBsL</a></p>&mdash; Umesh = EG = Educated Guess - NGI doing AI (@trading_indian) <a href="https://twitter.com/trading_indian/status/1745560583548670250?ref_src=twsrc%5Etfw">January 11, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8" loading="lazy"></script>
</div>
<div>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">came across <a href="https://twitter.com/janframework?ref_src=twsrc%5Etfw">@janframework</a> yesterday and it&#39;s my fav native Apple Silicon LLM app yet. Love that I can switch to GPT 4 API and offline LLM models seamlessly. Looks promising! <a href="https://t.co/gyOX9gHbKQ">https://t.co/gyOX9gHbKQ</a></p>&mdash; Keith Hawkins (@kph_practice) <a href="https://twitter.com/kph_practice/status/1744729548074459310?ref_src=twsrc%5Etfw">January 9, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8" loading="lazy"></script>
</div>
<div>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">i just ran some ai models locally on my laptop using @janhq_ and can&#39;t believe how easy and cool it is. so, now i can have the same experience as with ChatGPT, but offline and without any data concerns</p>&mdash; Sergey Kaplich (@sergey_kaplich) <a href="https://twitter.com/sergey_kaplich/status/1742993414986068423?ref_src=twsrc%5Etfw">January 4, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8" loading="lazy"></script>
</div>
<div>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr"><a href="https://t.co/scBqJ3kIzj">https://t.co/scBqJ3kIzj</a> Great way to try open source all models, like Mixtral8x7b offline. Love to see</p>&mdash; Chubby♨ (@kimmonismus) <a href="https://twitter.com/kimmonismus/status/1742843063938994469?ref_src=twsrc%5Etfw">January 4, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8" loading="lazy"></script>
</div>
Please share your love for Jan on Twitter and tag us [@janframework](https://twitter.com/janframework)! We would love to hear from you!
## YouTube
Watch these amazing videos to see how Jan is being used and loved by the community!
### Run Any Chatbot FREE Locally on Your Computer
<div>
<iframe width="100%" height="600" src="https://www.youtube.com/embed/zkafOIyQM8s" title="Run Any Chatbot FREE Locally on Your Computer" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
<br></br>
### Jan AI: Run Open Source LLM 100% Local with OpenAI endpoints
<div>
<iframe width="100%" height="705" src="https://www.youtube.com/embed/9ta2S425Zu8" title="Jan AI: Run Open Source LLM 100% Local with OpenAI endpoints" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
<br></br>
### Setup Tutorial on Jan.ai. JAN AI: Run open source LLM on local Windows PC. 100% offline LLM and AI.
<div>
<iframe width="100%" height="705" src="https://www.youtube.com/embed/ZCiEQVOjH5U" title="Setup Tutorial on Jan.ai. JAN AI: Run open source LLM on local Windows PC. 100% offline LLM and AI." frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
<br></br>
### Jan.ai: Like Offline ChatGPT on Your Computer 💡
<div>
<iframe width="100%" height="600" src="https://www.youtube.com/embed/ES021_sY6WQ" title="Jan.ai: Like Offline ChatGPT on Your Computer 💡" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
<br></br>
### Jan: Bring AI to your Desktop With 100% Offline AI
<div>
<iframe width="100%" height="600" src="https://www.youtube.com/embed/QpMQgJL4AZA" title="Jan: Bring AI to your Desktop With 100% Offline AI" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
<br></br>
### AI on Your Local PC: Install JanAI (ChatGPT alternative) for Enhanced Privacy
<div>
<iframe width="100%" height="600" src="https://www.youtube.com/embed/CbJGxNmdWws" title="AI on Your Local PC: Install JanAI (ChatGPT alternative) for Enhanced Privacy" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
<br></br>
### Install Jan to Run LLM Offline and Local First
<div>
<iframe width="100%" height="600" src="https://www.youtube.com/embed/7JpzE-_cKo4" title="Install Jan to Run LLM Offline and Local First" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>

View File

@ -86,6 +86,10 @@ const menus = [
path: "https://janai.bamboohr.com/careers", path: "https://janai.bamboohr.com/careers",
external: true, external: true,
}, },
{
menu: "Newsletter",
path: "/community#newsletter",
}
], ],
}, },
]; ];

View File

@ -1 +1 @@
0.3.13 0.3.14

View File

@ -16,6 +16,7 @@ import {
OptionType, OptionType,
ImportingModel, ImportingModel,
LocalImportModelEvent, LocalImportModelEvent,
baseName,
} from '@janhq/core' } from '@janhq/core'
import { extractFileName } from './helpers/path' import { extractFileName } from './helpers/path'
@ -488,7 +489,7 @@ export default class JanModelExtension extends ModelExtension {
return return
} }
const binaryFileName = extractFileName(modelBinaryPath, '') const binaryFileName = await baseName(modelBinaryPath)
const model: Model = { const model: Model = {
...defaultModel, ...defaultModel,
@ -555,7 +556,7 @@ export default class JanModelExtension extends ModelExtension {
model: ImportingModel, model: ImportingModel,
optionType: OptionType optionType: OptionType
): Promise<Model> { ): Promise<Model> {
const binaryName = extractFileName(model.path, '').replace(/\s/g, '') const binaryName = (await baseName(model.path)).replace(/\s/g, '')
let modelFolderName = binaryName let modelFolderName = binaryName
if (binaryName.endsWith(JanModelExtension._supportedModelFormat)) { if (binaryName.endsWith(JanModelExtension._supportedModelFormat)) {
@ -568,7 +569,7 @@ export default class JanModelExtension extends ModelExtension {
const modelFolderPath = await this.getModelFolderName(modelFolderName) const modelFolderPath = await this.getModelFolderName(modelFolderName)
await fs.mkdirSync(modelFolderPath) await fs.mkdirSync(modelFolderPath)
const uniqueFolderName = modelFolderPath.split('/').pop() const uniqueFolderName = await baseName(modelFolderPath)
const modelBinaryFile = binaryName.endsWith( const modelBinaryFile = binaryName.endsWith(
JanModelExtension._supportedModelFormat JanModelExtension._supportedModelFormat
) )
@ -637,14 +638,21 @@ export default class JanModelExtension extends ModelExtension {
for (const model of models) { for (const model of models) {
events.emit(LocalImportModelEvent.onLocalImportModelUpdate, model) events.emit(LocalImportModelEvent.onLocalImportModelUpdate, model)
const importedModel = await this.importModel(model, optionType) try {
const importedModel = await this.importModel(model, optionType)
events.emit(LocalImportModelEvent.onLocalImportModelSuccess, { events.emit(LocalImportModelEvent.onLocalImportModelSuccess, {
...model, ...model,
modelId: importedModel.id, modelId: importedModel.id,
}) })
importedModels.push(importedModel) importedModels.push(importedModel)
} catch (err) {
events.emit(LocalImportModelEvent.onLocalImportModelFailed, {
...model,
error: err,
})
}
} }
events.emit( events.emit(
LocalImportModelEvent.onLocalImportModelFinished, LocalImportModelEvent.onLocalImportModelFinished,
importedModels importedModels

View File

@ -5,11 +5,11 @@
@apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400; @apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400;
&-primary { &-primary {
@apply bg-primary hover:bg-primary/90 text-white; @apply bg-blue-600 text-white hover:bg-blue-600/90;
} }
&-secondary-blue { &-secondary-blue {
@apply bg-blue-200 text-blue-600 hover:bg-blue-300/50 dark:hover:bg-blue-200/80; @apply bg-blue-200 text-blue-600 hover:bg-blue-300/50;
} }
&-danger { &-danger {
@ -17,7 +17,7 @@
} }
&-secondary-danger { &-secondary-danger {
@apply bg-red-200 text-red-600 hover:bg-red-300/50 dark:hover:bg-red-200/80; @apply bg-red-200 text-red-600 hover:bg-red-300/50;
} }
&-outline { &-outline {
@ -66,7 +66,7 @@
[type='reset'], [type='reset'],
[type='submit'] { [type='submit'] {
&.btn-primary { &.btn-primary {
@apply bg-primary hover:bg-primary/90; @apply bg-blue-600 hover:bg-blue-600/90;
@apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400; @apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400;
} }
&.btn-secondary { &.btn-secondary {

View File

@ -1,5 +1,5 @@
.checkbox { .checkbox {
@apply border-border data-[state=checked]:bg-primary h-5 w-5 flex-shrink-0 rounded-md border data-[state=checked]:text-white; @apply border-border h-5 w-5 flex-shrink-0 rounded-md border data-[state=checked]:bg-blue-600 data-[state=checked]:text-white;
&--icon { &--icon {
@apply h-4 w-4; @apply h-4 w-4;

View File

@ -1,6 +1,6 @@
.input { .input {
@apply border-border placeholder:text-muted-foreground flex h-9 w-full rounded-lg border bg-transparent px-3 py-1 transition-colors; @apply border-border placeholder:text-muted-foreground flex h-9 w-full rounded-lg border bg-transparent px-3 py-1 transition-colors;
@apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100 disabled:dark:bg-zinc-800 disabled:dark:text-zinc-600; @apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100;
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1; @apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1;
@apply file:border-0 file:bg-transparent file:font-medium; @apply file:border-0 file:bg-transparent file:font-medium;
} }

View File

@ -42,69 +42,10 @@
--danger: 346.8 77.2% 49.8%; --danger: 346.8 77.2% 49.8%;
--danger-foreground: 355.7 100% 97.3%; --danger-foreground: 355.7 100% 97.3%;
--secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
--border: 20 5.9% 90%; --border: 20 5.9% 90%;
--input: 20 5.9% 90%; --input: 20 5.9% 90%;
--ring: 20 14.3% 4.1%; --ring: 20 14.3% 4.1%;
.primary-blue {
--primary: 221 83% 53%;
--primary-foreground: 210 40% 98%;
--secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
}
.primary-green {
--primary: 142.1 76.2% 36.3%;
--primary-foreground: 355.7 100% 97.3%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
}
.primary-purple {
--primary: 262.1 83.3% 57.8%;
--primary-foreground: 210 20% 98%;
--secondary: 220 14.3% 95.9%;
--secondary-foreground: 220.9 39.3% 11%;
}
}
.dark {
--background: 20 14.3% 4.1%;
--foreground: 60 9.1% 97.8%;
--muted: 12 6.5% 15.1%;
--muted-foreground: 24 5.4% 63.9%;
--danger: 346.8 77.2% 49.8%;
--danger-foreground: 355.7 100% 97.3%;
--border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
--ring: 35.5 91.7% 32.9%;
.primary-blue {
--primary: 221 83% 53%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 12 6.5% 15.1%;
--secondary-foreground: 60 9.1% 97.8%;
}
.primary-green {
--primary: 142.1 70.6% 45.3%;
--primary-foreground: 144.9 80.4% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
}
.primary-purple {
--primary: 263.4 70% 50.4%;
--primary-foreground: 210 20% 98%;
--secondary: 215 27.9% 16.9%;
--secondary-foreground: 210 20% 98%;
}
} }

View File

@ -1,7 +1,7 @@
.progress { .progress {
@apply bg-secondary relative h-4 w-full overflow-hidden rounded-full; @apply relative h-4 w-full overflow-hidden rounded-full bg-gray-100;
&-indicator { &-indicator {
@apply bg-primary h-full w-full flex-1 transition-all; @apply h-full w-full flex-1 bg-blue-600 transition-all;
} }
} }

View File

@ -1,6 +1,6 @@
.select { .select {
@apply placeholder:text-muted-foreground border-border flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm disabled:cursor-not-allowed [&>span]:line-clamp-1; @apply placeholder:text-muted-foreground border-border flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm disabled:cursor-not-allowed [&>span]:line-clamp-1;
@apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100 disabled:dark:bg-zinc-800 disabled:dark:text-zinc-600; @apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100;
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1; @apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1;
&-caret { &-caret {

View File

@ -2,7 +2,7 @@
@apply relative flex w-full touch-none select-none items-center; @apply relative flex w-full touch-none select-none items-center;
&-track { &-track {
@apply relative h-1.5 w-full grow overflow-hidden rounded-full bg-gray-200 dark:bg-gray-800; @apply relative h-1.5 w-full grow overflow-hidden rounded-full bg-gray-200;
[data-disabled] { [data-disabled] {
@apply cursor-not-allowed opacity-50; @apply cursor-not-allowed opacity-50;
} }
@ -13,6 +13,6 @@
} }
&-thumb { &-thumb {
@apply border-primary/50 bg-background focus-visible:ring-ring block h-4 w-4 rounded-full border shadow transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50; @apply bg-background focus-visible:ring-ring block h-4 w-4 rounded-full border border-blue-600/50 shadow transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50;
} }
} }

View File

@ -1,7 +1,7 @@
.switch { .switch {
@apply inline-flex h-[20px] w-[36px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent; @apply inline-flex h-[20px] w-[36px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent;
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2; @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
@apply data-[state=checked]:bg-primary data-[state=unchecked]:bg-input; @apply data-[state=unchecked]:bg-input data-[state=checked]:bg-blue-600;
@apply disabled:cursor-not-allowed disabled:opacity-50; @apply disabled:cursor-not-allowed disabled:opacity-50;
&-toggle { &-toggle {

View File

@ -1,6 +1,6 @@
.tooltip { .tooltip {
@apply dark:bg-input dark:text-foreground z-50 overflow-hidden rounded-md bg-gray-950 px-2 py-1.5 text-xs font-medium text-gray-200 shadow-md; @apply z-50 overflow-hidden rounded-md bg-gray-950 px-2 py-1.5 text-xs font-medium text-gray-200 shadow-md;
&-arrow { &-arrow {
@apply dark:fill-input fill-gray-950; @apply fill-gray-950;
} }
} }

View File

@ -15,7 +15,7 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: PropsWithChildren) { export default function RootLayout({ children }: PropsWithChildren) {
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body className="bg-white font-sans text-sm antialiased dark:bg-background"> <body className="bg-white font-sans text-sm antialiased">
<div className="title-bar" /> <div className="title-bar" />
<Providers>{children}</Providers> <Providers>{children}</Providers>
</body> </body>

View File

@ -45,7 +45,7 @@ export default function CardSidebar({
return ( return (
<div <div
className={twMerge( className={twMerge(
'flex w-full flex-col border-t border-border bg-zinc-100 dark:bg-zinc-900', 'flex w-full flex-col border-t border-border bg-zinc-100',
asChild ? 'rounded-lg border' : 'border-t' asChild ? 'rounded-lg border' : 'border-t'
)} )}
> >
@ -61,7 +61,7 @@ export default function CardSidebar({
if (!children) return if (!children) return
setShow(!show) setShow(!show)
}} }}
className="flex w-full flex-1 items-center space-x-2 rounded-lg bg-zinc-100 py-2 pr-2 dark:bg-zinc-900" className="flex w-full flex-1 items-center space-x-2 rounded-lg bg-zinc-100 py-2 pr-2"
> >
<ChevronDownIcon <ChevronDownIcon
className={twMerge( className={twMerge(
@ -79,7 +79,7 @@ export default function CardSidebar({
{!hideMoreVerticalAction && ( {!hideMoreVerticalAction && (
<div <div
ref={setToggle} ref={setToggle}
className="cursor-pointer rounded-lg bg-zinc-100 p-2 px-3 dark:bg-zinc-900" className="cursor-pointer rounded-lg bg-zinc-100 p-2 px-3"
onClick={() => setMore(!more)} onClick={() => setMore(!more)}
> >
<MoreVerticalIcon className="h-5 w-5" /> <MoreVerticalIcon className="h-5 w-5" />
@ -114,7 +114,7 @@ export default function CardSidebar({
<> <>
{title === 'Model' ? ( {title === 'Model' ? (
<div className="flex flex-col"> <div className="flex flex-col">
<span className="font-medium text-black dark:text-muted-foreground"> <span className="font-medium text-black">
{openFileTitle()} {openFileTitle()}
</span> </span>
<span className="mt-1 text-muted-foreground"> <span className="mt-1 text-muted-foreground">
@ -122,7 +122,7 @@ export default function CardSidebar({
</span> </span>
</div> </div>
) : ( ) : (
<span className="text-bold text-black dark:text-muted-foreground"> <span className="text-bold text-black">
{openFileTitle()} {openFileTitle()}
</span> </span>
)} )}
@ -141,7 +141,7 @@ export default function CardSidebar({
/> />
<> <>
<div className="flex flex-col"> <div className="flex flex-col">
<span className="line-clamp-1 font-medium text-black dark:text-muted-foreground"> <span className="line-clamp-1 font-medium text-black">
Edit Global Defaults for{' '} Edit Global Defaults for{' '}
<span <span
className="font-bold" className="font-bold"
@ -175,7 +175,7 @@ export default function CardSidebar({
{show && ( {show && (
<div <div
className={twMerge( className={twMerge(
'flex flex-col gap-2 bg-white px-2 dark:bg-background', 'flex flex-col gap-2 bg-white px-2',
asChild && 'rounded-b-lg' asChild && 'rounded-b-lg'
)} )}
> >

View File

@ -34,12 +34,10 @@ const Checkbox: React.FC<Props> = ({
return ( return (
<div className="flex justify-between"> <div className="flex justify-between">
<div className="mb-1 flex items-center gap-x-2"> <div className="mb-1 flex items-center gap-x-2">
<p className="text-sm font-semibold text-zinc-500 dark:text-gray-300"> <p className="text-sm font-semibold text-zinc-500">{title}</p>
{title}
</p>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<InfoIcon size={16} className="flex-shrink-0 dark:text-gray-500" /> <InfoIcon size={16} className="flex-shrink-0" />
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal> <TooltipPortal>
<TooltipContent side="top" className="max-w-[240px]"> <TooltipContent side="top" className="max-w-[240px]">

View File

@ -203,15 +203,14 @@ const DropdownListSidebar = ({
isTabActive === 1 && '[&_.select-scroll-down-button]:hidden' isTabActive === 1 && '[&_.select-scroll-down-button]:hidden'
)} )}
> >
<div className="relative px-2 py-2 dark:bg-secondary/50"> <div className="relative px-2 py-2">
<ul className="inline-flex w-full space-x-2 rounded-lg bg-zinc-100 px-1 dark:bg-secondary"> <ul className="inline-flex w-full space-x-2 rounded-lg bg-zinc-100 px-1">
{engineOptions.map((name, i) => { {engineOptions.map((name, i) => {
return ( return (
<li <li
className={twMerge( className={twMerge(
'relative my-1 flex w-full cursor-pointer items-center justify-center space-x-2 px-2 py-2', 'relative my-1 flex w-full cursor-pointer items-center justify-center space-x-2 px-2 py-2',
isTabActive === i && isTabActive === i && 'rounded-md bg-background'
'rounded-md bg-background dark:bg-white'
)} )}
key={i} key={i}
onClick={() => setIsTabActive(i)} onClick={() => setIsTabActive(i)}
@ -230,8 +229,7 @@ const DropdownListSidebar = ({
<span <span
className={twMerge( className={twMerge(
'relative z-50 font-medium text-muted-foreground', 'relative z-50 font-medium text-muted-foreground',
isTabActive === i && isTabActive === i && 'font-bold text-foreground'
'font-bold text-foreground dark:text-black'
)} )}
> >
{name} {name}

View File

@ -60,7 +60,7 @@ const GPUDriverPrompt: React.FC = () => {
id="default-checkbox" id="default-checkbox"
type="checkbox" type="checkbox"
onChange={onDoNotShowAgainChange} onChange={onDoNotShowAgainChange}
className="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600" className="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500"
/> />
<span>Don&apos;t show again</span> <span>Don&apos;t show again</span>
</div> </div>

View File

@ -47,7 +47,7 @@ export default function DownloadingState() {
</span> </span>
</Button> </Button>
<span <span
className="absolute left-0 h-full rounded-md rounded-l-md bg-primary/20" className="absolute left-0 h-full rounded-md rounded-l-md bg-blue-500/20"
style={{ style={{
width: `${totalPercentage}%`, width: `${totalPercentage}%`,
}} }}

View File

@ -48,7 +48,7 @@ const ImportingModelState: React.FC = () => {
className="h-2 w-24" className="h-2 w-24"
value={transferredSize / totalSize} value={transferredSize / totalSize}
/> />
<span className="text-xs font-bold text-primary"> <span className="text-xs font-bold text-blue-600">
{progress.toFixed(2)}% {progress.toFixed(2)}%
</span> </span>
</div> </div>

View File

@ -57,17 +57,6 @@ const SystemMonitor = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
const calculateUtilization = () => {
let sum = 0
const util = gpus.map((x) => {
return Number(x['utilization'])
})
util.forEach((num) => {
sum += num
})
return sum
}
return ( return (
<Fragment> <Fragment>
<div <div
@ -131,10 +120,11 @@ const SystemMonitor = () => {
</div> </div>
</div> </div>
<div className="mb-4 border-b border-border pb-4"> <div className="mb-4 border-b border-border pb-4">
<div className="flex items-center gap-2"> <div className="flex items-center justify-between gap-2">
<h6 className="font-bold">Memory</h6> <h6 className="font-bold">Memory</h6>
<span className="text-xs text-muted-foreground"> <span className="text-sm text-muted-foreground">
{toGibibytes(usedRam)} of {toGibibytes(totalRam)} used {toGibibytes(usedRam, { hideUnit: true })}/
{toGibibytes(totalRam, { hideUnit: true })} GB
</span> </span>
</div> </div>
<div className="flex items-center gap-x-4"> <div className="flex items-center gap-x-4">
@ -148,30 +138,29 @@ const SystemMonitor = () => {
</div> </div>
</div> </div>
{gpus.length > 0 && ( {gpus.length > 0 && (
<div className="mb-4 border-b border-border pb-4"> <div className="mb-4 border-b border-border pb-4 last:border-none">
<h6 className="font-bold">GPU</h6>
<div className="flex items-center gap-x-4">
<Progress value={calculateUtilization()} className="h-2" />
<span className="flex-shrink-0 text-muted-foreground">
{calculateUtilization()}%
</span>
</div>
{gpus.map((gpu, index) => ( {gpus.map((gpu, index) => (
<div <div key={index} className="mt-4 flex flex-col gap-x-2">
key={index} <div className="flex w-full items-start justify-between">
className="mt-4 flex items-start justify-between gap-4" <span className="line-clamp-1 w-1/2 font-bold">
> {gpu.name}
<span className="line-clamp-1 w-1/2 font-medium text-muted-foreground"> </span>
{gpu.name} <div className="flex gap-x-2">
</span> <div className="text-muted-foreground">
<div className="flex gap-x-2"> <span>
<span className="font-semibold"> {gpu.memoryTotal - gpu.memoryFree}/
{gpu.memoryTotal}
</span>
<span> MB</span>
</div>
</div>
</div>
<div className="flex items-center gap-x-4">
<Progress value={gpu.utilization} className="h-2" />
<span className="flex-shrink-0 text-muted-foreground">
{gpu.utilization}% {gpu.utilization}%
</span> </span>
<div>
<span className="font-semibold">{gpu.vram}</span>
<span>MB VRAM</span>
</div>
</div> </div>
</div> </div>
))} ))}

View File

@ -45,7 +45,7 @@ export default function RibbonNav() {
size={20} size={20}
className={twMerge( className={twMerge(
'flex-shrink-0 text-muted-foreground', 'flex-shrink-0 text-muted-foreground',
serverEnabled && 'text-gray-300 dark:text-gray-700' serverEnabled && 'text-gray-300'
)} )}
/> />
), ),
@ -114,7 +114,7 @@ export default function RibbonNav() {
</div> </div>
{isActive && ( {isActive && (
<m.div <m.div
className="absolute inset-0 left-0 h-full w-full rounded-md bg-gray-200 dark:bg-secondary" className="absolute inset-0 left-0 h-full w-full rounded-md bg-gray-200"
layoutId="active-state-primary" layoutId="active-state-primary"
/> />
)} )}
@ -166,7 +166,7 @@ export default function RibbonNav() {
</div> </div>
{isActive && ( {isActive && (
<m.div <m.div
className="absolute inset-0 left-0 h-full w-full rounded-md bg-gray-200 dark:bg-secondary" className="absolute inset-0 left-0 h-full w-full rounded-md bg-gray-200"
layoutId="active-state-secondary" layoutId="active-state-secondary"
/> />
)} )}

View File

@ -159,7 +159,7 @@ const TopBar = () => {
size={16} size={16}
className="text-muted-foreground" className="text-muted-foreground"
/> />
<span className="font-medium text-black dark:text-muted-foreground"> <span className="font-medium text-black ">
{openFileTitle()} {openFileTitle()}
</span> </span>
</div> </div>
@ -175,7 +175,7 @@ const TopBar = () => {
className="mt-0.5 flex-shrink-0 text-muted-foreground" className="mt-0.5 flex-shrink-0 text-muted-foreground"
/> />
<div className="flex flex-col"> <div className="flex flex-col">
<span className="font-medium text-black dark:text-muted-foreground"> <span className="font-medium text-black ">
Edit Threads Settings Edit Threads Settings
</span> </span>
<span className="mt-1 text-muted-foreground"> <span className="mt-1 text-muted-foreground">
@ -204,7 +204,7 @@ const TopBar = () => {
className="text-muted-foreground" className="text-muted-foreground"
/> />
<div className="flex flex-col"> <div className="flex flex-col">
<span className="font-medium text-black dark:text-muted-foreground"> <span className="font-medium text-black ">
{openFileTitle()} {openFileTitle()}
</span> </span>
</div> </div>

View File

@ -7,12 +7,12 @@ export default function Loader({ description }: Props) {
<div className="space-y-16"> <div className="space-y-16">
<div className="loader"> <div className="loader">
<div className="loader-inner"> <div className="loader-inner">
<label className="h-2 w-2 rounded-full bg-primary" /> <label className="h-2 w-2 rounded-full bg-blue-500" />
<label className="h-2 w-2 rounded-full bg-primary" /> <label className="h-2 w-2 rounded-full bg-blue-500" />
<label className="h-2 w-2 rounded-full bg-primary" /> <label className="h-2 w-2 rounded-full bg-blue-500" />
<label className="h-2 w-2 rounded-full bg-primary" /> <label className="h-2 w-2 rounded-full bg-blue-500" />
<label className="h-2 w-2 rounded-full bg-primary" /> <label className="h-2 w-2 rounded-full bg-blue-500" />
<label className="h-2 w-2 rounded-full bg-primary" /> <label className="h-2 w-2 rounded-full bg-blue-500" />
</div> </div>
</div> </div>
<p className="font-medium text-muted-foreground">{description}</p> <p className="font-medium text-muted-foreground">{description}</p>

View File

@ -28,7 +28,7 @@ const AppLogs = () => {
<div className="absolute -top-11 right-2"> <div className="absolute -top-11 right-2">
<Button <Button
themes="outline" themes="outline"
className="bg-white dark:bg-secondary/50" className="bg-white"
onClick={() => { onClick={() => {
clipboard.copy(logs.slice(-50) ?? '') clipboard.copy(logs.slice(-50) ?? '')
}} }}

View File

@ -16,7 +16,7 @@ const DeviceSpecs = () => {
<div className="absolute -top-11 right-2"> <div className="absolute -top-11 right-2">
<Button <Button
themes="outline" themes="outline"
className="bg-white dark:bg-secondary/50" className="bg-white"
onClick={() => { onClick={() => {
clipboard.copy(userAgent ?? '') clipboard.copy(userAgent ?? '')
}} }}

View File

@ -38,7 +38,7 @@ const ModalTroubleShooting: React.FC = () => {
<a <a
href="https://jan.ai/guides/troubleshooting" href="https://jan.ai/guides/troubleshooting"
target="_blank" target="_blank"
className="text-blue-600 hover:underline dark:text-blue-300" className="text-blue-600 hover:underline"
> >
troubleshooting guide troubleshooting guide
</a> </a>
@ -65,7 +65,7 @@ const ModalTroubleShooting: React.FC = () => {
<a <a
href="https://discord.gg/AsJ8krTT3N" href="https://discord.gg/AsJ8krTT3N"
target="_blank" target="_blank"
className="text-blue-600 hover:underline dark:text-blue-300" className="text-blue-600 hover:underline"
> >
Discord Discord
</a> </a>
@ -77,8 +77,8 @@ const ModalTroubleShooting: React.FC = () => {
<div className="flex flex-col pt-4"> <div className="flex flex-col pt-4">
{/* TODO @faisal replace this once we have better tabs component UI */} {/* TODO @faisal replace this once we have better tabs component UI */}
<div className="relative bg-zinc-100 px-4 py-2 dark:bg-secondary/50"> <div className="relative bg-zinc-100 px-4 py-2">
<ul className="inline-flex space-x-2 rounded-lg bg-zinc-200 px-1 dark:bg-secondary"> <ul className="inline-flex space-x-2 rounded-lg bg-zinc-200 px-1">
{logOption.map((name, i) => { {logOption.map((name, i) => {
return ( return (
<li <li
@ -89,15 +89,14 @@ const ModalTroubleShooting: React.FC = () => {
<span <span
className={twMerge( className={twMerge(
'relative z-50 font-medium text-muted-foreground', 'relative z-50 font-medium text-muted-foreground',
isTabActive === i && isTabActive === i && 'font-bold text-foreground'
'font-bold text-foreground dark:text-black'
)} )}
> >
{name} {name}
</span> </span>
{isTabActive === i && ( {isTabActive === i && (
<m.div <m.div
className="absolute left-0 top-1 h-[calc(100%-8px)] w-full rounded-md bg-background dark:bg-white" className="absolute left-0 top-1 h-[calc(100%-8px)] w-full rounded-md bg-background"
layoutId="log-state-active" layoutId="log-state-active"
/> />
)} )}

View File

@ -30,12 +30,10 @@ const ModelConfigInput: React.FC<Props> = ({
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<div className="mb-2 flex items-center gap-x-2"> <div className="mb-2 flex items-center gap-x-2">
<p className="text-sm font-semibold text-zinc-500 dark:text-gray-300"> <p className="text-sm font-semibold text-zinc-500">{title}</p>
{title}
</p>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<InfoIcon size={16} className="flex-shrink-0 dark:text-gray-500" /> <InfoIcon size={16} className="flex-shrink-0" />
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal> <TooltipPortal>
<TooltipContent side="top" className="max-w-[240px]"> <TooltipContent side="top" className="max-w-[240px]">

View File

@ -33,7 +33,7 @@ const OpenAiKeyInput: React.FC = () => {
<div className="my-4"> <div className="my-4">
<label <label
id="thread-title" id="thread-title"
className="mb-2 inline-block font-bold text-gray-600 dark:text-gray-300" className="mb-2 inline-block font-bold text-gray-600"
> >
API Key API Key
</label> </label>

View File

@ -12,6 +12,7 @@ import { useSetAtom } from 'jotai'
import { snackbar } from '../Toast' import { snackbar } from '../Toast'
import { import {
setImportingModelErrorAtom,
setImportingModelSuccessAtom, setImportingModelSuccessAtom,
updateImportingModelProgressAtom, updateImportingModelProgressAtom,
} from '@/helpers/atoms/Model.atom' } from '@/helpers/atoms/Model.atom'
@ -21,6 +22,7 @@ const ModelImportListener = ({ children }: PropsWithChildren) => {
updateImportingModelProgressAtom updateImportingModelProgressAtom
) )
const setImportingModelSuccess = useSetAtom(setImportingModelSuccessAtom) const setImportingModelSuccess = useSetAtom(setImportingModelSuccessAtom)
const setImportingModelFailed = useSetAtom(setImportingModelErrorAtom)
const onImportModelUpdate = useCallback( const onImportModelUpdate = useCallback(
async (state: ImportingModel) => { async (state: ImportingModel) => {
@ -30,6 +32,14 @@ const ModelImportListener = ({ children }: PropsWithChildren) => {
[updateImportingModelProgress] [updateImportingModelProgress]
) )
const onImportModelFailed = useCallback(
async (state: ImportingModel) => {
if (!state.importId) return
setImportingModelFailed(state.importId, state.error ?? '')
},
[setImportingModelFailed]
)
const onImportModelSuccess = useCallback( const onImportModelSuccess = useCallback(
(state: ImportingModel) => { (state: ImportingModel) => {
if (!state.modelId) return if (!state.modelId) return
@ -62,6 +72,10 @@ const ModelImportListener = ({ children }: PropsWithChildren) => {
LocalImportModelEvent.onLocalImportModelFinished, LocalImportModelEvent.onLocalImportModelFinished,
onImportModelFinished onImportModelFinished
) )
events.on(
LocalImportModelEvent.onLocalImportModelFailed,
onImportModelFailed
)
return () => { return () => {
console.debug('ModelImportListener: unregistering event listeners...') console.debug('ModelImportListener: unregistering event listeners...')
@ -77,8 +91,17 @@ const ModelImportListener = ({ children }: PropsWithChildren) => {
LocalImportModelEvent.onLocalImportModelFinished, LocalImportModelEvent.onLocalImportModelFinished,
onImportModelFinished onImportModelFinished
) )
events.off(
LocalImportModelEvent.onLocalImportModelFailed,
onImportModelFailed
)
} }
}, [onImportModelUpdate, onImportModelSuccess, onImportModelFinished]) }, [
onImportModelUpdate,
onImportModelSuccess,
onImportModelFinished,
onImportModelFailed,
])
return <Fragment>{children}</Fragment> return <Fragment>{children}</Fragment>
} }

View File

@ -6,17 +6,9 @@ import { ThemeProvider } from 'next-themes'
import { motion as m } from 'framer-motion' import { motion as m } from 'framer-motion'
import { useBodyClass } from '@/hooks/useBodyClass'
import { useUserConfigs } from '@/hooks/useUserConfigs'
export default function ThemeWrapper({ children }: PropsWithChildren) { export default function ThemeWrapper({ children }: PropsWithChildren) {
const [config] = useUserConfigs()
useBodyClass(config.primaryColor || 'primary-yellow')
return ( return (
<ThemeProvider attribute="class" enableSystem> <ThemeProvider attribute="class" forcedTheme="light">
<m.div <m.div
initial={{ opacity: 0, y: -10 }} initial={{ opacity: 0, y: -10 }}
animate={{ animate={{

View File

@ -57,7 +57,7 @@ const ServerLogs = (props: ServerLogsProps) => {
<div className="absolute -top-11 right-2"> <div className="absolute -top-11 right-2">
<Button <Button
themes="outline" themes="outline"
className="bg-white dark:bg-secondary/50" className="bg-white"
onClick={() => { onClick={() => {
clipboard.copy(logs.slice(-100) ?? '') clipboard.copy(logs.slice(-100) ?? '')
}} }}

View File

@ -42,12 +42,10 @@ const SliderRightPanel: React.FC<Props> = ({
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<div className="mb-3 flex items-center gap-x-2"> <div className="mb-3 flex items-center gap-x-2">
<p className="text-sm font-semibold text-zinc-500 dark:text-gray-300"> <p className="text-sm font-semibold text-zinc-500">{title}</p>
{title}
</p>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<InfoIcon size={16} className="flex-shrink-0 dark:text-gray-500" /> <InfoIcon size={16} className="flex-shrink-0" />
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal> <TooltipPortal>
<TooltipContent side="top" className="max-w-[240px]"> <TooltipContent side="top" className="max-w-[240px]">

View File

@ -108,11 +108,11 @@ export function toaster(props: Props) {
return ( return (
<div <div
className={twMerge( className={twMerge(
'unset-drag dark:bg-zinc-white relative flex animate-enter items-center gap-x-4 rounded-lg bg-foreground px-4 py-2 text-white dark:border dark:border-border', 'unset-drag relative flex animate-enter items-center gap-x-4 rounded-lg bg-foreground px-4 py-2 text-white',
t.visible ? 'animate-enter' : 'animate-leave' t.visible ? 'animate-enter' : 'animate-leave'
)} )}
> >
<div className="flex items-start gap-x-3 dark:text-black"> <div className="flex items-start gap-x-3">
<div className="mt-1">{renderIcon(type)}</div> <div className="mt-1">{renderIcon(type)}</div>
<div className="pr-4"> <div className="pr-4">
<h1 className="font-bold">{title}</h1> <h1 className="font-bold">{title}</h1>
@ -120,7 +120,7 @@ export function toaster(props: Props) {
</div> </div>
<XIcon <XIcon
size={24} size={24}
className="absolute right-2 top-2 w-4 cursor-pointer dark:text-black" className="absolute right-2 top-2 w-4 cursor-pointer"
onClick={() => toast.dismiss(t.id)} onClick={() => toast.dismiss(t.id)}
/> />
</div> </div>
@ -138,16 +138,16 @@ export function snackbar(props: Props) {
return ( return (
<div <div
className={twMerge( className={twMerge(
'unset-drag dark:bg-zinc-white relative bottom-2 flex animate-enter items-center gap-x-4 rounded-lg bg-foreground px-4 py-2 text-white dark:border dark:border-border', 'unset-drag relative bottom-2 flex animate-enter items-center gap-x-4 rounded-lg bg-foreground px-4 py-2 text-white',
t.visible ? 'animate-enter' : 'animate-leave' t.visible ? 'animate-enter' : 'animate-leave'
)} )}
> >
<div className="flex items-start gap-x-3 dark:text-black"> <div className="flex items-start gap-x-3">
<div>{renderIcon(type)}</div> <div>{renderIcon(type)}</div>
<p className="pr-4">{description}</p> <p className="pr-4">{description}</p>
<XIcon <XIcon
size={24} size={24}
className="absolute right-2 top-1/2 w-4 -translate-y-1/2 cursor-pointer dark:text-black" className="absolute right-2 top-1/2 w-4 -translate-y-1/2 cursor-pointer"
onClick={() => toast.dismiss(t.id)} onClick={() => toast.dismiss(t.id)}
/> />
</div> </div>

View File

@ -67,6 +67,24 @@ export const updateImportingModelProgressAtom = atom(
} }
) )
export const setImportingModelErrorAtom = atom(
null,
(get, set, importId: string, error: string) => {
const model = get(importingModelsAtom).find((x) => x.importId === importId)
if (!model) return
const newModel: ImportingModel = {
...model,
status: 'FAILED',
}
console.error(`Importing model ${model} failed`, error)
const newList = get(importingModelsAtom).map((m) =>
m.importId === importId ? newModel : m
)
set(importingModelsAtom, newList)
}
)
export const setImportingModelSuccessAtom = atom( export const setImportingModelSuccessAtom = atom(
null, null,
(get, set, importId: string, modelId: string) => { (get, set, importId: string, modelId: string) => {

View File

@ -0,0 +1,55 @@
import { useCallback } from 'react'
import { ImportingModel } from '@janhq/core'
import { useSetAtom } from 'jotai'
import { v4 as uuidv4 } from 'uuid'
import { snackbar } from '@/containers/Toast'
import { getFileInfoFromFile } from '@/utils/file'
import { setImportModelStageAtom } from './useImportModel'
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
export default function useDropModelBinaries() {
const setImportingModels = useSetAtom(importingModelsAtom)
const setImportModelStage = useSetAtom(setImportModelStageAtom)
const onDropModels = useCallback(
async (acceptedFiles: File[]) => {
const files = await getFileInfoFromFile(acceptedFiles)
const unsupportedFiles = files.filter(
(file) => !file.path.endsWith('.gguf')
)
const supportedFiles = files.filter((file) => file.path.endsWith('.gguf'))
const importingModels: ImportingModel[] = supportedFiles.map((file) => ({
importId: uuidv4(),
modelId: undefined,
name: file.name.replace('.gguf', ''),
description: '',
path: file.path,
tags: [],
size: file.size,
status: 'PREPARING',
format: 'gguf',
}))
if (unsupportedFiles.length > 0) {
snackbar({
description: `File has to be a .gguf file`,
type: 'error',
})
}
if (importingModels.length === 0) return
setImportingModels(importingModels)
setImportModelStage('MODEL_SELECTED')
},
[setImportModelStage, setImportingModels]
)
return { onDropModels }
}

View File

@ -1,11 +0,0 @@
import { useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
export const userConfigs = atomWithStorage<UserConfig>('config', {
gettingStartedShow: true,
primaryColor: 'primary-blue',
})
export function useUserConfigs() {
return useAtom(userConfigs)
}

View File

@ -34,9 +34,7 @@ const CleanThreadModal: React.FC<Props> = ({ threadId }) => {
<ModalTrigger asChild onClick={(e) => e.stopPropagation()}> <ModalTrigger asChild onClick={(e) => e.stopPropagation()}>
<div className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary"> <div className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary">
<Paintbrush size={16} className="text-muted-foreground" /> <Paintbrush size={16} className="text-muted-foreground" />
<span className="text-bold text-black dark:text-muted-foreground"> <span className="text-bold text-black">Clean thread</span>
Clean thread
</span>
</div> </div>
</ModalTrigger> </ModalTrigger>
<ModalPortal /> <ModalPortal />

View File

@ -33,10 +33,8 @@ const DeleteThreadModal: React.FC<Props> = ({ threadId }) => {
<Modal> <Modal>
<ModalTrigger asChild onClick={(e) => e.stopPropagation()}> <ModalTrigger asChild onClick={(e) => e.stopPropagation()}>
<div className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary"> <div className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary">
<Trash2Icon size={16} className="text-red-600 dark:text-red-300" /> <Trash2Icon size={16} className="text-red-600" />
<span className="text-bold text-red-600 dark:text-red-300"> <span className="text-bold text-red-600">Delete thread</span>
Delete thread
</span>
</div> </div>
</ModalTrigger> </ModalTrigger>
<ModalPortal /> <ModalPortal />

View File

@ -54,7 +54,7 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => {
Port 3928 is currently unavailable. Check for conflicting apps, Port 3928 is currently unavailable. Check for conflicting apps,
or access&nbsp; or access&nbsp;
<span <span
className="cursor-pointer text-primary dark:text-blue-400" className="cursor-pointer text-blue-600"
onClick={() => setModalTroubleShooting(true)} onClick={() => setModalTroubleShooting(true)}
> >
troubleshooting assistance troubleshooting assistance
@ -72,7 +72,7 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => {
<p> <p>
Jans in beta. Access&nbsp; Jans in beta. Access&nbsp;
<span <span
className="cursor-pointer text-primary dark:text-blue-400" className="cursor-pointer text-blue-600"
onClick={() => setModalTroubleShooting(true)} onClick={() => setModalTroubleShooting(true)}
> >
troubleshooting assistance troubleshooting assistance

View File

@ -71,7 +71,7 @@ const Sidebar: React.FC = () => {
return ( return (
<div <div
className={twMerge( className={twMerge(
'h-full flex-shrink-0 overflow-x-hidden border-l border-border bg-background pb-6 transition-all duration-100 dark:bg-background/20', 'h-full flex-shrink-0 overflow-x-hidden border-l border-border bg-background pb-6 transition-all duration-100',
showing showing
? 'w-80 translate-x-0 opacity-100' ? 'w-80 translate-x-0 opacity-100'
: 'w-0 translate-x-full opacity-0' : 'w-0 translate-x-full opacity-0'
@ -87,7 +87,7 @@ const Sidebar: React.FC = () => {
<div> <div>
<label <label
id="thread-title" id="thread-title"
className="mb-2 inline-block font-bold text-gray-600 dark:text-gray-300" className="mb-2 inline-block font-bold text-gray-600"
> >
Title Title
</label> </label>
@ -106,7 +106,7 @@ const Sidebar: React.FC = () => {
<div className="flex flex-col"> <div className="flex flex-col">
<label <label
id="thread-title" id="thread-title"
className="mb-2 inline-block font-bold text-zinc-500 dark:text-gray-300" className="mb-2 inline-block font-bold text-zinc-500"
> >
Threads ID Threads ID
</label> </label>
@ -127,7 +127,7 @@ const Sidebar: React.FC = () => {
<div> <div>
<label <label
id="thread-title" id="thread-title"
className="mb-2 inline-block font-bold text-zinc-500 dark:text-gray-300" className="mb-2 inline-block font-bold text-zinc-500"
> >
Instructions Instructions
</label> </label>
@ -203,14 +203,14 @@ const Sidebar: React.FC = () => {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label <label
id="retrieval" id="retrieval"
className="inline-flex items-center font-bold text-zinc-500 dark:text-gray-300" className="inline-flex items-center font-bold text-zinc-500"
> >
Retrieval Retrieval
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<InfoIcon <InfoIcon
size={16} size={16}
className="ml-2 flex-shrink-0 text-black dark:text-gray-500" className="ml-2 flex-shrink-0 text-black"
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal> <TooltipPortal>
@ -269,7 +269,7 @@ const Sidebar: React.FC = () => {
<div className="item-center mb-2 flex"> <div className="item-center mb-2 flex">
<label <label
id="embedding-model" id="embedding-model"
className="inline-flex font-bold text-zinc-500 dark:text-gray-300" className="inline-flex font-bold text-zinc-500"
> >
Embedding Model Embedding Model
</label> </label>
@ -277,7 +277,7 @@ const Sidebar: React.FC = () => {
<TooltipTrigger asChild> <TooltipTrigger asChild>
<InfoIcon <InfoIcon
size={16} size={16}
className="ml-2 flex-shrink-0 dark:text-gray-500" className="ml-2 flex-shrink-0"
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal> <TooltipPortal>
@ -309,7 +309,7 @@ const Sidebar: React.FC = () => {
<div className="mb-2 flex items-center"> <div className="mb-2 flex items-center">
<label <label
id="vector-database" id="vector-database"
className="inline-block font-bold text-zinc-500 dark:text-gray-300" className="inline-block font-bold text-zinc-500"
> >
Vector Database Vector Database
</label> </label>
@ -317,7 +317,7 @@ const Sidebar: React.FC = () => {
<TooltipTrigger asChild> <TooltipTrigger asChild>
<InfoIcon <InfoIcon
size={16} size={16}
className="ml-2 flex-shrink-0 dark:text-gray-500" className="ml-2 flex-shrink-0"
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal> <TooltipPortal>

View File

@ -18,7 +18,7 @@ import hljs from 'highlight.js'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import { FolderOpenIcon } from 'lucide-react' import { FolderOpenIcon } from 'lucide-react'
import { Marked, Renderer, marked as markedDefault } from 'marked' import { Marked, Renderer } from 'marked'
import { markedHighlight } from 'marked-highlight' import { markedHighlight } from 'marked-highlight'
@ -43,19 +43,6 @@ import {
getCurrentChatMessagesAtom, getCurrentChatMessagesAtom,
} from '@/helpers/atoms/ChatMessage.atom' } 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<ThreadMessage> = (props) => { const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
let text = '' let text = ''
const isUser = props.role === ChatCompletionRole.User const isUser = props.role === ChatCompletionRole.User
@ -282,7 +269,7 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
</div> </div>
)} )}
{isUser && !isMarkdownValue(text) ? ( {isUser ? (
<> <>
{editMessage === props.id ? ( {editMessage === props.id ? (
<div> <div>

View File

@ -79,7 +79,7 @@ export default function ThreadList() {
<div <div
key={thread.id} key={thread.id}
className={twMerge( className={twMerge(
`group/message relative mb-1 flex cursor-pointer flex-col transition-all hover:rounded-lg hover:bg-gray-100 hover:dark:bg-secondary/50` `group/message relative mb-1 flex cursor-pointer flex-col transition-all hover:rounded-lg hover:bg-gray-100`
)} )}
onClick={() => { onClick={() => {
onThreadClick(thread) onThreadClick(thread)
@ -90,7 +90,7 @@ export default function ThreadList() {
{thread.updated && displayDate(thread.updated)} {thread.updated && displayDate(thread.updated)}
</p> </p>
<h2 className="line-clamp-1 font-bold">{thread.title}</h2> <h2 className="line-clamp-1 font-bold">{thread.title}</h2>
<p className="mt-1 line-clamp-1 text-xs text-gray-700 group-hover/message:max-w-[160px] dark:text-gray-300"> <p className="mt-1 line-clamp-1 text-xs text-gray-700 group-hover/message:max-w-[160px]">
{threadStates[thread.id]?.lastMessage {threadStates[thread.id]?.lastMessage
? threadStates[thread.id]?.lastMessage ? threadStates[thread.id]?.lastMessage
: 'No new message'} : 'No new message'}
@ -98,7 +98,7 @@ export default function ThreadList() {
</div> </div>
<div <div
className={twMerge( className={twMerge(
`group/icon invisible absolute bottom-2 right-2 z-20 rounded-lg p-1 text-muted-foreground hover:bg-gray-200 group-hover/message:visible hover:dark:bg-secondary` `group/icon invisible absolute bottom-2 right-2 z-20 rounded-lg p-1 text-muted-foreground hover:bg-gray-200 group-hover/message:visible`
)} )}
> >
<MoreVerticalIcon /> <MoreVerticalIcon />
@ -109,7 +109,7 @@ export default function ThreadList() {
</div> </div>
{activeThreadId === thread.id && ( {activeThreadId === thread.id && (
<m.div <m.div
className="absolute inset-0 left-0 h-full w-full rounded-lg bg-gray-100 p-4 dark:bg-secondary/50" className="absolute inset-0 left-0 h-full w-full rounded-lg bg-gray-100 p-4"
layoutId="active-thread" layoutId="active-thread"
/> />
)} )}

View File

@ -26,7 +26,7 @@ export const HuggingFaceSearchModal = () => {
</div> </div>
<Input <Input
placeholder="e.g. username/repo-name" placeholder="e.g. username/repo-name"
className="bg-white dark:bg-background" className="bg-white"
onChange={(e) => { onChange={(e) => {
setRepoID(e.target.value) setRepoID(e.target.value)
}} }}

View File

@ -13,7 +13,7 @@ import {
} from '@janhq/uikit' } from '@janhq/uikit'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtomValue, useSetAtom } from 'jotai'
import { Plus, SearchIcon } from 'lucide-react' import { UploadIcon, SearchIcon } from 'lucide-react'
import { FeatureToggleContext } from '@/context/FeatureToggle' import { FeatureToggleContext } from '@/context/FeatureToggle'
@ -91,17 +91,17 @@ const ExploreModelsScreen = () => {
/> />
<Input <Input
placeholder="Search models" placeholder="Search models"
className="bg-white pl-9 dark:bg-background" className="bg-white pl-9"
onChange={(e) => setsearchValue(e.target.value)} onChange={(e) => setsearchValue(e.target.value)}
/> />
</div> </div>
<Button <Button
themes={'primary'} themes="outline"
className="space-x-2" className="gap-2 bg-white"
onClick={onImportModelClick} onClick={onImportModelClick}
> >
<Plus className="h-3 w-3" /> <UploadIcon size={16} />
<p>Import Model</p> Import Model
</Button> </Button>
</div> </div>
{experimentalFeature && ( {experimentalFeature && (

View File

@ -181,7 +181,7 @@ const LocalServerScreen = () => {
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div className="space-y-4 p-4"> <div className="space-y-4 p-4">
<div> <div>
<p className="mb-2 block text-sm font-semibold text-zinc-500 dark:text-gray-300"> <p className="mb-2 block text-sm font-semibold text-zinc-500 ">
Server Options Server Options
</p> </p>
<div className="flex w-full flex-shrink-0 items-center gap-x-2"> <div className="flex w-full flex-shrink-0 items-center gap-x-2">
@ -231,15 +231,12 @@ const LocalServerScreen = () => {
<div> <div>
<label <label
id="cors" id="cors"
className="mb-2 inline-flex items-start gap-x-2 font-bold text-zinc-500 dark:text-gray-300" className="mb-2 inline-flex items-start gap-x-2 font-bold text-zinc-500"
> >
Cross-Origin-Resource-Sharing (CORS) Cross-Origin-Resource-Sharing (CORS)
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<InfoIcon <InfoIcon size={16} className="mt-0.5 flex-shrink-0" />
size={16}
className="mt-0.5 flex-shrink-0 dark:text-gray-500"
/>
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal> <TooltipPortal>
<TooltipContent side="top" className="max-w-[240px]"> <TooltipContent side="top" className="max-w-[240px]">
@ -266,15 +263,12 @@ const LocalServerScreen = () => {
<div> <div>
<label <label
id="verbose" id="verbose"
className="mb-2 inline-flex items-start gap-x-2 font-bold text-zinc-500 dark:text-gray-300" className="mb-2 inline-flex items-start gap-x-2 font-bold text-zinc-500"
> >
Verbose Server Logs Verbose Server Logs
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<InfoIcon <InfoIcon size={16} className="mt-0.5 flex-shrink-0" />
size={16}
className="mt-0.5 flex-shrink-0 dark:text-gray-500"
/>
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal> <TooltipPortal>
<TooltipContent side="top" className="max-w-[240px]"> <TooltipContent side="top" className="max-w-[240px]">
@ -315,13 +309,13 @@ const LocalServerScreen = () => {
{/* Middle Bar */} {/* Middle Bar */}
<ScrollToBottom className="relative flex h-full w-full flex-col overflow-auto bg-background"> <ScrollToBottom className="relative flex h-full w-full flex-col overflow-auto bg-background">
<div className="sticky top-0 flex items-center justify-between bg-zinc-100 px-4 py-2 dark:bg-zinc-600"> <div className="sticky top-0 flex items-center justify-between bg-zinc-100 px-4 py-2">
<h2 className="font-bold">Server Logs</h2> <h2 className="font-bold">Server Logs</h2>
<div className="space-x-2"> <div className="space-x-2">
<Button <Button
size="sm" size="sm"
themes="outline" themes="outline"
className="bg-white dark:bg-secondary" className="bg-white"
onClick={() => openServerLog()} onClick={() => openServerLog()}
> >
<CodeIcon size={16} className="mr-2" /> <CodeIcon size={16} className="mr-2" />
@ -330,7 +324,7 @@ const LocalServerScreen = () => {
<Button <Button
size="sm" size="sm"
themes="outline" themes="outline"
className="bg-white dark:bg-secondary" className="bg-white"
onClick={() => clearServerLog()} onClick={() => clearServerLog()}
> >
<Paintbrush size={16} className="mr-2" /> <Paintbrush size={16} className="mr-2" />
@ -386,7 +380,7 @@ const LocalServerScreen = () => {
{/* Right bar */} {/* Right bar */}
<div <div
className={twMerge( className={twMerge(
'h-full flex-shrink-0 overflow-x-hidden border-l border-border bg-background transition-all duration-100 dark:bg-background/20', 'h-full flex-shrink-0 overflow-x-hidden border-l border-border bg-background transition-all duration-100',
showRightSideBar showRightSideBar
? 'w-80 translate-x-0 opacity-100' ? 'w-80 translate-x-0 opacity-100'
: 'w-0 translate-x-full opacity-0' : 'w-0 translate-x-full opacity-0'
@ -422,7 +416,7 @@ const LocalServerScreen = () => {
<span> <span>
Model failed to start. Access{' '} Model failed to start. Access{' '}
<span <span
className="cursor-pointer text-primary dark:text-blue-400" className="cursor-pointer text-blue-600"
onClick={() => setModalTroubleShooting(true)} onClick={() => setModalTroubleShooting(true)}
> >
troubleshooting assistance troubleshooting assistance

View File

@ -282,7 +282,7 @@ const Advanced = () => {
disabled={gpuList.length === 0 || !gpuEnabled} disabled={gpuList.length === 0 || !gpuEnabled}
value={selectedGpu.join()} value={selectedGpu.join()}
> >
<SelectTrigger className="w-[340px] bg-white dark:bg-gray-500"> <SelectTrigger className="w-[340px] bg-white">
<SelectValue placeholder={gpuSelectionPlaceHolder}> <SelectValue placeholder={gpuSelectionPlaceHolder}>
<span className="line-clamp-1 w-full pr-8"> <span className="line-clamp-1 w-full pr-8">
{selectedGpu.join()} {selectedGpu.join()}

View File

@ -1,57 +0,0 @@
import { motion as m } from 'framer-motion'
import { twMerge } from 'tailwind-merge'
import { useUserConfigs } from '@/hooks/useUserConfigs'
type PrimaryColorOption = {
value: PrimaryColor
class: string
}
const primaryColorOptions: PrimaryColorOption[] = [
{
value: 'primary-blue',
class: 'bg-blue-500',
},
{
value: 'primary-purple',
class: 'bg-purple-500',
},
{
value: 'primary-green',
class: 'bg-green-500',
},
]
export default function TogglePrimary() {
const [config, setUserConfig] = useUserConfigs()
const handleChangeAccent = (primaryColor: PrimaryColor) => {
setUserConfig({ ...config, primaryColor })
}
return (
<div className="flex items-center">
{primaryColorOptions.map((option, i) => {
const isActive = config.primaryColor === option.value
return (
<div
className="relative flex h-6 w-6 items-center justify-center"
key={i}
>
<button
className={twMerge('h-3.5 w-3.5 rounded-full', option.class)}
onClick={() => handleChangeAccent(option.value)}
/>
{isActive ? (
<m.div
className="absolute inset-0 h-full w-full rounded-full border border-primary/50 bg-primary/20"
layoutId="active-primary-menu"
/>
) : null}
</div>
)
})}
</div>
)
}

View File

@ -28,7 +28,7 @@ export default function ToggleTheme() {
</button> </button>
{isActive ? ( {isActive ? (
<m.div <m.div
className="absolute inset-0 h-full w-full rounded-md border border-primary/50 bg-primary/20" className="absolute inset-0 h-full w-full rounded-md border border-primary/50 bg-blue-500/20"
layoutId="active-theme-menu" layoutId="active-theme-menu"
/> />
) : null} ) : null}

View File

@ -1,4 +1,3 @@
import ToggleAccent from '@/screens/Settings/Appearance/TogglePrimary'
import ToggleTheme from '@/screens/Settings/Appearance/ToggleTheme' import ToggleTheme from '@/screens/Settings/Appearance/ToggleTheme'
export default function AppearanceOptions() { export default function AppearanceOptions() {
@ -22,7 +21,6 @@ export default function AppearanceOptions() {
Choose the primary accent color used throughout the app. Choose the primary accent color used throughout the app.
</p> </p>
</div> </div>
<ToggleAccent />
</div> </div>
</div> </div>
) )

View File

@ -1,6 +1,12 @@
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { Model, ModelEvent, events, openFileExplorer } from '@janhq/core' import {
Model,
ModelEvent,
events,
joinPath,
openFileExplorer,
} from '@janhq/core'
import { import {
Modal, Modal,
ModalContent, ModalContent,
@ -47,6 +53,7 @@ const EditModelInfoModal: React.FC = () => {
const janDataFolder = useAtomValue(janDataFolderPathAtom) const janDataFolder = useAtomValue(janDataFolderPathAtom)
const updateImportingModel = useSetAtom(updateImportingModelAtom) const updateImportingModel = useSetAtom(updateImportingModelAtom)
const { updateModelInfo } = useImportModel() const { updateModelInfo } = useImportModel()
const [modelPath, setModelPath] = useState<string>('')
const editingModel = importingModels.find( const editingModel = importingModels.find(
(model) => model.importId === editingModelId (model) => model.importId === editingModelId
@ -88,13 +95,19 @@ const EditModelInfoModal: React.FC = () => {
setEditingModelId(undefined) setEditingModelId(undefined)
} }
const modelFolderPath = useMemo(() => { useEffect(() => {
return `${janDataFolder}/models/${editingModel?.modelId}` const getModelPath = async () => {
const modelId = editingModel?.modelId
if (!modelId) return ''
const path = await joinPath([janDataFolder, 'models', modelId])
setModelPath(path)
}
getModelPath()
}, [janDataFolder, editingModel]) }, [janDataFolder, editingModel])
const onShowInFinderClick = useCallback(() => { const onShowInFinderClick = useCallback(() => {
openFileExplorer(modelFolderPath) openFileExplorer(modelPath)
}, [modelFolderPath]) }, [modelPath])
if (!editingModel) { if (!editingModel) {
setImportModelStage('IMPORTING_MODEL') setImportModelStage('IMPORTING_MODEL')
@ -104,7 +117,10 @@ const EditModelInfoModal: React.FC = () => {
} }
return ( return (
<Modal open={importModelStage === 'EDIT_MODEL_INFO'}> <Modal
open={importModelStage === 'EDIT_MODEL_INFO'}
onOpenChange={onCancelClick}
>
<ModalContent> <ModalContent>
<ModalHeader> <ModalHeader>
<ModalTitle>Edit Model Information</ModalTitle> <ModalTitle>Edit Model Information</ModalTitle>
@ -130,7 +146,7 @@ const EditModelInfoModal: React.FC = () => {
</div> </div>
<div className="mt-1 flex flex-row items-center space-x-2"> <div className="mt-1 flex flex-row items-center space-x-2">
<span className="line-clamp-1 text-xs font-normal text-[#71717A]"> <span className="line-clamp-1 text-xs font-normal text-[#71717A]">
{modelFolderPath} {modelPath}
</span> </span>
<Button themes="ghost" onClick={onShowInFinderClick}> <Button themes="ghost" onClick={onShowInFinderClick}>
{openFileTitle()} {openFileTitle()}

View File

@ -15,7 +15,8 @@ const ImportInProgressIcon: React.FC<Props> = ({
const [isHovered, setIsHovered] = useState(false) const [isHovered, setIsHovered] = useState(false)
const onMouseOver = () => { const onMouseOver = () => {
setIsHovered(true) // for now we don't allow user to cancel importing
setIsHovered(false)
} }
const onMouseOut = () => { const onMouseOut = () => {

View File

@ -16,7 +16,7 @@ const ImportModelOptionSelection: React.FC<Props> = ({
onClick={() => setSelectedOptionType(option.type)} onClick={() => setSelectedOptionType(option.type)}
> >
<div className="flex h-5 w-5 items-center justify-center rounded-full border border-[#2563EB]"> <div className="flex h-5 w-5 items-center justify-center rounded-full border border-[#2563EB]">
{checked && <div className="h-2 w-2 rounded-full bg-primary" />} {checked && <div className="h-2 w-2 rounded-full bg-blue-500" />}
</div> </div>
<div className="ml-2 flex-1"> <div className="ml-2 flex-1">

View File

@ -29,7 +29,7 @@ const ImportSuccessIcon: React.FC<Props> = ({ onEditModelClick }) => {
} }
const SuccessIcon: React.FC = React.memo(() => ( const SuccessIcon: React.FC = React.memo(() => (
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary"> <div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-500">
<Check color="#FFF" /> <Check color="#FFF" />
</div> </div>
)) ))

View File

@ -1,6 +1,10 @@
import { useCallback, useMemo } from 'react'
import { ImportingModel } from '@janhq/core/.' import { ImportingModel } from '@janhq/core/.'
import { useSetAtom } from 'jotai' import { useSetAtom } from 'jotai'
import { AlertCircle } from 'lucide-react'
import { setImportModelStageAtom } from '@/hooks/useImportModel' import { setImportModelStageAtom } from '@/hooks/useImportModel'
import { toGibibytes } from '@/utils/converter' import { toGibibytes } from '@/utils/converter'
@ -16,28 +20,39 @@ type Props = {
const ImportingModelItem: React.FC<Props> = ({ model }) => { const ImportingModelItem: React.FC<Props> = ({ model }) => {
const setImportModelStage = useSetAtom(setImportModelStageAtom) const setImportModelStage = useSetAtom(setImportModelStageAtom)
const setEditingModelId = useSetAtom(editingModelIdAtom) const setEditingModelId = useSetAtom(editingModelIdAtom)
const sizeInGb = toGibibytes(model.size)
const onEditModelInfoClick = () => { const onEditModelInfoClick = useCallback(() => {
setEditingModelId(model.importId) setEditingModelId(model.importId)
setImportModelStage('EDIT_MODEL_INFO') setImportModelStage('EDIT_MODEL_INFO')
} }, [setImportModelStage, setEditingModelId, model.importId])
const onDeleteModelClick = () => {} const onDeleteModelClick = useCallback(() => {}, [])
const displayStatus = useMemo(() => {
if (model.status === 'FAILED') {
return 'Failed'
} else {
return toGibibytes(model.size)
}
}, [model.status, model.size])
return ( return (
<div className="flex w-full flex-row items-center space-x-3 rounded-lg border px-4 py-3"> <div className="flex w-full flex-row items-center space-x-3 rounded-lg border px-4 py-3">
<p className="line-clamp-1 flex-1">{model.name}</p> <p className="line-clamp-1 flex-1 font-semibold text-[#09090B]">
<p>{sizeInGb}</p> {model.name}
</p>
<p className="text-[#71717A]">{displayStatus}</p>
{model.status === 'IMPORTED' || model.status === 'FAILED' ? ( {model.status === 'IMPORTED' && (
<ImportSuccessIcon onEditModelClick={onEditModelInfoClick} /> <ImportSuccessIcon onEditModelClick={onEditModelInfoClick} />
) : ( )}
{(model.status === 'IMPORTING' || model.status === 'PREPARING') && (
<ImportInProgressIcon <ImportInProgressIcon
percentage={model.percentage ?? 0} percentage={model.percentage ?? 0}
onDeleteModelClick={onDeleteModelClick} onDeleteModelClick={onDeleteModelClick}
/> />
)} )}
{model.status === 'FAILED' && <AlertCircle size={24} color="#F00" />}
</div> </div>
) )
} }

View File

@ -1,6 +1,6 @@
import { useCallback, useMemo } from 'react' import { useCallback, useEffect, useState } from 'react'
import { openFileExplorer } from '@janhq/core' import { joinPath, openFileExplorer } from '@janhq/core'
import { import {
Button, Button,
Modal, Modal,
@ -31,7 +31,15 @@ const ImportingModelModal: React.FC = () => {
const setImportModelStage = useSetAtom(setImportModelStageAtom) const setImportModelStage = useSetAtom(setImportModelStageAtom)
const janDataFolder = useAtomValue(janDataFolderPathAtom) const janDataFolder = useAtomValue(janDataFolderPathAtom)
const modelFolder = useMemo(() => `${janDataFolder}/models`, [janDataFolder]) const [modelFolder, setModelFolder] = useState('')
useEffect(() => {
const getModelPath = async () => {
const modelPath = await joinPath([janDataFolder, 'models'])
setModelFolder(modelPath)
}
getModelPath()
}, [janDataFolder])
const finishedImportModel = importingModels.filter( const finishedImportModel = importingModels.filter(
(model) => model.status === 'IMPORTED' (model) => model.status === 'IMPORTED'

View File

@ -152,7 +152,7 @@ export default function RowModel(props: RowModelProps) {
) : ( ) : (
<PlayIcon size={16} className="text-muted-foreground" /> <PlayIcon size={16} className="text-muted-foreground" />
)} )}
<span className="text-bold capitalize text-black dark:text-muted-foreground"> <span className="text-bold capitalize text-black">
{isActiveModel ? stateModel.state : 'Start'} {isActiveModel ? stateModel.state : 'Start'}
&nbsp;Model &nbsp;Model
</span> </span>
@ -189,9 +189,7 @@ export default function RowModel(props: RowModelProps) {
}} }}
> >
<Trash2Icon size={16} className="text-muted-foreground" /> <Trash2Icon size={16} className="text-muted-foreground" />
<span className="text-bold text-black dark:text-muted-foreground"> <span className="text-bold text-black">Delete Model</span>
Delete Model
</span>
</div> </div>
</div> </div>
)} )}

View File

@ -2,7 +2,6 @@ import { useCallback, useState } from 'react'
import { useDropzone } from 'react-dropzone' import { useDropzone } from 'react-dropzone'
import { ImportingModel } from '@janhq/core'
import { Button, Input, ScrollArea } from '@janhq/uikit' import { Button, Input, ScrollArea } from '@janhq/uikit'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtomValue, useSetAtom } from 'jotai'
@ -10,60 +9,29 @@ import { Plus, SearchIcon, UploadCloudIcon } from 'lucide-react'
import { twMerge } from 'tailwind-merge' import { twMerge } from 'tailwind-merge'
import { v4 as uuidv4 } from 'uuid' import useDropModelBinaries from '@/hooks/useDropModelBinaries'
import { setImportModelStageAtom } from '@/hooks/useImportModel' import { setImportModelStageAtom } from '@/hooks/useImportModel'
import { getFileInfoFromFile } from '@/utils/file'
import RowModel from './Row' import RowModel from './Row'
import { import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
downloadedModelsAtom,
importingModelsAtom,
} from '@/helpers/atoms/Model.atom'
const Column = ['Name', 'Model ID', 'Size', 'Version', 'Status', ''] const Column = ['Name', 'Model ID', 'Size', 'Version', 'Status', '']
const Models: React.FC = () => { const Models: React.FC = () => {
const downloadedModels = useAtomValue(downloadedModelsAtom) const downloadedModels = useAtomValue(downloadedModelsAtom)
const setImportModelStage = useSetAtom(setImportModelStageAtom) const setImportModelStage = useSetAtom(setImportModelStageAtom)
const setImportingModels = useSetAtom(importingModelsAtom)
const [searchValue, setsearchValue] = useState('') const [searchValue, setsearchValue] = useState('')
const { onDropModels } = useDropModelBinaries()
const filteredDownloadedModels = downloadedModels const filteredDownloadedModels = downloadedModels
.filter((x) => x.name?.toLowerCase().includes(searchValue.toLowerCase())) .filter((x) => x.name?.toLowerCase().includes(searchValue.toLowerCase()))
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
const onDrop = useCallback(
(acceptedFiles: File[]) => {
const filePathWithSize = getFileInfoFromFile(acceptedFiles)
const importingModels: ImportingModel[] = filePathWithSize.map(
(file) => ({
importId: uuidv4(),
modelId: undefined,
name: file.name,
description: '',
path: file.path,
tags: [],
size: file.size,
status: 'PREPARING',
format: 'gguf',
})
)
if (importingModels.length === 0) return
setImportingModels(importingModels)
setImportModelStage('MODEL_SELECTED')
},
[setImportModelStage, setImportingModels]
)
const { getRootProps, isDragActive } = useDropzone({ const { getRootProps, isDragActive } = useDropzone({
noClick: true, noClick: true,
multiple: true, multiple: true,
onDrop, onDrop: onDropModels,
}) })
const onImportModelClick = useCallback(() => { const onImportModelClick = useCallback(() => {

View File

@ -1,7 +1,7 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import { useDropzone } from 'react-dropzone' import { useDropzone } from 'react-dropzone'
import { ImportingModel, fs } from '@janhq/core' import { ImportingModel, baseName, fs } from '@janhq/core'
import { Modal, ModalContent, ModalHeader, ModalTitle } from '@janhq/uikit' import { Modal, ModalContent, ModalHeader, ModalTitle } from '@janhq/uikit'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtomValue, useSetAtom } from 'jotai'
@ -9,16 +9,15 @@ import { UploadCloudIcon } from 'lucide-react'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { snackbar } from '@/containers/Toast'
import useDropModelBinaries from '@/hooks/useDropModelBinaries'
import { import {
getImportModelStageAtom, getImportModelStageAtom,
setImportModelStageAtom, setImportModelStageAtom,
} from '@/hooks/useImportModel' } from '@/hooks/useImportModel'
import { import { FilePathWithSize } from '@/utils/file'
FilePathWithSize,
getFileInfoFromFile,
getFileNameFromPath,
} from '@/utils/file'
import { importingModelsAtom } from '@/helpers/atoms/Model.atom' import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
@ -26,6 +25,7 @@ const SelectingModelModal: React.FC = () => {
const setImportModelStage = useSetAtom(setImportModelStageAtom) const setImportModelStage = useSetAtom(setImportModelStageAtom)
const importModelStage = useAtomValue(getImportModelStageAtom) const importModelStage = useAtomValue(getImportModelStageAtom)
const setImportingModels = useSetAtom(importingModelsAtom) const setImportingModels = useSetAtom(importingModelsAtom)
const { onDropModels } = useDropModelBinaries()
const onSelectFileClick = useCallback(async () => { const onSelectFileClick = useCallback(async () => {
const filePaths = await window.core?.api?.selectModelFiles() const filePaths = await window.core?.api?.selectModelFiles()
@ -36,7 +36,7 @@ const SelectingModelModal: React.FC = () => {
const fileStats = await fs.fileStat(filePath, true) const fileStats = await fs.fileStat(filePath, true)
if (!fileStats || fileStats.isDirectory) continue if (!fileStats || fileStats.isDirectory) continue
const fileName = getFileNameFromPath(filePath) const fileName = await baseName(filePath)
sanitizedFilePaths.push({ sanitizedFilePaths.push({
path: filePath, path: filePath,
name: fileName, name: fileName,
@ -44,12 +44,19 @@ const SelectingModelModal: React.FC = () => {
}) })
} }
const importingModels: ImportingModel[] = sanitizedFilePaths.map( const unsupportedFiles = sanitizedFilePaths.filter(
(file) => !file.path.endsWith('.gguf')
)
const supportedFiles = sanitizedFilePaths.filter((file) =>
file.path.endsWith('.gguf')
)
const importingModels: ImportingModel[] = supportedFiles.map(
({ path, name, size }: FilePathWithSize) => { ({ path, name, size }: FilePathWithSize) => {
return { return {
importId: uuidv4(), importId: uuidv4(),
modelId: undefined, modelId: undefined,
name: name, name: name.replace('.gguf', ''),
description: '', description: '',
path: path, path: path,
tags: [], tags: [],
@ -59,45 +66,26 @@ const SelectingModelModal: React.FC = () => {
} }
} }
) )
if (unsupportedFiles.length > 0) {
snackbar({
description: `File has to be a .gguf file`,
type: 'error',
})
}
if (importingModels.length === 0) return if (importingModels.length === 0) return
setImportingModels(importingModels) setImportingModels(importingModels)
setImportModelStage('MODEL_SELECTED') setImportModelStage('MODEL_SELECTED')
}, [setImportingModels, setImportModelStage]) }, [setImportingModels, setImportModelStage])
const onDrop = useCallback(
(acceptedFiles: File[]) => {
const filePathWithSize = getFileInfoFromFile(acceptedFiles)
const importingModels: ImportingModel[] = filePathWithSize.map(
(file) => ({
importId: uuidv4(),
modelId: undefined,
name: file.name,
description: '',
path: file.path,
tags: [],
size: file.size,
status: 'PREPARING',
format: 'gguf',
})
)
if (importingModels.length === 0) return
setImportingModels(importingModels)
setImportModelStage('MODEL_SELECTED')
},
[setImportModelStage, setImportingModels]
)
const { isDragActive, getRootProps } = useDropzone({ const { isDragActive, getRootProps } = useDropzone({
noClick: true, noClick: true,
multiple: true, multiple: true,
onDrop, onDrop: onDropModels,
}) })
const borderColor = isDragActive ? 'border-primary' : 'border-[#F4F4F5]' const borderColor = isDragActive ? 'border-primary' : 'border-[#F4F4F5]'
const textColor = isDragActive ? 'text-primary' : 'text-[#71717A]' const textColor = isDragActive ? 'text-blue-600' : 'text-[#71717A]'
const dragAndDropBgColor = isDragActive ? 'bg-[#EFF6FF]' : 'bg-white' const dragAndDropBgColor = isDragActive ? 'bg-[#EFF6FF]' : 'bg-white'
return ( return (
@ -128,7 +116,7 @@ const SelectingModelModal: React.FC = () => {
</div> </div>
<div className="mt-4"> <div className="mt-4">
<span className="text-sm font-bold text-primary"> <span className="text-sm font-bold text-blue-600">
Click to upload Click to upload
</span> </span>
<span className={`text-sm ${textColor} font-medium`}> <span className={`text-sm ${textColor} font-medium`}>

View File

@ -15,7 +15,6 @@ const SettingMenu: React.FC<Props> = ({ activeMenu, onMenuClick }) => {
useEffect(() => { useEffect(() => {
setMenus([ setMenus([
'My Models', 'My Models',
'My Settings',
'Advanced Settings', 'Advanced Settings',
...(window.electronAPI ? ['Extensions'] : []), ...(window.electronAPI ? ['Extensions'] : []),
]) ])
@ -39,7 +38,7 @@ const SettingMenu: React.FC<Props> = ({ activeMenu, onMenuClick }) => {
{isActive && ( {isActive && (
<m.div <m.div
className="absolute inset-0 -left-3 h-full w-[calc(100%+24px)] rounded-md bg-primary/50" className="absolute inset-0 -left-3 h-full w-[calc(100%+24px)] rounded-md bg-gray-200"
layoutId="active-static-menu" layoutId="active-static-menu"
/> />
)} )}

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import Advanced from '@/screens/Settings/Advanced' import Advanced from '@/screens/Settings/Advanced'
import AppearanceOptions from '@/screens/Settings/Appearance'
import ExtensionCatalog from '@/screens/Settings/CoreExtensions' import ExtensionCatalog from '@/screens/Settings/CoreExtensions'
import Models from '@/screens/Settings/Models' import Models from '@/screens/Settings/Models'
@ -14,9 +14,6 @@ const handleShowOptions = (menu: string) => {
case 'Extensions': case 'Extensions':
return <ExtensionCatalog /> return <ExtensionCatalog />
case 'My Settings':
return <AppearanceOptions />
case 'Advanced Settings': case 'Advanced Settings':
return <Advanced /> return <Advanced />

View File

@ -1,5 +1,5 @@
.message { .message {
@apply text-black dark:text-gray-300; @apply text-black;
white-space: pre-line; white-space: pre-line;
ul, ul,
@ -10,7 +10,7 @@
} }
a { a {
@apply text-blue-600 dark:text-blue-300; @apply text-blue-600;
&:hover { &:hover {
@apply underline; @apply underline;
} }

View File

@ -1,6 +0,0 @@
type PrimaryColor = 'primary-blue' | 'primary-green' | 'primary-purple'
type UserConfig = {
gettingStartedShow?: boolean
primaryColor?: PrimaryColor
}

View File

@ -1,13 +1,16 @@
export const toGibibytes = (input: number) => { export const toGibibytes = (
input: number,
options?: { hideUnit?: boolean }
) => {
if (!input) return '' if (!input) return ''
if (input > 1024 ** 3) { if (input > 1024 ** 3) {
return (input / 1024 ** 3).toFixed(2) + 'GB' return (input / 1024 ** 3).toFixed(2) + (options?.hideUnit ? '' : 'GB')
} else if (input > 1024 ** 2) { } else if (input > 1024 ** 2) {
return (input / 1024 ** 2).toFixed(2) + 'MB' return (input / 1024 ** 2).toFixed(2) + (options?.hideUnit ? '' : 'MB')
} else if (input > 1024) { } else if (input > 1024) {
return (input / 1024).toFixed(2) + 'KB' return (input / 1024).toFixed(2) + (options?.hideUnit ? '' : 'KB')
} else { } else {
return input + 'B' return input + (options?.hideUnit ? '' : 'B')
} }
} }

View File

@ -1,3 +1,5 @@
import { baseName } from '@janhq/core'
export type FilePathWithSize = { export type FilePathWithSize = {
path: string path: string
name: string name: string
@ -8,24 +10,17 @@ export interface FileWithPath extends File {
path?: string path?: string
} }
export const getFileNameFromPath = (filePath: string): string => { export const getFileInfoFromFile = async (
let fileName = filePath.split('/').pop() ?? ''
if (fileName.split('.').length > 1) {
fileName = fileName.split('.').slice(0, -1).join('.')
}
return fileName
}
export const getFileInfoFromFile = (
files: FileWithPath[] files: FileWithPath[]
): FilePathWithSize[] => { ): Promise<FilePathWithSize[]> => {
const result: FilePathWithSize[] = [] const result: FilePathWithSize[] = []
for (const file of files) { for (const file of files) {
if (file.path && file.path.length > 0) { if (file.path && file.path.length > 0) {
const fileName = await baseName(file.path)
result.push({ result.push({
path: file.path, path: file.path,
name: getFileNameFromPath(file.path), name: fileName,
size: file.size, size: file.size,
}) })
} }