Merge branch 'docs-pena-team' of github.com:janhq/jan into docs-pena-team
This commit is contained in:
commit
67cfb838b2
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">
|
<tr style="text-align:center">
|
||||||
<td style="text-align:center"><b>Stable (Recommended)</b></td>
|
<td style="text-align:center"><b>Stable (Recommended)</b></td>
|
||||||
<td style="text-align:center">
|
<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" />
|
<img src='./docs/static/img/windows.png' style="height:14px; width: 14px" />
|
||||||
<b>jan.exe</b>
|
<b>jan.exe</b>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align:center">
|
<td style="text-align:center">
|
||||||
<a href='https://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" />
|
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
|
||||||
<b>Intel</b>
|
<b>Intel</b>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align:center">
|
<td style="text-align:center">
|
||||||
<a href='https://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" />
|
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
|
||||||
<b>M1/M2</b>
|
<b>M1/M2</b>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align:center">
|
<td style="text-align:center">
|
||||||
<a href='https://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" />
|
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
|
||||||
<b>jan.deb</b>
|
<b>jan.deb</b>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align:center">
|
<td style="text-align:center">
|
||||||
<a href='https://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" />
|
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
|
||||||
<b>jan.AppImage</b>
|
<b>jan.AppImage</b>
|
||||||
</a>
|
</a>
|
||||||
@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
|
|||||||
<tr style="text-align:center">
|
<tr style="text-align:center">
|
||||||
<td style="text-align:center"><b>Experimental (Nightly Build)</b></td>
|
<td style="text-align:center"><b>Experimental (Nightly Build)</b></td>
|
||||||
<td style="text-align:center">
|
<td style="text-align:center">
|
||||||
<a href='https://delta.jan.ai/latest/jan-win-x64-0.4.6-285.exe'>
|
<a href='https://delta.jan.ai/latest/jan-win-x64-0.4.7-289.exe'>
|
||||||
<img src='./docs/static/img/windows.png' style="height:14px; width: 14px" />
|
<img src='./docs/static/img/windows.png' style="height:14px; width: 14px" />
|
||||||
<b>jan.exe</b>
|
<b>jan.exe</b>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align:center">
|
<td style="text-align:center">
|
||||||
<a href='https://delta.jan.ai/latest/jan-mac-x64-0.4.6-285.dmg'>
|
<a href='https://delta.jan.ai/latest/jan-mac-x64-0.4.7-289.dmg'>
|
||||||
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
|
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
|
||||||
<b>Intel</b>
|
<b>Intel</b>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align:center">
|
<td style="text-align:center">
|
||||||
<a href='https://delta.jan.ai/latest/jan-mac-arm64-0.4.6-285.dmg'>
|
<a href='https://delta.jan.ai/latest/jan-mac-arm64-0.4.7-289.dmg'>
|
||||||
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
|
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
|
||||||
<b>M1/M2</b>
|
<b>M1/M2</b>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align:center">
|
<td style="text-align:center">
|
||||||
<a href='https://delta.jan.ai/latest/jan-linux-amd64-0.4.6-285.deb'>
|
<a href='https://delta.jan.ai/latest/jan-linux-amd64-0.4.7-289.deb'>
|
||||||
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
|
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
|
||||||
<b>jan.deb</b>
|
<b>jan.deb</b>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align:center">
|
<td style="text-align:center">
|
||||||
<a href='https://delta.jan.ai/latest/jan-linux-x86_64-0.4.6-285.AppImage'>
|
<a href='https://delta.jan.ai/latest/jan-linux-x86_64-0.4.7-289.AppImage'>
|
||||||
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
|
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
|
||||||
<b>jan.AppImage</b>
|
<b>jan.AppImage</b>
|
||||||
</a>
|
</a>
|
||||||
@ -304,7 +304,7 @@ This will build the app MacOS m1/m2 for production (with code signing already do
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# GPU mode with default file system
|
# GPU mode with default file system
|
||||||
docker compose --profile gpu up -d
|
docker compose --profile gpu-fs up -d
|
||||||
|
|
||||||
# GPU mode with S3 file system
|
# GPU mode with S3 file system
|
||||||
docker compose --profile gpu-s3fs up -d
|
docker compose --profile gpu-s3fs up -d
|
||||||
@ -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:
|
Jan builds on top of other open-source projects:
|
||||||
|
|
||||||
- [llama.cpp](https://github.com/ggerganov/llama.cpp)
|
- [llama.cpp](https://github.com/ggerganov/llama.cpp)
|
||||||
|
- [LangChain](https://github.com/langchain-ai)
|
||||||
- [TensorRT](https://github.com/NVIDIA/TensorRT)
|
- [TensorRT](https://github.com/NVIDIA/TensorRT)
|
||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export enum NativeRoute {
|
|||||||
openAppDirectory = 'openAppDirectory',
|
openAppDirectory = 'openAppDirectory',
|
||||||
openFileExplore = 'openFileExplorer',
|
openFileExplore = 'openFileExplorer',
|
||||||
selectDirectory = 'selectDirectory',
|
selectDirectory = 'selectDirectory',
|
||||||
|
selectModelFiles = 'selectModelFiles',
|
||||||
relaunch = 'relaunch',
|
relaunch = 'relaunch',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,6 +47,13 @@ export enum DownloadEvent {
|
|||||||
onFileDownloadSuccess = 'onFileDownloadSuccess',
|
onFileDownloadSuccess = 'onFileDownloadSuccess',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum LocalImportModelEvent {
|
||||||
|
onLocalImportModelUpdate = 'onLocalImportModelUpdate',
|
||||||
|
onLocalImportModelError = 'onLocalImportModelError',
|
||||||
|
onLocalImportModelSuccess = 'onLocalImportModelSuccess',
|
||||||
|
onLocalImportModelFinished = 'onLocalImportModelFinished',
|
||||||
|
}
|
||||||
|
|
||||||
export enum ExtensionRoute {
|
export enum ExtensionRoute {
|
||||||
baseExtensions = 'baseExtensions',
|
baseExtensions = 'baseExtensions',
|
||||||
getActiveExtensions = 'getActiveExtensions',
|
getActiveExtensions = 'getActiveExtensions',
|
||||||
@ -67,6 +75,7 @@ export enum FileSystemRoute {
|
|||||||
}
|
}
|
||||||
export enum FileManagerRoute {
|
export enum FileManagerRoute {
|
||||||
syncFile = 'syncFile',
|
syncFile = 'syncFile',
|
||||||
|
copyFile = 'copyFile',
|
||||||
getJanDataFolderPath = 'getJanDataFolderPath',
|
getJanDataFolderPath = 'getJanDataFolderPath',
|
||||||
getResourcePath = 'getResourcePath',
|
getResourcePath = 'getResourcePath',
|
||||||
getUserHomePath = 'getUserHomePath',
|
getUserHomePath = 'getUserHomePath',
|
||||||
@ -126,4 +135,8 @@ export const CoreRoutes = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export const APIRoutes = [...CoreRoutes, ...Object.values(NativeRoute)]
|
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),
|
||||||
|
]
|
||||||
|
|||||||
@ -4,6 +4,7 @@ export enum ExtensionTypeEnum {
|
|||||||
Inference = 'inference',
|
Inference = 'inference',
|
||||||
Model = 'model',
|
Model = 'model',
|
||||||
SystemMonitoring = 'systemMonitoring',
|
SystemMonitoring = 'systemMonitoring',
|
||||||
|
HuggingFace = 'huggingFace',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtensionType {
|
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.
|
* Model extension for managing models.
|
||||||
*/
|
*/
|
||||||
export { ModelExtension } from './model'
|
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 { BaseExtension, ExtensionTypeEnum } from '../extension'
|
||||||
import { Model, ModelInterface } from '../index'
|
import { ImportingModel, Model, ModelInterface, OptionType } from '../index'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model extension for managing models.
|
* Model extension for managing models.
|
||||||
@ -21,4 +21,6 @@ export abstract class ModelExtension extends BaseExtension implements ModelInter
|
|||||||
abstract saveModel(model: Model): Promise<void>
|
abstract saveModel(model: Model): Promise<void>
|
||||||
abstract getDownloadedModels(): Promise<Model[]>
|
abstract getDownloadedModels(): Promise<Model[]>
|
||||||
abstract getConfiguredModels(): 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 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.
|
* Gets the file's stats.
|
||||||
*
|
*
|
||||||
* @param path - The path to the file.
|
* @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.
|
* @returns {Promise<FileStat>} - A promise that resolves with the file's stats.
|
||||||
*/
|
*/
|
||||||
const fileStat: (path: string) => Promise<FileStat | undefined> = (path) =>
|
const fileStat: (path: string, outsideJanDataFolder?: boolean) => Promise<FileStat | undefined> = (
|
||||||
global.core.api?.fileStat(path)
|
path,
|
||||||
|
outsideJanDataFolder
|
||||||
|
) => global.core.api?.fileStat(path, outsideJanDataFolder)
|
||||||
|
|
||||||
// TODO: Export `dummy` fs functions automatically
|
// TODO: Export `dummy` fs functions automatically
|
||||||
// Currently adding these manually
|
// Currently adding these manually
|
||||||
@ -90,6 +96,7 @@ export const fs = {
|
|||||||
unlinkSync,
|
unlinkSync,
|
||||||
appendFileSync,
|
appendFileSync,
|
||||||
copyFileSync,
|
copyFileSync,
|
||||||
|
copyFile,
|
||||||
syncFile,
|
syncFile,
|
||||||
fileStat,
|
fileStat,
|
||||||
writeBlob,
|
writeBlob,
|
||||||
|
|||||||
@ -50,7 +50,7 @@ export class Downloader implements Processor {
|
|||||||
fileName,
|
fileName,
|
||||||
downloadState: 'downloading',
|
downloadState: 'downloading',
|
||||||
}
|
}
|
||||||
console.log('progress: ', downloadState)
|
console.debug('progress: ', downloadState)
|
||||||
observer?.(DownloadEvent.onFileDownloadUpdate, downloadState)
|
observer?.(DownloadEvent.onFileDownloadUpdate, downloadState)
|
||||||
DownloadManager.instance.downloadProgressMap[modelId] = downloadState
|
DownloadManager.instance.downloadProgressMap[modelId] = downloadState
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { FileManagerRoute } from '../../../api'
|
|
||||||
import { appResourcePath, normalizeFilePath } from '../../helper/path'
|
import { appResourcePath, normalizeFilePath } from '../../helper/path'
|
||||||
import { getJanDataFolderPath, getJanDataFolderPath as getPath } from '../../helper'
|
import { getJanDataFolderPath, getJanDataFolderPath as getPath } from '../../helper'
|
||||||
import { Processor } from './Processor'
|
import { Processor } from './Processor'
|
||||||
@ -48,10 +47,12 @@ export class FSExt implements Processor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handle fs is directory here
|
// handle fs is directory here
|
||||||
fileStat(path: string) {
|
fileStat(path: string, outsideJanDataFolder?: boolean) {
|
||||||
const normalizedPath = normalizeFilePath(path)
|
const normalizedPath = normalizeFilePath(path)
|
||||||
|
|
||||||
const fullPath = join(getJanDataFolderPath(), normalizedPath)
|
const fullPath = outsideJanDataFolder
|
||||||
|
? normalizedPath
|
||||||
|
: join(getJanDataFolderPath(), normalizedPath)
|
||||||
const isExist = fs.existsSync(fullPath)
|
const isExist = fs.existsSync(fullPath)
|
||||||
if (!isExist) return undefined
|
if (!isExist) return undefined
|
||||||
|
|
||||||
@ -75,4 +76,16 @@ export class FSExt implements Processor {
|
|||||||
console.error(`writeFile ${path} result: ${err}`)
|
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 './monitoring'
|
||||||
export * from './file'
|
export * from './file'
|
||||||
export * from './config'
|
export * from './config'
|
||||||
|
export * from './huggingface'
|
||||||
export * from './miscellaneous'
|
export * from './miscellaneous'
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
export * from './modelEntity'
|
export * from './modelEntity'
|
||||||
export * from './modelInterface'
|
export * from './modelInterface'
|
||||||
export * from './modelEvent'
|
export * from './modelEvent'
|
||||||
|
export * from './modelImport'
|
||||||
|
|||||||
22
core/src/types/model/modelImport.ts
Normal file
22
core/src/types/model/modelImport.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
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)
|
||||||
@ -13,13 +13,13 @@ keywords:
|
|||||||
no-subscription fee,
|
no-subscription fee,
|
||||||
large language model,
|
large language model,
|
||||||
docker installation,
|
docker installation,
|
||||||
|
cpu mode,
|
||||||
|
gpu mode,
|
||||||
]
|
]
|
||||||
---
|
---
|
||||||
|
|
||||||
# Installing Jan using Docker
|
# Installing Jan using Docker
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Pre-requisites
|
### Pre-requisites
|
||||||
|
|
||||||
:::note
|
:::note
|
||||||
@ -37,14 +37,32 @@ 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.
|
- 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 |
|
||||||
|
|
||||||
|
| 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
|
- **Option 1**: Run Jan in CPU mode
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose --profile cpu up -d
|
# 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
|
- **Option 2**: Run Jan in GPU mode
|
||||||
@ -89,14 +107,17 @@ sudo sh ./get-docker.sh --dry-run
|
|||||||
- **Step 4**: Run command to start Jan in GPU mode
|
- **Step 4**: Run command to start Jan in GPU mode
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# GPU mode
|
# GPU mode with default file system
|
||||||
docker compose --profile gpu up -d
|
docker compose --profile gpu up -d
|
||||||
|
|
||||||
|
# GPU mode with S3 file system
|
||||||
|
docker compose --profile gpu-s3fs 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
|
:::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.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|||||||
@ -51,6 +51,7 @@ const sidebars = {
|
|||||||
"how-we-work/website-docs/website-docs",
|
"how-we-work/website-docs/website-docs",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
"acknowledgements",
|
||||||
],
|
],
|
||||||
productSidebar: [
|
productSidebar: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,10 @@
|
|||||||
.theme-doc-markdown {
|
.theme-doc-markdown {
|
||||||
|
a,
|
||||||
|
p,
|
||||||
|
span,
|
||||||
|
li {
|
||||||
|
@apply leading-loose;
|
||||||
|
}
|
||||||
a {
|
a {
|
||||||
@apply text-blue-600 dark:text-blue-400;
|
@apply text-blue-600 dark:text-blue-400;
|
||||||
}
|
}
|
||||||
@ -10,9 +16,9 @@
|
|||||||
}
|
}
|
||||||
ul,
|
ul,
|
||||||
ol {
|
ol {
|
||||||
padding-left: 16px;
|
padding-left: 28px;
|
||||||
li {
|
li {
|
||||||
@apply leading-normal;
|
@apply leading-loose;
|
||||||
p {
|
p {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
// * Classname from Docusaurus template
|
// * Classname from Docusaurus template
|
||||||
// * We just overide the styling with applied class from tailwind
|
// * We just overide the styling with applied class from tailwind
|
||||||
|
|
||||||
[class*="docSidebarContainer_"] {
|
[class*='docSidebarContainer_'] {
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
@apply dark:border-gray-800 border-gray-300;
|
@apply dark:border-gray-800 border-gray-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
[class*="sidebar_"] {
|
[class*='sidebar_'] {
|
||||||
padding-top: 0px !important;
|
padding-top: 0px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,32 +14,40 @@
|
|||||||
padding-top: 20px !important;
|
padding-top: 20px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
[class*="sidebarViewport_"] {
|
[class*='sidebarViewport_'] {
|
||||||
top: 80px !important;
|
top: 80px !important;
|
||||||
// height: unset !important;
|
// height: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
[class*="docItemCol_"] {
|
[class*='docItemCol_'] {
|
||||||
@apply lg:px-8;
|
@apply lg:px-8;
|
||||||
}
|
}
|
||||||
|
|
||||||
// * Including custom sidebar table of content
|
// * Including custom sidebar table of content
|
||||||
.table-of-contents {
|
.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 {
|
.menu__caret:before {
|
||||||
background: var(--ifm-menu-link-sublist-icon) 50% / 1.5rem 1.5rem;
|
background: var(--ifm-menu-link-sublist-icon) 50% / 1.5rem 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
[class*="codeBlockContainer_"] {
|
[class*='codeBlockContainer_'] {
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[class*="codeBlockTitle_"] {
|
[class*='codeBlockTitle_'] {
|
||||||
border-bottom: 1px solid #52525a !important;
|
border-bottom: 1px solid #52525a !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
[class*="iconExternalLink_"] {
|
[class*='iconExternalLink_'] {
|
||||||
display: none;
|
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]
|
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}",
|
"build/**/*.{js,map}",
|
||||||
"pre-install",
|
"pre-install",
|
||||||
"models/**/*",
|
"models/**/*",
|
||||||
"docs/**/*"
|
"docs/**/*",
|
||||||
|
"scripts/**/*"
|
||||||
],
|
],
|
||||||
"asarUnpack": [
|
"asarUnpack": [
|
||||||
"pre-install",
|
"pre-install",
|
||||||
"models",
|
"models",
|
||||||
"docs"
|
"docs",
|
||||||
|
"scripts"
|
||||||
],
|
],
|
||||||
"publish": [
|
"publish": [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -8,10 +8,9 @@ export const setupReactDevTool = async () => {
|
|||||||
) // Don't use import on top level, since the installer package is dev-only
|
) // Don't use import on top level, since the installer package is dev-only
|
||||||
try {
|
try {
|
||||||
const name = await installExtension(REACT_DEVELOPER_TOOLS)
|
const name = await installExtension(REACT_DEVELOPER_TOOLS)
|
||||||
console.log(`Added Extension: ${name}`)
|
console.debug(`Added Extension: ${name}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('An error occurred while installing devtools:')
|
console.error('An error occurred while installing devtools:', err)
|
||||||
console.error(err)
|
|
||||||
// Only log the error and don't throw it because it's not critical
|
// 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)
|
console.error('Error deleting log file:', err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.log(
|
console.debug(
|
||||||
`Deleted log file due to exceeding size limit: ${filePath}`
|
`Deleted log file due to exceeding size limit: ${filePath}`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -52,7 +52,7 @@ export function cleanLogs(
|
|||||||
console.error('Error deleting log file:', err)
|
console.error('Error deleting log file:', err)
|
||||||
return
|
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: [],
|
gpus_in_use: [],
|
||||||
is_initial: true,
|
is_initial: true,
|
||||||
// TODO: This needs to be set based on user toggle in settings
|
// TODO: This needs to be set based on user toggle in settings
|
||||||
vulkan: {
|
vulkan: false
|
||||||
enabled: true,
|
|
||||||
gpu_in_use: '1',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -152,7 +149,7 @@ export function updateCudaExistence(
|
|||||||
|
|
||||||
data['cuda'].exist = cudaExists
|
data['cuda'].exist = cudaExists
|
||||||
data['cuda'].version = cudaVersion
|
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) {
|
if (cudaExists && data['is_initial'] && data['gpus_in_use'].length > 0) {
|
||||||
data.run_mode = 'gpu'
|
data.run_mode = 'gpu'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,7 +67,7 @@ export const executableNitroFile = (): NitroExecutableOptions => {
|
|||||||
|
|
||||||
if (gpuInfo['vulkan'] === true) {
|
if (gpuInfo['vulkan'] === true) {
|
||||||
binaryFolder = path.join(__dirname, '..', 'bin')
|
binaryFolder = path.join(__dirname, '..', 'bin')
|
||||||
binaryFolder = path.join(binaryFolder, 'win-vulkan')
|
binaryFolder = path.join(binaryFolder, 'linux-vulkan')
|
||||||
vkVisibleDevices = gpuInfo['gpus_in_use'].toString()
|
vkVisibleDevices = gpuInfo['gpus_in_use'].toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,9 @@ import {
|
|||||||
DownloadRoute,
|
DownloadRoute,
|
||||||
ModelEvent,
|
ModelEvent,
|
||||||
DownloadState,
|
DownloadState,
|
||||||
|
OptionType,
|
||||||
|
ImportingModel,
|
||||||
|
LocalImportModelEvent,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
|
|
||||||
import { extractFileName } from './helpers/path'
|
import { extractFileName } from './helpers/path'
|
||||||
@ -158,18 +161,18 @@ export default class JanModelExtension extends ModelExtension {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancels the download of a specific machine learning model.
|
* Cancels the download of a specific machine learning model.
|
||||||
|
*
|
||||||
* @param {string} modelId - The ID of the model whose download is to be cancelled.
|
* @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.
|
* @returns {Promise<void>} A promise that resolves when the download has been cancelled.
|
||||||
*/
|
*/
|
||||||
async cancelModelDownload(modelId: string): Promise<void> {
|
async cancelModelDownload(modelId: string): Promise<void> {
|
||||||
const model = await this.getConfiguredModels()
|
const path = await joinPath([JanModelExtension._homeDir, modelId, modelId])
|
||||||
return abortDownload(
|
try {
|
||||||
await joinPath([JanModelExtension._homeDir, modelId, modelId])
|
await abortDownload(path)
|
||||||
).then(async () => {
|
await fs.unlinkSync(path)
|
||||||
fs.unlinkSync(
|
} catch (e) {
|
||||||
await joinPath([JanModelExtension._homeDir, modelId, modelId])
|
console.error(e)
|
||||||
)
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -180,6 +183,20 @@ export default class JanModelExtension extends ModelExtension {
|
|||||||
async deleteModel(modelId: string): Promise<void> {
|
async deleteModel(modelId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const dirPath = await joinPath([JanModelExtension._homeDir, modelId])
|
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
|
// remove all files under dirPath except model.json
|
||||||
const files = await fs.readdirSync(dirPath)
|
const files = await fs.readdirSync(dirPath)
|
||||||
@ -389,7 +406,7 @@ export default class JanModelExtension extends ModelExtension {
|
|||||||
llama_model_path: binaryFileName,
|
llama_model_path: binaryFileName,
|
||||||
},
|
},
|
||||||
created: Date.now(),
|
created: Date.now(),
|
||||||
description: `${dirName} - user self import model`,
|
description: '',
|
||||||
metadata: {
|
metadata: {
|
||||||
size: binaryFileSize,
|
size: binaryFileSize,
|
||||||
author: 'User',
|
author: 'User',
|
||||||
@ -455,4 +472,182 @@ 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 = extractFileName(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 = extractFileName(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 = modelFolderPath.split('/').pop()
|
||||||
|
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)
|
||||||
|
const importedModel = await this.importModel(model, optionType)
|
||||||
|
|
||||||
|
events.emit(LocalImportModelEvent.onLocalImportModelSuccess, {
|
||||||
|
...model,
|
||||||
|
modelId: importedModel.id,
|
||||||
|
})
|
||||||
|
importedModels.push(importedModel)
|
||||||
|
}
|
||||||
|
events.emit(
|
||||||
|
LocalImportModelEvent.onLocalImportModelFinished,
|
||||||
|
importedModels
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export const s3 = (req: any, reply: any, done: any) => {
|
|||||||
reply.status(200).send(result)
|
reply.status(200).send(result)
|
||||||
return
|
return
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
console.log(ex)
|
console.error(ex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
@ -17,6 +17,7 @@
|
|||||||
@import './select/styles.scss';
|
@import './select/styles.scss';
|
||||||
@import './slider/styles.scss';
|
@import './slider/styles.scss';
|
||||||
@import './checkbox/styles.scss';
|
@import './checkbox/styles.scss';
|
||||||
|
@import './circular-progress/styles.scss';
|
||||||
|
|
||||||
.animate-spin {
|
.animate-spin {
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
|
|||||||
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"]
|
||||||
|
}
|
||||||
@ -1,19 +1,21 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
import BaseLayout from '@/containers/Layout'
|
import BaseLayout from '@/containers/Layout'
|
||||||
|
|
||||||
import { MainViewState } from '@/constants/screens'
|
import { MainViewState } from '@/constants/screens'
|
||||||
|
|
||||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
|
||||||
|
|
||||||
import ChatScreen from '@/screens/Chat'
|
import ChatScreen from '@/screens/Chat'
|
||||||
import ExploreModelsScreen from '@/screens/ExploreModels'
|
import ExploreModelsScreen from '@/screens/ExploreModels'
|
||||||
|
|
||||||
import LocalServerScreen from '@/screens/LocalServer'
|
import LocalServerScreen from '@/screens/LocalServer'
|
||||||
import SettingsScreen from '@/screens/Settings'
|
import SettingsScreen from '@/screens/Settings'
|
||||||
|
|
||||||
|
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const { mainViewState } = useMainViewState()
|
const mainViewState = useAtomValue(mainViewStateAtom)
|
||||||
|
|
||||||
let children = null
|
let children = null
|
||||||
switch (mainViewState) {
|
switch (mainViewState) {
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export default function CardSidebar({
|
|||||||
const [menu, setMenu] = useState<HTMLDivElement | null>(null)
|
const [menu, setMenu] = useState<HTMLDivElement | null>(null)
|
||||||
const [toggle, setToggle] = useState<HTMLDivElement | null>(null)
|
const [toggle, setToggle] = useState<HTMLDivElement | null>(null)
|
||||||
const activeThread = useAtomValue(activeThreadAtom)
|
const activeThread = useAtomValue(activeThreadAtom)
|
||||||
const { onReviewInFinder, onViewJson } = usePath()
|
const { onRevealInFinder, onViewJson } = usePath()
|
||||||
|
|
||||||
useClickOutside(() => setMore(false), null, [menu, toggle])
|
useClickOutside(() => setMore(false), null, [menu, toggle])
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ export default function CardSidebar({
|
|||||||
title === 'Model' ? 'items-start' : 'items-center'
|
title === 'Model' ? 'items-start' : 'items-center'
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onReviewInFinder && onReviewInFinder(title)
|
onRevealInFinder && onRevealInFinder(title)
|
||||||
setMore(false)
|
setMore(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -30,7 +30,6 @@ import { MainViewState } from '@/constants/screens'
|
|||||||
import { useActiveModel } from '@/hooks/useActiveModel'
|
import { useActiveModel } from '@/hooks/useActiveModel'
|
||||||
|
|
||||||
import { useClipboard } from '@/hooks/useClipboard'
|
import { useClipboard } from '@/hooks/useClipboard'
|
||||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
|
||||||
|
|
||||||
import useRecommendedModel from '@/hooks/useRecommendedModel'
|
import useRecommendedModel from '@/hooks/useRecommendedModel'
|
||||||
|
|
||||||
@ -41,6 +40,7 @@ import { toGibibytes } from '@/utils/converter'
|
|||||||
import ModelLabel from '../ModelLabel'
|
import ModelLabel from '../ModelLabel'
|
||||||
import OpenAiKeyInput from '../OpenAiKeyInput'
|
import OpenAiKeyInput from '../OpenAiKeyInput'
|
||||||
|
|
||||||
|
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
|
||||||
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
|
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -64,11 +64,13 @@ const DropdownListSidebar = ({
|
|||||||
const [isTabActive, setIsTabActive] = useState(0)
|
const [isTabActive, setIsTabActive] = useState(0)
|
||||||
const { stateModel } = useActiveModel()
|
const { stateModel } = useActiveModel()
|
||||||
const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom)
|
const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom)
|
||||||
const { setMainViewState } = useMainViewState()
|
|
||||||
|
const setMainViewState = useSetAtom(mainViewStateAtom)
|
||||||
const [loader, setLoader] = useState(0)
|
const [loader, setLoader] = useState(0)
|
||||||
const { recommendedModel, downloadedModels } = useRecommendedModel()
|
const { recommendedModel, downloadedModels } = useRecommendedModel()
|
||||||
const { updateModelParameter } = useUpdateModelParameters()
|
const { updateModelParameter } = useUpdateModelParameters()
|
||||||
const clipboard = useClipboard({ timeout: 1000 })
|
const clipboard = useClipboard({ timeout: 1000 })
|
||||||
|
|
||||||
const [copyId, setCopyId] = useState('')
|
const [copyId, setCopyId] = useState('')
|
||||||
|
|
||||||
const localModel = downloadedModels.filter(
|
const localModel = downloadedModels.filter(
|
||||||
|
|||||||
@ -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-primary">
|
||||||
|
{progress.toFixed(2)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImportingModelState
|
||||||
@ -25,8 +25,8 @@ const TableActiveModel = () => {
|
|||||||
const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom)
|
const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-shrink-0 m-4 mr-0 w-2/3">
|
<div className="m-4 mr-0 w-2/3 flex-shrink-0">
|
||||||
<div className="rounded-lg border border-border shadow-sm overflow-hidden">
|
<div className="overflow-hidden rounded-lg border border-border shadow-sm">
|
||||||
<table className="w-full px-8">
|
<table className="w-full px-8">
|
||||||
<thead className="w-full border-b border-border bg-secondary">
|
<thead className="w-full border-b border-border bg-secondary">
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@ -57,23 +57,12 @@ const SystemMonitor = () => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const calculateUtilization = () => {
|
|
||||||
let sum = 0
|
|
||||||
const util = gpus.map((x) => {
|
|
||||||
return Number(x['utilization'])
|
|
||||||
})
|
|
||||||
util.forEach((num) => {
|
|
||||||
sum += num
|
|
||||||
})
|
|
||||||
return sum
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div
|
<div
|
||||||
ref={setControl}
|
ref={setControl}
|
||||||
className={twMerge(
|
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'
|
systemMonitorCollapse && 'bg-secondary'
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -88,29 +77,29 @@ const SystemMonitor = () => {
|
|||||||
<div
|
<div
|
||||||
ref={setElementExpand}
|
ref={setElementExpand}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'fixed left-16 bottom-12 bg-background 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)]'
|
showFullScreen && 'h-[calc(100%-48px)]'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="h-12 flex items-center border-b border-border px-4 justify-between flex-shrink-0">
|
<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>
|
<h6 className="font-bold">Running Models</h6>
|
||||||
<div className="flex items-center gap-x-2 unset-drag">
|
<div className="unset-drag flex items-center gap-x-2">
|
||||||
{showFullScreen ? (
|
{showFullScreen ? (
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
size={20}
|
size={20}
|
||||||
className="text-muted-foreground cursor-pointer"
|
className="cursor-pointer text-muted-foreground"
|
||||||
onClick={() => setShowFullScreen(!showFullScreen)}
|
onClick={() => setShowFullScreen(!showFullScreen)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ChevronUp
|
<ChevronUp
|
||||||
size={20}
|
size={20}
|
||||||
className="text-muted-foreground cursor-pointer"
|
className="cursor-pointer text-muted-foreground"
|
||||||
onClick={() => setShowFullScreen(!showFullScreen)}
|
onClick={() => setShowFullScreen(!showFullScreen)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<XIcon
|
<XIcon
|
||||||
size={16}
|
size={16}
|
||||||
className="text-muted-foreground cursor-pointer"
|
className="cursor-pointer text-muted-foreground"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSystemMonitorCollapse(false)
|
setSystemMonitorCollapse(false)
|
||||||
setShowFullScreen(false)
|
setShowFullScreen(false)
|
||||||
@ -118,10 +107,10 @@ const SystemMonitor = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4 h-full">
|
<div className="flex h-full gap-4">
|
||||||
<TableActiveModel />
|
<TableActiveModel />
|
||||||
<div className="border-l border-border p-4 w-full">
|
<div className="w-full border-l border-border p-4">
|
||||||
<div className="mb-4 pb-4 border-b border-border">
|
<div className="mb-4 border-b border-border pb-4">
|
||||||
<h6 className="font-bold">CPU</h6>
|
<h6 className="font-bold">CPU</h6>
|
||||||
<div className="flex items-center gap-x-4">
|
<div className="flex items-center gap-x-4">
|
||||||
<Progress value={cpuUsage} className="h-2" />
|
<Progress value={cpuUsage} className="h-2" />
|
||||||
@ -130,11 +119,12 @@ const SystemMonitor = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4 pb-4 border-b border-border">
|
<div className="mb-4 border-b border-border pb-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<h6 className="font-bold">Memory</h6>
|
<h6 className="font-bold">Memory</h6>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{toGibibytes(usedRam)} of {toGibibytes(totalRam)} used
|
{toGibibytes(usedRam, { hideUnit: true })}/
|
||||||
|
{toGibibytes(totalRam, { hideUnit: true })} GB
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-x-4">
|
<div className="flex items-center gap-x-4">
|
||||||
@ -148,30 +138,29 @@ const SystemMonitor = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{gpus.length > 0 && (
|
{gpus.length > 0 && (
|
||||||
<div className="mb-4 pb-4 border-b border-border">
|
<div className="mb-4 border-b border-border pb-4 last:border-none">
|
||||||
<h6 className="font-bold">GPU</h6>
|
|
||||||
<div className="flex items-center gap-x-4">
|
|
||||||
<Progress value={calculateUtilization()} className="h-2" />
|
|
||||||
<span className="flex-shrink-0 text-muted-foreground">
|
|
||||||
{calculateUtilization()}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{gpus.map((gpu, index) => (
|
{gpus.map((gpu, index) => (
|
||||||
<div
|
<div key={index} className="mt-4 flex flex-col gap-2">
|
||||||
key={index}
|
<div className="flex w-full items-start justify-between">
|
||||||
className="flex items-start justify-between mt-4 gap-4"
|
<span className="line-clamp-1 w-1/2 font-bold">
|
||||||
>
|
|
||||||
<span className="text-muted-foreground font-medium line-clamp-1 w-1/2">
|
|
||||||
{gpu.name}
|
{gpu.name}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex gap-x-2">
|
<div className="flex gap-x-2">
|
||||||
<span className="font-semibold">
|
<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}%
|
{gpu.utilization}%
|
||||||
</span>
|
</span>
|
||||||
<div>
|
|
||||||
<span className="font-semibold">{gpu.vram}</span>
|
|
||||||
<span>MB VRAM</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import ProgressBar from '@/containers/ProgressBar'
|
|||||||
|
|
||||||
import { appDownloadProgress } from '@/containers/Providers/Jotai'
|
import { appDownloadProgress } from '@/containers/Providers/Jotai'
|
||||||
|
|
||||||
|
import ImportingModelState from './ImportingModelState'
|
||||||
import SystemMonitor from './SystemMonitor'
|
import SystemMonitor from './SystemMonitor'
|
||||||
|
|
||||||
const menuLinks = [
|
const menuLinks = [
|
||||||
@ -41,6 +42,7 @@ const BottomBar = () => {
|
|||||||
<ProgressBar total={100} used={progress} />
|
<ProgressBar total={100} used={progress} />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
<ImportingModelState />
|
||||||
<DownloadingState />
|
<DownloadingState />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-x-3">
|
<div className="flex items-center gap-x-3">
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
|
TooltipPortal,
|
||||||
TooltipArrow,
|
TooltipArrow,
|
||||||
} from '@janhq/uikit'
|
} from '@janhq/uikit'
|
||||||
import { motion as m } from 'framer-motion'
|
import { motion as m } from 'framer-motion'
|
||||||
@ -20,13 +21,12 @@ import LogoMark from '@/containers/Brand/Logo/Mark'
|
|||||||
|
|
||||||
import { MainViewState } from '@/constants/screens'
|
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 { editMessageAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||||
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
|
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
|
||||||
|
|
||||||
export default function RibbonNav() {
|
export default function RibbonNav() {
|
||||||
const { mainViewState, setMainViewState } = useMainViewState()
|
const [mainViewState, setMainViewState] = useAtom(mainViewStateAtom)
|
||||||
const [serverEnabled] = useAtom(serverEnabledAtom)
|
const [serverEnabled] = useAtom(serverEnabledAtom)
|
||||||
const setEditMessage = useSetAtom(editMessageAtom)
|
const setEditMessage = useSetAtom(editMessageAtom)
|
||||||
|
|
||||||
@ -119,6 +119,7 @@ export default function RibbonNav() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
<TooltipPortal>
|
||||||
{serverEnabled &&
|
{serverEnabled &&
|
||||||
primary.state === MainViewState.Thread ? (
|
primary.state === MainViewState.Thread ? (
|
||||||
<TooltipContent
|
<TooltipContent
|
||||||
@ -137,6 +138,7 @@ export default function RibbonNav() {
|
|||||||
<TooltipArrow />
|
<TooltipArrow />
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
)}
|
)}
|
||||||
|
</TooltipPortal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -169,10 +171,12 @@ export default function RibbonNav() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
<TooltipPortal>
|
||||||
<TooltipContent side="right" sideOffset={10}>
|
<TooltipContent side="right" sideOffset={10}>
|
||||||
<span>{secondary.name}</span>
|
<span>{secondary.name}</span>
|
||||||
<TooltipArrow />
|
<TooltipArrow />
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
|
</TooltipPortal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
Badge,
|
Badge,
|
||||||
} from '@janhq/uikit'
|
} from '@janhq/uikit'
|
||||||
|
|
||||||
import { useAtom, useAtomValue } from 'jotai'
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { DatabaseIcon, CpuIcon } from 'lucide-react'
|
import { DatabaseIcon, CpuIcon } from 'lucide-react'
|
||||||
|
|
||||||
import { showSelectModelModalAtom } from '@/containers/Providers/KeyListener'
|
import { showSelectModelModalAtom } from '@/containers/Providers/KeyListener'
|
||||||
@ -19,13 +19,13 @@ import { showSelectModelModalAtom } from '@/containers/Providers/KeyListener'
|
|||||||
import { MainViewState } from '@/constants/screens'
|
import { MainViewState } from '@/constants/screens'
|
||||||
|
|
||||||
import { useActiveModel } from '@/hooks/useActiveModel'
|
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 { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
|
||||||
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
|
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
export default function CommandListDownloadedModel() {
|
export default function CommandListDownloadedModel() {
|
||||||
const { setMainViewState } = useMainViewState()
|
const setMainViewState = useSetAtom(mainViewStateAtom)
|
||||||
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
||||||
const { activeModel, startModel, stopModel } = useActiveModel()
|
const { activeModel, startModel, stopModel } = useActiveModel()
|
||||||
const [serverEnabled] = useAtom(serverEnabledAtom)
|
const [serverEnabled] = useAtom(serverEnabledAtom)
|
||||||
|
|||||||
@ -10,20 +10,15 @@ import {
|
|||||||
CommandList,
|
CommandList,
|
||||||
} from '@janhq/uikit'
|
} from '@janhq/uikit'
|
||||||
|
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom, useSetAtom } from 'jotai'
|
||||||
import {
|
import { MessageCircleIcon, SettingsIcon, LayoutGridIcon } from 'lucide-react'
|
||||||
MessageCircleIcon,
|
|
||||||
SettingsIcon,
|
|
||||||
LayoutGridIcon,
|
|
||||||
MonitorIcon,
|
|
||||||
} from 'lucide-react'
|
|
||||||
|
|
||||||
import { showCommandSearchModalAtom } from '@/containers/Providers/KeyListener'
|
import { showCommandSearchModalAtom } from '@/containers/Providers/KeyListener'
|
||||||
import ShortCut from '@/containers/Shortcut'
|
import ShortCut from '@/containers/Shortcut'
|
||||||
|
|
||||||
import { MainViewState } from '@/constants/screens'
|
import { MainViewState } from '@/constants/screens'
|
||||||
|
|
||||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
|
||||||
|
|
||||||
const menus = [
|
const menus = [
|
||||||
{
|
{
|
||||||
@ -48,7 +43,7 @@ const menus = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export default function CommandSearch() {
|
export default function CommandSearch() {
|
||||||
const { setMainViewState } = useMainViewState()
|
const setMainViewState = useSetAtom(mainViewStateAtom)
|
||||||
const [showCommandSearchModal, setShowCommandSearchModal] = useAtom(
|
const [showCommandSearchModal, setShowCommandSearchModal] = useAtom(
|
||||||
showCommandSearchModalAtom
|
showCommandSearchModalAtom
|
||||||
)
|
)
|
||||||
|
|||||||
@ -20,7 +20,6 @@ import { MainViewState } from '@/constants/screens'
|
|||||||
|
|
||||||
import { useClickOutside } from '@/hooks/useClickOutside'
|
import { useClickOutside } from '@/hooks/useClickOutside'
|
||||||
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
||||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
|
||||||
|
|
||||||
import { usePath } from '@/hooks/usePath'
|
import { usePath } from '@/hooks/usePath'
|
||||||
|
|
||||||
@ -28,18 +27,19 @@ import { showRightSideBarAtom } from '@/screens/Chat/Sidebar'
|
|||||||
|
|
||||||
import { openFileTitle } from '@/utils/titleUtils'
|
import { openFileTitle } from '@/utils/titleUtils'
|
||||||
|
|
||||||
|
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
|
||||||
import { assistantsAtom } from '@/helpers/atoms/Assistant.atom'
|
import { assistantsAtom } from '@/helpers/atoms/Assistant.atom'
|
||||||
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
|
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
const TopBar = () => {
|
const TopBar = () => {
|
||||||
const activeThread = useAtomValue(activeThreadAtom)
|
const activeThread = useAtomValue(activeThreadAtom)
|
||||||
const { mainViewState } = useMainViewState()
|
const mainViewState = useAtomValue(mainViewStateAtom)
|
||||||
const { requestCreateNewThread } = useCreateNewThread()
|
const { requestCreateNewThread } = useCreateNewThread()
|
||||||
const assistants = useAtomValue(assistantsAtom)
|
const assistants = useAtomValue(assistantsAtom)
|
||||||
const [showRightSideBar, setShowRightSideBar] = useAtom(showRightSideBarAtom)
|
const [showRightSideBar, setShowRightSideBar] = useAtom(showRightSideBarAtom)
|
||||||
const [showLeftSideBar, setShowLeftSideBar] = useAtom(showLeftSideBarAtom)
|
const [showLeftSideBar, setShowLeftSideBar] = useAtom(showLeftSideBarAtom)
|
||||||
const showing = useAtomValue(showRightSideBarAtom)
|
const showing = useAtomValue(showRightSideBarAtom)
|
||||||
const { onReviewInFinder, onViewJson } = usePath()
|
const { onRevealInFinder, onViewJson } = usePath()
|
||||||
const [more, setMore] = useState(false)
|
const [more, setMore] = useState(false)
|
||||||
const [menu, setMenu] = useState<HTMLDivElement | null>(null)
|
const [menu, setMenu] = useState<HTMLDivElement | null>(null)
|
||||||
const [toggle, setToggle] = useState<HTMLDivElement | null>(null)
|
const [toggle, setToggle] = useState<HTMLDivElement | null>(null)
|
||||||
@ -151,7 +151,7 @@ const TopBar = () => {
|
|||||||
<div
|
<div
|
||||||
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary"
|
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onReviewInFinder('Thread')
|
onRevealInFinder('Thread')
|
||||||
setMore(false)
|
setMore(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -195,7 +195,7 @@ const TopBar = () => {
|
|||||||
<div
|
<div
|
||||||
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary"
|
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onReviewInFinder('Model')
|
onRevealInFinder('Model')
|
||||||
setMore(false)
|
setMore(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import { useTheme } from 'next-themes'
|
|||||||
|
|
||||||
import { motion as m } from 'framer-motion'
|
import { motion as m } from 'framer-motion'
|
||||||
|
|
||||||
|
import { useAtom, useAtomValue } from 'jotai'
|
||||||
|
|
||||||
import BottomBar from '@/containers/Layout/BottomBar'
|
import BottomBar from '@/containers/Layout/BottomBar'
|
||||||
import RibbonNav from '@/containers/Layout/Ribbon'
|
import RibbonNav from '@/containers/Layout/Ribbon'
|
||||||
|
|
||||||
@ -11,14 +13,21 @@ import TopBar from '@/containers/Layout/TopBar'
|
|||||||
|
|
||||||
import { MainViewState } from '@/constants/screens'
|
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 { 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 BaseLayout = (props: PropsWithChildren) => {
|
||||||
const { children } = props
|
const { children } = props
|
||||||
const { mainViewState, setMainViewState } = useMainViewState()
|
const [mainViewState, setMainViewState] = useAtom(mainViewStateAtom)
|
||||||
|
const importModelStage = useAtomValue(getImportModelStageAtom)
|
||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -54,6 +63,11 @@ const BaseLayout = (props: PropsWithChildren) => {
|
|||||||
<BottomBar />
|
<BottomBar />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{importModelStage === 'SELECTING_MODEL' && <SelectingModelModal />}
|
||||||
|
{importModelStage === 'MODEL_SELECTED' && <ImportModelOptionModal />}
|
||||||
|
{importModelStage === 'IMPORTING_MODEL' && <ImportingModelModal />}
|
||||||
|
{importModelStage === 'EDIT_MODEL_INFO' && <EditModelInfoModal />}
|
||||||
|
{importModelStage === 'CONFIRM_CANCEL' && <CancelModelImportModal />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
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'
|
'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 useAssistants from '@/hooks/useAssistants'
|
||||||
import useGetSystemResources from '@/hooks/useGetSystemResources'
|
import useGetSystemResources from '@/hooks/useGetSystemResources'
|
||||||
import useModels from '@/hooks/useModels'
|
import useModels from '@/hooks/useModels'
|
||||||
import useThreads from '@/hooks/useThreads'
|
import useThreads from '@/hooks/useThreads'
|
||||||
|
|
||||||
|
import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const DataLoader: React.FC<Props> = ({ children }) => {
|
const DataLoader: React.FC<Props> = ({ children }) => {
|
||||||
|
const setJanDataFolderPath = useSetAtom(janDataFolderPathAtom)
|
||||||
|
|
||||||
useModels()
|
useModels()
|
||||||
useThreads()
|
useThreads()
|
||||||
useAssistants()
|
useAssistants()
|
||||||
useGetSystemResources()
|
useGetSystemResources()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.core?.api
|
||||||
|
?.getAppConfigurations()
|
||||||
|
?.then((appConfig: AppConfiguration) => {
|
||||||
|
setJanDataFolderPath(appConfig.data_folder)
|
||||||
|
})
|
||||||
|
}, [setJanDataFolderPath])
|
||||||
|
|
||||||
console.debug('Load Data...')
|
console.debug('Load Data...')
|
||||||
|
|
||||||
return <Fragment>{children}</Fragment>
|
return <Fragment>{children}</Fragment>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { ReactNode, useCallback, useEffect, useRef } from 'react'
|
import { Fragment, ReactNode, useCallback, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChatCompletionMessage,
|
ChatCompletionMessage,
|
||||||
@ -302,5 +302,5 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
|||||||
events.off(MessageEvent.OnMessageUpdate, onMessageResponseUpdate)
|
events.off(MessageEvent.OnMessageUpdate, onMessageResponseUpdate)
|
||||||
}
|
}
|
||||||
}, [onNewMessageResponse, 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 { PropsWithChildren, useCallback, useEffect } from 'react'
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
@ -8,13 +7,13 @@ import { useSetAtom } from 'jotai'
|
|||||||
|
|
||||||
import { setDownloadStateAtom } from '@/hooks/useDownloadState'
|
import { setDownloadStateAtom } from '@/hooks/useDownloadState'
|
||||||
|
|
||||||
|
import AppUpdateListener from './AppUpdateListener'
|
||||||
import EventHandler from './EventHandler'
|
import EventHandler from './EventHandler'
|
||||||
|
|
||||||
import { appDownloadProgress } from './Jotai'
|
import ModelImportListener from './ModelImportListener'
|
||||||
|
|
||||||
const EventListenerWrapper = ({ children }: PropsWithChildren) => {
|
const EventListenerWrapper = ({ children }: PropsWithChildren) => {
|
||||||
const setDownloadState = useSetAtom(setDownloadStateAtom)
|
const setDownloadState = useSetAtom(setDownloadStateAtom)
|
||||||
const setProgress = useSetAtom(appDownloadProgress)
|
|
||||||
|
|
||||||
const onFileDownloadUpdate = useCallback(
|
const onFileDownloadUpdate = useCallback(
|
||||||
async (state: DownloadState) => {
|
async (state: DownloadState) => {
|
||||||
@ -42,7 +41,6 @@ const EventListenerWrapper = ({ children }: PropsWithChildren) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.debug('EventListenerWrapper: registering event listeners...')
|
console.debug('EventListenerWrapper: registering event listeners...')
|
||||||
|
|
||||||
events.on(DownloadEvent.onFileDownloadUpdate, onFileDownloadUpdate)
|
events.on(DownloadEvent.onFileDownloadUpdate, onFileDownloadUpdate)
|
||||||
events.on(DownloadEvent.onFileDownloadError, onFileDownloadError)
|
events.on(DownloadEvent.onFileDownloadError, onFileDownloadError)
|
||||||
events.on(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess)
|
events.on(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess)
|
||||||
@ -55,30 +53,13 @@ const EventListenerWrapper = ({ children }: PropsWithChildren) => {
|
|||||||
}
|
}
|
||||||
}, [onFileDownloadUpdate, onFileDownloadError, onFileDownloadSuccess])
|
}, [onFileDownloadUpdate, onFileDownloadError, onFileDownloadSuccess])
|
||||||
|
|
||||||
useEffect(() => {
|
return (
|
||||||
if (window && window.electronAPI) {
|
<AppUpdateListener>
|
||||||
window.electronAPI.onAppUpdateDownloadUpdate(
|
<ModelImportListener>
|
||||||
(_event: string, progress: any) => {
|
<EventHandler>{children}</EventHandler>
|
||||||
setProgress(progress.percent)
|
</ModelImportListener>
|
||||||
console.debug('app update progress:', progress.percent)
|
</AppUpdateListener>
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EventListenerWrapper
|
export default EventListenerWrapper
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { atom, useSetAtom } from 'jotai'
|
|||||||
|
|
||||||
import { MainViewState } from '@/constants/screens'
|
import { MainViewState } from '@/constants/screens'
|
||||||
|
|
||||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
@ -19,7 +19,7 @@ export const showCommandSearchModalAtom = atom<boolean>(false)
|
|||||||
export default function KeyListener({ children }: Props) {
|
export default function KeyListener({ children }: Props) {
|
||||||
const setShowLeftSideBar = useSetAtom(showLeftSideBarAtom)
|
const setShowLeftSideBar = useSetAtom(showLeftSideBarAtom)
|
||||||
const setShowSelectModelModal = useSetAtom(showSelectModelModalAtom)
|
const setShowSelectModelModal = useSetAtom(showSelectModelModalAtom)
|
||||||
const { setMainViewState } = useMainViewState()
|
const setMainViewState = useSetAtom(mainViewStateAtom)
|
||||||
const showCommandSearchModal = useSetAtom(showCommandSearchModalAtom)
|
const showCommandSearchModal = useSetAtom(showCommandSearchModalAtom)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -48,8 +48,12 @@ export default function KeyListener({ children }: Props) {
|
|||||||
}
|
}
|
||||||
document.addEventListener('keydown', onKeyDown)
|
document.addEventListener('keydown', onKeyDown)
|
||||||
return () => document.removeEventListener('keydown', onKeyDown)
|
return () => document.removeEventListener('keydown', onKeyDown)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [
|
||||||
}, [])
|
setMainViewState,
|
||||||
|
setShowLeftSideBar,
|
||||||
|
setShowSelectModelModal,
|
||||||
|
showCommandSearchModal,
|
||||||
|
])
|
||||||
|
|
||||||
return <Fragment>{children}</Fragment>
|
return <Fragment>{children}</Fragment>
|
||||||
}
|
}
|
||||||
|
|||||||
86
web/containers/Providers/ModelImportListener.tsx
Normal file
86
web/containers/Providers/ModelImportListener.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
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 {
|
||||||
|
setImportingModelSuccessAtom,
|
||||||
|
updateImportingModelProgressAtom,
|
||||||
|
} from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
|
const ModelImportListener = ({ children }: PropsWithChildren) => {
|
||||||
|
const updateImportingModelProgress = useSetAtom(
|
||||||
|
updateImportingModelProgressAtom
|
||||||
|
)
|
||||||
|
const setImportingModelSuccess = useSetAtom(setImportingModelSuccessAtom)
|
||||||
|
|
||||||
|
const onImportModelUpdate = useCallback(
|
||||||
|
async (state: ImportingModel) => {
|
||||||
|
if (!state.importId) return
|
||||||
|
updateImportingModelProgress(state.importId, state.percentage ?? 0)
|
||||||
|
},
|
||||||
|
[updateImportingModelProgress]
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
console.debug('ModelImportListener: unregistering event listeners...')
|
||||||
|
events.off(
|
||||||
|
LocalImportModelEvent.onLocalImportModelUpdate,
|
||||||
|
onImportModelUpdate
|
||||||
|
)
|
||||||
|
events.off(
|
||||||
|
LocalImportModelEvent.onLocalImportModelSuccess,
|
||||||
|
onImportModelSuccess
|
||||||
|
)
|
||||||
|
events.off(
|
||||||
|
LocalImportModelEvent.onLocalImportModelFinished,
|
||||||
|
onImportModelFinished
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [onImportModelUpdate, onImportModelSuccess, onImportModelFinished])
|
||||||
|
|
||||||
|
return <Fragment>{children}</Fragment>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModelImportListener
|
||||||
@ -1,4 +1,4 @@
|
|||||||
version: "3.8"
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
@ -14,6 +14,6 @@ services:
|
|||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
- /app/.next
|
- /app/.next
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- '3000:3000'
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: development
|
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'
|
import { atom } from 'jotai'
|
||||||
|
|
||||||
export const stateModel = atom({ state: 'start', loading: false, model: '' })
|
export const stateModel = atom({ state: 'start', loading: false, model: '' })
|
||||||
@ -32,4 +32,81 @@ export const removeDownloadingModelAtom = atom(
|
|||||||
|
|
||||||
export const downloadedModelsAtom = atom<Model[]>([])
|
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[]>([])
|
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 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 { ExtensionTypeEnum, ModelExtension, Model } from '@janhq/core'
|
||||||
|
|
||||||
import { useAtom } from 'jotai'
|
import { useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { toaster } from '@/containers/Toast'
|
import { toaster } from '@/containers/Toast'
|
||||||
|
|
||||||
import { extensionManager } from '@/extension/ExtensionManager'
|
import { extensionManager } from '@/extension/ExtensionManager'
|
||||||
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
|
import { removeDownloadedModelAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
export default function useDeleteModel() {
|
export default function useDeleteModel() {
|
||||||
const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelsAtom)
|
const removeDownloadedModel = useSetAtom(removeDownloadedModelAtom)
|
||||||
|
|
||||||
const deleteModel = async (model: Model) => {
|
const deleteModel = useCallback(
|
||||||
await extensionManager
|
async (model: Model) => {
|
||||||
.get<ModelExtension>(ExtensionTypeEnum.Model)
|
await localDeleteModel(model.id)
|
||||||
?.deleteModel(model.id)
|
removeDownloadedModel(model.id)
|
||||||
|
|
||||||
// reload models
|
|
||||||
setDownloadedModels(downloadedModels.filter((e) => e.id !== model.id))
|
|
||||||
toaster({
|
toaster({
|
||||||
title: 'Model Deletion Successful',
|
title: 'Model Deletion Successful',
|
||||||
description: `The model ${model.id} has been successfully deleted.`,
|
description: `Model ${model.name} has been successfully deleted.`,
|
||||||
type: 'success',
|
type: 'success',
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
|
[removeDownloadedModel]
|
||||||
|
)
|
||||||
|
|
||||||
return { deleteModel }
|
return { deleteModel }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const localDeleteModel = async (id: string) =>
|
||||||
|
extensionManager.get<ModelExtension>(ExtensionTypeEnum.Model)?.deleteModel(id)
|
||||||
|
|||||||
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
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { atom, useAtom } from 'jotai'
|
|
||||||
|
|
||||||
import { MainViewState } from '@/constants/screens'
|
|
||||||
|
|
||||||
const currentMainViewState = atom<MainViewState>(MainViewState.Thread)
|
|
||||||
|
|
||||||
export function useMainViewState() {
|
|
||||||
const [mainViewState, setMainViewState] = useAtom(currentMainViewState)
|
|
||||||
const viewStateName = MainViewState[mainViewState]
|
|
||||||
return { mainViewState, setMainViewState, viewStateName }
|
|
||||||
}
|
|
||||||
@ -9,7 +9,7 @@ export const usePath = () => {
|
|||||||
const activeThread = useAtomValue(activeThreadAtom)
|
const activeThread = useAtomValue(activeThreadAtom)
|
||||||
const selectedModel = useAtomValue(selectedModelAtom)
|
const selectedModel = useAtomValue(selectedModelAtom)
|
||||||
|
|
||||||
const onReviewInFinder = async (type: string) => {
|
const onRevealInFinder = async (type: string) => {
|
||||||
// TODO: this logic should be refactored.
|
// TODO: this logic should be refactored.
|
||||||
if (type !== 'Model' && !activeThread) return
|
if (type !== 'Model' && !activeThread) return
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ export const usePath = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onReviewInFinder,
|
onRevealInFinder,
|
||||||
onViewJson,
|
onViewJson,
|
||||||
onViewFile,
|
onViewFile,
|
||||||
onViewFileContainer,
|
onViewFileContainer,
|
||||||
|
|||||||
@ -32,6 +32,7 @@
|
|||||||
"postcss": "8.4.31",
|
"postcss": "8.4.31",
|
||||||
"posthog-js": "^1.95.1",
|
"posthog-js": "^1.95.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
|
"react-circular-progressbar": "^2.1.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-hook-form": "^7.47.0",
|
"react-hook-form": "^7.47.0",
|
||||||
|
|||||||
@ -4,27 +4,24 @@ import ScrollToBottom from 'react-scroll-to-bottom'
|
|||||||
|
|
||||||
import { InferenceEngine, MessageStatus } from '@janhq/core'
|
import { InferenceEngine, MessageStatus } from '@janhq/core'
|
||||||
import { Button } from '@janhq/uikit'
|
import { Button } from '@janhq/uikit'
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import LogoMark from '@/containers/Brand/Logo/Mark'
|
import LogoMark from '@/containers/Brand/Logo/Mark'
|
||||||
|
|
||||||
import { MainViewState } from '@/constants/screens'
|
import { MainViewState } from '@/constants/screens'
|
||||||
|
|
||||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
|
||||||
|
|
||||||
import ChatItem from '../ChatItem'
|
import ChatItem from '../ChatItem'
|
||||||
|
|
||||||
import ErrorMessage from '../ErrorMessage'
|
import ErrorMessage from '../ErrorMessage'
|
||||||
|
|
||||||
|
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
|
||||||
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||||
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
|
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
const ChatBody: React.FC = () => {
|
const ChatBody: React.FC = () => {
|
||||||
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||||
|
|
||||||
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
||||||
|
const setMainViewState = useSetAtom(mainViewStateAtom)
|
||||||
const { setMainViewState } = useMainViewState()
|
|
||||||
|
|
||||||
if (downloadedModels.length === 0)
|
if (downloadedModels.length === 0)
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -48,7 +48,7 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => {
|
|||||||
{loadModelError === PORT_NOT_AVAILABLE ? (
|
{loadModelError === PORT_NOT_AVAILABLE ? (
|
||||||
<div
|
<div
|
||||||
key={message.id}
|
key={message.id}
|
||||||
className="flex flex-col items-center text-center text-sm font-medium text-gray-500 w-full"
|
className="flex w-full flex-col items-center text-center text-sm font-medium text-gray-500"
|
||||||
>
|
>
|
||||||
<p className="w-[90%]">
|
<p className="w-[90%]">
|
||||||
Port 3928 is currently unavailable. Check for conflicting apps,
|
Port 3928 is currently unavailable. Check for conflicting apps,
|
||||||
|
|||||||
@ -2,19 +2,18 @@ import React, { Fragment, useCallback } from 'react'
|
|||||||
|
|
||||||
import { Button } from '@janhq/uikit'
|
import { Button } from '@janhq/uikit'
|
||||||
|
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import LogoMark from '@/containers/Brand/Logo/Mark'
|
import LogoMark from '@/containers/Brand/Logo/Mark'
|
||||||
|
|
||||||
import { MainViewState } from '@/constants/screens'
|
import { MainViewState } from '@/constants/screens'
|
||||||
|
|
||||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
|
||||||
|
|
||||||
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
|
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
const RequestDownloadModel: React.FC = () => {
|
const RequestDownloadModel: React.FC = () => {
|
||||||
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
||||||
const { setMainViewState } = useMainViewState()
|
const setMainViewState = useSetAtom(mainViewStateAtom)
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
setMainViewState(MainViewState.Hub)
|
setMainViewState(MainViewState.Hub)
|
||||||
|
|||||||
@ -32,6 +32,8 @@ import { usePath } from '@/hooks/usePath'
|
|||||||
import { toGibibytes } from '@/utils/converter'
|
import { toGibibytes } from '@/utils/converter'
|
||||||
import { displayDate } from '@/utils/datetime'
|
import { displayDate } from '@/utils/datetime'
|
||||||
|
|
||||||
|
import { openFileTitle } from '@/utils/titleUtils'
|
||||||
|
|
||||||
import EditChatInput from '../EditChatInput'
|
import EditChatInput from '../EditChatInput'
|
||||||
import Icon from '../FileUploadPreview/Icon'
|
import Icon from '../FileUploadPreview/Icon'
|
||||||
import MessageToolbar from '../MessageToolbar'
|
import MessageToolbar from '../MessageToolbar'
|
||||||
@ -207,7 +209,7 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
|||||||
{messages[messages.length - 1]?.id === props.id &&
|
{messages[messages.length - 1]?.id === props.id &&
|
||||||
(props.status === MessageStatus.Pending || tokenSpeed > 0) && (
|
(props.status === MessageStatus.Pending || tokenSpeed > 0) && (
|
||||||
<p className="absolute right-8 text-xs font-medium text-foreground">
|
<p className="absolute right-8 text-xs font-medium text-foreground">
|
||||||
Token Speed: {Number(tokenSpeed).toFixed(2)}/s
|
Token Speed: {Number(tokenSpeed).toFixed(2)}t/s
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -234,7 +236,7 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
|||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
<TooltipContent side="top" className="max-w-[154px] px-3">
|
<TooltipContent side="top" className="max-w-[154px] px-3">
|
||||||
<span>Show in finder</span>
|
<span>{openFileTitle()}</span>
|
||||||
<TooltipArrow />
|
<TooltipArrow />
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPortal>
|
</TooltipPortal>
|
||||||
@ -261,7 +263,7 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
|||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
<TooltipContent side="top" className="max-w-[154px] px-3">
|
<TooltipContent side="top" className="max-w-[154px] px-3">
|
||||||
<span>Show in finder</span>
|
<span>{openFileTitle()}</span>
|
||||||
<TooltipArrow />
|
<TooltipArrow />
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPortal>
|
</TooltipPortal>
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@janhq/uikit'
|
} from '@janhq/uikit'
|
||||||
|
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { ChevronDownIcon } from 'lucide-react'
|
import { ChevronDownIcon } from 'lucide-react'
|
||||||
|
|
||||||
@ -24,10 +24,9 @@ import { MainViewState } from '@/constants/screens'
|
|||||||
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
||||||
import useDownloadModel from '@/hooks/useDownloadModel'
|
import useDownloadModel from '@/hooks/useDownloadModel'
|
||||||
|
|
||||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
|
||||||
|
|
||||||
import { toGibibytes } from '@/utils/converter'
|
import { toGibibytes } from '@/utils/converter'
|
||||||
|
|
||||||
|
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
|
||||||
import { assistantsAtom } from '@/helpers/atoms/Assistant.atom'
|
import { assistantsAtom } from '@/helpers/atoms/Assistant.atom'
|
||||||
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
|
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
|
||||||
|
|
||||||
@ -70,7 +69,7 @@ const ExploreModelItemHeader: React.FC<Props> = ({ model, onClick, open }) => {
|
|||||||
const totalRam = useAtomValue(totalRamAtom)
|
const totalRam = useAtomValue(totalRamAtom)
|
||||||
|
|
||||||
const nvidiaTotalVram = useAtomValue(nvidiaTotalVramAtom)
|
const nvidiaTotalVram = useAtomValue(nvidiaTotalVramAtom)
|
||||||
const { setMainViewState } = useMainViewState()
|
const setMainViewState = useSetAtom(mainViewStateAtom)
|
||||||
|
|
||||||
// Default nvidia returns vram in MB, need to convert to bytes to match the unit of totalRamW
|
// Default nvidia returns vram in MB, need to convert to bytes to match the unit of totalRamW
|
||||||
let ram = nvidiaTotalVram * 1024 * 1024
|
let ram = nvidiaTotalVram * 1024 * 1024
|
||||||
|
|||||||
@ -0,0 +1,27 @@
|
|||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import {
|
||||||
|
conversionStatusAtom,
|
||||||
|
repoDataAtom,
|
||||||
|
} from '@/helpers/atoms/HFConverter.atom'
|
||||||
|
|
||||||
|
export const HuggingFaceConvertingErrorModal = () => {
|
||||||
|
// This component only loads when repoData is not null
|
||||||
|
const repoData = useAtomValue(repoDataAtom)!
|
||||||
|
// This component only loads when conversionStatus is not null
|
||||||
|
const conversionStatus = useAtomValue(conversionStatusAtom)!
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col items-center justify-center gap-1">
|
||||||
|
<p className="text-2xl font-bold">Hugging Face Converter</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center justify-center gap-1">
|
||||||
|
<p className="text-center">
|
||||||
|
An error occured while {conversionStatus} model {repoData.id}.
|
||||||
|
</p>
|
||||||
|
<p>Please close this modal and try again.</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import { Button } from '@janhq/uikit'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import { useConvertHuggingFaceModel } from '@/hooks/useConvertHuggingFaceModel'
|
||||||
|
|
||||||
|
import {
|
||||||
|
conversionStatusAtom,
|
||||||
|
repoDataAtom,
|
||||||
|
} from '@/helpers/atoms/HFConverter.atom'
|
||||||
|
|
||||||
|
export const HuggingFaceConvertingModal = () => {
|
||||||
|
// This component only loads when repoData is not null
|
||||||
|
const repoData = useAtomValue(repoDataAtom)!
|
||||||
|
// This component only loads when conversionStatus is not null
|
||||||
|
const conversionStatus = useAtomValue(conversionStatusAtom)!
|
||||||
|
const [status, setStatus] = useState('')
|
||||||
|
const { cancelConvertHuggingFaceModel } = useConvertHuggingFaceModel()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
switch (conversionStatus) {
|
||||||
|
case 'downloading':
|
||||||
|
setStatus('Downloading files...')
|
||||||
|
break
|
||||||
|
case 'converting':
|
||||||
|
setStatus('Converting...')
|
||||||
|
break
|
||||||
|
case 'quantizing':
|
||||||
|
setStatus('Quantizing...')
|
||||||
|
break
|
||||||
|
case 'stopping':
|
||||||
|
setStatus('Stopping...')
|
||||||
|
break
|
||||||
|
case 'generating':
|
||||||
|
setStatus('Generating metadata...')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}, [conversionStatus])
|
||||||
|
|
||||||
|
const onStopClick = () => {
|
||||||
|
cancelConvertHuggingFaceModel(repoData.id, repoData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col items-center justify-center gap-1">
|
||||||
|
<p className="text-2xl font-bold">Hugging Face Converter</p>
|
||||||
|
</div>
|
||||||
|
{conversionStatus === 'done' ? (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-1">
|
||||||
|
<p>Done!</p>
|
||||||
|
<p>Now you can use the model on Jan as usual. Have fun!</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col items-center justify-center gap-1">
|
||||||
|
<p>{status}</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={onStopClick}
|
||||||
|
className="w-full"
|
||||||
|
loading={conversionStatus === 'stopping'}
|
||||||
|
disabled={conversionStatus === 'stopping'}
|
||||||
|
themes="danger"
|
||||||
|
>
|
||||||
|
{conversionStatus === 'stopping' ? 'Stopping...' : 'Stop'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
70
web/screens/ExploreModels/HuggingFaceModal/index.tsx
Normal file
70
web/screens/ExploreModels/HuggingFaceModal/index.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { CommandModal, Modal, ModalContent } from '@janhq/uikit'
|
||||||
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import { HuggingFaceConvertingErrorModal } from '../HuggingFaceConvertingErrorModal'
|
||||||
|
import { HuggingFaceConvertingModal } from '../HuggingFaceConvertingModal'
|
||||||
|
import { HuggingFaceRepoDataLoadedModal } from '../HuggingFaceRepoDataLoadedModal'
|
||||||
|
import { HuggingFaceSearchErrorModal } from '../HuggingFaceSearchErrorModal'
|
||||||
|
import { HuggingFaceSearchModal } from '../HuggingFaceSearchModal'
|
||||||
|
|
||||||
|
import {
|
||||||
|
repoDataAtom,
|
||||||
|
fetchErrorAtom,
|
||||||
|
resetAtom,
|
||||||
|
conversionStatusAtom,
|
||||||
|
conversionErrorAtom,
|
||||||
|
} from '@/helpers/atoms/HFConverter.atom'
|
||||||
|
|
||||||
|
const HuggingFaceModal = ({
|
||||||
|
...props
|
||||||
|
}: Omit<Parameters<typeof CommandModal>[0], 'children'>) => {
|
||||||
|
const repoData = useAtomValue(repoDataAtom)
|
||||||
|
const fetchError = useAtomValue(fetchErrorAtom)
|
||||||
|
const conversionStatus = useAtomValue(conversionStatusAtom)
|
||||||
|
const conversionError = useAtomValue(conversionErrorAtom)
|
||||||
|
const setReset = useSetAtom(resetAtom)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
{...props}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (open === false) {
|
||||||
|
if (
|
||||||
|
!repoData ||
|
||||||
|
['done', 'stopping'].includes(conversionStatus ?? '') ||
|
||||||
|
conversionError
|
||||||
|
) {
|
||||||
|
setReset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (props.onOpenChange) {
|
||||||
|
props.onOpenChange(open)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent>
|
||||||
|
<div className="px-2 py-3">
|
||||||
|
<div className="flex w-full flex-col items-center justify-center gap-4 p-4">
|
||||||
|
{repoData ? (
|
||||||
|
conversionStatus ? (
|
||||||
|
conversionError ? (
|
||||||
|
<HuggingFaceConvertingErrorModal />
|
||||||
|
) : (
|
||||||
|
<HuggingFaceConvertingModal />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<HuggingFaceRepoDataLoadedModal />
|
||||||
|
)
|
||||||
|
) : fetchError ? (
|
||||||
|
<HuggingFaceSearchErrorModal />
|
||||||
|
) : (
|
||||||
|
<HuggingFaceSearchModal />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { HuggingFaceModal }
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import { Quantization } from '@janhq/core'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectPortal,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@janhq/uikit'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
import { useConvertHuggingFaceModel } from '@/hooks/useConvertHuggingFaceModel'
|
||||||
|
|
||||||
|
import {
|
||||||
|
loadingAtom,
|
||||||
|
repoDataAtom,
|
||||||
|
unsupportedAtom,
|
||||||
|
} from '@/helpers/atoms/HFConverter.atom'
|
||||||
|
|
||||||
|
export const HuggingFaceRepoDataLoadedModal = () => {
|
||||||
|
const loading = useAtomValue(loadingAtom)
|
||||||
|
// This component only loads when repoData is not null
|
||||||
|
const repoData = useAtomValue(repoDataAtom)!
|
||||||
|
const unsupported = useAtomValue(unsupportedAtom)
|
||||||
|
const [quantization, setQuantization] = useState<Quantization>(
|
||||||
|
Quantization.Q4_K_M
|
||||||
|
)
|
||||||
|
const { convertHuggingFaceModel } = useConvertHuggingFaceModel()
|
||||||
|
|
||||||
|
const onValueSelected = (value: Quantization) => {
|
||||||
|
setQuantization(value)
|
||||||
|
}
|
||||||
|
const onConvertClick = () => {
|
||||||
|
convertHuggingFaceModel(repoData.id, repoData, quantization)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col items-center justify-center gap-1">
|
||||||
|
<p className="text-2xl font-bold">Hugging Face Converter</p>
|
||||||
|
<p className="text-gray-500">Found the repository!</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center justify-center gap-1">
|
||||||
|
<p className="font-bold">{repoData.id}</p>
|
||||||
|
<p>
|
||||||
|
{unsupported
|
||||||
|
? '❌ This model is not supported!'
|
||||||
|
: '✅ This model is supported!'}
|
||||||
|
</p>
|
||||||
|
{repoData.tags.includes('gguf') ? (
|
||||||
|
<p>...But you can import it manually!</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={quantization}
|
||||||
|
onValueChange={onValueSelected}
|
||||||
|
disabled={unsupported}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="relative w-full">
|
||||||
|
<SelectValue placeholder="Quantization">
|
||||||
|
<span className={twMerge('relative z-20')}>{quantization}</span>
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectPortal>
|
||||||
|
<SelectContent className="right-2 block w-full min-w-[450px] pr-0">
|
||||||
|
<div className="border-b border-border" />
|
||||||
|
<SelectGroup>
|
||||||
|
{Object.values(Quantization).map((x, i) => (
|
||||||
|
<SelectItem
|
||||||
|
key={i}
|
||||||
|
value={x}
|
||||||
|
className={twMerge(x === quantization && 'bg-secondary')}
|
||||||
|
>
|
||||||
|
<div className="flex w-full justify-between">
|
||||||
|
<span className="line-clamp-1 block">{x}</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</SelectPortal>
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
onClick={onConvertClick}
|
||||||
|
className="w-full"
|
||||||
|
loading={loading}
|
||||||
|
disabled={unsupported}
|
||||||
|
themes={loading ? 'ghost' : 'primary'}
|
||||||
|
>
|
||||||
|
{loading ? '' : 'Convert'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { Button } from '@janhq/uikit'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import { useGetHFRepoData } from '@/hooks/useGetHFRepoData'
|
||||||
|
|
||||||
|
import { fetchErrorAtom, loadingAtom } from '@/helpers/atoms/HFConverter.atom'
|
||||||
|
|
||||||
|
export const HuggingFaceSearchErrorModal = () => {
|
||||||
|
// This component only loads when fetchError is not null
|
||||||
|
const fetchError = useAtomValue(fetchErrorAtom)!
|
||||||
|
const loading = useAtomValue(loadingAtom)
|
||||||
|
|
||||||
|
const getRepoData = useGetHFRepoData()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col items-center justify-center gap-1">
|
||||||
|
<p className="text-2xl font-bold">Error!</p>
|
||||||
|
<p className="text-gray-500">Fetch error</p>
|
||||||
|
</div>
|
||||||
|
<p>{fetchError.message}</p>
|
||||||
|
<Button
|
||||||
|
onClick={getRepoData}
|
||||||
|
className="w-full"
|
||||||
|
loading={loading}
|
||||||
|
themes={loading ? 'ghost' : 'danger'}
|
||||||
|
>
|
||||||
|
{loading ? '' : 'Try Again'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
45
web/screens/ExploreModels/HuggingFaceSearchModal/index.tsx
Normal file
45
web/screens/ExploreModels/HuggingFaceSearchModal/index.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { Button, Input } from '@janhq/uikit'
|
||||||
|
import { useSetAtom, useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import { useGetHFRepoData } from '@/hooks/useGetHFRepoData'
|
||||||
|
|
||||||
|
import { repoIDAtom, loadingAtom } from '@/helpers/atoms/HFConverter.atom'
|
||||||
|
|
||||||
|
export const HuggingFaceSearchModal = () => {
|
||||||
|
const setRepoID = useSetAtom(repoIDAtom)
|
||||||
|
const loading = useAtomValue(loadingAtom)
|
||||||
|
|
||||||
|
const getRepoData = useGetHFRepoData()
|
||||||
|
|
||||||
|
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
getRepoData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col items-center justify-center gap-1">
|
||||||
|
<p className="text-2xl font-bold">Hugging Face Convertor</p>
|
||||||
|
<p className="text-gray-500">Type the repository id below</p>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder="e.g. username/repo-name"
|
||||||
|
className="bg-white dark:bg-background"
|
||||||
|
onChange={(e) => {
|
||||||
|
setRepoID(e.target.value)
|
||||||
|
}}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={getRepoData}
|
||||||
|
className="w-full"
|
||||||
|
loading={loading}
|
||||||
|
themes={loading ? 'ghost' : 'primary'}
|
||||||
|
>
|
||||||
|
{loading ? '' : 'OK'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useContext, useState } from 'react'
|
||||||
|
|
||||||
import { openExternalUrl } from '@janhq/core'
|
|
||||||
import {
|
import {
|
||||||
Input,
|
Input,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
@ -10,24 +9,36 @@ import {
|
|||||||
SelectContent,
|
SelectContent,
|
||||||
SelectGroup,
|
SelectGroup,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
|
Button,
|
||||||
} from '@janhq/uikit'
|
} from '@janhq/uikit'
|
||||||
|
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { SearchIcon } from 'lucide-react'
|
import { Plus, SearchIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
import { FeatureToggleContext } from '@/context/FeatureToggle'
|
||||||
|
|
||||||
|
import { setImportModelStageAtom } from '@/hooks/useImportModel'
|
||||||
|
|
||||||
import ExploreModelList from './ExploreModelList'
|
import ExploreModelList from './ExploreModelList'
|
||||||
|
import { HuggingFaceModal } from './HuggingFaceModal'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
configuredModelsAtom,
|
configuredModelsAtom,
|
||||||
downloadedModelsAtom,
|
downloadedModelsAtom,
|
||||||
} from '@/helpers/atoms/Model.atom'
|
} from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
|
const sortMenu = ['All Models', 'Recommended', 'Downloaded']
|
||||||
|
|
||||||
const ExploreModelsScreen = () => {
|
const ExploreModelsScreen = () => {
|
||||||
const configuredModels = useAtomValue(configuredModelsAtom)
|
const configuredModels = useAtomValue(configuredModelsAtom)
|
||||||
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
||||||
const [searchValue, setsearchValue] = useState('')
|
const [searchValue, setsearchValue] = useState('')
|
||||||
const [sortSelected, setSortSelected] = useState('All Models')
|
const [sortSelected, setSortSelected] = useState('All Models')
|
||||||
const sortMenu = ['All Models', 'Recommended', 'Downloaded']
|
|
||||||
|
const [showHuggingFaceModal, setShowHuggingFaceModal] = useState(false)
|
||||||
|
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
||||||
|
|
||||||
|
const { experimentalFeature } = useContext(FeatureToggleContext)
|
||||||
|
|
||||||
const filteredModels = configuredModels.filter((x) => {
|
const filteredModels = configuredModels.filter((x) => {
|
||||||
if (sortSelected === 'Downloaded') {
|
if (sortSelected === 'Downloaded') {
|
||||||
@ -45,9 +56,13 @@ const ExploreModelsScreen = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const onHowToImportModelClick = useCallback(() => {
|
const onImportModelClick = useCallback(() => {
|
||||||
openExternalUrl('https://jan.ai/guides/using-models/import-manually/')
|
setImportModelStage('SELECTING_MODEL')
|
||||||
}, [])
|
}, [setImportModelStage])
|
||||||
|
|
||||||
|
const onHuggingFaceConverterClick = () => {
|
||||||
|
setShowHuggingFaceModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -56,6 +71,10 @@ const ExploreModelsScreen = () => {
|
|||||||
>
|
>
|
||||||
<div className="h-full w-full p-4">
|
<div className="h-full w-full p-4">
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
|
<HuggingFaceModal
|
||||||
|
open={showHuggingFaceModal}
|
||||||
|
onOpenChange={setShowHuggingFaceModal}
|
||||||
|
/>
|
||||||
<ScrollArea>
|
<ScrollArea>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<img
|
<img
|
||||||
@ -63,7 +82,8 @@ const ExploreModelsScreen = () => {
|
|||||||
alt="Hub Banner"
|
alt="Hub Banner"
|
||||||
className="w-full object-cover"
|
className="w-full object-cover"
|
||||||
/>
|
/>
|
||||||
<div className="absolute left-1/2 top-1/2 w-1/3 -translate-x-1/2 -translate-y-1/2">
|
<div className="absolute left-1/2 top-1/2 w-1/3 -translate-x-1/2 -translate-y-1/2 space-y-2">
|
||||||
|
<div className="flex flex-row space-x-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<SearchIcon
|
<SearchIcon
|
||||||
size={20}
|
size={20}
|
||||||
@ -72,19 +92,28 @@ const ExploreModelsScreen = () => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Search models"
|
placeholder="Search models"
|
||||||
className="bg-white pl-9 dark:bg-background"
|
className="bg-white pl-9 dark:bg-background"
|
||||||
onChange={(e) => {
|
onChange={(e) => setsearchValue(e.target.value)}
|
||||||
setsearchValue(e.target.value)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 text-center">
|
<Button
|
||||||
|
themes={'primary'}
|
||||||
|
className="space-x-2"
|
||||||
|
onClick={onImportModelClick}
|
||||||
|
>
|
||||||
|
<Plus className="h-3 w-3" />
|
||||||
|
<p>Import Model</p>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{experimentalFeature && (
|
||||||
|
<div className="text-center">
|
||||||
<p
|
<p
|
||||||
onClick={onHowToImportModelClick}
|
onClick={onHuggingFaceConverterClick}
|
||||||
className="cursor-pointer font-semibold text-white underline"
|
className="cursor-pointer font-semibold text-white underline"
|
||||||
>
|
>
|
||||||
How to manually import models
|
Convert from Hugging Face
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto w-4/5 py-6">
|
<div className="mx-auto w-4/5 py-6">
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { Fragment, useCallback, useEffect, useState } from 'react'
|
import { Fragment, useCallback, useState } from 'react'
|
||||||
|
|
||||||
import { fs, AppConfiguration, isSubdirectory } from '@janhq/core'
|
import { fs, AppConfiguration, isSubdirectory } from '@janhq/core'
|
||||||
import { Button, Input } from '@janhq/uikit'
|
import { Button, Input } from '@janhq/uikit'
|
||||||
import { useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { PencilIcon, FolderOpenIcon } from 'lucide-react'
|
import { PencilIcon, FolderOpenIcon } from 'lucide-react'
|
||||||
|
|
||||||
import Loader from '@/containers/Loader'
|
import Loader from '@/containers/Loader'
|
||||||
@ -21,22 +21,17 @@ import ModalErrorSetDestGlobal, {
|
|||||||
|
|
||||||
import ModalSameDirectory, { showSamePathModalAtom } from './ModalSameDirectory'
|
import ModalSameDirectory, { showSamePathModalAtom } from './ModalSameDirectory'
|
||||||
|
|
||||||
|
import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom'
|
||||||
|
|
||||||
const DataFolder = () => {
|
const DataFolder = () => {
|
||||||
const [janDataFolderPath, setJanDataFolderPath] = useState('')
|
|
||||||
const [showLoader, setShowLoader] = useState(false)
|
const [showLoader, setShowLoader] = useState(false)
|
||||||
const setShowDirectoryConfirm = useSetAtom(showDirectoryConfirmModalAtom)
|
const setShowDirectoryConfirm = useSetAtom(showDirectoryConfirmModalAtom)
|
||||||
const setShowSameDirectory = useSetAtom(showSamePathModalAtom)
|
const setShowSameDirectory = useSetAtom(showSamePathModalAtom)
|
||||||
const setShowChangeFolderError = useSetAtom(showChangeFolderErrorAtom)
|
const setShowChangeFolderError = useSetAtom(showChangeFolderErrorAtom)
|
||||||
const showDestNotEmptyConfirm = useSetAtom(showDestNotEmptyConfirmAtom)
|
const showDestNotEmptyConfirm = useSetAtom(showDestNotEmptyConfirmAtom)
|
||||||
const [destinationPath, setDestinationPath] = useState(undefined)
|
|
||||||
|
|
||||||
useEffect(() => {
|
const [destinationPath, setDestinationPath] = useState(undefined)
|
||||||
window.core?.api
|
const janDataFolderPath = useAtomValue(janDataFolderPathAtom)
|
||||||
?.getAppConfigurations()
|
|
||||||
?.then((appConfig: AppConfiguration) => {
|
|
||||||
setJanDataFolderPath(appConfig.data_folder)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onChangeFolderClick = useCallback(async () => {
|
const onChangeFolderClick = useCallback(async () => {
|
||||||
const destFolder = await window.core?.api?.selectDirectory()
|
const destFolder = await window.core?.api?.selectDirectory()
|
||||||
@ -56,8 +51,7 @@ const DataFolder = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
const newDestChildren: string[] = await fs.readdirSync(destFolder)
|
||||||
const newDestChildren: any[] = await fs.readdirSync(destFolder)
|
|
||||||
const isNotEmpty =
|
const isNotEmpty =
|
||||||
newDestChildren.filter((x) => x !== '.DS_Store').length > 0
|
newDestChildren.filter((x) => x !== '.DS_Store').length > 0
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import React, { useCallback, useState } from 'react'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalPortal,
|
|
||||||
ModalContent,
|
ModalContent,
|
||||||
ModalHeader,
|
ModalHeader,
|
||||||
ModalTitle,
|
ModalTitle,
|
||||||
@ -33,7 +32,6 @@ const ModalConfirmReset = () => {
|
|||||||
open={modalValidation}
|
open={modalValidation}
|
||||||
onOpenChange={() => setModalValidation(false)}
|
onOpenChange={() => setModalValidation(false)}
|
||||||
>
|
>
|
||||||
<ModalPortal />
|
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<ModalTitle>
|
<ModalTitle>
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import {
|
|||||||
TooltipArrow,
|
TooltipArrow,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
|
ScrollArea,
|
||||||
} from '@janhq/uikit'
|
} from '@janhq/uikit'
|
||||||
|
|
||||||
import { AlertTriangleIcon, AlertCircleIcon } from 'lucide-react'
|
import { AlertTriangleIcon, AlertCircleIcon } from 'lucide-react'
|
||||||
@ -138,9 +139,10 @@ const Advanced = () => {
|
|||||||
gpuList.length > 0 ? 'Select GPU' : "You don't have any compatible GPU"
|
gpuList.length > 0 ? 'Select GPU' : "You don't have any compatible GPU"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ScrollArea className="px-4">
|
||||||
<div className="block w-full">
|
<div className="block w-full">
|
||||||
{/* Keyboard shortcut */}
|
{/* Keyboard shortcut */}
|
||||||
<div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
|
<div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-4 last:border-none">
|
||||||
<div className="flex-shrink-0 space-y-1.5">
|
<div className="flex-shrink-0 space-y-1.5">
|
||||||
<div className="flex gap-x-2">
|
<div className="flex gap-x-2">
|
||||||
<h6 className="text-sm font-semibold capitalize">
|
<h6 className="text-sm font-semibold capitalize">
|
||||||
@ -175,7 +177,7 @@ const Advanced = () => {
|
|||||||
{/* CPU / GPU switching */}
|
{/* CPU / GPU switching */}
|
||||||
{!isMac && (
|
{!isMac && (
|
||||||
<div className="flex w-full flex-col items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
|
<div className="flex w-full flex-col items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
|
||||||
<div className="flex items-start justify-between w-full">
|
<div className="flex w-full items-start justify-between">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="flex gap-x-2">
|
<div className="flex gap-x-2">
|
||||||
<h6 className="text-sm font-semibold capitalize">
|
<h6 className="text-sm font-semibold capitalize">
|
||||||
@ -204,7 +206,10 @@ const Advanced = () => {
|
|||||||
{gpuList.length > 0 && !gpuEnabled && (
|
{gpuList.length > 0 && !gpuEnabled && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<AlertCircleIcon size={20} className="mr-2 text-yellow-600" />
|
<AlertCircleIcon
|
||||||
|
size={20}
|
||||||
|
className="mr-2 text-yellow-600"
|
||||||
|
/>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent
|
<TooltipContent
|
||||||
side="right"
|
side="right"
|
||||||
@ -242,7 +247,8 @@ const Advanced = () => {
|
|||||||
saveSettings({ runMode: 'cpu' })
|
saveSettings({ runMode: 'cpu' })
|
||||||
setGpuEnabled(false)
|
setGpuEnabled(false)
|
||||||
snackbar({
|
snackbar({
|
||||||
description: 'Successfully turned off GPU Accelertion',
|
description:
|
||||||
|
'Successfully turned off GPU Accelertion',
|
||||||
type: 'success',
|
type: 'success',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -276,7 +282,7 @@ const Advanced = () => {
|
|||||||
disabled={gpuList.length === 0 || !gpuEnabled}
|
disabled={gpuList.length === 0 || !gpuEnabled}
|
||||||
value={selectedGpu.join()}
|
value={selectedGpu.join()}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[340px] dark:bg-gray-500 bg-white">
|
<SelectTrigger className="w-[340px] bg-white dark:bg-gray-500">
|
||||||
<SelectValue placeholder={gpuSelectionPlaceHolder}>
|
<SelectValue placeholder={gpuSelectionPlaceHolder}>
|
||||||
<span className="line-clamp-1 w-full pr-8">
|
<span className="line-clamp-1 w-full pr-8">
|
||||||
{selectedGpu.join()}
|
{selectedGpu.join()}
|
||||||
@ -308,7 +314,9 @@ const Advanced = () => {
|
|||||||
className="bg-white"
|
className="bg-white"
|
||||||
value={gpu.id}
|
value={gpu.id}
|
||||||
checked={gpusInUse.includes(gpu.id)}
|
checked={gpusInUse.includes(gpu.id)}
|
||||||
onCheckedChange={() => handleGPUChange(gpu.id)}
|
onCheckedChange={() =>
|
||||||
|
handleGPUChange(gpu.id)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
className="flex w-full items-center justify-between"
|
className="flex w-full items-center justify-between"
|
||||||
@ -330,8 +338,8 @@ const Advanced = () => {
|
|||||||
className="flex-shrink-0"
|
className="flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs leading-relaxed">
|
<p className="text-xs leading-relaxed">
|
||||||
If multi-GPU is enabled with different GPU models or
|
If multi-GPU is enabled with different GPU models
|
||||||
without NVLink, it could impact token speed.
|
or without NVLink, it could impact token speed.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -356,8 +364,8 @@ const Advanced = () => {
|
|||||||
</h6>
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs leading-relaxed">
|
<p className="text-xs leading-relaxed">
|
||||||
Enable Vulkan with AMD GPU/APU and Intel Arc GPU for better model
|
Enable Vulkan with AMD GPU/APU and Intel Arc GPU for better
|
||||||
performance (reload needed).
|
model performance (reload needed).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -380,17 +388,17 @@ const Advanced = () => {
|
|||||||
<DataFolder />
|
<DataFolder />
|
||||||
{/* Proxy */}
|
{/* Proxy */}
|
||||||
<div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
|
<div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
|
||||||
<div className="flex-shrink-0 space-y-1.5 w-full">
|
<div className="w-full flex-shrink-0 space-y-1.5">
|
||||||
<div className="flex gap-x-2 justify-between w-full">
|
<div className="flex w-full justify-between gap-x-2">
|
||||||
<h6 className="text-sm font-semibold capitalize">HTTPS Proxy</h6>
|
<h6 className="text-sm font-semibold capitalize">HTTPS Proxy</h6>
|
||||||
<Switch
|
<Switch
|
||||||
checked={proxyEnabled}
|
checked={proxyEnabled}
|
||||||
onCheckedChange={(_) => setProxyEnabled(!proxyEnabled)}
|
onCheckedChange={() => setProxyEnabled(!proxyEnabled)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="leading-relaxed">
|
<p className="leading-relaxed">
|
||||||
Specify the HTTPS proxy or leave blank (proxy auto-configuration and
|
Specify the HTTPS proxy or leave blank (proxy auto-configuration
|
||||||
SOCKS not supported).
|
and SOCKS not supported).
|
||||||
</p>
|
</p>
|
||||||
<Input
|
<Input
|
||||||
placeholder={'http://<user>:<password>@<domain or IP>:<port>'}
|
placeholder={'http://<user>:<password>@<domain or IP>:<port>'}
|
||||||
@ -414,7 +422,10 @@ const Advanced = () => {
|
|||||||
certain proxies.
|
certain proxies.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch checked={ignoreSSL} onCheckedChange={(e) => setIgnoreSSL(e)} />
|
<Switch
|
||||||
|
checked={ignoreSSL}
|
||||||
|
onCheckedChange={(e) => setIgnoreSSL(e)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Clear log */}
|
{/* Clear log */}
|
||||||
@ -433,6 +444,7 @@ const Advanced = () => {
|
|||||||
{/* Factory Reset */}
|
{/* Factory Reset */}
|
||||||
<FactoryReset />
|
<FactoryReset />
|
||||||
</div>
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import ToggleTheme from '@/screens/Settings/Appearance/ToggleTheme'
|
|||||||
|
|
||||||
export default function AppearanceOptions() {
|
export default function AppearanceOptions() {
|
||||||
return (
|
return (
|
||||||
<div className="block w-full">
|
<div className="m-4 block w-full">
|
||||||
<div className="flex w-full items-center justify-between border-b border-border py-3 first:pt-0 last:border-none">
|
<div className="flex w-full items-center justify-between border-b border-border py-3 first:pt-0 last:border-none">
|
||||||
<div className="flex-shrink-0 space-y-1">
|
<div className="flex-shrink-0 space-y-1">
|
||||||
<h6 className="text-sm font-semibold capitalize">
|
<h6 className="text-sm font-semibold capitalize">
|
||||||
|
|||||||
61
web/screens/Settings/CancelModelImportModal/index.tsx
Normal file
61
web/screens/Settings/CancelModelImportModal/index.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalTitle,
|
||||||
|
ModalFooter,
|
||||||
|
ModalClose,
|
||||||
|
Button,
|
||||||
|
} from '@janhq/uikit'
|
||||||
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import {
|
||||||
|
getImportModelStageAtom,
|
||||||
|
setImportModelStageAtom,
|
||||||
|
} from '@/hooks/useImportModel'
|
||||||
|
|
||||||
|
const CancelModelImportModal: React.FC = () => {
|
||||||
|
const importModelStage = useAtomValue(getImportModelStageAtom)
|
||||||
|
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
||||||
|
|
||||||
|
const onContinueClick = () => {
|
||||||
|
setImportModelStage('IMPORTING_MODEL')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCancelAllClick = () => {
|
||||||
|
setImportModelStage('NONE')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={importModelStage === 'CONFIRM_CANCEL'}>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Cancel Model Import?</ModalTitle>
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The model import process is not complete. Are you sure you want to
|
||||||
|
cancel all ongoing model imports? This action is irreversible and the
|
||||||
|
progress will be lost.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<div className="flex gap-x-2">
|
||||||
|
<ModalClose asChild onClick={onContinueClick}>
|
||||||
|
<Button themes="ghost">Continue</Button>
|
||||||
|
</ModalClose>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button autoFocus themes="danger" onClick={onCancelAllClick}>
|
||||||
|
Cancel All
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(CancelModelImportModal)
|
||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect, useRef } from 'react'
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
import { Button } from '@janhq/uikit'
|
import { Button, ScrollArea } from '@janhq/uikit'
|
||||||
|
|
||||||
import { formatExtensionsName } from '@/utils/converter'
|
import { formatExtensionsName } from '@/utils/converter'
|
||||||
|
|
||||||
@ -68,12 +68,13 @@ const ExtensionCatalog = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ScrollArea className="h-full w-full px-4">
|
||||||
<div className="block w-full">
|
<div className="block w-full">
|
||||||
{activeExtensions.map((item, i) => {
|
{activeExtensions.map((item, i) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none"
|
className="flex w-full items-start justify-between border-b border-border py-4 first:pt-4 last:border-none"
|
||||||
>
|
>
|
||||||
<div className="w-4/5 flex-shrink-0 space-y-1.5">
|
<div className="w-4/5 flex-shrink-0 space-y-1.5">
|
||||||
<div className="flex gap-x-2">
|
<div className="flex gap-x-2">
|
||||||
@ -120,6 +121,7 @@ const ExtensionCatalog = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
197
web/screens/Settings/EditModelInfoModal/index.tsx
Normal file
197
web/screens/Settings/EditModelInfoModal/index.tsx
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
|
import { Model, ModelEvent, events, openFileExplorer } from '@janhq/core'
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalTitle,
|
||||||
|
ModalFooter,
|
||||||
|
ModalClose,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Textarea,
|
||||||
|
} from '@janhq/uikit'
|
||||||
|
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import { Paperclip } from 'lucide-react'
|
||||||
|
|
||||||
|
import useImportModel, {
|
||||||
|
getImportModelStageAtom,
|
||||||
|
setImportModelStageAtom,
|
||||||
|
} from '@/hooks/useImportModel'
|
||||||
|
|
||||||
|
import { toGibibytes } from '@/utils/converter'
|
||||||
|
|
||||||
|
import { openFileTitle } from '@/utils/titleUtils'
|
||||||
|
|
||||||
|
import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom'
|
||||||
|
import {
|
||||||
|
importingModelsAtom,
|
||||||
|
updateImportingModelAtom,
|
||||||
|
} from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
|
export const editingModelIdAtom = atom<string | undefined>(undefined)
|
||||||
|
|
||||||
|
const EditModelInfoModal: React.FC = () => {
|
||||||
|
const importModelStage = useAtomValue(getImportModelStageAtom)
|
||||||
|
const importingModels = useAtomValue(importingModelsAtom)
|
||||||
|
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
||||||
|
const [editingModelId, setEditingModelId] = useAtom(editingModelIdAtom)
|
||||||
|
|
||||||
|
const [modelName, setModelName] = useState('')
|
||||||
|
const [modelId, setModelId] = useState('')
|
||||||
|
const [description, setDescription] = useState('')
|
||||||
|
const [tags, setTags] = useState<string[]>([])
|
||||||
|
|
||||||
|
const janDataFolder = useAtomValue(janDataFolderPathAtom)
|
||||||
|
const updateImportingModel = useSetAtom(updateImportingModelAtom)
|
||||||
|
const { updateModelInfo } = useImportModel()
|
||||||
|
|
||||||
|
const editingModel = importingModels.find(
|
||||||
|
(model) => model.importId === editingModelId
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editingModel && editingModel.modelId != null) {
|
||||||
|
setModelName(editingModel.name)
|
||||||
|
setModelId(editingModel.modelId)
|
||||||
|
setDescription(editingModel.description)
|
||||||
|
setTags(editingModel.tags)
|
||||||
|
}
|
||||||
|
}, [editingModel])
|
||||||
|
|
||||||
|
const onCancelClick = () => {
|
||||||
|
setImportModelStage('IMPORTING_MODEL')
|
||||||
|
setEditingModelId(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSaveClick = async () => {
|
||||||
|
if (!editingModel || !editingModel.modelId) return
|
||||||
|
|
||||||
|
const modelInfo: Partial<Model> = {
|
||||||
|
id: editingModel.modelId,
|
||||||
|
name: modelName,
|
||||||
|
description,
|
||||||
|
metadata: {
|
||||||
|
author: 'User',
|
||||||
|
tags,
|
||||||
|
size: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateModelInfo(modelInfo)
|
||||||
|
events.emit(ModelEvent.OnModelsUpdate, {})
|
||||||
|
updateImportingModel(editingModel.importId, modelName, description, tags)
|
||||||
|
|
||||||
|
setImportModelStage('IMPORTING_MODEL')
|
||||||
|
setEditingModelId(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelFolderPath = useMemo(() => {
|
||||||
|
return `${janDataFolder}/models/${editingModel?.modelId}`
|
||||||
|
}, [janDataFolder, editingModel])
|
||||||
|
|
||||||
|
const onShowInFinderClick = useCallback(() => {
|
||||||
|
openFileExplorer(modelFolderPath)
|
||||||
|
}, [modelFolderPath])
|
||||||
|
|
||||||
|
if (!editingModel) {
|
||||||
|
setImportModelStage('IMPORTING_MODEL')
|
||||||
|
setEditingModelId(undefined)
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={importModelStage === 'EDIT_MODEL_INFO'}>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Edit Model Information</ModalTitle>
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<div className="flex flex-row space-x-4 rounded-xl border p-4">
|
||||||
|
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-blue-400">
|
||||||
|
<Paperclip />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<p>{editingModel.name}</p>
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<span className="mr-2 text-sm text-[#71717A]">
|
||||||
|
{toGibibytes(editingModel.size)}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-semibold text-[#71717A]">
|
||||||
|
Format:{' '}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-normal text-[#71717A]">
|
||||||
|
{editingModel.format.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 flex flex-row items-center space-x-2">
|
||||||
|
<span className="line-clamp-1 text-xs font-normal text-[#71717A]">
|
||||||
|
{modelFolderPath}
|
||||||
|
</span>
|
||||||
|
<Button themes="ghost" onClick={onShowInFinderClick}>
|
||||||
|
{openFileTitle()}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form className="flex flex-col space-y-4">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="mb-1">Model Name</label>
|
||||||
|
<Input
|
||||||
|
value={modelName}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setModelName(e.target.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="mb-1">Model ID</label>
|
||||||
|
<Input
|
||||||
|
disabled
|
||||||
|
value={modelId}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setModelId(e.target.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="mb-1">Description</label>
|
||||||
|
<Textarea
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setDescription(e.target.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="mb-1">Tags</label>
|
||||||
|
<Input />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<div className="flex gap-x-2">
|
||||||
|
<ModalClose asChild onClick={onCancelClick}>
|
||||||
|
<Button themes="ghost">Cancel</Button>
|
||||||
|
</ModalClose>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button autoFocus themes="primary" onClick={onSaveClick}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditModelInfoModal
|
||||||
59
web/screens/Settings/ImportInProgressIcon/index.tsx
Normal file
59
web/screens/Settings/ImportInProgressIcon/index.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import React, { useCallback, useState } from 'react'
|
||||||
|
import { CircularProgressbar } from 'react-circular-progressbar'
|
||||||
|
|
||||||
|
import { X } from 'lucide-react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
percentage: number
|
||||||
|
onDeleteModelClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImportInProgressIcon: React.FC<Props> = ({
|
||||||
|
percentage,
|
||||||
|
onDeleteModelClick,
|
||||||
|
}) => {
|
||||||
|
const [isHovered, setIsHovered] = useState(false)
|
||||||
|
|
||||||
|
const onMouseOver = () => {
|
||||||
|
setIsHovered(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseOut = () => {
|
||||||
|
setIsHovered(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div onMouseOver={onMouseOver} onMouseOut={onMouseOut}>
|
||||||
|
{isHovered ? (
|
||||||
|
<DeleteIcon onDeleteModelClick={onDeleteModelClick} />
|
||||||
|
) : (
|
||||||
|
<ProgressIcon percentage={percentage} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProgressIcon: React.FC<Partial<Props>> = ({ percentage }) => (
|
||||||
|
<div className="h-8 w-8 rounded-full">
|
||||||
|
<CircularProgressbar value={(percentage ?? 0) * 100} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const DeleteIcon: React.FC<Partial<Props>> = React.memo(
|
||||||
|
({ onDeleteModelClick }) => {
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
onDeleteModelClick?.()
|
||||||
|
}, [onDeleteModelClick])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg bg-gray-100"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<X />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default ImportInProgressIcon
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import { ModelImportOption, OptionType } from '@janhq/core'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
option: ModelImportOption
|
||||||
|
checked: boolean
|
||||||
|
setSelectedOptionType: (type: OptionType) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImportModelOptionSelection: React.FC<Props> = ({
|
||||||
|
option,
|
||||||
|
checked,
|
||||||
|
setSelectedOptionType,
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer flex-row"
|
||||||
|
onClick={() => setSelectedOptionType(option.type)}
|
||||||
|
>
|
||||||
|
<div className="flex h-5 w-5 items-center justify-center rounded-full border border-[#2563EB]">
|
||||||
|
{checked && <div className="h-2 w-2 rounded-full bg-primary" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ml-2 flex-1">
|
||||||
|
<p className="mb-2 text-sm font-medium">{option.title}</p>
|
||||||
|
<p className="text-sm font-normal text-[#71717A]">{option.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default ImportModelOptionSelection
|
||||||
105
web/screens/Settings/ImportModelOptionModal/index.tsx
Normal file
105
web/screens/Settings/ImportModelOptionModal/index.tsx
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import React, { useCallback, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import { ModelImportOption } from '@janhq/core'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
ModalClose,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalTitle,
|
||||||
|
} from '@janhq/uikit'
|
||||||
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import useImportModel, {
|
||||||
|
getImportModelStageAtom,
|
||||||
|
setImportModelStageAtom,
|
||||||
|
} from '@/hooks/useImportModel'
|
||||||
|
|
||||||
|
import ImportModelOptionSelection from './ImportModelOptionSelection'
|
||||||
|
|
||||||
|
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
|
const importOptions: ModelImportOption[] = [
|
||||||
|
{
|
||||||
|
type: 'SYMLINK',
|
||||||
|
title: 'Keep Original Files & Symlink',
|
||||||
|
description:
|
||||||
|
'You maintain your model files outside of Jan. Keeping your files where they are, and Jan will create a smart link to them.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'MOVE_BINARY_FILE',
|
||||||
|
title: 'Move model binary file',
|
||||||
|
description:
|
||||||
|
'Jan will move your model binary file from your current folder into Jan Data Folder.',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const ImportModelOptionModal: React.FC = () => {
|
||||||
|
const importingModels = useAtomValue(importingModelsAtom)
|
||||||
|
const importStage = useAtomValue(getImportModelStageAtom)
|
||||||
|
const setImportStage = useSetAtom(setImportModelStageAtom)
|
||||||
|
const { importModels } = useImportModel()
|
||||||
|
|
||||||
|
const [importOption, setImportOption] = useState(importOptions[0])
|
||||||
|
const destinationModal = useRef<'NONE' | 'IMPORTING_MODEL'>('NONE')
|
||||||
|
|
||||||
|
const onCancelClick = useCallback(() => {
|
||||||
|
setImportStage('NONE')
|
||||||
|
}, [setImportStage])
|
||||||
|
|
||||||
|
const onContinueClick = useCallback(() => {
|
||||||
|
importModels(importingModels, importOption.type)
|
||||||
|
setImportStage('IMPORTING_MODEL')
|
||||||
|
}, [importingModels, importOption, setImportStage, importModels])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={importStage === 'MODEL_SELECTED'}
|
||||||
|
onOpenChange={() => {
|
||||||
|
if (destinationModal.current === 'NONE') {
|
||||||
|
setImportStage('NONE')
|
||||||
|
} else {
|
||||||
|
onContinueClick()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>How would you like Jan to handle your models?</ModalTitle>
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
{importOptions.map((option) => (
|
||||||
|
<ImportModelOptionSelection
|
||||||
|
key={option.type}
|
||||||
|
option={option}
|
||||||
|
checked={importOption.type === option.type}
|
||||||
|
setSelectedOptionType={() => setImportOption(option)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<div className="flex gap-x-2">
|
||||||
|
<ModalClose asChild onClick={onCancelClick}>
|
||||||
|
<Button themes="ghost">Cancel</Button>
|
||||||
|
</ModalClose>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button
|
||||||
|
autoFocus
|
||||||
|
themes="primary"
|
||||||
|
onClick={() => {
|
||||||
|
destinationModal.current = 'IMPORTING_MODEL'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Continue Importing
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImportModelOptionModal
|
||||||
52
web/screens/Settings/ImportSuccessIcon/index.tsx
Normal file
52
web/screens/Settings/ImportSuccessIcon/index.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React, { useCallback, useState } from 'react'
|
||||||
|
|
||||||
|
import { Check, Pencil } from 'lucide-react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onEditModelClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImportSuccessIcon: React.FC<Props> = ({ onEditModelClick }) => {
|
||||||
|
const [isHovered, setIsHovered] = useState(false)
|
||||||
|
|
||||||
|
const onMouseOver = () => {
|
||||||
|
setIsHovered(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseOut = () => {
|
||||||
|
setIsHovered(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div onMouseOver={onMouseOver} onMouseOut={onMouseOut}>
|
||||||
|
{isHovered ? (
|
||||||
|
<EditIcon onEditModelClick={onEditModelClick} />
|
||||||
|
) : (
|
||||||
|
<SuccessIcon />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SuccessIcon: React.FC = React.memo(() => (
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary">
|
||||||
|
<Check color="#FFF" />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
|
||||||
|
const EditIcon: React.FC<Props> = React.memo(({ onEditModelClick }) => {
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
onEditModelClick()
|
||||||
|
}, [onEditModelClick])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg bg-gray-100"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<Pencil />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default ImportSuccessIcon
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
import { ImportingModel } from '@janhq/core/.'
|
||||||
|
import { useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import { setImportModelStageAtom } from '@/hooks/useImportModel'
|
||||||
|
|
||||||
|
import { toGibibytes } from '@/utils/converter'
|
||||||
|
|
||||||
|
import { editingModelIdAtom } from '../EditModelInfoModal'
|
||||||
|
import ImportInProgressIcon from '../ImportInProgressIcon'
|
||||||
|
import ImportSuccessIcon from '../ImportSuccessIcon'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
model: ImportingModel
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImportingModelItem: React.FC<Props> = ({ model }) => {
|
||||||
|
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
||||||
|
const setEditingModelId = useSetAtom(editingModelIdAtom)
|
||||||
|
const sizeInGb = toGibibytes(model.size)
|
||||||
|
|
||||||
|
const onEditModelInfoClick = () => {
|
||||||
|
setEditingModelId(model.importId)
|
||||||
|
setImportModelStage('EDIT_MODEL_INFO')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDeleteModelClick = () => {}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-row items-center space-x-3 rounded-lg border px-4 py-3">
|
||||||
|
<p className="line-clamp-1 flex-1">{model.name}</p>
|
||||||
|
<p>{sizeInGb}</p>
|
||||||
|
|
||||||
|
{model.status === 'IMPORTED' || model.status === 'FAILED' ? (
|
||||||
|
<ImportSuccessIcon onEditModelClick={onEditModelInfoClick} />
|
||||||
|
) : (
|
||||||
|
<ImportInProgressIcon
|
||||||
|
percentage={model.percentage ?? 0}
|
||||||
|
onDeleteModelClick={onDeleteModelClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImportingModelItem
|
||||||
85
web/screens/Settings/ImportingModelModal/index.tsx
Normal file
85
web/screens/Settings/ImportingModelModal/index.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { useCallback, useMemo } from 'react'
|
||||||
|
|
||||||
|
import { openFileExplorer } from '@janhq/core'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalTitle,
|
||||||
|
} from '@janhq/uikit'
|
||||||
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import { AlertCircle } from 'lucide-react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
getImportModelStageAtom,
|
||||||
|
setImportModelStageAtom,
|
||||||
|
} from '@/hooks/useImportModel'
|
||||||
|
|
||||||
|
import { openFileTitle } from '@/utils/titleUtils'
|
||||||
|
|
||||||
|
import ImportingModelItem from './ImportingModelItem'
|
||||||
|
|
||||||
|
import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom'
|
||||||
|
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
|
const ImportingModelModal: React.FC = () => {
|
||||||
|
const importingModels = useAtomValue(importingModelsAtom)
|
||||||
|
const importModelStage = useAtomValue(getImportModelStageAtom)
|
||||||
|
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
||||||
|
const janDataFolder = useAtomValue(janDataFolderPathAtom)
|
||||||
|
|
||||||
|
const modelFolder = useMemo(() => `${janDataFolder}/models`, [janDataFolder])
|
||||||
|
|
||||||
|
const finishedImportModel = importingModels.filter(
|
||||||
|
(model) => model.status === 'IMPORTED'
|
||||||
|
).length
|
||||||
|
|
||||||
|
const onOpenModelFolderClick = useCallback(() => {
|
||||||
|
openFileExplorer(modelFolder)
|
||||||
|
}, [modelFolder])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={importModelStage === 'IMPORTING_MODEL'}
|
||||||
|
onOpenChange={() => {
|
||||||
|
setImportModelStage('NONE')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>
|
||||||
|
Importing model ({finishedImportModel}/{importingModels.length})
|
||||||
|
</ModalTitle>
|
||||||
|
<div className="flex flex-row items-center space-x-2">
|
||||||
|
<label className="text-xs text-[#71717A]">{modelFolder}</label>
|
||||||
|
<Button
|
||||||
|
themes="ghost"
|
||||||
|
className="text-blue-500"
|
||||||
|
onClick={onOpenModelFolderClick}
|
||||||
|
>
|
||||||
|
{openFileTitle()}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{importingModels.map((model) => (
|
||||||
|
<ImportingModelItem key={model.importId} model={model} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<ModalFooter className="mx-[-16px] mb-[-16px] flex flex-row rounded-b-lg bg-[#F4F4F5] px-2 py-2 ">
|
||||||
|
<AlertCircle size={20} />
|
||||||
|
<p className="text-sm font-semibold text-[#71717A]">
|
||||||
|
Own your model configurations, use at your own risk.
|
||||||
|
Misconfigurations may result in lower quality or unexpected outputs.{' '}
|
||||||
|
</p>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImportingModelModal
|
||||||
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