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">
<td style="text-align:center"><b>Experimental (Nightly Build)</b></td>
<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" />
<b>jan.exe</b>
</a>
</td>
<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" />
<b>Intel</b>
</a>
</td>
<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" />
<b>M1/M2</b>
</a>
</td>
<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" />
<b>jan.deb</b>
</a>
</td>
<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" />
<b>jan.AppImage</b>
</a>

View File

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

View File

@ -65,7 +65,7 @@ const joinPath: (paths: string[]) => Promise<string> = (paths) => global.core.ap
* @param path - The path to retrieve.
* @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.

View File

@ -42,6 +42,24 @@ export class Downloader implements Processor {
// Downloading file to a temp file first
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, {})
.on('progress', (state: any) => {
const downloadState: DownloadState = {

View File

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

View File

@ -29,3 +29,18 @@ keywords:
## 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
# 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
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
description: Antivirus compatibility testing documentation for the Jan App v0.4.4 release.
description: Antivirus compatibility testing documentation
keywords:
[
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 |
| McAfee | 4.21.0.0 | Scanned and 0 threat(s) detected |
| Microsoft Defender | 1.403.2259.0 | Scanned and 0 threat(s) detected |
## Conclusion
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.
To report issues, false positives, or to request additional testing, please email devops@jan.ai

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 `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 `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)
### 5. Try Out the Integration of Jan and Continue in VS Code
### 5. Use Continue in VS 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`.
![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`.

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
- [ ] :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.
- [ ] Validate that the app is correctly installed in the default or user-specified directory.
- [ ] Ensure that all necessary dependencies are installed along with the app.
- [ ] :key: :rocket: Confirm that the app launches successfully after installation.
- [ ] :key: Confirm that the app launches successfully after installation.
### 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: 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.
- [ ] 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::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
### 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.).
### 2. Users check the memory usage 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: 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.
- [ ] Verify the troubleshooting assistant correctly capture hardware / log info #1784
## C. Thread
### 1. Users can chat with Jan, the default assistant
- [ ] Verify that the input box for messages is present and functional.
- [ ] :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: Verify sending a message enables users to receive responses from model.
- [ ] :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).
- [ ] :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, ...).
- [ ] :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.
- [ ] 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.
- [ ] Check the `regenerate` button renews the response (single / multiple times).
- [ ] Check the `Instructions` update correctly after the user updates it midway (mid-thread).
### 2. Users can customize chat settings like model parameters via both the GUI & thread.json
- [ ] :key: Confirm that the Threads settings options are accessible.
- [ ] Test the functionality to adjust model parameters (e.g., Temperature, Top K, Top P) from the GUI and verify they are reflected in the chat behavior.
- [ ] :key: Ensure that changes can be saved and persisted between sessions.
- [ ] Validate that users can access and modify the thread.json file.
- [ ] :key: Check that changes made in thread.json are correctly applied to the chat session upon reload or restart.
- [ ] Check the maximum and minimum limits of the adjustable parameters and how they affect the assistant's responses.
- [ ] :key: Validate user permissions for those who can change settings and persist them.
- [ ] :key: Ensure that users switch between threads with different models, the app can handle it.
### 3. Model dropdown
@ -89,25 +75,16 @@
- [ ] Model size should display (for both installed and imported models)
### 4. Users can click on a history thread
- [ ] Test the ability to click on any thread in the history panel.
- [ ] :key: Verify that clicking a thread brings up the past conversation in the main chat window.
- [ ] :key: Ensure that the selected thread is highlighted or otherwise indicated in the history panel.
- [ ] Confirm that the chat window displays the entire conversation from the selected history thread without any missing messages.
- [ ] :key: Check the performance and accuracy of the history feature when dealing with a large number of threads.
- [ ] Validate that historical threads reflect the exact state of the chat at that time, including settings.
- [ ] :key: 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.
- [ ] Ensure there is a clear interface to input or change instructions for the assistant.
- [ ] Test if the instructions set by the user are being followed by the assistant in subsequent conversations.
- [ ] :key: Validate that changes to instructions are updated in real time and do not require a restart of the application or session.
- [ ] :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: 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.
@ -115,7 +92,6 @@
### 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.
- [ ] Test the functionality of any filters that refine model recommendations.
@ -123,7 +99,6 @@
- [ ] Display the best model for their RAM at the top.
- [ ] :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.
### 3. Users can download models via a HuggingFace URL (coming soon)
@ -132,22 +107,21 @@
- [ ] :key: Check the progress bar reflects the right process.
- [ ] 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.
- [ ] :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
- [ ] :key: Check `start` button response exactly what it does.
- [ ] :key: Check `stop` button response exactly what it does.
- [ ] :key: Check `delete` button response exactly what it does.
- [ ] :key: Check `start` / `stop` / `delete` button response exactly what it does.
- [ ] 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.
- [ ] :warning:The recommended tags should present right for the user's hardware.
- [ ] Assess that the descriptions of models are accurate and informative.
### 6. Users can Integrate With a Remote Server
- [ ] :key: Import openAI GPT model https://jan.ai/guides/using-models/integrate-with-remote-server/ and the model displayed in Hub / Thread dropdown
@ -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: 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.
## 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.
- [ ] Confirm that the application saves the theme preference and persists it across sessions.
- [ ] Validate that all elements of the UI are compatible with the theme changes and maintain legibility and contrast.
### 2. Users change the extensions [TBU]
### 2. Extensions [TBU]
- [ ] Confirm that the `Extensions` tab lists all available plugins.
- [ ] :key: Test the toggle switch for each plugin to ensure it enables or disables the plugin correctly.
- [ ] 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.
- [ ] 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] :key: Test the toggle switch for each plugin to ensure it enables or disables the plugin correctly.
- [x] Verify that plugin changes take effect without needing to restart the application unless specified.
- [x] :key: Check that the plugin's status (`Installed the latest version`) updates accurately after any changes.
- [x] Validate the `Manual Installation` process by selecting and installing a plugin file.
- [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: 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**
- [ ] 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.
## G. Local API server

View File

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

View File

@ -5,11 +5,11 @@
@apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400;
&-primary {
@apply bg-primary hover:bg-primary/90 text-white;
@apply bg-blue-600 text-white hover:bg-blue-600/90;
}
&-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 {
@ -17,7 +17,7 @@
}
&-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 {
@ -66,7 +66,7 @@
[type='reset'],
[type='submit'] {
&.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;
}
&.btn-secondary {

View File

@ -1,5 +1,5 @@
.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 {
@apply h-4 w-4;

View File

@ -1,6 +1,6 @@
.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 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 file:border-0 file:bg-transparent file:font-medium;
}

View File

@ -42,69 +42,10 @@
--danger: 346.8 77.2% 49.8%;
--danger-foreground: 355.7 100% 97.3%;
--secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
--border: 20 5.9% 90%;
--input: 20 5.9% 90%;
--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 {
@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 {
@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 {
@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;
&-caret {

View File

@ -2,7 +2,7 @@
@apply relative flex w-full touch-none select-none items-center;
&-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] {
@apply cursor-not-allowed opacity-50;
}
@ -13,6 +13,6 @@
}
&-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 {
@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 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;
&-toggle {

View File

@ -1,6 +1,6 @@
.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 {
@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) {
return (
<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" />
<Providers>{children}</Providers>
</body>

View File

@ -45,7 +45,7 @@ export default function CardSidebar({
return (
<div
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'
)}
>
@ -61,7 +61,7 @@ export default function CardSidebar({
if (!children) return
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
className={twMerge(
@ -79,7 +79,7 @@ export default function CardSidebar({
{!hideMoreVerticalAction && (
<div
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)}
>
<MoreVerticalIcon className="h-5 w-5" />
@ -114,7 +114,7 @@ export default function CardSidebar({
<>
{title === 'Model' ? (
<div className="flex flex-col">
<span className="font-medium text-black dark:text-muted-foreground">
<span className="font-medium text-black">
{openFileTitle()}
</span>
<span className="mt-1 text-muted-foreground">
@ -122,7 +122,7 @@ export default function CardSidebar({
</span>
</div>
) : (
<span className="text-bold text-black dark:text-muted-foreground">
<span className="text-bold text-black">
{openFileTitle()}
</span>
)}
@ -141,7 +141,7 @@ export default function CardSidebar({
/>
<>
<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{' '}
<span
className="font-bold"
@ -175,7 +175,7 @@ export default function CardSidebar({
{show && (
<div
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'
)}
>

View File

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

View File

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

View File

@ -60,7 +60,7 @@ const GPUDriverPrompt: React.FC = () => {
id="default-checkbox"
type="checkbox"
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>
</div>

View File

@ -47,7 +47,7 @@ export default function DownloadingState() {
</span>
</Button>
<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={{
width: `${totalPercentage}%`,
}}

View File

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

View File

@ -57,17 +57,6 @@ const SystemMonitor = () => {
// 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 (
<Fragment>
<div
@ -131,10 +120,11 @@ const SystemMonitor = () => {
</div>
</div>
<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>
<span className="text-xs text-muted-foreground">
{toGibibytes(usedRam)} of {toGibibytes(totalRam)} used
<span className="text-sm text-muted-foreground">
{toGibibytes(usedRam, { hideUnit: true })}/
{toGibibytes(totalRam, { hideUnit: true })} GB
</span>
</div>
<div className="flex items-center gap-x-4">
@ -148,30 +138,29 @@ const SystemMonitor = () => {
</div>
</div>
{gpus.length > 0 && (
<div className="mb-4 border-b border-border pb-4">
<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>
<div className="mb-4 border-b border-border pb-4 last:border-none">
{gpus.map((gpu, index) => (
<div
key={index}
className="mt-4 flex items-start justify-between gap-4"
>
<span className="line-clamp-1 w-1/2 font-medium text-muted-foreground">
{gpu.name}
</span>
<div className="flex gap-x-2">
<span className="font-semibold">
<div key={index} className="mt-4 flex flex-col gap-x-2">
<div className="flex w-full items-start justify-between">
<span className="line-clamp-1 w-1/2 font-bold">
{gpu.name}
</span>
<div className="flex gap-x-2">
<div className="text-muted-foreground">
<span>
{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}%
</span>
<div>
<span className="font-semibold">{gpu.vram}</span>
<span>MB VRAM</span>
</div>
</div>
</div>
))}

View File

@ -45,7 +45,7 @@ export default function RibbonNav() {
size={20}
className={twMerge(
'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>
{isActive && (
<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"
/>
)}
@ -166,7 +166,7 @@ export default function RibbonNav() {
</div>
{isActive && (
<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"
/>
)}

View File

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

View File

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

View File

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

View File

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

View File

@ -38,7 +38,7 @@ const ModalTroubleShooting: React.FC = () => {
<a
href="https://jan.ai/guides/troubleshooting"
target="_blank"
className="text-blue-600 hover:underline dark:text-blue-300"
className="text-blue-600 hover:underline"
>
troubleshooting guide
</a>
@ -65,7 +65,7 @@ const ModalTroubleShooting: React.FC = () => {
<a
href="https://discord.gg/AsJ8krTT3N"
target="_blank"
className="text-blue-600 hover:underline dark:text-blue-300"
className="text-blue-600 hover:underline"
>
Discord
</a>
@ -77,8 +77,8 @@ const ModalTroubleShooting: React.FC = () => {
<div className="flex flex-col pt-4">
{/* 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">
<ul className="inline-flex space-x-2 rounded-lg bg-zinc-200 px-1 dark:bg-secondary">
<div className="relative bg-zinc-100 px-4 py-2">
<ul className="inline-flex space-x-2 rounded-lg bg-zinc-200 px-1">
{logOption.map((name, i) => {
return (
<li
@ -89,15 +89,14 @@ const ModalTroubleShooting: React.FC = () => {
<span
className={twMerge(
'relative z-50 font-medium text-muted-foreground',
isTabActive === i &&
'font-bold text-foreground dark:text-black'
isTabActive === i && 'font-bold text-foreground'
)}
>
{name}
</span>
{isTabActive === i && (
<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"
/>
)}

View File

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

View File

@ -33,7 +33,7 @@ const OpenAiKeyInput: React.FC = () => {
<div className="my-4">
<label
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
</label>

View File

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

View File

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

View File

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

View File

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

View File

@ -108,11 +108,11 @@ export function toaster(props: Props) {
return (
<div
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'
)}
>
<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="pr-4">
<h1 className="font-bold">{title}</h1>
@ -120,7 +120,7 @@ export function toaster(props: Props) {
</div>
<XIcon
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)}
/>
</div>
@ -138,16 +138,16 @@ export function snackbar(props: Props) {
return (
<div
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'
)}
>
<div className="flex items-start gap-x-3 dark:text-black">
<div className="flex items-start gap-x-3">
<div>{renderIcon(type)}</div>
<p className="pr-4">{description}</p>
<XIcon
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)}
/>
</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(
null,
(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()}>
<div className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary">
<Paintbrush size={16} className="text-muted-foreground" />
<span className="text-bold text-black dark:text-muted-foreground">
Clean thread
</span>
<span className="text-bold text-black">Clean thread</span>
</div>
</ModalTrigger>
<ModalPortal />

View File

@ -33,10 +33,8 @@ const DeleteThreadModal: React.FC<Props> = ({ threadId }) => {
<Modal>
<ModalTrigger asChild onClick={(e) => e.stopPropagation()}>
<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" />
<span className="text-bold text-red-600 dark:text-red-300">
Delete thread
</span>
<Trash2Icon size={16} className="text-red-600" />
<span className="text-bold text-red-600">Delete thread</span>
</div>
</ModalTrigger>
<ModalPortal />

View File

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

View File

@ -71,7 +71,7 @@ const Sidebar: React.FC = () => {
return (
<div
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
? 'w-80 translate-x-0 opacity-100'
: 'w-0 translate-x-full opacity-0'
@ -87,7 +87,7 @@ const Sidebar: React.FC = () => {
<div>
<label
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
</label>
@ -106,7 +106,7 @@ const Sidebar: React.FC = () => {
<div className="flex flex-col">
<label
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
</label>
@ -127,7 +127,7 @@ const Sidebar: React.FC = () => {
<div>
<label
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
</label>
@ -203,14 +203,14 @@ const Sidebar: React.FC = () => {
<div className="flex items-center justify-between">
<label
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
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon
size={16}
className="ml-2 flex-shrink-0 text-black dark:text-gray-500"
className="ml-2 flex-shrink-0 text-black"
/>
</TooltipTrigger>
<TooltipPortal>
@ -269,7 +269,7 @@ const Sidebar: React.FC = () => {
<div className="item-center mb-2 flex">
<label
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
</label>
@ -277,7 +277,7 @@ const Sidebar: React.FC = () => {
<TooltipTrigger asChild>
<InfoIcon
size={16}
className="ml-2 flex-shrink-0 dark:text-gray-500"
className="ml-2 flex-shrink-0"
/>
</TooltipTrigger>
<TooltipPortal>
@ -309,7 +309,7 @@ const Sidebar: React.FC = () => {
<div className="mb-2 flex items-center">
<label
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
</label>
@ -317,7 +317,7 @@ const Sidebar: React.FC = () => {
<TooltipTrigger asChild>
<InfoIcon
size={16}
className="ml-2 flex-shrink-0 dark:text-gray-500"
className="ml-2 flex-shrink-0"
/>
</TooltipTrigger>
<TooltipPortal>

View File

@ -18,7 +18,7 @@ import hljs from 'highlight.js'
import { useAtomValue } from 'jotai'
import { FolderOpenIcon } from 'lucide-react'
import { Marked, Renderer, marked as markedDefault } from 'marked'
import { Marked, Renderer } from 'marked'
import { markedHighlight } from 'marked-highlight'
@ -43,19 +43,6 @@ import {
getCurrentChatMessagesAtom,
} from '@/helpers/atoms/ChatMessage.atom'
function isMarkdownValue(value: string): boolean {
const tokenTypes: string[] = []
markedDefault(value, {
walkTokens: (token) => {
tokenTypes.push(token.type)
},
})
const isMarkdown = ['code', 'codespan'].some((tokenType) => {
return tokenTypes.includes(tokenType)
})
return isMarkdown
}
const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
let text = ''
const isUser = props.role === ChatCompletionRole.User
@ -282,7 +269,7 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
</div>
)}
{isUser && !isMarkdownValue(text) ? (
{isUser ? (
<>
{editMessage === props.id ? (
<div>

View File

@ -79,7 +79,7 @@ export default function ThreadList() {
<div
key={thread.id}
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={() => {
onThreadClick(thread)
@ -90,7 +90,7 @@ export default function ThreadList() {
{thread.updated && displayDate(thread.updated)}
</p>
<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
: 'No new message'}
@ -98,7 +98,7 @@ export default function ThreadList() {
</div>
<div
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 />
@ -109,7 +109,7 @@ export default function ThreadList() {
</div>
{activeThreadId === thread.id && (
<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"
/>
)}

View File

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

View File

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

View File

@ -181,7 +181,7 @@ const LocalServerScreen = () => {
<TooltipTrigger asChild>
<div className="space-y-4 p-4">
<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
</p>
<div className="flex w-full flex-shrink-0 items-center gap-x-2">
@ -231,15 +231,12 @@ const LocalServerScreen = () => {
<div>
<label
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)
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon
size={16}
className="mt-0.5 flex-shrink-0 dark:text-gray-500"
/>
<InfoIcon size={16} className="mt-0.5 flex-shrink-0" />
</TooltipTrigger>
<TooltipPortal>
<TooltipContent side="top" className="max-w-[240px]">
@ -266,15 +263,12 @@ const LocalServerScreen = () => {
<div>
<label
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
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon
size={16}
className="mt-0.5 flex-shrink-0 dark:text-gray-500"
/>
<InfoIcon size={16} className="mt-0.5 flex-shrink-0" />
</TooltipTrigger>
<TooltipPortal>
<TooltipContent side="top" className="max-w-[240px]">
@ -315,13 +309,13 @@ const LocalServerScreen = () => {
{/* Middle Bar */}
<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>
<div className="space-x-2">
<Button
size="sm"
themes="outline"
className="bg-white dark:bg-secondary"
className="bg-white"
onClick={() => openServerLog()}
>
<CodeIcon size={16} className="mr-2" />
@ -330,7 +324,7 @@ const LocalServerScreen = () => {
<Button
size="sm"
themes="outline"
className="bg-white dark:bg-secondary"
className="bg-white"
onClick={() => clearServerLog()}
>
<Paintbrush size={16} className="mr-2" />
@ -386,7 +380,7 @@ const LocalServerScreen = () => {
{/* Right bar */}
<div
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
? 'w-80 translate-x-0 opacity-100'
: 'w-0 translate-x-full opacity-0'
@ -422,7 +416,7 @@ const LocalServerScreen = () => {
<span>
Model failed to start. Access{' '}
<span
className="cursor-pointer text-primary dark:text-blue-400"
className="cursor-pointer text-blue-600"
onClick={() => setModalTroubleShooting(true)}
>
troubleshooting assistance

View File

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

View File

@ -1,4 +1,3 @@
import ToggleAccent from '@/screens/Settings/Appearance/TogglePrimary'
import ToggleTheme from '@/screens/Settings/Appearance/ToggleTheme'
export default function AppearanceOptions() {
@ -22,7 +21,6 @@ export default function AppearanceOptions() {
Choose the primary accent color used throughout the app.
</p>
</div>
<ToggleAccent />
</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 {
Modal,
ModalContent,
@ -47,6 +53,7 @@ const EditModelInfoModal: React.FC = () => {
const janDataFolder = useAtomValue(janDataFolderPathAtom)
const updateImportingModel = useSetAtom(updateImportingModelAtom)
const { updateModelInfo } = useImportModel()
const [modelPath, setModelPath] = useState<string>('')
const editingModel = importingModels.find(
(model) => model.importId === editingModelId
@ -88,13 +95,19 @@ const EditModelInfoModal: React.FC = () => {
setEditingModelId(undefined)
}
const modelFolderPath = useMemo(() => {
return `${janDataFolder}/models/${editingModel?.modelId}`
useEffect(() => {
const getModelPath = async () => {
const modelId = editingModel?.modelId
if (!modelId) return ''
const path = await joinPath([janDataFolder, 'models', modelId])
setModelPath(path)
}
getModelPath()
}, [janDataFolder, editingModel])
const onShowInFinderClick = useCallback(() => {
openFileExplorer(modelFolderPath)
}, [modelFolderPath])
openFileExplorer(modelPath)
}, [modelPath])
if (!editingModel) {
setImportModelStage('IMPORTING_MODEL')
@ -104,7 +117,10 @@ const EditModelInfoModal: React.FC = () => {
}
return (
<Modal open={importModelStage === 'EDIT_MODEL_INFO'}>
<Modal
open={importModelStage === 'EDIT_MODEL_INFO'}
onOpenChange={onCancelClick}
>
<ModalContent>
<ModalHeader>
<ModalTitle>Edit Model Information</ModalTitle>
@ -130,7 +146,7 @@ const EditModelInfoModal: React.FC = () => {
</div>
<div className="mt-1 flex flex-row items-center space-x-2">
<span className="line-clamp-1 text-xs font-normal text-[#71717A]">
{modelFolderPath}
{modelPath}
</span>
<Button themes="ghost" onClick={onShowInFinderClick}>
{openFileTitle()}

View File

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

View File

@ -16,7 +16,7 @@ const ImportModelOptionSelection: React.FC<Props> = ({
onClick={() => setSelectedOptionType(option.type)}
>
<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 className="ml-2 flex-1">

View File

@ -29,7 +29,7 @@ const ImportSuccessIcon: React.FC<Props> = ({ onEditModelClick }) => {
}
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" />
</div>
))

View File

@ -1,6 +1,10 @@
import { useCallback, useMemo } from 'react'
import { ImportingModel } from '@janhq/core/.'
import { useSetAtom } from 'jotai'
import { AlertCircle } from 'lucide-react'
import { setImportModelStageAtom } from '@/hooks/useImportModel'
import { toGibibytes } from '@/utils/converter'
@ -16,28 +20,39 @@ type Props = {
const ImportingModelItem: React.FC<Props> = ({ model }) => {
const setImportModelStage = useSetAtom(setImportModelStageAtom)
const setEditingModelId = useSetAtom(editingModelIdAtom)
const sizeInGb = toGibibytes(model.size)
const onEditModelInfoClick = () => {
const onEditModelInfoClick = useCallback(() => {
setEditingModelId(model.importId)
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 (
<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>{sizeInGb}</p>
<p className="line-clamp-1 flex-1 font-semibold text-[#09090B]">
{model.name}
</p>
<p className="text-[#71717A]">{displayStatus}</p>
{model.status === 'IMPORTED' || model.status === 'FAILED' ? (
{model.status === 'IMPORTED' && (
<ImportSuccessIcon onEditModelClick={onEditModelInfoClick} />
) : (
)}
{(model.status === 'IMPORTING' || model.status === 'PREPARING') && (
<ImportInProgressIcon
percentage={model.percentage ?? 0}
onDeleteModelClick={onDeleteModelClick}
/>
)}
{model.status === 'FAILED' && <AlertCircle size={24} color="#F00" />}
</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 {
Button,
Modal,
@ -31,7 +31,15 @@ const ImportingModelModal: React.FC = () => {
const setImportModelStage = useSetAtom(setImportModelStageAtom)
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(
(model) => model.status === 'IMPORTED'

View File

@ -152,7 +152,7 @@ export default function RowModel(props: RowModelProps) {
) : (
<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'}
&nbsp;Model
</span>
@ -189,9 +189,7 @@ export default function RowModel(props: RowModelProps) {
}}
>
<Trash2Icon size={16} className="text-muted-foreground" />
<span className="text-bold text-black dark:text-muted-foreground">
Delete Model
</span>
<span className="text-bold text-black">Delete Model</span>
</div>
</div>
)}

View File

@ -2,7 +2,6 @@ import { useCallback, useState } from 'react'
import { useDropzone } from 'react-dropzone'
import { ImportingModel } from '@janhq/core'
import { Button, Input, ScrollArea } from '@janhq/uikit'
import { useAtomValue, useSetAtom } from 'jotai'
@ -10,60 +9,29 @@ import { Plus, SearchIcon, UploadCloudIcon } from 'lucide-react'
import { twMerge } from 'tailwind-merge'
import { v4 as uuidv4 } from 'uuid'
import useDropModelBinaries from '@/hooks/useDropModelBinaries'
import { setImportModelStageAtom } from '@/hooks/useImportModel'
import { getFileInfoFromFile } from '@/utils/file'
import RowModel from './Row'
import {
downloadedModelsAtom,
importingModelsAtom,
} from '@/helpers/atoms/Model.atom'
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
const Column = ['Name', 'Model ID', 'Size', 'Version', 'Status', '']
const Models: React.FC = () => {
const downloadedModels = useAtomValue(downloadedModelsAtom)
const setImportModelStage = useSetAtom(setImportModelStageAtom)
const setImportingModels = useSetAtom(importingModelsAtom)
const [searchValue, setsearchValue] = useState('')
const { onDropModels } = useDropModelBinaries()
const filteredDownloadedModels = downloadedModels
.filter((x) => x.name?.toLowerCase().includes(searchValue.toLowerCase()))
.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({
noClick: true,
multiple: true,
onDrop,
onDrop: onDropModels,
})
const onImportModelClick = useCallback(() => {

View File

@ -1,7 +1,7 @@
import { useCallback } from 'react'
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 { useAtomValue, useSetAtom } from 'jotai'
@ -9,16 +9,15 @@ import { UploadCloudIcon } from 'lucide-react'
import { v4 as uuidv4 } from 'uuid'
import { snackbar } from '@/containers/Toast'
import useDropModelBinaries from '@/hooks/useDropModelBinaries'
import {
getImportModelStageAtom,
setImportModelStageAtom,
} from '@/hooks/useImportModel'
import {
FilePathWithSize,
getFileInfoFromFile,
getFileNameFromPath,
} from '@/utils/file'
import { FilePathWithSize } from '@/utils/file'
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
@ -26,6 +25,7 @@ const SelectingModelModal: React.FC = () => {
const setImportModelStage = useSetAtom(setImportModelStageAtom)
const importModelStage = useAtomValue(getImportModelStageAtom)
const setImportingModels = useSetAtom(importingModelsAtom)
const { onDropModels } = useDropModelBinaries()
const onSelectFileClick = useCallback(async () => {
const filePaths = await window.core?.api?.selectModelFiles()
@ -36,7 +36,7 @@ const SelectingModelModal: React.FC = () => {
const fileStats = await fs.fileStat(filePath, true)
if (!fileStats || fileStats.isDirectory) continue
const fileName = getFileNameFromPath(filePath)
const fileName = await baseName(filePath)
sanitizedFilePaths.push({
path: filePath,
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) => {
return {
importId: uuidv4(),
modelId: undefined,
name: name,
name: name.replace('.gguf', ''),
description: '',
path: path,
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
setImportingModels(importingModels)
setImportModelStage('MODEL_SELECTED')
}, [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({
noClick: true,
multiple: true,
onDrop,
onDrop: onDropModels,
})
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'
return (
@ -128,7 +116,7 @@ const SelectingModelModal: React.FC = () => {
</div>
<div className="mt-4">
<span className="text-sm font-bold text-primary">
<span className="text-sm font-bold text-blue-600">
Click to upload
</span>
<span className={`text-sm ${textColor} font-medium`}>

View File

@ -15,7 +15,6 @@ const SettingMenu: React.FC<Props> = ({ activeMenu, onMenuClick }) => {
useEffect(() => {
setMenus([
'My Models',
'My Settings',
'Advanced Settings',
...(window.electronAPI ? ['Extensions'] : []),
])
@ -39,7 +38,7 @@ const SettingMenu: React.FC<Props> = ({ activeMenu, onMenuClick }) => {
{isActive && (
<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"
/>
)}

View File

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

View File

@ -1,5 +1,5 @@
.message {
@apply text-black dark:text-gray-300;
@apply text-black;
white-space: pre-line;
ul,
@ -10,7 +10,7 @@
}
a {
@apply text-blue-600 dark:text-blue-300;
@apply text-blue-600;
&:hover {
@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 > 1024 ** 3) {
return (input / 1024 ** 3).toFixed(2) + 'GB'
return (input / 1024 ** 3).toFixed(2) + (options?.hideUnit ? '' : 'GB')
} 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) {
return (input / 1024).toFixed(2) + 'KB'
return (input / 1024).toFixed(2) + (options?.hideUnit ? '' : 'KB')
} else {
return input + 'B'
return input + (options?.hideUnit ? '' : 'B')
}
}

View File

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