Merge pull request #2185 from janhq/dev

docs: Sync dev branch to docs branch
This commit is contained in:
Henry 2024-02-28 13:22:48 +09:00 committed by GitHub
commit 8e3b472b82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
145 changed files with 4224 additions and 993 deletions

View File

@ -43,31 +43,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>Stable (Recommended)</b></td>
<td style="text-align:center">
<a href='https://github.com/janhq/jan/releases/download/v0.4.6/jan-win-x64-0.4.6.exe'>
<a href='https://github.com/janhq/jan/releases/download/v0.4.7/jan-win-x64-0.4.7.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://github.com/janhq/jan/releases/download/v0.4.6/jan-mac-x64-0.4.6.dmg'>
<a href='https://github.com/janhq/jan/releases/download/v0.4.7/jan-mac-x64-0.4.7.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://github.com/janhq/jan/releases/download/v0.4.6/jan-mac-arm64-0.4.6.dmg'>
<a href='https://github.com/janhq/jan/releases/download/v0.4.7/jan-mac-arm64-0.4.7.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://github.com/janhq/jan/releases/download/v0.4.6/jan-linux-amd64-0.4.6.deb'>
<a href='https://github.com/janhq/jan/releases/download/v0.4.7/jan-linux-amd64-0.4.7.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://github.com/janhq/jan/releases/download/v0.4.6/jan-linux-x86_64-0.4.6.AppImage'>
<a href='https://github.com/janhq/jan/releases/download/v0.4.7/jan-linux-x86_64-0.4.7.AppImage'>
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.AppImage</b>
</a>
@ -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.6-285.exe'>
<a href='https://delta.jan.ai/latest/jan-win-x64-0.4.7-290.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.6-285.dmg'>
<a href='https://delta.jan.ai/latest/jan-mac-x64-0.4.7-290.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.6-285.dmg'>
<a href='https://delta.jan.ai/latest/jan-mac-arm64-0.4.7-290.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.6-285.deb'>
<a href='https://delta.jan.ai/latest/jan-linux-amd64-0.4.7-290.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.6-285.AppImage'>
<a href='https://delta.jan.ai/latest/jan-linux-x86_64-0.4.7-290.AppImage'>
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.AppImage</b>
</a>
@ -304,7 +304,7 @@ This will build the app MacOS m1/m2 for production (with code signing already do
```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
@ -319,6 +319,7 @@ This will start the web server and you can access Jan at `http://localhost:3000`
Jan builds on top of other open-source projects:
- [llama.cpp](https://github.com/ggerganov/llama.cpp)
- [LangChain](https://github.com/langchain-ai)
- [TensorRT](https://github.com/NVIDIA/TensorRT)
## Contact

View File

@ -7,6 +7,7 @@ export enum NativeRoute {
openAppDirectory = 'openAppDirectory',
openFileExplore = 'openFileExplorer',
selectDirectory = 'selectDirectory',
selectModelFiles = 'selectModelFiles',
relaunch = 'relaunch',
}
@ -46,6 +47,13 @@ export enum DownloadEvent {
onFileDownloadSuccess = 'onFileDownloadSuccess',
}
export enum LocalImportModelEvent {
onLocalImportModelUpdate = 'onLocalImportModelUpdate',
onLocalImportModelFailed = 'onLocalImportModelFailed',
onLocalImportModelSuccess = 'onLocalImportModelSuccess',
onLocalImportModelFinished = 'onLocalImportModelFinished',
}
export enum ExtensionRoute {
baseExtensions = 'baseExtensions',
getActiveExtensions = 'getActiveExtensions',
@ -67,6 +75,7 @@ export enum FileSystemRoute {
}
export enum FileManagerRoute {
syncFile = 'syncFile',
copyFile = 'copyFile',
getJanDataFolderPath = 'getJanDataFolderPath',
getResourcePath = 'getResourcePath',
getUserHomePath = 'getUserHomePath',
@ -126,4 +135,8 @@ export const CoreRoutes = [
]
export const APIRoutes = [...CoreRoutes, ...Object.values(NativeRoute)]
export const APIEvents = [...Object.values(AppEvent), ...Object.values(DownloadEvent)]
export const APIEvents = [
...Object.values(AppEvent),
...Object.values(DownloadEvent),
...Object.values(LocalImportModelEvent),
]

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

@ -4,6 +4,7 @@ export enum ExtensionTypeEnum {
Inference = 'inference',
Model = 'model',
SystemMonitoring = 'systemMonitoring',
HuggingFace = 'huggingFace',
}
export interface ExtensionType {

View File

@ -0,0 +1,30 @@
import { BaseExtension, ExtensionTypeEnum } from '../extension'
import { HuggingFaceInterface, HuggingFaceRepoData, Quantization } from '../types/huggingface'
import { Model } from '../types/model'
/**
* Hugging Face extension for converting HF models to GGUF.
*/
export abstract class HuggingFaceExtension extends BaseExtension implements HuggingFaceInterface {
interrupted = false
/**
* Hugging Face extension type.
*/
type(): ExtensionTypeEnum | undefined {
return ExtensionTypeEnum.HuggingFace
}
abstract downloadModelFiles(
repoID: string,
repoData: HuggingFaceRepoData,
network?: { ignoreSSL?: boolean; proxy?: string }
): Promise<void>
abstract convert(repoID: string): Promise<void>
abstract quantize(repoID: string, quantization: Quantization): Promise<void>
abstract generateMetadata(
repoID: string,
repoData: HuggingFaceRepoData,
quantization: Quantization
): Promise<void>
abstract cancelConvert(repoID: string, repoData: HuggingFaceRepoData): Promise<void>
}

View File

@ -23,3 +23,8 @@ export { AssistantExtension } from './assistant'
* Model extension for managing models.
*/
export { ModelExtension } from './model'
/**
* Hugging Face extension for converting HF models to GGUF.
*/
export { HuggingFaceExtension } from './huggingface'

View File

@ -1,5 +1,5 @@
import { BaseExtension, ExtensionTypeEnum } from '../extension'
import { Model, ModelInterface } from '../index'
import { ImportingModel, Model, ModelInterface, OptionType } from '../index'
/**
* Model extension for managing models.
@ -21,4 +21,6 @@ export abstract class ModelExtension extends BaseExtension implements ModelInter
abstract saveModel(model: Model): Promise<void>
abstract getDownloadedModels(): Promise<Model[]>
abstract getConfiguredModels(): Promise<Model[]>
abstract importModels(models: ImportingModel[], optionType: OptionType): Promise<void>
abstract updateModelInfo(modelInfo: Partial<Model>): Promise<Model>
}

View File

@ -69,14 +69,20 @@ const syncFile: (src: string, dest: string) => Promise<any> = (src, dest) =>
*/
const copyFileSync = (...args: any[]) => global.core.api?.copyFileSync(...args)
const copyFile: (src: string, dest: string) => Promise<void> = (src, dest) =>
global.core.api?.copyFile(src, dest)
/**
* Gets the file's stats.
*
* @param path - The path to the file.
* @param outsideJanDataFolder - Whether the file is outside the Jan data folder.
* @returns {Promise<FileStat>} - A promise that resolves with the file's stats.
*/
const fileStat: (path: string) => Promise<FileStat | undefined> = (path) =>
global.core.api?.fileStat(path)
const fileStat: (path: string, outsideJanDataFolder?: boolean) => Promise<FileStat | undefined> = (
path,
outsideJanDataFolder
) => global.core.api?.fileStat(path, outsideJanDataFolder)
// TODO: Export `dummy` fs functions automatically
// Currently adding these manually
@ -90,6 +96,7 @@ export const fs = {
unlinkSync,
appendFileSync,
copyFileSync,
copyFile,
syncFile,
fileStat,
writeBlob,

View File

@ -50,7 +50,7 @@ export class Downloader implements Processor {
fileName,
downloadState: 'downloading',
}
console.log('progress: ', downloadState)
console.debug('progress: ', downloadState)
observer?.(DownloadEvent.onFileDownloadUpdate, downloadState)
DownloadManager.instance.downloadProgressMap[modelId] = downloadState
})

View File

@ -1,6 +1,5 @@
import { join } from 'path'
import fs from 'fs'
import { FileManagerRoute } from '../../../api'
import { appResourcePath, normalizeFilePath } from '../../helper/path'
import { getJanDataFolderPath, getJanDataFolderPath as getPath } from '../../helper'
import { Processor } from './Processor'
@ -48,10 +47,12 @@ export class FSExt implements Processor {
}
// handle fs is directory here
fileStat(path: string) {
fileStat(path: string, outsideJanDataFolder?: boolean) {
const normalizedPath = normalizeFilePath(path)
const fullPath = join(getJanDataFolderPath(), normalizedPath)
const fullPath = outsideJanDataFolder
? normalizedPath
: join(getJanDataFolderPath(), normalizedPath)
const isExist = fs.existsSync(fullPath)
if (!isExist) return undefined
@ -75,4 +76,16 @@ export class FSExt implements Processor {
console.error(`writeFile ${path} result: ${err}`)
}
}
copyFile(src: string, dest: string): Promise<void> {
return new Promise((resolve, reject) => {
fs.copyFile(src, dest, (err) => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
}
}

View File

@ -0,0 +1,34 @@
export interface HuggingFaceRepoData {
id: string
author: string
tags: Array<'transformers' | 'pytorch' | 'safetensors' | string>
siblings: {
rfilename: string
}[]
createdAt: string // ISO 8601 timestamp
}
/* eslint-disable @typescript-eslint/naming-convention */
export enum Quantization {
Q3_K_S = 'Q3_K_S',
Q3_K_M = 'Q3_K_M', // eslint-disable-line @typescript-eslint/no-duplicate-enum-values
Q3_K_L = 'Q3_K_L',
Q4_K_S = 'Q4_K_S',
Q4_K_M = 'Q4_K_M', // eslint-disable-line @typescript-eslint/no-duplicate-enum-values
Q5_K_S = 'Q5_K_S',
Q5_K_M = 'Q5_K_M', // eslint-disable-line @typescript-eslint/no-duplicate-enum-values
Q4_0 = 'Q4_0',
Q4_1 = 'Q4_1',
Q5_0 = 'Q5_0',
Q5_1 = 'Q5_1',
IQ2_XXS = 'IQ2_XXS',
IQ2_XS = 'IQ2_XS',
Q2_K = 'Q2_K',
Q2_K_S = 'Q2_K_S',
Q6_K = 'Q6_K',
Q8_0 = 'Q8_0',
F16 = 'F16',
F32 = 'F32',
COPY = 'COPY',
}
/* eslint-enable @typescript-eslint/naming-convention */

View File

@ -0,0 +1,58 @@
import { Model } from '../model'
import { HuggingFaceRepoData, Quantization } from './huggingfaceEntity'
/**
* Hugging Face extension for converting HF models to GGUF.
* @extends BaseExtension
*/
export interface HuggingFaceInterface {
interrupted: boolean
/**
* Downloads a Hugging Face model.
* @param repoID - The repo ID of the model to convert.
* @param repoData - The repo data of the model to convert.
* @param network - Optional object to specify proxy/whether to ignore SSL certificates.
* @returns A promise that resolves when the download is complete.
*/
downloadModelFiles(
repoID: string,
repoData: HuggingFaceRepoData,
network?: { ignoreSSL?: boolean; proxy?: string }
): Promise<void>
/**
* Converts a Hugging Face model to GGUF.
* @param repoID - The repo ID of the model to convert.
* @returns A promise that resolves when the conversion is complete.
*/
convert(repoID: string): Promise<void>
/**
* Quantizes a GGUF model.
* @param repoID - The repo ID of the model to quantize.
* @param quantization - The quantization to use.
* @returns A promise that resolves when the quantization is complete.
*/
quantize(repoID: string, quantization: Quantization): Promise<void>
/**
* Generates Jan model metadata from a Hugging Face model.
* @param repoID - The repo ID of the model to generate metadata for.
* @param repoData - The repo data of the model to generate metadata for.
* @param quantization - The quantization of the model.
* @returns A promise that resolves when the model metadata generation is complete.
*/
generateMetadata(
repoID: string,
repoData: HuggingFaceRepoData,
quantization: Quantization
): Promise<void>
/**
* Cancels the convert of current Hugging Face model.
* @param repoID - The repository ID to cancel.
* @param repoData - The repository data to cancel.
* @returns {Promise<void>} A promise that resolves when the download has been cancelled.
*/
cancelConvert(repoID: string, repoData: HuggingFaceRepoData): Promise<void>
}

View File

@ -0,0 +1,2 @@
export * from './huggingfaceInterface'
export * from './huggingfaceEntity'

View File

@ -6,4 +6,5 @@ export * from './inference'
export * from './monitoring'
export * from './file'
export * from './config'
export * from './huggingface'
export * from './miscellaneous'

View File

@ -1,3 +1,4 @@
export * from './modelEntity'
export * from './modelInterface'
export * from './modelEvent'
export * from './modelImport'

View File

@ -0,0 +1,23 @@
export type OptionType = 'SYMLINK' | 'MOVE_BINARY_FILE'
export type ModelImportOption = {
type: OptionType
title: string
description: string
}
export type ImportingModelStatus = 'PREPARING' | 'IMPORTING' | 'IMPORTED' | 'FAILED'
export type ImportingModel = {
importId: string
modelId: string | undefined
name: string
description: string
path: string
tags: string[]
size: number
status: ImportingModelStatus
format: string
percentage?: number
error?: string
}

View File

@ -0,0 +1,26 @@
---
title: Acknowledgements
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
slug: /acknowledgements
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
acknowledgements,
third-party libraries,
]
---
# Acknowledgements
We would like to express our gratitude to the following third-party libraries that have made the development of Jan possible.
- [llama.cpp](https://github.com/ggerganov/llama.cpp/blob/master/LICENSE)
- [LangChain.js](https://github.com/langchain-ai/langchainjs/blob/main/LICENSE)
- [TensorRT](https://github.com/NVIDIA/TensorRT/blob/main/LICENSE)

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

@ -13,13 +13,13 @@ keywords:
no-subscription fee,
large language model,
docker installation,
cpu mode,
gpu mode,
]
---
# Installing Jan using Docker
## Installation
### Pre-requisites
:::note
@ -37,66 +37,87 @@ sudo sh ./get-docker.sh --dry-run
- If you intend to run Jan in GPU mode, you need to install `nvidia-driver` and `nvidia-docker2`. Follow the instruction [here](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) for installation.
### Instructions
### Run Jan in Docker Mode
- Run Jan in Docker mode
| Docker compose Profile | Description |
| ---------------------- | -------------------------------------------- |
| `cpu-fs` | Run Jan in CPU mode with default file system |
| `cpu-s3fs` | Run Jan in CPU mode with S3 file system |
| `gpu-fs` | Run Jan in GPU mode with default file system |
| `gpu-s3fs` | Run Jan in GPU mode with S3 file system |
- **Option 1**: Run Jan in CPU mode
| Environment Variable | Description |
| ----------------------- | ------------------------------------------------------------------------------------------------------- |
| `S3_BUCKET_NAME` | S3 bucket name - leave blank for default file system |
| `AWS_ACCESS_KEY_ID` | AWS access key ID - leave blank for default file system |
| `AWS_SECRET_ACCESS_KEY` | AWS secret access key - leave blank for default file system |
| `AWS_ENDPOINT` | AWS endpoint URL - leave blank for default file system |
| `AWS_REGION` | AWS region - leave blank for default file system |
| `API_BASE_URL` | Jan Server URL, please modify it as your public ip address or domain name default http://localhost:1377 |
- **Option 1**: Run Jan in CPU mode
```bash
# cpu mode with default file system
docker compose --profile cpu-fs up -d
# cpu mode with S3 file system
docker compose --profile cpu-s3fs up -d
```
- **Option 2**: Run Jan in GPU mode
- **Step 1**: Check CUDA compatibility with your NVIDIA driver by running `nvidia-smi` and check the CUDA version in the output
```bash
nvidia-smi
# Output
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 531.18 Driver Version: 531.18 CUDA Version: 12.1 |
|-----------------------------------------+----------------------+----------------------+
| GPU Name TCC/WDDM | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+======================+======================|
| 0 NVIDIA GeForce RTX 4070 Ti WDDM | 00000000:01:00.0 On | N/A |
| 0% 44C P8 16W / 285W| 1481MiB / 12282MiB | 2% Default |
| | | N/A |
+-----------------------------------------+----------------------+----------------------+
| 1 NVIDIA GeForce GTX 1660 Ti WDDM | 00000000:02:00.0 Off | N/A |
| 0% 49C P8 14W / 120W| 0MiB / 6144MiB | 0% Default |
| | | N/A |
+-----------------------------------------+----------------------+----------------------+
| 2 NVIDIA GeForce GTX 1660 Ti WDDM | 00000000:05:00.0 Off | N/A |
| 29% 38C P8 11W / 120W| 0MiB / 6144MiB | 0% Default |
| | | N/A |
+-----------------------------------------+----------------------+----------------------+
+---------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=======================================================================================|
```
- **Step 2**: Visit [NVIDIA NGC Catalog ](https://catalog.ngc.nvidia.com/orgs/nvidia/containers/cuda/tags) and find the smallest minor version of image tag that matches your CUDA version (e.g., 12.1 -> 12.1.0)
- **Step 3**: Update the `Dockerfile.gpu` line number 5 with the latest minor version of the image tag from step 2 (e.g. change `FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04 AS base` to `FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 AS base`)
- **Step 4**: Run command to start Jan in GPU mode
```bash
docker compose --profile cpu up -d
# GPU mode with default file system
docker compose --profile gpu-fs up -d
# GPU mode with S3 file system
docker compose --profile gpu-s3fs up -d
```
- **Option 2**: Run Jan in GPU mode
- **Step 1**: Check CUDA compatibility with your NVIDIA driver by running `nvidia-smi` and check the CUDA version in the output
```bash
nvidia-smi
# Output
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 531.18 Driver Version: 531.18 CUDA Version: 12.1 |
|-----------------------------------------+----------------------+----------------------+
| GPU Name TCC/WDDM | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+======================+======================|
| 0 NVIDIA GeForce RTX 4070 Ti WDDM | 00000000:01:00.0 On | N/A |
| 0% 44C P8 16W / 285W| 1481MiB / 12282MiB | 2% Default |
| | | N/A |
+-----------------------------------------+----------------------+----------------------+
| 1 NVIDIA GeForce GTX 1660 Ti WDDM | 00000000:02:00.0 Off | N/A |
| 0% 49C P8 14W / 120W| 0MiB / 6144MiB | 0% Default |
| | | N/A |
+-----------------------------------------+----------------------+----------------------+
| 2 NVIDIA GeForce GTX 1660 Ti WDDM | 00000000:05:00.0 Off | N/A |
| 29% 38C P8 11W / 120W| 0MiB / 6144MiB | 0% Default |
| | | N/A |
+-----------------------------------------+----------------------+----------------------+
+---------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=======================================================================================|
```
- **Step 2**: Visit [NVIDIA NGC Catalog ](https://catalog.ngc.nvidia.com/orgs/nvidia/containers/cuda/tags) and find the smallest minor version of image tag that matches your CUDA version (e.g., 12.1 -> 12.1.0)
- **Step 3**: Update the `Dockerfile.gpu` line number 5 with the latest minor version of the image tag from step 2 (e.g. change `FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04 AS base` to `FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 AS base`)
- **Step 4**: Run command to start Jan in GPU mode
```bash
# GPU mode
docker compose --profile gpu up -d
```
This will start the web server and you can access Jan at `http://localhost:3000`.
This will start the web server and you can access Jan at `http://localhost:3000`.
:::warning
- Docker mode is currently only suitable for development and localhost. Production is not supported yet, and the RAG feature is not available in Docker mode.
- RAG feature is not supported in Docker mode with s3fs yet.
:::

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

@ -51,6 +51,7 @@ const sidebars = {
"how-we-work/website-docs/website-docs",
],
},
"acknowledgements",
],
productSidebar: [
{

View File

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

View File

@ -1,4 +1,10 @@
.theme-doc-markdown {
a,
p,
span,
li {
@apply leading-loose;
}
a {
@apply text-blue-600 dark:text-blue-400;
}
@ -10,9 +16,9 @@
}
ul,
ol {
padding-left: 16px;
padding-left: 28px;
li {
@apply leading-normal;
@apply leading-loose;
p {
margin-bottom: 0;
}

View File

@ -1,12 +1,12 @@
// * Classname from Docusaurus template
// * We just overide the styling with applied class from tailwind
[class*="docSidebarContainer_"] {
[class*='docSidebarContainer_'] {
margin-top: 0 !important;
@apply dark:border-gray-800 border-gray-300;
}
[class*="sidebar_"] {
[class*='sidebar_'] {
padding-top: 0px !important;
}
@ -14,32 +14,40 @@
padding-top: 20px !important;
}
[class*="sidebarViewport_"] {
[class*='sidebarViewport_'] {
top: 80px !important;
// height: unset !important;
}
[class*="docItemCol_"] {
[class*='docItemCol_'] {
@apply lg:px-8;
}
// * Including custom sidebar table of content
.table-of-contents {
@apply text-base py-0 dark:border-gray-800 border-gray-300;
@apply text-sm py-0 dark:border-gray-800 border-gray-300;
}
.menu__caret:before {
background: var(--ifm-menu-link-sublist-icon) 50% / 1.5rem 1.5rem;
}
[class*="codeBlockContainer_"] {
[class*='codeBlockContainer_'] {
margin: 4px;
}
[class*="codeBlockTitle_"] {
[class*='codeBlockTitle_'] {
border-bottom: 1px solid #52525a !important;
}
[class*="iconExternalLink_"] {
[class*='iconExternalLink_'] {
display: none;
}
[class*='docMainContainer'] {
@media (min-width: 1440px) {
.container {
max-width: var(--ifm-container-width-xl);
}
}
}

View File

@ -83,4 +83,22 @@ export function handleAppIPCs() {
return filePaths[0]
}
})
ipcMain.handle(NativeRoute.selectModelFiles, async () => {
const mainWindow = WindowManager.instance.currentWindow
if (!mainWindow) {
console.error('No main window found')
return
}
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
title: 'Select model files',
buttonLabel: 'Select',
properties: ['openFile', 'multiSelections'],
})
if (canceled) {
return
} else {
return filePaths
}
})
}

View File

@ -15,12 +15,14 @@
"build/**/*.{js,map}",
"pre-install",
"models/**/*",
"docs/**/*"
"docs/**/*",
"scripts/**/*"
],
"asarUnpack": [
"pre-install",
"models",
"docs"
"docs",
"scripts"
],
"publish": [
{

View File

@ -8,10 +8,9 @@ export const setupReactDevTool = async () => {
) // Don't use import on top level, since the installer package is dev-only
try {
const name = await installExtension(REACT_DEVELOPER_TOOLS)
console.log(`Added Extension: ${name}`)
console.debug(`Added Extension: ${name}`)
} catch (err) {
console.log('An error occurred while installing devtools:')
console.error(err)
console.error('An error occurred while installing devtools:', err)
// Only log the error and don't throw it because it's not critical
}
}

View File

@ -35,7 +35,7 @@ export function cleanLogs(
console.error('Error deleting log file:', err)
return
}
console.log(
console.debug(
`Deleted log file due to exceeding size limit: ${filePath}`
)
})
@ -52,7 +52,7 @@ export function cleanLogs(
console.error('Error deleting log file:', err)
return
}
console.log(`Deleted old log file: ${filePath}`)
console.debug(`Deleted old log file: ${filePath}`)
})
}
}

View File

@ -0,0 +1,3 @@
bin
scripts/convert*
scripts/gguf-py

View File

@ -0,0 +1,8 @@
{
"semi": false,
"singleQuote": true,
"quoteProps": "consistent",
"trailingComma": "es5",
"endOfLine": "auto",
"plugins": ["prettier-plugin-tailwindcss"]
}

View File

@ -0,0 +1,73 @@
# Create a Jan Plugin using Typescript
Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀
## Create Your Own Plugin
To create your own plugin, you can use this repository as a template! Just follow the below instructions:
1. Click the Use this template button at the top of the repository
2. Select Create a new repository
3. Select an owner and name for your new repository
4. Click Create repository
5. Clone your new repository
## Initial Setup
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin.
> [!NOTE]
>
> You'll need to have a reasonably modern version of
> [Node.js](https://nodejs.org) handy. If you are using a version manager like
> [`nodenv`](https://github.com/nodenv/nodenv) or
> [`nvm`](https://github.com/nvm-sh/nvm), you can run `nodenv install` in the
> root of your repository to install the version specified in
> [`package.json`](./package.json). Otherwise, 20.x or later should work!
1. :hammer_and_wrench: Install the dependencies
```bash
npm install
```
1. :building_construction: Package the TypeScript for distribution
```bash
npm run bundle
```
1. :white_check_mark: Check your artifact
There will be a tgz file in your plugin directory now
## Update the Plugin Metadata
The [`package.json`](package.json) file defines metadata about your plugin, such as
plugin name, main entry, description and version.
When you copy this repository, update `package.json` with the name, description for your plugin.
## Update the Plugin Code
The [`src/`](./src/) directory is the heart of your plugin! This contains the
source code that will be run when your plugin extension functions are invoked. You can replace the
contents of this directory with your own code.
There are a few things to keep in mind when writing your plugin code:
- Most Jan Plugin Extension functions are processed asynchronously.
In `index.ts`, you will see that the extension function will return a `Promise<any>`.
```typescript
import { core } from "@janhq/core";
function onStart(): Promise<any> {
return core.invokePluginFunc(MODULE_PATH, "run", 0);
}
```
For more information about the Jan Plugin Core module, see the
[documentation](https://github.com/janhq/jan/blob/main/core/README.md).
So, what are you waiting for? Go ahead and start customizing your plugin!

Binary file not shown.

View File

@ -0,0 +1,3 @@
@echo off
set /p LLAMA_CPP_VERSION=<./scripts/version.txt
.\node_modules\.bin\download https://github.com/ggerganov/llama.cpp/archive/refs/tags/%LLAMA_CPP_VERSION%.tar.gz -o . --filename ./scripts/llama.cpp.tar.gz && tar -xzf .\scripts\llama.cpp.tar.gz "llama.cpp-%LLAMA_CPP_VERSION%/convert.py" "llama.cpp-%LLAMA_CPP_VERSION%/convert-hf-to-gguf.py" "llama.cpp-%LLAMA_CPP_VERSION%/gguf-py" && cpx "./llama.cpp-%LLAMA_CPP_VERSION%/**" "scripts" && rimraf "./scripts/llama.cpp.tar.gz" && rimraf "./llama.cpp-%LLAMA_CPP_VERSION%"

View File

@ -0,0 +1,57 @@
{
"name": "@janhq/huggingface-extension",
"version": "1.0.0",
"description": "Hugging Face extension for converting HF models to GGUF",
"main": "dist/index.js",
"node": "dist/node/index.cjs.js",
"author": "Jan <service@jan.ai>",
"license": "AGPL-3.0",
"scripts": {
"build": "tsc --module commonjs && rollup -c rollup.config.ts --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs",
"download:llama": "run-script-os",
"download:llama:linux": "LLAMA_CPP_VERSION=$(cat ./scripts/version.txt) && download https://github.com/ggerganov/llama.cpp/archive/refs/tags/${LLAMA_CPP_VERSION}.tar.gz -o . --filename ./scripts/llama.cpp.tar.gz && tar -xzf ./scripts/llama.cpp.tar.gz --wildcards '*/convert.py' '*/convert-hf-to-gguf.py' '*/gguf-py' && cpx \"./llama.cpp-$LLAMA_CPP_VERSION/**\" \"scripts\" && rimraf \"./scripts/llama.cpp.tar.gz\" && rimraf \"./llama.cpp-$LLAMA_CPP_VERSION\"",
"download:llama:darwin": "LLAMA_CPP_VERSION=$(cat ./scripts/version.txt) && download https://github.com/ggerganov/llama.cpp/archive/refs/tags/${LLAMA_CPP_VERSION}.tar.gz -o . --filename ./scripts/llama.cpp.tar.gz && tar -xzf ./scripts/llama.cpp.tar.gz '*/convert.py' '*/convert-hf-to-gguf.py' '*/gguf-py' && cpx \"./llama.cpp-$LLAMA_CPP_VERSION/**\" \"scripts\" && rimraf \"./scripts/llama.cpp.tar.gz\" && rimraf \"./llama.cpp-$LLAMA_CPP_VERSION\"",
"download:llama:win32": "download.bat",
"build:publish:linux": "rimraf *.tgz --glob && npm run build && npm run download:llama && cpx \"scripts/**\" \"dist/scripts\" && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../pre-install",
"build:publish:darwin": "rimraf *.tgz --glob && npm run build && npm run download:llama && cpx \"scripts/**\" \"dist/scripts\" && cpx \"bin/**\" \"dist/bin\" && ../../.github/scripts/auto-sign.sh && npm pack && cpx *.tgz ../../pre-install",
"build:publish:win32": "rimraf *.tgz --glob && npm run build && npm run download:llama && cpx \"scripts/**\" \"dist/scripts\" && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../pre-install",
"build:publish": "run-script-os"
},
"exports": {
".": "./dist/index.js",
"./main": "./dist/node/index.cjs.js"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.5",
"@rollup/plugin-typescript": "^11.1.6",
"@types/node": "^20.11.16",
"cpx": "^1.5.0",
"download-cli": "^1.1.1",
"rimraf": "^5.0.5",
"rollup": "^4.9.6",
"rollup-plugin-sourcemaps": "^0.6.3",
"rollup-plugin-typescript2": "^0.36.0",
"run-script-os": "^1.1.6",
"typescript": "^5.3.3"
},
"dependencies": {
"@janhq/core": "file:../../core",
"hyllama": "^0.1.2",
"python-shell": "^5.0.0",
"ts-loader": "^9.5.0"
},
"bundledDependencies": [
"python-shell"
],
"engines": {
"node": ">=18.0.0"
},
"files": [
"dist/*",
"package.json",
"README.md"
]
}

View File

@ -0,0 +1,72 @@
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import sourceMaps from 'rollup-plugin-sourcemaps'
import typescript from 'rollup-plugin-typescript2'
import json from '@rollup/plugin-json'
import replace from '@rollup/plugin-replace'
const packageJson = require('./package.json')
export default [
{
input: `src/index.ts`,
output: [{ file: packageJson.main, format: 'es', sourcemap: true }],
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
external: [],
watch: {
include: 'src/**',
},
plugins: [
replace({
EXTENSION_NAME: JSON.stringify(packageJson.name),
NODE_MODULE_PATH: JSON.stringify(
`${packageJson.name}/${packageJson.node}`
),
}),
// Allow json resolution
json(),
// Compile TypeScript files
typescript({ useTsconfigDeclarationDir: true }),
// Compile TypeScript files
// Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
commonjs(),
// Allow node_modules resolution, so you can use 'external' to control
// which external modules to include in the bundle
// https://github.com/rollup/rollup-plugin-node-resolve#usage
resolve({
extensions: ['.js', '.ts'],
}),
// Resolve source maps to the original source
sourceMaps(),
],
},
{
input: `src/node/index.ts`,
output: [
{ file: 'dist/node/index.cjs.js', format: 'cjs', sourcemap: true },
],
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
external: [],
watch: {
include: 'src/node/**',
},
plugins: [
// Allow json resolution
json(),
// Compile TypeScript files
typescript({ useTsconfigDeclarationDir: true }),
// Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
commonjs(),
// Allow node_modules resolution, so you can use 'external' to control
// which external modules to include in the bundle
// https://github.com/rollup/rollup-plugin-node-resolve#usage
resolve({
extensions: ['.ts', '.js', '.json'],
}),
// Resolve source maps to the original source
sourceMaps(),
],
},
]

View File

@ -0,0 +1,14 @@
import subprocess
import sys
deps = [
'numpy~=1.24.4',
'sentencepiece~=0.1.98',
'transformers>=4.35.2,<5.0.0',
'gguf>=0.1.0',
'protobuf>=4.21.0,<5.0.0',
'torch~=2.1.1',
'packaging>=20.0',
'tiktoken~=0.5.0'
]
subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--upgrade', '--force-reinstall', *deps])

View File

@ -0,0 +1 @@
b2106

View File

@ -0,0 +1,2 @@
declare const EXTENSION_NAME: string
declare const NODE_MODULE_PATH: string

View File

@ -0,0 +1,396 @@
import {
fs,
downloadFile,
abortDownload,
joinPath,
HuggingFaceExtension,
HuggingFaceRepoData,
executeOnMain,
Quantization,
Model,
InferenceEngine,
getJanDataFolderPath,
events,
DownloadEvent,
log,
} from '@janhq/core'
import { ggufMetadata } from 'hyllama'
declare global {
interface Window {
electronAPI?: any
}
}
/**
* A extension for models
*/
export default class JanHuggingFaceExtension extends HuggingFaceExtension {
private static readonly _safetensorsRegexs = [
/model\.safetensors$/,
/model-[0-9]+-of-[0-9]+\.safetensors$/,
]
private static readonly _pytorchRegexs = [
/pytorch_model\.bin$/,
/consolidated\.[0-9]+\.pth$/,
/pytorch_model-[0-9]+-of-[0-9]+\.bin$/,
/.*\.pt$/,
]
interrupted = false
/**
* Called when the extension is loaded.
* @override
*/
onLoad() {}
/**
* Called when the extension is unloaded.
* @override
*/
onUnload(): void {}
private getFileList(repoData: HuggingFaceRepoData): string[] {
// SafeTensors first, if not, then PyTorch
const modelFiles = repoData.siblings
.map((file) => file.rfilename)
.filter((file) =>
JanHuggingFaceExtension._safetensorsRegexs.some((regex) =>
regex.test(file)
)
)
if (modelFiles.length === 0) {
repoData.siblings.forEach((file) => {
if (
JanHuggingFaceExtension._pytorchRegexs.some((regex) =>
regex.test(file.rfilename)
)
) {
modelFiles.push(file.rfilename)
}
})
}
const vocabFiles = [
'tokenizer.model',
'vocab.json',
'tokenizer.json',
].filter((file) =>
repoData.siblings.some((sibling) => sibling.rfilename === file)
)
const etcFiles = repoData.siblings
.map((file) => file.rfilename)
.filter(
(file) =>
(file.endsWith('.json') && !vocabFiles.includes(file)) ||
file.endsWith('.txt') ||
file.endsWith('.py') ||
file.endsWith('.tiktoken')
)
return [...modelFiles, ...vocabFiles, ...etcFiles]
}
private async getModelDirPath(repoID: string): Promise<string> {
const modelName = repoID.split('/').slice(1).join('/')
return joinPath([await getJanDataFolderPath(), 'models', modelName])
}
private async getConvertedModelPath(repoID: string): Promise<string> {
const modelName = repoID.split('/').slice(1).join('/')
const modelDirPath = await this.getModelDirPath(repoID)
return joinPath([modelDirPath, modelName + '.gguf'])
}
private async getQuantizedModelPath(
repoID: string,
quantization: Quantization
): Promise<string> {
const modelName = repoID.split('/').slice(1).join('/')
const modelDirPath = await this.getModelDirPath(repoID)
return joinPath([
modelDirPath,
modelName + `-${quantization.toLowerCase()}.gguf`,
])
}
private getCtxLength(config: {
max_sequence_length?: number
max_position_embeddings?: number
n_ctx?: number
}): number {
if (config.max_sequence_length) return config.max_sequence_length
if (config.max_position_embeddings) return config.max_position_embeddings
if (config.n_ctx) return config.n_ctx
return 4096
}
/**
* Downloads a Hugging Face model.
* @param repoID - The repo ID of the model to convert.
* @param repoData - The repo data of the model to convert.
* @param network - Optional object to specify proxy/whether to ignore SSL certificates.
* @returns A promise that resolves when the download is complete.
*/
async downloadModelFiles(
repoID: string,
repoData: HuggingFaceRepoData,
network?: { ignoreSSL?: boolean; proxy?: string }
): Promise<void> {
if (this.interrupted) return
const modelDirPath = await this.getModelDirPath(repoID)
if (!(await fs.existsSync(modelDirPath))) await fs.mkdirSync(modelDirPath)
const files = this.getFileList(repoData)
const filePaths: string[] = []
for (const file of files) {
const filePath = file
const localPath = await joinPath([modelDirPath, filePath])
const url = `https://huggingface.co/${repoID}/resolve/main/${filePath}`
if (this.interrupted) return
if (!(await fs.existsSync(localPath))) {
downloadFile(url, localPath, network)
filePaths.push(filePath)
}
}
await new Promise<void>((resolve, reject) => {
if (filePaths.length === 0) resolve()
const onDownloadSuccess = async ({ fileName }: { fileName: string }) => {
if (filePaths.includes(fileName)) {
filePaths.splice(filePaths.indexOf(fileName), 1)
if (filePaths.length === 0) {
events.off(DownloadEvent.onFileDownloadSuccess, onDownloadSuccess)
events.off(DownloadEvent.onFileDownloadError, onDownloadError)
resolve()
}
}
}
const onDownloadError = async ({
fileName,
error,
}: {
fileName: string
error: Error
}) => {
if (filePaths.includes(fileName)) {
this.cancelConvert(repoID, repoData)
events.off(DownloadEvent.onFileDownloadSuccess, onDownloadSuccess)
events.off(DownloadEvent.onFileDownloadError, onDownloadError)
reject(error)
}
}
events.on(DownloadEvent.onFileDownloadSuccess, onDownloadSuccess)
events.on(DownloadEvent.onFileDownloadError, onDownloadError)
})
}
/**
* Converts a Hugging Face model to GGUF.
* @param repoID - The repo ID of the model to convert.
* @returns A promise that resolves when the conversion is complete.
*/
async convert(repoID: string): Promise<void> {
if (this.interrupted) return
const modelDirPath = await this.getModelDirPath(repoID)
const modelOutPath = await this.getConvertedModelPath(repoID)
if (!(await fs.existsSync(modelDirPath))) {
throw new Error('Model dir not found')
}
if (await fs.existsSync(modelOutPath)) return
await executeOnMain(NODE_MODULE_PATH, 'installDeps')
if (this.interrupted) return
try {
await executeOnMain(
NODE_MODULE_PATH,
'convertHf',
modelDirPath,
modelOutPath + '.temp'
)
} catch (err) {
log(`[Conversion]::Debug: Error using hf-to-gguf.py, trying convert.py`)
let ctx = 4096
try {
const config = await fs.readFileSync(
await joinPath([modelDirPath, 'config.json']),
'utf8'
)
const configParsed = JSON.parse(config)
ctx = this.getCtxLength(configParsed)
configParsed.max_sequence_length = ctx
await fs.writeFileSync(
await joinPath([modelDirPath, 'config.json']),
JSON.stringify(configParsed, null, 2)
)
} catch (err) {
log(`${err}`)
// ignore missing config.json
}
const bpe = await fs.existsSync(
await joinPath([modelDirPath, 'vocab.json'])
)
await executeOnMain(
NODE_MODULE_PATH,
'convert',
modelDirPath,
modelOutPath + '.temp',
{
ctx,
bpe,
}
)
}
await executeOnMain(
NODE_MODULE_PATH,
'renameSync',
modelOutPath + '.temp',
modelOutPath
)
for (const file of await fs.readdirSync(modelDirPath)) {
if (
modelOutPath.endsWith(file) ||
(file.endsWith('config.json') && !file.endsWith('_config.json'))
)
continue
await fs.unlinkSync(await joinPath([modelDirPath, file]))
}
}
/**
* Quantizes a GGUF model.
* @param repoID - The repo ID of the model to quantize.
* @param quantization - The quantization to use.
* @returns A promise that resolves when the quantization is complete.
*/
async quantize(repoID: string, quantization: Quantization): Promise<void> {
if (this.interrupted) return
const modelDirPath = await this.getModelDirPath(repoID)
const modelOutPath = await this.getQuantizedModelPath(repoID, quantization)
if (!(await fs.existsSync(modelDirPath))) {
throw new Error('Model dir not found')
}
if (await fs.existsSync(modelOutPath)) return
await executeOnMain(
NODE_MODULE_PATH,
'quantize',
await this.getConvertedModelPath(repoID),
modelOutPath + '.temp',
quantization
)
await executeOnMain(
NODE_MODULE_PATH,
'renameSync',
modelOutPath + '.temp',
modelOutPath
)
await fs.unlinkSync(await this.getConvertedModelPath(repoID))
}
/**
* Generates Jan model metadata from a Hugging Face model.
* @param repoID - The repo ID of the model to generate metadata for.
* @param repoData - The repo data of the model to generate metadata for.
* @param quantization - The quantization of the model.
* @returns A promise that resolves when the model metadata generation is complete.
*/
async generateMetadata(
repoID: string,
repoData: HuggingFaceRepoData,
quantization: Quantization
): Promise<void> {
const modelName = repoID.split('/').slice(1).join('/')
const filename = `${modelName}-${quantization.toLowerCase()}.gguf`
const modelDirPath = await this.getModelDirPath(repoID)
const modelPath = await this.getQuantizedModelPath(repoID, quantization)
const modelConfigPath = await joinPath([modelDirPath, 'model.json'])
if (!(await fs.existsSync(modelPath))) {
throw new Error('Model not found')
}
const size = await executeOnMain(NODE_MODULE_PATH, 'getSize', modelPath)
let ctx = 4096
try {
const config = await fs.readFileSync(
await joinPath([modelDirPath, 'config.json']),
'utf8'
)
ctx = this.getCtxLength(JSON.parse(config))
fs.unlinkSync(await joinPath([modelDirPath, 'config.json']))
} catch (err) {
// ignore missing config.json
}
// maybe later, currently it's gonna use too much memory
// const buffer = await fs.readFileSync(quantizedModelPath)
// const ggufData = ggufMetadata(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength))
const metadata: Model = {
object: 'model',
version: 1,
format: 'gguf',
sources: [
{
url: `https://huggingface.co/${repoID}`, // i think this is just for download but not sure,
filename,
},
],
id: modelName,
name: modelName,
created: Date.now(),
description: `Auto converted from Hugging Face model: ${repoID}`,
settings: {
ctx_len: ctx,
prompt_template: '',
llama_model_path: modelName,
},
parameters: {
temperature: 0.7,
top_p: 0.95,
stream: true,
max_tokens: 4096,
// stop: [''], seems like we dont really need this..?
frequency_penalty: 0,
presence_penalty: 0,
},
metadata: {
author: repoData.author,
tags: repoData.tags,
size,
},
engine: InferenceEngine.nitro,
}
await fs.writeFileSync(modelConfigPath, JSON.stringify(metadata, null, 2))
}
/**
* Cancels the convert of current Hugging Face model.
* @param repoID - The repository ID to cancel.
* @param repoData - The repository data to cancel.
* @returns {Promise<void>} A promise that resolves when the download has been cancelled.
*/
async cancelConvert(
repoID: string,
repoData: HuggingFaceRepoData
): Promise<void> {
this.interrupted = true
const modelDirPath = await this.getModelDirPath(repoID)
const files = this.getFileList(repoData)
for (const file of files) {
const filePath = file
const localPath = await joinPath([modelDirPath, filePath])
await abortDownload(localPath)
}
// ;(await fs.existsSync(modelDirPath)) && (await fs.rmdirSync(modelDirPath))
executeOnMain(NODE_MODULE_PATH, 'killProcesses')
}
}

View File

@ -0,0 +1,187 @@
import { PythonShell } from 'python-shell'
import { spawn, ChildProcess } from 'child_process'
import { resolve as presolve, join as pjoin } from 'path'
import type { Quantization } from '@janhq/core'
import { log } from '@janhq/core/node'
import { statSync } from 'fs'
export { renameSync } from 'fs'
let pythonShell: PythonShell | undefined = undefined
let quantizeProcess: ChildProcess | undefined = undefined
export const getSize = (path: string): number => statSync(path).size
export const killProcesses = () => {
if (pythonShell) {
pythonShell.kill()
pythonShell = undefined
}
if (quantizeProcess) {
quantizeProcess.kill()
quantizeProcess = undefined
}
}
export const getQuantizeExecutable = (): string => {
let binaryFolder = pjoin(__dirname, '..', 'bin') // Current directory by default
let binaryName = 'quantize'
/**
* The binary folder is different for each platform.
*/
if (process.platform === 'win32') {
binaryFolder = pjoin(binaryFolder, 'win')
binaryName = 'quantize.exe'
} else if (process.platform === 'darwin') {
/**
* For MacOS: mac-arm64 (Silicon), mac-x64 (InteL)
*/
if (process.arch === 'arm64') {
binaryFolder = pjoin(binaryFolder, 'mac-arm64')
} else {
binaryFolder = pjoin(binaryFolder, 'mac-x64')
}
} else {
binaryFolder = pjoin(binaryFolder, 'linux-cpu')
}
return pjoin(binaryFolder, binaryName)
}
export const installDeps = (): Promise<void> => {
return new Promise((resolve, reject) => {
const _pythonShell = new PythonShell(
presolve(__dirname, '..', 'scripts', 'install_deps.py')
)
_pythonShell.on('message', (message) => {
log(`[Install Deps]::Debug: ${message}`)
})
_pythonShell.on('stderr', (stderr) => {
log(`[Install Deps]::Error: ${stderr}`)
})
_pythonShell.on('error', (err) => {
pythonShell = undefined
log(`[Install Deps]::Error: ${err}`)
reject(err)
})
_pythonShell.on('close', () => {
const exitCode = _pythonShell.exitCode
pythonShell = undefined
log(
`[Install Deps]::Debug: Deps installation exited with code: ${exitCode}`
)
exitCode === 0 ? resolve() : reject(exitCode)
})
})
}
export const convertHf = async (
modelDirPath: string,
outPath: string
): Promise<void> => {
return await new Promise<void>((resolve, reject) => {
const _pythonShell = new PythonShell(
presolve(__dirname, '..', 'scripts', 'convert-hf-to-gguf.py'),
{
args: [modelDirPath, '--outfile', outPath],
}
)
pythonShell = _pythonShell
_pythonShell.on('message', (message) => {
log(`[Conversion]::Debug: ${message}`)
})
_pythonShell.on('stderr', (stderr) => {
log(`[Conversion]::Error: ${stderr}`)
})
_pythonShell.on('error', (err) => {
pythonShell = undefined
log(`[Conversion]::Error: ${err}`)
reject(err)
})
_pythonShell.on('close', () => {
const exitCode = _pythonShell.exitCode
pythonShell = undefined
if (exitCode !== 0) {
log(`[Conversion]::Debug: Conversion exited with code: ${exitCode}`)
reject(exitCode)
} else {
resolve()
}
})
})
}
export const convert = async (
modelDirPath: string,
outPath: string,
{ ctx, bpe }: { ctx?: number; bpe?: boolean }
): Promise<void> => {
const args = [modelDirPath, '--outfile', outPath]
if (ctx) {
args.push('--ctx')
args.push(ctx.toString())
}
if (bpe) {
args.push('--vocab-type')
args.push('bpe')
}
return await new Promise<void>((resolve, reject) => {
const _pythonShell = new PythonShell(
presolve(__dirname, '..', 'scripts', 'convert.py'),
{
args,
}
)
_pythonShell.on('message', (message) => {
log(`[Conversion]::Debug: ${message}`)
})
_pythonShell.on('stderr', (stderr) => {
log(`[Conversion]::Error: ${stderr}`)
})
_pythonShell.on('error', (err) => {
pythonShell = undefined
log(`[Conversion]::Error: ${err}`)
reject(err)
})
_pythonShell.on('close', () => {
const exitCode = _pythonShell.exitCode
pythonShell = undefined
if (exitCode !== 0) {
log(`[Conversion]::Debug: Conversion exited with code: ${exitCode}`)
reject(exitCode)
} else {
resolve()
}
})
})
}
export const quantize = async (
modelPath: string,
outPath: string,
quantization: Quantization
): Promise<void> => {
return await new Promise<void>((resolve, reject) => {
const quantizeExecutable = getQuantizeExecutable()
const _quantizeProcess = spawn(quantizeExecutable, [
modelPath,
outPath,
quantization,
])
quantizeProcess = _quantizeProcess
_quantizeProcess.stdout?.on('data', (data) => {
log(`[Quantization]::Debug: ${data}`)
})
_quantizeProcess.stderr?.on('data', (data) => {
log(`[Quantization]::Error: ${data}`)
})
_quantizeProcess.on('close', (code) => {
if (code !== 0) {
log(`[Quantization]::Debug: Quantization exited with code: ${code}`)
reject(code)
} else {
resolve()
}
})
})
}

View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"moduleResolution": "node",
"target": "es2020",
"module": "ES2020",
"lib": ["es2015", "es2016", "es2017", "dom"],
"strict": true,
"sourceMap": true,
"declaration": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"declarationDir": "dist/types",
"outDir": "dist",
"importHelpers": true,
"typeRoots": ["node_modules/@types"],
"resolveJsonModule": true,
},
"include": ["src"],
}

View File

@ -1 +1 @@
0.3.12
0.3.13

View File

@ -23,10 +23,7 @@ const DEFALT_SETTINGS = {
gpus_in_use: [],
is_initial: true,
// TODO: This needs to be set based on user toggle in settings
vulkan: {
enabled: true,
gpu_in_use: '1',
},
vulkan: false
}
/**
@ -152,7 +149,7 @@ export function updateCudaExistence(
data['cuda'].exist = cudaExists
data['cuda'].version = cudaVersion
console.log(data['is_initial'], data['gpus_in_use'])
console.debug(data['is_initial'], data['gpus_in_use'])
if (cudaExists && data['is_initial'] && data['gpus_in_use'].length > 0) {
data.run_mode = 'gpu'
}

View File

@ -67,7 +67,7 @@ export const executableNitroFile = (): NitroExecutableOptions => {
if (gpuInfo['vulkan'] === true) {
binaryFolder = path.join(__dirname, '..', 'bin')
binaryFolder = path.join(binaryFolder, 'win-vulkan')
binaryFolder = path.join(binaryFolder, 'linux-vulkan')
vkVisibleDevices = gpuInfo['gpus_in_use'].toString()
}
}

View File

@ -13,6 +13,10 @@ import {
DownloadRoute,
ModelEvent,
DownloadState,
OptionType,
ImportingModel,
LocalImportModelEvent,
baseName,
} from '@janhq/core'
import { extractFileName } from './helpers/path'
@ -158,18 +162,18 @@ export default class JanModelExtension extends ModelExtension {
/**
* Cancels the download of a specific machine learning model.
*
* @param {string} modelId - The ID of the model whose download is to be cancelled.
* @returns {Promise<void>} A promise that resolves when the download has been cancelled.
*/
async cancelModelDownload(modelId: string): Promise<void> {
const model = await this.getConfiguredModels()
return abortDownload(
await joinPath([JanModelExtension._homeDir, modelId, modelId])
).then(async () => {
fs.unlinkSync(
await joinPath([JanModelExtension._homeDir, modelId, modelId])
)
})
const path = await joinPath([JanModelExtension._homeDir, modelId, modelId])
try {
await abortDownload(path)
await fs.unlinkSync(path)
} catch (e) {
console.error(e)
}
}
/**
@ -180,6 +184,20 @@ export default class JanModelExtension extends ModelExtension {
async deleteModel(modelId: string): Promise<void> {
try {
const dirPath = await joinPath([JanModelExtension._homeDir, modelId])
const jsonFilePath = await joinPath([
dirPath,
JanModelExtension._modelMetadataFileName,
])
const modelInfo = JSON.parse(
await this.readModelMetadata(jsonFilePath)
) as Model
const isUserImportModel =
modelInfo.metadata?.author?.toLowerCase() === 'user'
if (isUserImportModel) {
// just delete the folder
return fs.rmdirSync(dirPath)
}
// remove all files under dirPath except model.json
const files = await fs.readdirSync(dirPath)
@ -389,7 +407,7 @@ export default class JanModelExtension extends ModelExtension {
llama_model_path: binaryFileName,
},
created: Date.now(),
description: `${dirName} - user self import model`,
description: '',
metadata: {
size: binaryFileSize,
author: 'User',
@ -455,4 +473,189 @@ export default class JanModelExtension extends ModelExtension {
)
}
}
private async importModelSymlink(
modelBinaryPath: string,
modelFolderName: string,
modelFolderPath: string
): Promise<Model> {
const fileStats = await fs.fileStat(modelBinaryPath, true)
const binaryFileSize = fileStats.size
// Just need to generate model.json there
const defaultModel = (await this.getDefaultModel()) as Model
if (!defaultModel) {
console.error('Unable to find default model')
return
}
const binaryFileName = await baseName(modelBinaryPath)
const model: Model = {
...defaultModel,
id: modelFolderName,
name: modelFolderName,
sources: [
{
url: modelBinaryPath,
filename: binaryFileName,
},
],
settings: {
...defaultModel.settings,
llama_model_path: binaryFileName,
},
created: Date.now(),
description: '',
metadata: {
size: binaryFileSize,
author: 'User',
tags: [],
},
}
const modelFilePath = await joinPath([
modelFolderPath,
JanModelExtension._modelMetadataFileName,
])
await fs.writeFileSync(modelFilePath, JSON.stringify(model, null, 2))
return model
}
async updateModelInfo(modelInfo: Partial<Model>): Promise<Model> {
const modelId = modelInfo.id
if (modelInfo.id == null) throw new Error('Model ID is required')
const janDataFolderPath = await getJanDataFolderPath()
const jsonFilePath = await joinPath([
janDataFolderPath,
'models',
modelId,
JanModelExtension._modelMetadataFileName,
])
const model = JSON.parse(
await this.readModelMetadata(jsonFilePath)
) as Model
const updatedModel: Model = {
...model,
...modelInfo,
metadata: {
...model.metadata,
tags: modelInfo.metadata?.tags ?? [],
},
}
await fs.writeFileSync(jsonFilePath, JSON.stringify(updatedModel, null, 2))
return updatedModel
}
private async importModel(
model: ImportingModel,
optionType: OptionType
): Promise<Model> {
const binaryName = (await baseName(model.path)).replace(/\s/g, '')
let modelFolderName = binaryName
if (binaryName.endsWith(JanModelExtension._supportedModelFormat)) {
modelFolderName = binaryName.replace(
JanModelExtension._supportedModelFormat,
''
)
}
const modelFolderPath = await this.getModelFolderName(modelFolderName)
await fs.mkdirSync(modelFolderPath)
const uniqueFolderName = await baseName(modelFolderPath)
const modelBinaryFile = binaryName.endsWith(
JanModelExtension._supportedModelFormat
)
? binaryName
: `${binaryName}${JanModelExtension._supportedModelFormat}`
const binaryPath = await joinPath([modelFolderPath, modelBinaryFile])
if (optionType === 'SYMLINK') {
return this.importModelSymlink(
model.path,
uniqueFolderName,
modelFolderPath
)
}
const srcStat = await fs.fileStat(model.path, true)
// interval getting the file size to calculate the percentage
const interval = setInterval(async () => {
const destStats = await fs.fileStat(binaryPath, true)
const percentage = destStats.size / srcStat.size
events.emit(LocalImportModelEvent.onLocalImportModelUpdate, {
...model,
percentage,
})
}, 1000)
await fs.copyFile(model.path, binaryPath)
clearInterval(interval)
// generate model json
return this.generateModelMetadata(uniqueFolderName)
}
private async getModelFolderName(
modelFolderName: string,
count?: number
): Promise<string> {
const newModelFolderName = count
? `${modelFolderName}-${count}`
: modelFolderName
const janDataFolderPath = await getJanDataFolderPath()
const modelFolderPath = await joinPath([
janDataFolderPath,
'models',
newModelFolderName,
])
const isFolderExist = await fs.existsSync(modelFolderPath)
if (!isFolderExist) {
return modelFolderPath
} else {
const newCount = (count ?? 0) + 1
return this.getModelFolderName(modelFolderName, newCount)
}
}
async importModels(
models: ImportingModel[],
optionType: OptionType
): Promise<void> {
const importedModels: Model[] = []
for (const model of models) {
events.emit(LocalImportModelEvent.onLocalImportModelUpdate, model)
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

@ -38,7 +38,7 @@ export const s3 = (req: any, reply: any, done: any) => {
reply.status(200).send(result)
return
} catch (ex) {
console.log(ex)
console.error(ex)
}
}
}

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

@ -0,0 +1,66 @@
/*
* react-circular-progressbar styles
* All of the styles in this file are configurable!
*/
.CircularProgressbar {
/*
* This fixes an issue where the CircularProgressbar svg has
* 0 width inside a "display: flex" container, and thus not visible.
*/
width: 100%;
/*
* This fixes a centering issue with CircularProgressbarWithChildren:
* https://github.com/kevinsqi/react-circular-progressbar/issues/94
*/
vertical-align: middle;
}
.CircularProgressbar .CircularProgressbar-path {
stroke: #3e98c7;
stroke-linecap: round;
transition: stroke-dashoffset 0.5s ease 0s;
}
.CircularProgressbar .CircularProgressbar-trail {
stroke: #d6d6d6;
/* Used when trail is not full diameter, i.e. when props.circleRatio is set */
stroke-linecap: round;
}
.CircularProgressbar .CircularProgressbar-text {
fill: #3e98c7;
font-size: 20px;
dominant-baseline: middle;
text-anchor: middle;
}
.CircularProgressbar .CircularProgressbar-background {
fill: #d6d6d6;
}
/*
* Sample background styles. Use these with e.g.:
*
* <CircularProgressbar
* className="CircularProgressbar-inverted"
* background
* percentage={50}
* />
*/
.CircularProgressbar.CircularProgressbar-inverted
.CircularProgressbar-background {
fill: #3e98c7;
}
.CircularProgressbar.CircularProgressbar-inverted .CircularProgressbar-text {
fill: #fff;
}
.CircularProgressbar.CircularProgressbar-inverted .CircularProgressbar-path {
stroke: #fff;
}
.CircularProgressbar.CircularProgressbar-inverted .CircularProgressbar-trail {
stroke: transparent;
}

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

@ -17,6 +17,7 @@
@import './select/styles.scss';
@import './slider/styles.scss';
@import './checkbox/styles.scss';
@import './circular-progress/styles.scss';
.animate-spin {
animation: spin 1s linear infinite;

View File

@ -19,7 +19,7 @@ const ModalOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<ModalPrimitive.Overlay
ref={ref}
className={twMerge(' modal-backdrop', className)}
className={twMerge('modal-backdrop', className)}
{...props}
/>
))

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;
}
}

8
web/.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"semi": false,
"singleQuote": true,
"quoteProps": "consistent",
"trailingComma": "es5",
"endOfLine": "auto",
"plugins": ["prettier-plugin-tailwindcss"]
}

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

@ -1,19 +1,21 @@
'use client'
import { useAtomValue } from 'jotai'
import BaseLayout from '@/containers/Layout'
import { MainViewState } from '@/constants/screens'
import { useMainViewState } from '@/hooks/useMainViewState'
import ChatScreen from '@/screens/Chat'
import ExploreModelsScreen from '@/screens/ExploreModels'
import LocalServerScreen from '@/screens/LocalServer'
import SettingsScreen from '@/screens/Settings'
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
export default function Page() {
const { mainViewState } = useMainViewState()
const mainViewState = useAtomValue(mainViewStateAtom)
let children = null
switch (mainViewState) {

View File

@ -38,14 +38,14 @@ export default function CardSidebar({
const [menu, setMenu] = useState<HTMLDivElement | null>(null)
const [toggle, setToggle] = useState<HTMLDivElement | null>(null)
const activeThread = useAtomValue(activeThreadAtom)
const { onReviewInFinder, onViewJson } = usePath()
const { onRevealInFinder, onViewJson } = usePath()
useClickOutside(() => setMore(false), null, [menu, toggle])
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" />
@ -100,7 +100,7 @@ export default function CardSidebar({
title === 'Model' ? 'items-start' : 'items-center'
)}
onClick={() => {
onReviewInFinder && onReviewInFinder(title)
onRevealInFinder && onRevealInFinder(title)
setMore(false)
}}
>
@ -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

@ -30,7 +30,6 @@ import { MainViewState } from '@/constants/screens'
import { useActiveModel } from '@/hooks/useActiveModel'
import { useClipboard } from '@/hooks/useClipboard'
import { useMainViewState } from '@/hooks/useMainViewState'
import useRecommendedModel from '@/hooks/useRecommendedModel'
@ -41,6 +40,7 @@ import { toGibibytes } from '@/utils/converter'
import ModelLabel from '../ModelLabel'
import OpenAiKeyInput from '../OpenAiKeyInput'
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
import {
@ -64,11 +64,13 @@ const DropdownListSidebar = ({
const [isTabActive, setIsTabActive] = useState(0)
const { stateModel } = useActiveModel()
const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom)
const { setMainViewState } = useMainViewState()
const setMainViewState = useSetAtom(mainViewStateAtom)
const [loader, setLoader] = useState(0)
const { recommendedModel, downloadedModels } = useRecommendedModel()
const { updateModelParameter } = useUpdateModelParameters()
const clipboard = useClipboard({ timeout: 1000 })
const [copyId, setCopyId] = useState('')
const localModel = downloadedModels.filter(
@ -201,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)}
@ -228,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

@ -0,0 +1,61 @@
import { Fragment, useCallback } from 'react'
import { Progress } from '@janhq/uikit'
import { useAtomValue, useSetAtom } from 'jotai'
import { setImportModelStageAtom } from '@/hooks/useImportModel'
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
const ImportingModelState: React.FC = () => {
const importingModels = useAtomValue(importingModelsAtom)
const setImportModelStage = useSetAtom(setImportModelStageAtom)
const isImportingModels =
importingModels.filter((m) => m.status === 'IMPORTING').length > 0
const finishedImportModelCount = importingModels.filter(
(model) => model.status === 'IMPORTED' || model.status === 'FAILED'
).length
let transferredSize = 0
importingModels.forEach((model) => {
transferredSize += (model.percentage ?? 0) * 100 * model.size
})
const totalSize = importingModels.reduce((acc, model) => acc + model.size, 0)
const progress = totalSize === 0 ? 0 : transferredSize / totalSize
const onClick = useCallback(() => {
setImportModelStage('IMPORTING_MODEL')
}, [setImportModelStage])
return (
<Fragment>
{isImportingModels ? (
<div
className="flex cursor-pointer flex-row items-center space-x-2"
onClick={onClick}
>
<p className="text-xs font-semibold text-[#09090B]">
Importing model ({finishedImportModelCount}/{importingModels.length}
)
</p>
<div className="flex flex-row items-center justify-center space-x-2 rounded-md bg-[#F4F4F5] px-2 py-[2px]">
<Progress
className="h-2 w-24"
value={transferredSize / totalSize}
/>
<span className="text-xs font-bold text-blue-600">
{progress.toFixed(2)}%
</span>
</div>
</div>
) : null}
</Fragment>
)
}
export default ImportingModelState

View File

@ -25,8 +25,8 @@ const TableActiveModel = () => {
const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom)
return (
<div className="flex-shrink-0 m-4 mr-0 w-2/3">
<div className="rounded-lg border border-border shadow-sm overflow-hidden">
<div className="m-4 mr-0 w-2/3 flex-shrink-0">
<div className="overflow-hidden rounded-lg border border-border shadow-sm">
<table className="w-full px-8">
<thead className="w-full border-b border-border bg-secondary">
<tr>

View File

@ -57,23 +57,12 @@ 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
ref={setControl}
className={twMerge(
'flex items-center gap-x-2 cursor-pointer p-2 rounded-md hover:bg-secondary',
'flex cursor-pointer items-center gap-x-2 rounded-md p-2 hover:bg-secondary',
systemMonitorCollapse && 'bg-secondary'
)}
onClick={() => {
@ -88,29 +77,29 @@ const SystemMonitor = () => {
<div
ref={setElementExpand}
className={twMerge(
'fixed left-16 bottom-12 bg-white w-[calc(100%-64px)] z-50 border-t border-border flex flex-col flex-shrink-0',
'fixed bottom-12 left-16 z-50 flex w-[calc(100%-64px)] flex-shrink-0 flex-col border-t border-border bg-background',
showFullScreen && 'h-[calc(100%-48px)]'
)}
>
<div className="h-12 flex items-center border-b border-border px-4 justify-between flex-shrink-0">
<h6 className="font-medium">Running Models</h6>
<div className="flex items-center gap-x-2 unset-drag">
<div className="flex h-12 flex-shrink-0 items-center justify-between border-b border-border px-4">
<h6 className="font-bold">Running Models</h6>
<div className="unset-drag flex items-center gap-x-2">
{showFullScreen ? (
<ChevronDown
size={20}
className="text-muted-foreground cursor-pointer"
className="cursor-pointer text-muted-foreground"
onClick={() => setShowFullScreen(!showFullScreen)}
/>
) : (
<ChevronUp
size={20}
className="text-muted-foreground cursor-pointer"
className="cursor-pointer text-muted-foreground"
onClick={() => setShowFullScreen(!showFullScreen)}
/>
)}
<XIcon
size={16}
className="text-muted-foreground cursor-pointer"
className="cursor-pointer text-muted-foreground"
onClick={() => {
setSystemMonitorCollapse(false)
setShowFullScreen(false)
@ -118,10 +107,10 @@ const SystemMonitor = () => {
/>
</div>
</div>
<div className="flex gap-4 h-full">
<div className="flex h-full gap-4">
<TableActiveModel />
<div className="border-l border-border p-4 w-full">
<div className="mb-4 pb-4 border-b border-border">
<div className="w-full border-l border-border p-4">
<div className="mb-4 border-b border-border pb-4">
<h6 className="font-bold">CPU</h6>
<div className="flex items-center gap-x-4">
<Progress value={cpuUsage} className="h-2" />
@ -130,11 +119,12 @@ const SystemMonitor = () => {
</span>
</div>
</div>
<div className="mb-4 pb-4 border-b border-border">
<div className="flex items-center gap-2">
<div className="mb-4 border-b border-border pb-4">
<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 pb-4 border-b border-border">
<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="flex items-start justify-between mt-4 gap-4"
>
<span className="text-muted-foreground font-medium line-clamp-1 w-1/2">
{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

@ -15,6 +15,7 @@ import ProgressBar from '@/containers/ProgressBar'
import { appDownloadProgress } from '@/containers/Providers/Jotai'
import ImportingModelState from './ImportingModelState'
import SystemMonitor from './SystemMonitor'
const menuLinks = [
@ -34,13 +35,14 @@ const BottomBar = () => {
const progress = useAtomValue(appDownloadProgress)
return (
<div className="fixed bottom-0 left-16 z-20 flex h-12 w-[calc(100%-64px)] items-center justify-between border-t border-border bg-background/80 px-3">
<div className="fixed bottom-0 left-16 z-50 flex h-12 w-[calc(100%-64px)] items-center justify-between border-t border-border bg-background/80 px-3">
<div className="flex flex-shrink-0 items-center gap-x-2">
<div className="flex items-center space-x-2">
{progress && progress > 0 ? (
<ProgressBar total={100} used={progress} />
) : null}
</div>
<ImportingModelState />
<DownloadingState />
</div>
<div className="flex items-center gap-x-3">

View File

@ -2,6 +2,7 @@ import {
Tooltip,
TooltipContent,
TooltipTrigger,
TooltipPortal,
TooltipArrow,
} from '@janhq/uikit'
import { motion as m } from 'framer-motion'
@ -20,13 +21,12 @@ import LogoMark from '@/containers/Brand/Logo/Mark'
import { MainViewState } from '@/constants/screens'
import { useMainViewState } from '@/hooks/useMainViewState'
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
import { editMessageAtom } from '@/helpers/atoms/ChatMessage.atom'
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
export default function RibbonNav() {
const { mainViewState, setMainViewState } = useMainViewState()
const [mainViewState, setMainViewState] = useAtom(mainViewStateAtom)
const [serverEnabled] = useAtom(serverEnabledAtom)
const setEditMessage = useSetAtom(editMessageAtom)
@ -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,29 +114,31 @@ 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"
/>
)}
</TooltipTrigger>
{serverEnabled &&
primary.state === MainViewState.Thread ? (
<TooltipContent
side="right"
sideOffset={10}
className="max-w-[180px]"
>
<span>
Threads are disabled while the server is running
</span>
<TooltipArrow />
</TooltipContent>
) : (
<TooltipContent side="right" sideOffset={10}>
<span>{primary.name}</span>
<TooltipArrow />
</TooltipContent>
)}
<TooltipPortal>
{serverEnabled &&
primary.state === MainViewState.Thread ? (
<TooltipContent
side="right"
sideOffset={10}
className="max-w-[180px]"
>
<span>
Threads are disabled while the server is running
</span>
<TooltipArrow />
</TooltipContent>
) : (
<TooltipContent side="right" sideOffset={10}>
<span>{primary.name}</span>
<TooltipArrow />
</TooltipContent>
)}
</TooltipPortal>
</Tooltip>
</div>
)
@ -164,15 +166,17 @@ 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"
/>
)}
</TooltipTrigger>
<TooltipContent side="right" sideOffset={10}>
<span>{secondary.name}</span>
<TooltipArrow />
</TooltipContent>
<TooltipPortal>
<TooltipContent side="right" sideOffset={10}>
<span>{secondary.name}</span>
<TooltipArrow />
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
)

View File

@ -11,7 +11,7 @@ import {
Badge,
} from '@janhq/uikit'
import { useAtom, useAtomValue } from 'jotai'
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import { DatabaseIcon, CpuIcon } from 'lucide-react'
import { showSelectModelModalAtom } from '@/containers/Providers/KeyListener'
@ -19,13 +19,13 @@ import { showSelectModelModalAtom } from '@/containers/Providers/KeyListener'
import { MainViewState } from '@/constants/screens'
import { useActiveModel } from '@/hooks/useActiveModel'
import { useMainViewState } from '@/hooks/useMainViewState'
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
export default function CommandListDownloadedModel() {
const { setMainViewState } = useMainViewState()
const setMainViewState = useSetAtom(mainViewStateAtom)
const downloadedModels = useAtomValue(downloadedModelsAtom)
const { activeModel, startModel, stopModel } = useActiveModel()
const [serverEnabled] = useAtom(serverEnabledAtom)

View File

@ -10,20 +10,15 @@ import {
CommandList,
} from '@janhq/uikit'
import { useAtom } from 'jotai'
import {
MessageCircleIcon,
SettingsIcon,
LayoutGridIcon,
MonitorIcon,
} from 'lucide-react'
import { useAtom, useSetAtom } from 'jotai'
import { MessageCircleIcon, SettingsIcon, LayoutGridIcon } from 'lucide-react'
import { showCommandSearchModalAtom } from '@/containers/Providers/KeyListener'
import ShortCut from '@/containers/Shortcut'
import { MainViewState } from '@/constants/screens'
import { useMainViewState } from '@/hooks/useMainViewState'
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
const menus = [
{
@ -48,7 +43,7 @@ const menus = [
]
export default function CommandSearch() {
const { setMainViewState } = useMainViewState()
const setMainViewState = useSetAtom(mainViewStateAtom)
const [showCommandSearchModal, setShowCommandSearchModal] = useAtom(
showCommandSearchModalAtom
)

View File

@ -20,7 +20,6 @@ import { MainViewState } from '@/constants/screens'
import { useClickOutside } from '@/hooks/useClickOutside'
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
import { useMainViewState } from '@/hooks/useMainViewState'
import { usePath } from '@/hooks/usePath'
@ -28,18 +27,19 @@ import { showRightSideBarAtom } from '@/screens/Chat/Sidebar'
import { openFileTitle } from '@/utils/titleUtils'
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
import { assistantsAtom } from '@/helpers/atoms/Assistant.atom'
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
const TopBar = () => {
const activeThread = useAtomValue(activeThreadAtom)
const { mainViewState } = useMainViewState()
const mainViewState = useAtomValue(mainViewStateAtom)
const { requestCreateNewThread } = useCreateNewThread()
const assistants = useAtomValue(assistantsAtom)
const [showRightSideBar, setShowRightSideBar] = useAtom(showRightSideBarAtom)
const [showLeftSideBar, setShowLeftSideBar] = useAtom(showLeftSideBarAtom)
const showing = useAtomValue(showRightSideBarAtom)
const { onReviewInFinder, onViewJson } = usePath()
const { onRevealInFinder, onViewJson } = usePath()
const [more, setMore] = useState(false)
const [menu, setMenu] = useState<HTMLDivElement | null>(null)
const [toggle, setToggle] = useState<HTMLDivElement | null>(null)
@ -151,7 +151,7 @@ const TopBar = () => {
<div
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary"
onClick={() => {
onReviewInFinder('Thread')
onRevealInFinder('Thread')
setMore(false)
}}
>
@ -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">
@ -195,7 +195,7 @@ const TopBar = () => {
<div
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary"
onClick={() => {
onReviewInFinder('Model')
onRevealInFinder('Model')
setMore(false)
}}
>
@ -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

@ -4,6 +4,8 @@ import { useTheme } from 'next-themes'
import { motion as m } from 'framer-motion'
import { useAtom, useAtomValue } from 'jotai'
import BottomBar from '@/containers/Layout/BottomBar'
import RibbonNav from '@/containers/Layout/Ribbon'
@ -11,14 +13,21 @@ import TopBar from '@/containers/Layout/TopBar'
import { MainViewState } from '@/constants/screens'
import { useMainViewState } from '@/hooks/useMainViewState'
import { getImportModelStageAtom } from '@/hooks/useImportModel'
import { SUCCESS_SET_NEW_DESTINATION } from '@/screens/Settings/Advanced/DataFolder'
import CancelModelImportModal from '@/screens/Settings/CancelModelImportModal'
import EditModelInfoModal from '@/screens/Settings/EditModelInfoModal'
import ImportModelOptionModal from '@/screens/Settings/ImportModelOptionModal'
import ImportingModelModal from '@/screens/Settings/ImportingModelModal'
import SelectingModelModal from '@/screens/Settings/SelectingModelModal'
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
const BaseLayout = (props: PropsWithChildren) => {
const { children } = props
const { mainViewState, setMainViewState } = useMainViewState()
const [mainViewState, setMainViewState] = useAtom(mainViewStateAtom)
const importModelStage = useAtomValue(getImportModelStageAtom)
const { theme, setTheme } = useTheme()
useEffect(() => {
@ -54,6 +63,11 @@ const BaseLayout = (props: PropsWithChildren) => {
<BottomBar />
</div>
</div>
{importModelStage === 'SELECTING_MODEL' && <SelectingModelModal />}
{importModelStage === 'MODEL_SELECTED' && <ImportModelOptionModal />}
{importModelStage === 'IMPORTING_MODEL' && <ImportingModelModal />}
{importModelStage === 'EDIT_MODEL_INFO' && <EditModelInfoModal />}
{importModelStage === 'CONFIRM_CANCEL' && <CancelModelImportModal />}
</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,11 +65,11 @@ 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>
&nbsp; & send it to #🆘|get-help channel for further support.
&nbsp;& send it to #🆘|get-help channel for further support.
</p>
</li>
</ul>
@ -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

@ -0,0 +1,37 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Fragment, PropsWithChildren, useEffect } from 'react'
import { useSetAtom } from 'jotai'
import { appDownloadProgress } from './Jotai'
const AppUpdateListener = ({ children }: PropsWithChildren) => {
const setProgress = useSetAtom(appDownloadProgress)
useEffect(() => {
if (window && window.electronAPI) {
window.electronAPI.onAppUpdateDownloadUpdate(
(_event: string, progress: any) => {
setProgress(progress.percent)
console.debug('app update progress:', progress.percent)
}
)
window.electronAPI.onAppUpdateDownloadError(
(_event: string, callback: any) => {
console.error('Download error', callback)
setProgress(-1)
}
)
window.electronAPI.onAppUpdateDownloadSuccess(() => {
setProgress(-1)
})
}
return () => {}
}, [setProgress])
return <Fragment>{children}</Fragment>
}
export default AppUpdateListener

View File

@ -1,21 +1,37 @@
'use client'
import { Fragment, ReactNode } from 'react'
import { Fragment, ReactNode, useEffect } from 'react'
import { AppConfiguration } from '@janhq/core/.'
import { useSetAtom } from 'jotai'
import useAssistants from '@/hooks/useAssistants'
import useGetSystemResources from '@/hooks/useGetSystemResources'
import useModels from '@/hooks/useModels'
import useThreads from '@/hooks/useThreads'
import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom'
type Props = {
children: ReactNode
}
const DataLoader: React.FC<Props> = ({ children }) => {
const setJanDataFolderPath = useSetAtom(janDataFolderPathAtom)
useModels()
useThreads()
useAssistants()
useGetSystemResources()
useEffect(() => {
window.core?.api
?.getAppConfigurations()
?.then((appConfig: AppConfiguration) => {
setJanDataFolderPath(appConfig.data_folder)
})
}, [setJanDataFolderPath])
console.debug('Load Data...')
return <Fragment>{children}</Fragment>

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ReactNode, useCallback, useEffect, useRef } from 'react'
import { Fragment, ReactNode, useCallback, useEffect, useRef } from 'react'
import {
ChatCompletionMessage,
@ -302,5 +302,5 @@ export default function EventHandler({ children }: { children: ReactNode }) {
events.off(MessageEvent.OnMessageUpdate, onMessageResponseUpdate)
}
}, [onNewMessageResponse, onMessageResponseUpdate])
return <>{children}</>
return <Fragment>{children}</Fragment>
}

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { PropsWithChildren, useCallback, useEffect } from 'react'
import React from 'react'
@ -8,13 +7,13 @@ import { useSetAtom } from 'jotai'
import { setDownloadStateAtom } from '@/hooks/useDownloadState'
import AppUpdateListener from './AppUpdateListener'
import EventHandler from './EventHandler'
import { appDownloadProgress } from './Jotai'
import ModelImportListener from './ModelImportListener'
const EventListenerWrapper = ({ children }: PropsWithChildren) => {
const setDownloadState = useSetAtom(setDownloadStateAtom)
const setProgress = useSetAtom(appDownloadProgress)
const onFileDownloadUpdate = useCallback(
async (state: DownloadState) => {
@ -42,7 +41,6 @@ const EventListenerWrapper = ({ children }: PropsWithChildren) => {
useEffect(() => {
console.debug('EventListenerWrapper: registering event listeners...')
events.on(DownloadEvent.onFileDownloadUpdate, onFileDownloadUpdate)
events.on(DownloadEvent.onFileDownloadError, onFileDownloadError)
events.on(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess)
@ -55,30 +53,13 @@ const EventListenerWrapper = ({ children }: PropsWithChildren) => {
}
}, [onFileDownloadUpdate, onFileDownloadError, onFileDownloadSuccess])
useEffect(() => {
if (window && window.electronAPI) {
window.electronAPI.onAppUpdateDownloadUpdate(
(_event: string, progress: any) => {
setProgress(progress.percent)
console.debug('app update progress:', progress.percent)
}
)
window.electronAPI.onAppUpdateDownloadError(
(_event: string, callback: any) => {
console.error('Download error', callback)
setProgress(-1)
}
)
window.electronAPI.onAppUpdateDownloadSuccess(() => {
setProgress(-1)
})
}
return () => {}
}, [setDownloadState, setProgress])
return <EventHandler>{children}</EventHandler>
return (
<AppUpdateListener>
<ModelImportListener>
<EventHandler>{children}</EventHandler>
</ModelImportListener>
</AppUpdateListener>
)
}
export default EventListenerWrapper

View File

@ -6,7 +6,7 @@ import { atom, useSetAtom } from 'jotai'
import { MainViewState } from '@/constants/screens'
import { useMainViewState } from '@/hooks/useMainViewState'
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
type Props = {
children: ReactNode
@ -19,7 +19,7 @@ export const showCommandSearchModalAtom = atom<boolean>(false)
export default function KeyListener({ children }: Props) {
const setShowLeftSideBar = useSetAtom(showLeftSideBarAtom)
const setShowSelectModelModal = useSetAtom(showSelectModelModalAtom)
const { setMainViewState } = useMainViewState()
const setMainViewState = useSetAtom(mainViewStateAtom)
const showCommandSearchModal = useSetAtom(showCommandSearchModalAtom)
useEffect(() => {
@ -48,8 +48,12 @@ export default function KeyListener({ children }: Props) {
}
document.addEventListener('keydown', onKeyDown)
return () => document.removeEventListener('keydown', onKeyDown)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
}, [
setMainViewState,
setShowLeftSideBar,
setShowSelectModelModal,
showCommandSearchModal,
])
return <Fragment>{children}</Fragment>
}

View File

@ -0,0 +1,109 @@
import { Fragment, PropsWithChildren, useCallback, useEffect } from 'react'
import {
ImportingModel,
LocalImportModelEvent,
Model,
ModelEvent,
events,
} from '@janhq/core'
import { useSetAtom } from 'jotai'
import { snackbar } from '../Toast'
import {
setImportingModelErrorAtom,
setImportingModelSuccessAtom,
updateImportingModelProgressAtom,
} from '@/helpers/atoms/Model.atom'
const ModelImportListener = ({ children }: PropsWithChildren) => {
const updateImportingModelProgress = useSetAtom(
updateImportingModelProgressAtom
)
const setImportingModelSuccess = useSetAtom(setImportingModelSuccessAtom)
const setImportingModelFailed = useSetAtom(setImportingModelErrorAtom)
const onImportModelUpdate = useCallback(
async (state: ImportingModel) => {
if (!state.importId) return
updateImportingModelProgress(state.importId, state.percentage ?? 0)
},
[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
events.emit(ModelEvent.OnModelsUpdate, {})
setImportingModelSuccess(state.importId, state.modelId)
},
[setImportingModelSuccess]
)
const onImportModelFinished = useCallback((importedModels: Model[]) => {
const modelText = importedModels.length === 1 ? 'model' : 'models'
snackbar({
description: `Successfully imported ${importedModels.length} ${modelText}`,
type: 'success',
})
}, [])
useEffect(() => {
console.debug('ModelImportListener: registering event listeners..')
events.on(
LocalImportModelEvent.onLocalImportModelUpdate,
onImportModelUpdate
)
events.on(
LocalImportModelEvent.onLocalImportModelSuccess,
onImportModelSuccess
)
events.on(
LocalImportModelEvent.onLocalImportModelFinished,
onImportModelFinished
)
events.on(
LocalImportModelEvent.onLocalImportModelFailed,
onImportModelFailed
)
return () => {
console.debug('ModelImportListener: unregistering event listeners...')
events.off(
LocalImportModelEvent.onLocalImportModelUpdate,
onImportModelUpdate
)
events.off(
LocalImportModelEvent.onLocalImportModelSuccess,
onImportModelSuccess
)
events.off(
LocalImportModelEvent.onLocalImportModelFinished,
onImportModelFinished
)
events.off(
LocalImportModelEvent.onLocalImportModelFailed,
onImportModelFailed
)
}
}, [
onImportModelUpdate,
onImportModelSuccess,
onImportModelFinished,
onImportModelFailed,
])
return <Fragment>{children}</Fragment>
}
export default ModelImportListener

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

@ -1,4 +1,4 @@
version: "3.8"
version: '3.8'
services:
web:
@ -14,6 +14,6 @@ services:
- /app/node_modules
- /app/.next
ports:
- "3000:3000"
- '3000:3000'
environment:
NODE_ENV: development

View File

@ -0,0 +1,5 @@
import { atom } from 'jotai'
import { MainViewState } from '@/constants/screens'
export const mainViewStateAtom = atom<MainViewState>(MainViewState.Thread)

View File

@ -0,0 +1,3 @@
import { atom } from 'jotai'
export const janDataFolderPathAtom = atom('')

View File

@ -0,0 +1,44 @@
import { HuggingFaceRepoData } from '@janhq/core'
import { atom } from 'jotai'
export const repoIDAtom = atom<string | null>(null)
export const loadingAtom = atom<boolean>(false)
export const fetchErrorAtom = atom<Error | null>(null)
export const conversionStatusAtom = atom<
| 'downloading'
| 'converting'
| 'quantizing'
| 'done'
| 'stopping'
| 'generating'
| null
>(null)
export const conversionErrorAtom = atom<Error | null>(null)
const _repoDataAtom = atom<HuggingFaceRepoData | null>(null)
const _unsupportedAtom = atom<boolean>(false)
export const resetAtom = atom(null, (_get, set) => {
set(repoIDAtom, null)
set(loadingAtom, false)
set(fetchErrorAtom, null)
set(conversionStatusAtom, null)
set(conversionErrorAtom, null)
set(_repoDataAtom, null)
set(_unsupportedAtom, false)
})
export const repoDataAtom = atom(
(get) => get(_repoDataAtom),
(_get, set, repoData: HuggingFaceRepoData) => {
set(_repoDataAtom, repoData)
if (
!repoData.tags.includes('transformers') ||
(!repoData.tags.includes('pytorch') &&
!repoData.tags.includes('safetensors'))
) {
set(_unsupportedAtom, true)
}
}
)
export const unsupportedAtom = atom((get) => get(_unsupportedAtom))

View File

@ -1,4 +1,4 @@
import { Model } from '@janhq/core'
import { ImportingModel, Model } from '@janhq/core'
import { atom } from 'jotai'
export const stateModel = atom({ state: 'start', loading: false, model: '' })
@ -32,4 +32,99 @@ export const removeDownloadingModelAtom = atom(
export const downloadedModelsAtom = atom<Model[]>([])
export const removeDownloadedModelAtom = atom(
null,
(get, set, modelId: string) => {
const downloadedModels = get(downloadedModelsAtom)
set(
downloadedModelsAtom,
downloadedModels.filter((e) => e.id !== modelId)
)
}
)
export const configuredModelsAtom = atom<Model[]>([])
/// TODO: move this part to another atom
// store the paths of the models that are being imported
export const importingModelsAtom = atom<ImportingModel[]>([])
export const updateImportingModelProgressAtom = atom(
null,
(get, set, importId: string, percentage: number) => {
const model = get(importingModelsAtom).find((x) => x.importId === importId)
if (!model) return
const newModel: ImportingModel = {
...model,
status: 'IMPORTING',
percentage,
}
const newList = get(importingModelsAtom).map((x) =>
x.importId === importId ? newModel : x
)
set(importingModelsAtom, newList)
}
)
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) => {
const model = get(importingModelsAtom).find((x) => x.importId === importId)
if (!model) return
const newModel: ImportingModel = {
...model,
modelId,
status: 'IMPORTED',
percentage: 1,
}
const newList = get(importingModelsAtom).map((x) =>
x.importId === importId ? newModel : x
)
set(importingModelsAtom, newList)
}
)
export const updateImportingModelAtom = atom(
null,
(
get,
set,
importId: string,
name: string,
description: string,
tags: string[]
) => {
const model = get(importingModelsAtom).find((x) => x.importId === importId)
if (!model) return
const newModel: ImportingModel = {
...model,
name,
importId,
description,
tags,
}
const newList = get(importingModelsAtom).map((x) =>
x.importId === importId ? newModel : x
)
set(importingModelsAtom, newList)
}
)

View File

@ -0,0 +1,81 @@
import { useContext } from 'react'
import {
ExtensionTypeEnum,
HuggingFaceExtension,
HuggingFaceRepoData,
Quantization,
} from '@janhq/core'
import { useSetAtom } from 'jotai'
import { FeatureToggleContext } from '@/context/FeatureToggle'
import { extensionManager } from '@/extension/ExtensionManager'
import {
conversionStatusAtom,
conversionErrorAtom,
} from '@/helpers/atoms/HFConverter.atom'
export const useConvertHuggingFaceModel = () => {
const { ignoreSSL, proxy } = useContext(FeatureToggleContext)
const setConversionStatus = useSetAtom(conversionStatusAtom)
const setConversionError = useSetAtom(conversionErrorAtom)
const convertHuggingFaceModel = async (
repoID: string,
repoData: HuggingFaceRepoData,
quantization: Quantization
) => {
const extension = await extensionManager.get<HuggingFaceExtension>(
ExtensionTypeEnum.HuggingFace
)
try {
if (extension) {
extension.interrupted = false
}
setConversionStatus('downloading')
await extension?.downloadModelFiles(repoID, repoData, {
ignoreSSL,
proxy,
})
if (extension?.interrupted) return
setConversionStatus('converting')
await extension?.convert(repoID)
if (extension?.interrupted) return
setConversionStatus('quantizing')
await extension?.quantize(repoID, quantization)
if (extension?.interrupted) return
setConversionStatus('generating')
await extension?.generateMetadata(repoID, repoData, quantization)
setConversionStatus('done')
} catch (err) {
if (extension?.interrupted) return
extension?.cancelConvert(repoID, repoData)
if (typeof err === 'number') {
setConversionError(new Error(`exit code: ${err}`))
} else {
setConversionError(err as Error)
}
console.error(err)
}
}
const cancelConvertHuggingFaceModel = async (
repoID: string,
repoData: HuggingFaceRepoData
) => {
const extension = await extensionManager.get<HuggingFaceExtension>(
ExtensionTypeEnum.HuggingFace
)
setConversionStatus('stopping')
await extension?.cancelConvert(repoID, repoData)
setConversionStatus(null)
}
return {
convertHuggingFaceModel,
cancelConvertHuggingFaceModel,
}
}

View File

@ -1,28 +1,32 @@
import { useCallback } from 'react'
import { ExtensionTypeEnum, ModelExtension, Model } from '@janhq/core'
import { useAtom } from 'jotai'
import { useSetAtom } from 'jotai'
import { toaster } from '@/containers/Toast'
import { extensionManager } from '@/extension/ExtensionManager'
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
import { removeDownloadedModelAtom } from '@/helpers/atoms/Model.atom'
export default function useDeleteModel() {
const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelsAtom)
const removeDownloadedModel = useSetAtom(removeDownloadedModelAtom)
const deleteModel = async (model: Model) => {
await extensionManager
.get<ModelExtension>(ExtensionTypeEnum.Model)
?.deleteModel(model.id)
// reload models
setDownloadedModels(downloadedModels.filter((e) => e.id !== model.id))
toaster({
title: 'Model Deletion Successful',
description: `The model ${model.id} has been successfully deleted.`,
type: 'success',
})
}
const deleteModel = useCallback(
async (model: Model) => {
await localDeleteModel(model.id)
removeDownloadedModel(model.id)
toaster({
title: 'Model Deletion Successful',
description: `Model ${model.name} has been successfully deleted.`,
type: 'success',
})
},
[removeDownloadedModel]
)
return { deleteModel }
}
const localDeleteModel = async (id: string) =>
extensionManager.get<ModelExtension>(ExtensionTypeEnum.Model)?.deleteModel(id)

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

@ -0,0 +1,29 @@
import { useAtomValue, useSetAtom } from 'jotai'
import {
repoDataAtom,
repoIDAtom,
loadingAtom,
fetchErrorAtom,
} from '@/helpers/atoms/HFConverter.atom'
export const useGetHFRepoData = () => {
const repoID = useAtomValue(repoIDAtom)
const setRepoData = useSetAtom(repoDataAtom)
const setLoading = useSetAtom(loadingAtom)
const setFetchError = useSetAtom(fetchErrorAtom)
const getRepoData = async () => {
setLoading(true)
try {
const res = await fetch(`https://huggingface.co/api/models/${repoID}`)
const data = await res.json()
setRepoData(data)
} catch (err) {
setFetchError(err as Error)
}
setLoading(false)
}
return getRepoData
}

View File

@ -0,0 +1,70 @@
import { useCallback } from 'react'
import {
ExtensionTypeEnum,
ImportingModel,
Model,
ModelExtension,
OptionType,
} from '@janhq/core'
import { atom } from 'jotai'
import { extensionManager } from '@/extension'
export type ImportModelStage =
| 'NONE'
| 'SELECTING_MODEL'
| 'MODEL_SELECTED'
| 'IMPORTING_MODEL'
| 'EDIT_MODEL_INFO'
| 'CONFIRM_CANCEL'
const importModelStageAtom = atom<ImportModelStage>('NONE')
export const getImportModelStageAtom = atom((get) => get(importModelStageAtom))
export const setImportModelStageAtom = atom(
null,
(_get, set, stage: ImportModelStage) => {
set(importModelStageAtom, stage)
}
)
export type ModelUpdate = {
name: string
description: string
tags: string[]
}
const useImportModel = () => {
const importModels = useCallback(
(models: ImportingModel[], optionType: OptionType) =>
localImportModels(models, optionType),
[]
)
const updateModelInfo = useCallback(
async (modelInfo: Partial<Model>) => localUpdateModelInfo(modelInfo),
[]
)
return { importModels, updateModelInfo }
}
const localImportModels = async (
models: ImportingModel[],
optionType: OptionType
): Promise<void> =>
extensionManager
.get<ModelExtension>(ExtensionTypeEnum.Model)
?.importModels(models, optionType)
const localUpdateModelInfo = async (
modelInfo: Partial<Model>
): Promise<Model | undefined> =>
extensionManager
.get<ModelExtension>(ExtensionTypeEnum.Model)
?.updateModelInfo(modelInfo)
export default useImportModel

Some files were not shown because too many files have changed in this diff Show More