Merge pull request #2185 from janhq/dev
docs: Sync dev branch to docs branch
This commit is contained in:
commit
8e3b472b82
23
README.md
23
README.md
@ -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
|
||||
|
||||
@ -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),
|
||||
]
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -4,6 +4,7 @@ export enum ExtensionTypeEnum {
|
||||
Inference = 'inference',
|
||||
Model = 'model',
|
||||
SystemMonitoring = 'systemMonitoring',
|
||||
HuggingFace = 'huggingFace',
|
||||
}
|
||||
|
||||
export interface ExtensionType {
|
||||
|
||||
30
core/src/extensions/huggingface.ts
Normal file
30
core/src/extensions/huggingface.ts
Normal 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>
|
||||
}
|
||||
@ -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'
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
})
|
||||
|
||||
@ -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()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
34
core/src/types/huggingface/huggingfaceEntity.ts
Normal file
34
core/src/types/huggingface/huggingfaceEntity.ts
Normal 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 */
|
||||
58
core/src/types/huggingface/huggingfaceInterface.ts
Normal file
58
core/src/types/huggingface/huggingfaceInterface.ts
Normal 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>
|
||||
}
|
||||
2
core/src/types/huggingface/index.ts
Normal file
2
core/src/types/huggingface/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './huggingfaceInterface'
|
||||
export * from './huggingfaceEntity'
|
||||
@ -6,4 +6,5 @@ export * from './inference'
|
||||
export * from './monitoring'
|
||||
export * from './file'
|
||||
export * from './config'
|
||||
export * from './huggingface'
|
||||
export * from './miscellaneous'
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from './modelEntity'
|
||||
export * from './modelInterface'
|
||||
export * from './modelEvent'
|
||||
export * from './modelImport'
|
||||
|
||||
23
core/src/types/model/modelImport.ts
Normal file
23
core/src/types/model/modelImport.ts
Normal 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
|
||||
}
|
||||
26
docs/docs/acknowledgements.md
Normal file
26
docs/docs/acknowledgements.md
Normal 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)
|
||||
@ -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>
|
||||
@ -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.
|
||||
|
||||
:::
|
||||
|
||||
@ -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>— 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>— 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>— 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 "my-own-ai" 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>— 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'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>— 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'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>— 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>— 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>
|
||||
|
||||
@ -51,6 +51,7 @@ const sidebars = {
|
||||
"how-we-work/website-docs/website-docs",
|
||||
],
|
||||
},
|
||||
"acknowledgements",
|
||||
],
|
||||
productSidebar: [
|
||||
{
|
||||
|
||||
@ -86,6 +86,10 @@ const menus = [
|
||||
path: "https://janai.bamboohr.com/careers",
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
menu: "Newsletter",
|
||||
path: "/community#newsletter",
|
||||
}
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -15,12 +15,14 @@
|
||||
"build/**/*.{js,map}",
|
||||
"pre-install",
|
||||
"models/**/*",
|
||||
"docs/**/*"
|
||||
"docs/**/*",
|
||||
"scripts/**/*"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"pre-install",
|
||||
"models",
|
||||
"docs"
|
||||
"docs",
|
||||
"scripts"
|
||||
],
|
||||
"publish": [
|
||||
{
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
3
extensions/huggingface-extension/.gitignore
vendored
Normal file
3
extensions/huggingface-extension/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
bin
|
||||
scripts/convert*
|
||||
scripts/gguf-py
|
||||
8
extensions/huggingface-extension/.prettierrc
Normal file
8
extensions/huggingface-extension/.prettierrc
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "consistent",
|
||||
"trailingComma": "es5",
|
||||
"endOfLine": "auto",
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
||||
73
extensions/huggingface-extension/README.md
Normal file
73
extensions/huggingface-extension/README.md
Normal 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!
|
||||
BIN
extensions/huggingface-extension/bin/mac-arm64/quantize
Executable file
BIN
extensions/huggingface-extension/bin/mac-arm64/quantize
Executable file
Binary file not shown.
3
extensions/huggingface-extension/download.bat
Normal file
3
extensions/huggingface-extension/download.bat
Normal 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%"
|
||||
57
extensions/huggingface-extension/package.json
Normal file
57
extensions/huggingface-extension/package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
72
extensions/huggingface-extension/rollup.config.ts
Normal file
72
extensions/huggingface-extension/rollup.config.ts
Normal 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(),
|
||||
],
|
||||
},
|
||||
]
|
||||
14
extensions/huggingface-extension/scripts/install_deps.py
Normal file
14
extensions/huggingface-extension/scripts/install_deps.py
Normal 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])
|
||||
1
extensions/huggingface-extension/scripts/version.txt
Normal file
1
extensions/huggingface-extension/scripts/version.txt
Normal file
@ -0,0 +1 @@
|
||||
b2106
|
||||
2
extensions/huggingface-extension/src/@types/global.d.ts
vendored
Normal file
2
extensions/huggingface-extension/src/@types/global.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
declare const EXTENSION_NAME: string
|
||||
declare const NODE_MODULE_PATH: string
|
||||
396
extensions/huggingface-extension/src/index.ts
Normal file
396
extensions/huggingface-extension/src/index.ts
Normal 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')
|
||||
}
|
||||
}
|
||||
187
extensions/huggingface-extension/src/node/index.ts
Normal file
187
extensions/huggingface-extension/src/node/index.ts
Normal 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()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
20
extensions/huggingface-extension/tsconfig.json
Normal file
20
extensions/huggingface-extension/tsconfig.json
Normal 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"],
|
||||
}
|
||||
@ -1 +1 @@
|
||||
0.3.12
|
||||
0.3.13
|
||||
|
||||
@ -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'
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
66
uikit/src/circular-progress/styles.scss
Normal file
66
uikit/src/circular-progress/styles.scss
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
))
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
8
web/.prettierrc
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "consistent",
|
||||
"trailingComma": "es5",
|
||||
"endOfLine": "auto",
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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'
|
||||
)}
|
||||
>
|
||||
|
||||
@ -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]">
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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't show again</span>
|
||||
</div>
|
||||
|
||||
@ -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}%`,
|
||||
}}
|
||||
|
||||
@ -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
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
))}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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) ?? '')
|
||||
}}
|
||||
|
||||
@ -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 ?? '')
|
||||
}}
|
||||
|
||||
@ -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>
|
||||
& send it to #🆘|get-help channel for further support.
|
||||
& 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"
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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]">
|
||||
|
||||
@ -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>
|
||||
|
||||
37
web/containers/Providers/AppUpdateListener.tsx
Normal file
37
web/containers/Providers/AppUpdateListener.tsx
Normal 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
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
109
web/containers/Providers/ModelImportListener.tsx
Normal file
109
web/containers/Providers/ModelImportListener.tsx
Normal 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
|
||||
@ -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={{
|
||||
|
||||
@ -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) ?? '')
|
||||
}}
|
||||
|
||||
@ -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]">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
5
web/helpers/atoms/App.atom.ts
Normal file
5
web/helpers/atoms/App.atom.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { atom } from 'jotai'
|
||||
|
||||
import { MainViewState } from '@/constants/screens'
|
||||
|
||||
export const mainViewStateAtom = atom<MainViewState>(MainViewState.Thread)
|
||||
3
web/helpers/atoms/AppConfig.atom.ts
Normal file
3
web/helpers/atoms/AppConfig.atom.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { atom } from 'jotai'
|
||||
|
||||
export const janDataFolderPathAtom = atom('')
|
||||
44
web/helpers/atoms/HFConverter.atom.ts
Normal file
44
web/helpers/atoms/HFConverter.atom.ts
Normal 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))
|
||||
@ -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)
|
||||
}
|
||||
)
|
||||
|
||||
81
web/hooks/useConvertHuggingFaceModel.ts
Normal file
81
web/hooks/useConvertHuggingFaceModel.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
55
web/hooks/useDropModelBinaries.ts
Normal file
55
web/hooks/useDropModelBinaries.ts
Normal 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 }
|
||||
}
|
||||
29
web/hooks/useGetHFRepoData.ts
Normal file
29
web/hooks/useGetHFRepoData.ts
Normal 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
|
||||
}
|
||||
70
web/hooks/useImportModel.ts
Normal file
70
web/hooks/useImportModel.ts
Normal 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
Loading…
x
Reference in New Issue
Block a user