Merge pull request #2185 from janhq/dev
docs: Sync dev branch to docs branch
This commit is contained in:
commit
8e3b472b82
23
README.md
23
README.md
@ -43,31 +43,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
|
|||||||
<tr style="text-align:center">
|
<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-290.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-290.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-290.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-290.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-290.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',
|
||||||
|
onLocalImportModelFailed = 'onLocalImportModelFailed',
|
||||||
|
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),
|
||||||
|
]
|
||||||
|
|||||||
@ -65,7 +65,7 @@ const joinPath: (paths: string[]) => Promise<string> = (paths) => global.core.ap
|
|||||||
* @param path - The path to retrieve.
|
* @param path - The path to retrieve.
|
||||||
* @returns {Promise<string>} A promise that resolves with the basename.
|
* @returns {Promise<string>} A promise that resolves with the basename.
|
||||||
*/
|
*/
|
||||||
const baseName: (paths: string[]) => Promise<string> = (path) => global.core.api?.baseName(path)
|
const baseName: (paths: string) => Promise<string> = (path) => global.core.api?.baseName(path)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens an external URL in the default web browser.
|
* Opens an external URL in the default web browser.
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
23
core/src/types/model/modelImport.ts
Normal file
23
core/src/types/model/modelImport.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export type OptionType = 'SYMLINK' | 'MOVE_BINARY_FILE'
|
||||||
|
|
||||||
|
export type ModelImportOption = {
|
||||||
|
type: OptionType
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ImportingModelStatus = 'PREPARING' | 'IMPORTING' | 'IMPORTED' | 'FAILED'
|
||||||
|
|
||||||
|
export type ImportingModel = {
|
||||||
|
importId: string
|
||||||
|
modelId: string | undefined
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
path: string
|
||||||
|
tags: string[]
|
||||||
|
size: number
|
||||||
|
status: ImportingModelStatus
|
||||||
|
format: string
|
||||||
|
percentage?: number
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
26
docs/docs/acknowledgements.md
Normal file
26
docs/docs/acknowledgements.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: Acknowledgements
|
||||||
|
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
|
||||||
|
slug: /acknowledgements
|
||||||
|
keywords:
|
||||||
|
[
|
||||||
|
Jan AI,
|
||||||
|
Jan,
|
||||||
|
ChatGPT alternative,
|
||||||
|
local AI,
|
||||||
|
private AI,
|
||||||
|
conversational AI,
|
||||||
|
no-subscription fee,
|
||||||
|
large language model,
|
||||||
|
acknowledgements,
|
||||||
|
third-party libraries,
|
||||||
|
]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Acknowledgements
|
||||||
|
|
||||||
|
We would like to express our gratitude to the following third-party libraries that have made the development of Jan possible.
|
||||||
|
|
||||||
|
- [llama.cpp](https://github.com/ggerganov/llama.cpp/blob/master/LICENSE)
|
||||||
|
- [LangChain.js](https://github.com/langchain-ai/langchainjs/blob/main/LICENSE)
|
||||||
|
- [TensorRT](https://github.com/NVIDIA/TensorRT/blob/main/LICENSE)
|
||||||
@ -29,3 +29,18 @@ keywords:
|
|||||||
## Careers
|
## Careers
|
||||||
|
|
||||||
- [Jobs](https://janai.bamboohr.com/careers)
|
- [Jobs](https://janai.bamboohr.com/careers)
|
||||||
|
|
||||||
|
## Newsletter
|
||||||
|
|
||||||
|
<iframe
|
||||||
|
width="100%"
|
||||||
|
height="600px"
|
||||||
|
src="https://c0c7c086.sibforms.com/serve/MUIFAEWm49nC1OONIibGnlV44yxPMw6Fu1Yc8pK7nP3jp7rZ6rvrb5uOmCD8IIhrRj6-h-_AYrw-sz7JNpcUZ8LAAZoUIOjGmSvNWHwoFhxX5lb-38-fxXj933yIdGzEMBZJv4Nu2BqC2A4uThDGmjM-n_DZBV1v_mKbTcVUWVUE7VutWhRqrDr69IWI4SgbuIMACkcTiWX8ZNLw"
|
||||||
|
frameborder="0"
|
||||||
|
scrolling="auto"
|
||||||
|
allowfullscreen
|
||||||
|
style={{
|
||||||
|
margin: 'auto',
|
||||||
|
maxWidth: '100%',
|
||||||
|
}}
|
||||||
|
></iframe>
|
||||||
@ -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-fs 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.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|||||||
@ -1,3 +1,95 @@
|
|||||||
---
|
---
|
||||||
title: Wall of Love ❤️
|
title: Wall of Love ❤️
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Twitter
|
||||||
|
|
||||||
|
Check out our amazing users and what they are saying about Jan!
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">I can confirm <a href="https://t.co/Hvrfp0iaf9">https://t.co/Hvrfp0iaf9</a> is awesome 👌</p>— Cristian (@cristianmoreno) <a href="https://twitter.com/cristianmoreno/status/1757504717519749292?ref_src=twsrc%5Etfw">February 13, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8" loading="lazy"></script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">downloaded this a few weeks ago. amazed by the speed and quality</p>— siddharth (@siddharthd01) <a href="https://twitter.com/siddharthd01/status/1757500111629025788?ref_src=twsrc%5Etfw">February 13, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8" loading="lazy"></script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Anyone else out there running LLMs on steam deck? <a href="https://twitter.com/janframework?ref_src=twsrc%5Etfw">@janframework</a> bringing nerd dreams to life! <a href="https://t.co/7XpnBmc8MN">pic.twitter.com/7XpnBmc8MN</a></p>— crossdefault (@crossdefault) <a href="https://twitter.com/crossdefault/status/1750801065132384302?ref_src=twsrc%5Etfw">January 26, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8" loading="lazy"></script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">If you are like me, always wanting your own ChatGPT and have sufficient coding knowledge, you would watch open sourced <a href="https://twitter.com/janframework?ref_src=twsrc%5Etfw">@janframework</a> by <a href="https://twitter.com/0xSage?ref_src=twsrc%5Etfw">@0xSage</a> like a "my-own-ai" hawk<br></br>Still under development, the architecture is really futuristic. The desktop app for Windows, Mac, Linux are… <a href="https://t.co/0HrNquhBsL">pic.twitter.com/0HrNquhBsL</a></p>— Umesh = EG = Educated Guess - NGI doing AI (@trading_indian) <a href="https://twitter.com/trading_indian/status/1745560583548670250?ref_src=twsrc%5Etfw">January 11, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8" loading="lazy"></script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">came across <a href="https://twitter.com/janframework?ref_src=twsrc%5Etfw">@janframework</a> yesterday and it's my fav native Apple Silicon LLM app yet. Love that I can switch to GPT 4 API and offline LLM models seamlessly. Looks promising! <a href="https://t.co/gyOX9gHbKQ">https://t.co/gyOX9gHbKQ</a></p>— Keith Hawkins (@kph_practice) <a href="https://twitter.com/kph_practice/status/1744729548074459310?ref_src=twsrc%5Etfw">January 9, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8" loading="lazy"></script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">i just ran some ai models locally on my laptop using @janhq_ and can't believe how easy and cool it is. so, now i can have the same experience as with ChatGPT, but offline and without any data concerns</p>— Sergey Kaplich (@sergey_kaplich) <a href="https://twitter.com/sergey_kaplich/status/1742993414986068423?ref_src=twsrc%5Etfw">January 4, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8" loading="lazy"></script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<blockquote class="twitter-tweet"><p lang="en" dir="ltr"><a href="https://t.co/scBqJ3kIzj">https://t.co/scBqJ3kIzj</a> Great way to try open source all models, like Mixtral8x7b offline. Love to see</p>— Chubby♨️ (@kimmonismus) <a href="https://twitter.com/kimmonismus/status/1742843063938994469?ref_src=twsrc%5Etfw">January 4, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8" loading="lazy"></script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Please share your love for Jan on Twitter and tag us [@janframework](https://twitter.com/janframework)! We would love to hear from you!
|
||||||
|
|
||||||
|
## YouTube
|
||||||
|
|
||||||
|
Watch these amazing videos to see how Jan is being used and loved by the community!
|
||||||
|
|
||||||
|
### Run Any Chatbot FREE Locally on Your Computer
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<iframe width="100%" height="600" src="https://www.youtube.com/embed/zkafOIyQM8s" title="Run Any Chatbot FREE Locally on Your Computer" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
### Jan AI: Run Open Source LLM 100% Local with OpenAI endpoints
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<iframe width="100%" height="705" src="https://www.youtube.com/embed/9ta2S425Zu8" title="Jan AI: Run Open Source LLM 100% Local with OpenAI endpoints" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
### Setup Tutorial on Jan.ai. JAN AI: Run open source LLM on local Windows PC. 100% offline LLM and AI.
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<iframe width="100%" height="705" src="https://www.youtube.com/embed/ZCiEQVOjH5U" title="Setup Tutorial on Jan.ai. JAN AI: Run open source LLM on local Windows PC. 100% offline LLM and AI." frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
### Jan.ai: Like Offline ChatGPT on Your Computer 💡
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<iframe width="100%" height="600" src="https://www.youtube.com/embed/ES021_sY6WQ" title="Jan.ai: Like Offline ChatGPT on Your Computer 💡" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
### Jan: Bring AI to your Desktop With 100% Offline AI
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<iframe width="100%" height="600" src="https://www.youtube.com/embed/QpMQgJL4AZA" title="Jan: Bring AI to your Desktop With 100% Offline AI" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
### AI on Your Local PC: Install JanAI (ChatGPT alternative) for Enhanced Privacy
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<iframe width="100%" height="600" src="https://www.youtube.com/embed/CbJGxNmdWws" title="AI on Your Local PC: Install JanAI (ChatGPT alternative) for Enhanced Privacy" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
### Install Jan to Run LLM Offline and Local First
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<iframe width="100%" height="600" src="https://www.youtube.com/embed/7JpzE-_cKo4" title="Install Jan to Run LLM Offline and Local First" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
|||||||
@ -51,6 +51,7 @@ const sidebars = {
|
|||||||
"how-we-work/website-docs/website-docs",
|
"how-we-work/website-docs/website-docs",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
"acknowledgements",
|
||||||
],
|
],
|
||||||
productSidebar: [
|
productSidebar: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -86,6 +86,10 @@ const menus = [
|
|||||||
path: "https://janai.bamboohr.com/careers",
|
path: "https://janai.bamboohr.com/careers",
|
||||||
external: true,
|
external: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
menu: "Newsletter",
|
||||||
|
path: "/community#newsletter",
|
||||||
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -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,10 @@ import {
|
|||||||
DownloadRoute,
|
DownloadRoute,
|
||||||
ModelEvent,
|
ModelEvent,
|
||||||
DownloadState,
|
DownloadState,
|
||||||
|
OptionType,
|
||||||
|
ImportingModel,
|
||||||
|
LocalImportModelEvent,
|
||||||
|
baseName,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
|
|
||||||
import { extractFileName } from './helpers/path'
|
import { extractFileName } from './helpers/path'
|
||||||
@ -158,18 +162,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 +184,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 +407,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 +473,189 @@ export default class JanModelExtension extends ModelExtension {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async importModelSymlink(
|
||||||
|
modelBinaryPath: string,
|
||||||
|
modelFolderName: string,
|
||||||
|
modelFolderPath: string
|
||||||
|
): Promise<Model> {
|
||||||
|
const fileStats = await fs.fileStat(modelBinaryPath, true)
|
||||||
|
const binaryFileSize = fileStats.size
|
||||||
|
|
||||||
|
// Just need to generate model.json there
|
||||||
|
const defaultModel = (await this.getDefaultModel()) as Model
|
||||||
|
if (!defaultModel) {
|
||||||
|
console.error('Unable to find default model')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const binaryFileName = await baseName(modelBinaryPath)
|
||||||
|
|
||||||
|
const model: Model = {
|
||||||
|
...defaultModel,
|
||||||
|
id: modelFolderName,
|
||||||
|
name: modelFolderName,
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
url: modelBinaryPath,
|
||||||
|
filename: binaryFileName,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
...defaultModel.settings,
|
||||||
|
llama_model_path: binaryFileName,
|
||||||
|
},
|
||||||
|
created: Date.now(),
|
||||||
|
description: '',
|
||||||
|
metadata: {
|
||||||
|
size: binaryFileSize,
|
||||||
|
author: 'User',
|
||||||
|
tags: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelFilePath = await joinPath([
|
||||||
|
modelFolderPath,
|
||||||
|
JanModelExtension._modelMetadataFileName,
|
||||||
|
])
|
||||||
|
|
||||||
|
await fs.writeFileSync(modelFilePath, JSON.stringify(model, null, 2))
|
||||||
|
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateModelInfo(modelInfo: Partial<Model>): Promise<Model> {
|
||||||
|
const modelId = modelInfo.id
|
||||||
|
if (modelInfo.id == null) throw new Error('Model ID is required')
|
||||||
|
|
||||||
|
const janDataFolderPath = await getJanDataFolderPath()
|
||||||
|
const jsonFilePath = await joinPath([
|
||||||
|
janDataFolderPath,
|
||||||
|
'models',
|
||||||
|
modelId,
|
||||||
|
JanModelExtension._modelMetadataFileName,
|
||||||
|
])
|
||||||
|
const model = JSON.parse(
|
||||||
|
await this.readModelMetadata(jsonFilePath)
|
||||||
|
) as Model
|
||||||
|
|
||||||
|
const updatedModel: Model = {
|
||||||
|
...model,
|
||||||
|
...modelInfo,
|
||||||
|
metadata: {
|
||||||
|
...model.metadata,
|
||||||
|
tags: modelInfo.metadata?.tags ?? [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFileSync(jsonFilePath, JSON.stringify(updatedModel, null, 2))
|
||||||
|
return updatedModel
|
||||||
|
}
|
||||||
|
|
||||||
|
private async importModel(
|
||||||
|
model: ImportingModel,
|
||||||
|
optionType: OptionType
|
||||||
|
): Promise<Model> {
|
||||||
|
const binaryName = (await baseName(model.path)).replace(/\s/g, '')
|
||||||
|
|
||||||
|
let modelFolderName = binaryName
|
||||||
|
if (binaryName.endsWith(JanModelExtension._supportedModelFormat)) {
|
||||||
|
modelFolderName = binaryName.replace(
|
||||||
|
JanModelExtension._supportedModelFormat,
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelFolderPath = await this.getModelFolderName(modelFolderName)
|
||||||
|
await fs.mkdirSync(modelFolderPath)
|
||||||
|
|
||||||
|
const uniqueFolderName = await baseName(modelFolderPath)
|
||||||
|
const modelBinaryFile = binaryName.endsWith(
|
||||||
|
JanModelExtension._supportedModelFormat
|
||||||
|
)
|
||||||
|
? binaryName
|
||||||
|
: `${binaryName}${JanModelExtension._supportedModelFormat}`
|
||||||
|
|
||||||
|
const binaryPath = await joinPath([modelFolderPath, modelBinaryFile])
|
||||||
|
|
||||||
|
if (optionType === 'SYMLINK') {
|
||||||
|
return this.importModelSymlink(
|
||||||
|
model.path,
|
||||||
|
uniqueFolderName,
|
||||||
|
modelFolderPath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const srcStat = await fs.fileStat(model.path, true)
|
||||||
|
|
||||||
|
// interval getting the file size to calculate the percentage
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
const destStats = await fs.fileStat(binaryPath, true)
|
||||||
|
const percentage = destStats.size / srcStat.size
|
||||||
|
events.emit(LocalImportModelEvent.onLocalImportModelUpdate, {
|
||||||
|
...model,
|
||||||
|
percentage,
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
await fs.copyFile(model.path, binaryPath)
|
||||||
|
|
||||||
|
clearInterval(interval)
|
||||||
|
|
||||||
|
// generate model json
|
||||||
|
return this.generateModelMetadata(uniqueFolderName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getModelFolderName(
|
||||||
|
modelFolderName: string,
|
||||||
|
count?: number
|
||||||
|
): Promise<string> {
|
||||||
|
const newModelFolderName = count
|
||||||
|
? `${modelFolderName}-${count}`
|
||||||
|
: modelFolderName
|
||||||
|
|
||||||
|
const janDataFolderPath = await getJanDataFolderPath()
|
||||||
|
const modelFolderPath = await joinPath([
|
||||||
|
janDataFolderPath,
|
||||||
|
'models',
|
||||||
|
newModelFolderName,
|
||||||
|
])
|
||||||
|
|
||||||
|
const isFolderExist = await fs.existsSync(modelFolderPath)
|
||||||
|
if (!isFolderExist) {
|
||||||
|
return modelFolderPath
|
||||||
|
} else {
|
||||||
|
const newCount = (count ?? 0) + 1
|
||||||
|
return this.getModelFolderName(modelFolderName, newCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async importModels(
|
||||||
|
models: ImportingModel[],
|
||||||
|
optionType: OptionType
|
||||||
|
): Promise<void> {
|
||||||
|
const importedModels: Model[] = []
|
||||||
|
|
||||||
|
for (const model of models) {
|
||||||
|
events.emit(LocalImportModelEvent.onLocalImportModelUpdate, model)
|
||||||
|
try {
|
||||||
|
const importedModel = await this.importModel(model, optionType)
|
||||||
|
events.emit(LocalImportModelEvent.onLocalImportModelSuccess, {
|
||||||
|
...model,
|
||||||
|
modelId: importedModel.id,
|
||||||
|
})
|
||||||
|
importedModels.push(importedModel)
|
||||||
|
} catch (err) {
|
||||||
|
events.emit(LocalImportModelEvent.onLocalImportModelFailed, {
|
||||||
|
...model,
|
||||||
|
error: err,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
events.emit(
|
||||||
|
LocalImportModelEvent.onLocalImportModelFinished,
|
||||||
|
importedModels
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export const s3 = (req: any, reply: any, done: any) => {
|
|||||||
reply.status(200).send(result)
|
reply.status(200).send(result)
|
||||||
return
|
return
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
console.log(ex)
|
console.error(ex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,11 +5,11 @@
|
|||||||
@apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400;
|
@apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400;
|
||||||
|
|
||||||
&-primary {
|
&-primary {
|
||||||
@apply bg-primary hover:bg-primary/90 text-white;
|
@apply bg-blue-600 text-white hover:bg-blue-600/90;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-secondary-blue {
|
&-secondary-blue {
|
||||||
@apply bg-blue-200 text-blue-600 hover:bg-blue-300/50 dark:hover:bg-blue-200/80;
|
@apply bg-blue-200 text-blue-600 hover:bg-blue-300/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-danger {
|
&-danger {
|
||||||
@ -17,7 +17,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-secondary-danger {
|
&-secondary-danger {
|
||||||
@apply bg-red-200 text-red-600 hover:bg-red-300/50 dark:hover:bg-red-200/80;
|
@apply bg-red-200 text-red-600 hover:bg-red-300/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-outline {
|
&-outline {
|
||||||
@ -66,7 +66,7 @@
|
|||||||
[type='reset'],
|
[type='reset'],
|
||||||
[type='submit'] {
|
[type='submit'] {
|
||||||
&.btn-primary {
|
&.btn-primary {
|
||||||
@apply bg-primary hover:bg-primary/90;
|
@apply bg-blue-600 hover:bg-blue-600/90;
|
||||||
@apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400;
|
@apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400;
|
||||||
}
|
}
|
||||||
&.btn-secondary {
|
&.btn-secondary {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
.checkbox {
|
.checkbox {
|
||||||
@apply border-border data-[state=checked]:bg-primary h-5 w-5 flex-shrink-0 rounded-md border data-[state=checked]:text-white;
|
@apply border-border h-5 w-5 flex-shrink-0 rounded-md border data-[state=checked]:bg-blue-600 data-[state=checked]:text-white;
|
||||||
|
|
||||||
&--icon {
|
&--icon {
|
||||||
@apply h-4 w-4;
|
@apply h-4 w-4;
|
||||||
|
|||||||
66
uikit/src/circular-progress/styles.scss
Normal file
66
uikit/src/circular-progress/styles.scss
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* react-circular-progressbar styles
|
||||||
|
* All of the styles in this file are configurable!
|
||||||
|
*/
|
||||||
|
|
||||||
|
.CircularProgressbar {
|
||||||
|
/*
|
||||||
|
* This fixes an issue where the CircularProgressbar svg has
|
||||||
|
* 0 width inside a "display: flex" container, and thus not visible.
|
||||||
|
*/
|
||||||
|
width: 100%;
|
||||||
|
/*
|
||||||
|
* This fixes a centering issue with CircularProgressbarWithChildren:
|
||||||
|
* https://github.com/kevinsqi/react-circular-progressbar/issues/94
|
||||||
|
*/
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CircularProgressbar .CircularProgressbar-path {
|
||||||
|
stroke: #3e98c7;
|
||||||
|
stroke-linecap: round;
|
||||||
|
transition: stroke-dashoffset 0.5s ease 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CircularProgressbar .CircularProgressbar-trail {
|
||||||
|
stroke: #d6d6d6;
|
||||||
|
/* Used when trail is not full diameter, i.e. when props.circleRatio is set */
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CircularProgressbar .CircularProgressbar-text {
|
||||||
|
fill: #3e98c7;
|
||||||
|
font-size: 20px;
|
||||||
|
dominant-baseline: middle;
|
||||||
|
text-anchor: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CircularProgressbar .CircularProgressbar-background {
|
||||||
|
fill: #d6d6d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Sample background styles. Use these with e.g.:
|
||||||
|
*
|
||||||
|
* <CircularProgressbar
|
||||||
|
* className="CircularProgressbar-inverted"
|
||||||
|
* background
|
||||||
|
* percentage={50}
|
||||||
|
* />
|
||||||
|
*/
|
||||||
|
.CircularProgressbar.CircularProgressbar-inverted
|
||||||
|
.CircularProgressbar-background {
|
||||||
|
fill: #3e98c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CircularProgressbar.CircularProgressbar-inverted .CircularProgressbar-text {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CircularProgressbar.CircularProgressbar-inverted .CircularProgressbar-path {
|
||||||
|
stroke: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CircularProgressbar.CircularProgressbar-inverted .CircularProgressbar-trail {
|
||||||
|
stroke: transparent;
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
.input {
|
.input {
|
||||||
@apply border-border placeholder:text-muted-foreground flex h-9 w-full rounded-lg border bg-transparent px-3 py-1 transition-colors;
|
@apply border-border placeholder:text-muted-foreground flex h-9 w-full rounded-lg border bg-transparent px-3 py-1 transition-colors;
|
||||||
@apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100 disabled:dark:bg-zinc-800 disabled:dark:text-zinc-600;
|
@apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100;
|
||||||
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1;
|
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1;
|
||||||
@apply file:border-0 file:bg-transparent file:font-medium;
|
@apply file:border-0 file:bg-transparent file:font-medium;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
.progress {
|
.progress {
|
||||||
@apply bg-secondary relative h-4 w-full overflow-hidden rounded-full;
|
@apply relative h-4 w-full overflow-hidden rounded-full bg-gray-100;
|
||||||
|
|
||||||
&-indicator {
|
&-indicator {
|
||||||
@apply bg-primary h-full w-full flex-1 transition-all;
|
@apply h-full w-full flex-1 bg-blue-600 transition-all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
.select {
|
.select {
|
||||||
@apply placeholder:text-muted-foreground border-border flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm disabled:cursor-not-allowed [&>span]:line-clamp-1;
|
@apply placeholder:text-muted-foreground border-border flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm disabled:cursor-not-allowed [&>span]:line-clamp-1;
|
||||||
@apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100 disabled:dark:bg-zinc-800 disabled:dark:text-zinc-600;
|
@apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100;
|
||||||
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1;
|
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1;
|
||||||
|
|
||||||
&-caret {
|
&-caret {
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
@apply relative flex w-full touch-none select-none items-center;
|
@apply relative flex w-full touch-none select-none items-center;
|
||||||
|
|
||||||
&-track {
|
&-track {
|
||||||
@apply relative h-1.5 w-full grow overflow-hidden rounded-full bg-gray-200 dark:bg-gray-800;
|
@apply relative h-1.5 w-full grow overflow-hidden rounded-full bg-gray-200;
|
||||||
[data-disabled] {
|
[data-disabled] {
|
||||||
@apply cursor-not-allowed opacity-50;
|
@apply cursor-not-allowed opacity-50;
|
||||||
}
|
}
|
||||||
@ -13,6 +13,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-thumb {
|
&-thumb {
|
||||||
@apply border-primary/50 bg-background focus-visible:ring-ring block h-4 w-4 rounded-full border shadow transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50;
|
@apply bg-background focus-visible:ring-ring block h-4 w-4 rounded-full border border-blue-600/50 shadow transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
.switch {
|
.switch {
|
||||||
@apply inline-flex h-[20px] w-[36px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent;
|
@apply inline-flex h-[20px] w-[36px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent;
|
||||||
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
|
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
|
||||||
@apply data-[state=checked]:bg-primary data-[state=unchecked]:bg-input;
|
@apply data-[state=unchecked]:bg-input data-[state=checked]:bg-blue-600;
|
||||||
@apply disabled:cursor-not-allowed disabled:opacity-50;
|
@apply disabled:cursor-not-allowed disabled:opacity-50;
|
||||||
|
|
||||||
&-toggle {
|
&-toggle {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
.tooltip {
|
.tooltip {
|
||||||
@apply dark:bg-input dark:text-foreground z-50 overflow-hidden rounded-md bg-gray-950 px-2 py-1.5 text-xs font-medium text-gray-200 shadow-md;
|
@apply z-50 overflow-hidden rounded-md bg-gray-950 px-2 py-1.5 text-xs font-medium text-gray-200 shadow-md;
|
||||||
&-arrow {
|
&-arrow {
|
||||||
@apply dark:fill-input fill-gray-950;
|
@apply fill-gray-950;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
web/.prettierrc
Normal file
8
web/.prettierrc
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"quoteProps": "consistent",
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"endOfLine": "auto",
|
||||||
|
"plugins": ["prettier-plugin-tailwindcss"]
|
||||||
|
}
|
||||||
@ -15,7 +15,7 @@ export const metadata: Metadata = {
|
|||||||
export default function RootLayout({ children }: PropsWithChildren) {
|
export default function RootLayout({ children }: PropsWithChildren) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body className="bg-white font-sans text-sm antialiased dark:bg-background">
|
<body className="bg-white font-sans text-sm antialiased">
|
||||||
<div className="title-bar" />
|
<div className="title-bar" />
|
||||||
<Providers>{children}</Providers>
|
<Providers>{children}</Providers>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -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,14 +38,14 @@ 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])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'flex w-full flex-col border-t border-border bg-zinc-100 dark:bg-zinc-900',
|
'flex w-full flex-col border-t border-border bg-zinc-100',
|
||||||
asChild ? 'rounded-lg border' : 'border-t'
|
asChild ? 'rounded-lg border' : 'border-t'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -61,7 +61,7 @@ export default function CardSidebar({
|
|||||||
if (!children) return
|
if (!children) return
|
||||||
setShow(!show)
|
setShow(!show)
|
||||||
}}
|
}}
|
||||||
className="flex w-full flex-1 items-center space-x-2 rounded-lg bg-zinc-100 py-2 pr-2 dark:bg-zinc-900"
|
className="flex w-full flex-1 items-center space-x-2 rounded-lg bg-zinc-100 py-2 pr-2"
|
||||||
>
|
>
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
@ -79,7 +79,7 @@ export default function CardSidebar({
|
|||||||
{!hideMoreVerticalAction && (
|
{!hideMoreVerticalAction && (
|
||||||
<div
|
<div
|
||||||
ref={setToggle}
|
ref={setToggle}
|
||||||
className="cursor-pointer rounded-lg bg-zinc-100 p-2 px-3 dark:bg-zinc-900"
|
className="cursor-pointer rounded-lg bg-zinc-100 p-2 px-3"
|
||||||
onClick={() => setMore(!more)}
|
onClick={() => setMore(!more)}
|
||||||
>
|
>
|
||||||
<MoreVerticalIcon className="h-5 w-5" />
|
<MoreVerticalIcon className="h-5 w-5" />
|
||||||
@ -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)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -114,7 +114,7 @@ export default function CardSidebar({
|
|||||||
<>
|
<>
|
||||||
{title === 'Model' ? (
|
{title === 'Model' ? (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-medium text-black dark:text-muted-foreground">
|
<span className="font-medium text-black">
|
||||||
{openFileTitle()}
|
{openFileTitle()}
|
||||||
</span>
|
</span>
|
||||||
<span className="mt-1 text-muted-foreground">
|
<span className="mt-1 text-muted-foreground">
|
||||||
@ -122,7 +122,7 @@ export default function CardSidebar({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-bold text-black dark:text-muted-foreground">
|
<span className="text-bold text-black">
|
||||||
{openFileTitle()}
|
{openFileTitle()}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -141,7 +141,7 @@ export default function CardSidebar({
|
|||||||
/>
|
/>
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="line-clamp-1 font-medium text-black dark:text-muted-foreground">
|
<span className="line-clamp-1 font-medium text-black">
|
||||||
Edit Global Defaults for{' '}
|
Edit Global Defaults for{' '}
|
||||||
<span
|
<span
|
||||||
className="font-bold"
|
className="font-bold"
|
||||||
@ -175,7 +175,7 @@ export default function CardSidebar({
|
|||||||
{show && (
|
{show && (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'flex flex-col gap-2 bg-white px-2 dark:bg-background',
|
'flex flex-col gap-2 bg-white px-2',
|
||||||
asChild && 'rounded-b-lg'
|
asChild && 'rounded-b-lg'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -34,12 +34,10 @@ const Checkbox: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div className="mb-1 flex items-center gap-x-2">
|
<div className="mb-1 flex items-center gap-x-2">
|
||||||
<p className="text-sm font-semibold text-zinc-500 dark:text-gray-300">
|
<p className="text-sm font-semibold text-zinc-500">{title}</p>
|
||||||
{title}
|
|
||||||
</p>
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<InfoIcon size={16} className="flex-shrink-0 dark:text-gray-500" />
|
<InfoIcon size={16} className="flex-shrink-0" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
<TooltipContent side="top" className="max-w-[240px]">
|
<TooltipContent side="top" className="max-w-[240px]">
|
||||||
|
|||||||
@ -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(
|
||||||
@ -201,15 +203,14 @@ const DropdownListSidebar = ({
|
|||||||
isTabActive === 1 && '[&_.select-scroll-down-button]:hidden'
|
isTabActive === 1 && '[&_.select-scroll-down-button]:hidden'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="relative px-2 py-2 dark:bg-secondary/50">
|
<div className="relative px-2 py-2">
|
||||||
<ul className="inline-flex w-full space-x-2 rounded-lg bg-zinc-100 px-1 dark:bg-secondary">
|
<ul className="inline-flex w-full space-x-2 rounded-lg bg-zinc-100 px-1">
|
||||||
{engineOptions.map((name, i) => {
|
{engineOptions.map((name, i) => {
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'relative my-1 flex w-full cursor-pointer items-center justify-center space-x-2 px-2 py-2',
|
'relative my-1 flex w-full cursor-pointer items-center justify-center space-x-2 px-2 py-2',
|
||||||
isTabActive === i &&
|
isTabActive === i && 'rounded-md bg-background'
|
||||||
'rounded-md bg-background dark:bg-white'
|
|
||||||
)}
|
)}
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => setIsTabActive(i)}
|
onClick={() => setIsTabActive(i)}
|
||||||
@ -228,8 +229,7 @@ const DropdownListSidebar = ({
|
|||||||
<span
|
<span
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'relative z-50 font-medium text-muted-foreground',
|
'relative z-50 font-medium text-muted-foreground',
|
||||||
isTabActive === i &&
|
isTabActive === i && 'font-bold text-foreground'
|
||||||
'font-bold text-foreground dark:text-black'
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
|
|||||||
@ -60,7 +60,7 @@ const GPUDriverPrompt: React.FC = () => {
|
|||||||
id="default-checkbox"
|
id="default-checkbox"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
onChange={onDoNotShowAgainChange}
|
onChange={onDoNotShowAgainChange}
|
||||||
className="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600"
|
className="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
<span>Don't show again</span>
|
<span>Don't show again</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -47,7 +47,7 @@ export default function DownloadingState() {
|
|||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
<span
|
<span
|
||||||
className="absolute left-0 h-full rounded-md rounded-l-md bg-primary/20"
|
className="absolute left-0 h-full rounded-md rounded-l-md bg-blue-500/20"
|
||||||
style={{
|
style={{
|
||||||
width: `${totalPercentage}%`,
|
width: `${totalPercentage}%`,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -0,0 +1,61 @@
|
|||||||
|
import { Fragment, useCallback } from 'react'
|
||||||
|
|
||||||
|
import { Progress } from '@janhq/uikit'
|
||||||
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import { setImportModelStageAtom } from '@/hooks/useImportModel'
|
||||||
|
|
||||||
|
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
|
const ImportingModelState: React.FC = () => {
|
||||||
|
const importingModels = useAtomValue(importingModelsAtom)
|
||||||
|
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
||||||
|
|
||||||
|
const isImportingModels =
|
||||||
|
importingModels.filter((m) => m.status === 'IMPORTING').length > 0
|
||||||
|
|
||||||
|
const finishedImportModelCount = importingModels.filter(
|
||||||
|
(model) => model.status === 'IMPORTED' || model.status === 'FAILED'
|
||||||
|
).length
|
||||||
|
|
||||||
|
let transferredSize = 0
|
||||||
|
importingModels.forEach((model) => {
|
||||||
|
transferredSize += (model.percentage ?? 0) * 100 * model.size
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalSize = importingModels.reduce((acc, model) => acc + model.size, 0)
|
||||||
|
|
||||||
|
const progress = totalSize === 0 ? 0 : transferredSize / totalSize
|
||||||
|
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
setImportModelStage('IMPORTING_MODEL')
|
||||||
|
}, [setImportModelStage])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{isImportingModels ? (
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer flex-row items-center space-x-2"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<p className="text-xs font-semibold text-[#09090B]">
|
||||||
|
Importing model ({finishedImportModelCount}/{importingModels.length}
|
||||||
|
)
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center justify-center space-x-2 rounded-md bg-[#F4F4F5] px-2 py-[2px]">
|
||||||
|
<Progress
|
||||||
|
className="h-2 w-24"
|
||||||
|
value={transferredSize / totalSize}
|
||||||
|
/>
|
||||||
|
<span className="text-xs font-bold text-blue-600">
|
||||||
|
{progress.toFixed(2)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImportingModelState
|
||||||
@ -25,8 +25,8 @@ const TableActiveModel = () => {
|
|||||||
const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom)
|
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-white w-[calc(100%-64px)] z-50 border-t border-border flex flex-col flex-shrink-0',
|
'fixed bottom-12 left-16 z-50 flex w-[calc(100%-64px)] flex-shrink-0 flex-col border-t border-border bg-background',
|
||||||
showFullScreen && 'h-[calc(100%-48px)]'
|
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-medium">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-x-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 = [
|
||||||
@ -34,13 +35,14 @@ const BottomBar = () => {
|
|||||||
const progress = useAtomValue(appDownloadProgress)
|
const progress = useAtomValue(appDownloadProgress)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-0 left-16 z-20 flex h-12 w-[calc(100%-64px)] items-center justify-between border-t border-border bg-background/80 px-3">
|
<div className="fixed bottom-0 left-16 z-50 flex h-12 w-[calc(100%-64px)] items-center justify-between border-t border-border bg-background/80 px-3">
|
||||||
<div className="flex flex-shrink-0 items-center gap-x-2">
|
<div className="flex flex-shrink-0 items-center gap-x-2">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{progress && progress > 0 ? (
|
{progress && progress > 0 ? (
|
||||||
<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)
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ export default function RibbonNav() {
|
|||||||
size={20}
|
size={20}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'flex-shrink-0 text-muted-foreground',
|
'flex-shrink-0 text-muted-foreground',
|
||||||
serverEnabled && 'text-gray-300 dark:text-gray-700'
|
serverEnabled && 'text-gray-300'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -114,11 +114,12 @@ export default function RibbonNav() {
|
|||||||
</div>
|
</div>
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<m.div
|
<m.div
|
||||||
className="absolute inset-0 left-0 h-full w-full rounded-md bg-gray-200 dark:bg-secondary"
|
className="absolute inset-0 left-0 h-full w-full rounded-md bg-gray-200"
|
||||||
layoutId="active-state-primary"
|
layoutId="active-state-primary"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</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>
|
||||||
)
|
)
|
||||||
@ -164,15 +166,17 @@ export default function RibbonNav() {
|
|||||||
</div>
|
</div>
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<m.div
|
<m.div
|
||||||
className="absolute inset-0 left-0 h-full w-full rounded-md bg-gray-200 dark:bg-secondary"
|
className="absolute inset-0 left-0 h-full w-full rounded-md bg-gray-200"
|
||||||
layoutId="active-state-secondary"
|
layoutId="active-state-secondary"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</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)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -159,7 +159,7 @@ const TopBar = () => {
|
|||||||
size={16}
|
size={16}
|
||||||
className="text-muted-foreground"
|
className="text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<span className="font-medium text-black dark:text-muted-foreground">
|
<span className="font-medium text-black ">
|
||||||
{openFileTitle()}
|
{openFileTitle()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -175,7 +175,7 @@ const TopBar = () => {
|
|||||||
className="mt-0.5 flex-shrink-0 text-muted-foreground"
|
className="mt-0.5 flex-shrink-0 text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-medium text-black dark:text-muted-foreground">
|
<span className="font-medium text-black ">
|
||||||
Edit Threads Settings
|
Edit Threads Settings
|
||||||
</span>
|
</span>
|
||||||
<span className="mt-1 text-muted-foreground">
|
<span className="mt-1 text-muted-foreground">
|
||||||
@ -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)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -204,7 +204,7 @@ const TopBar = () => {
|
|||||||
className="text-muted-foreground"
|
className="text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-medium text-black dark:text-muted-foreground">
|
<span className="font-medium text-black ">
|
||||||
{openFileTitle()}
|
{openFileTitle()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,12 +7,12 @@ export default function Loader({ description }: Props) {
|
|||||||
<div className="space-y-16">
|
<div className="space-y-16">
|
||||||
<div className="loader">
|
<div className="loader">
|
||||||
<div className="loader-inner">
|
<div className="loader-inner">
|
||||||
<label className="h-2 w-2 rounded-full bg-primary" />
|
<label className="h-2 w-2 rounded-full bg-blue-500" />
|
||||||
<label className="h-2 w-2 rounded-full bg-primary" />
|
<label className="h-2 w-2 rounded-full bg-blue-500" />
|
||||||
<label className="h-2 w-2 rounded-full bg-primary" />
|
<label className="h-2 w-2 rounded-full bg-blue-500" />
|
||||||
<label className="h-2 w-2 rounded-full bg-primary" />
|
<label className="h-2 w-2 rounded-full bg-blue-500" />
|
||||||
<label className="h-2 w-2 rounded-full bg-primary" />
|
<label className="h-2 w-2 rounded-full bg-blue-500" />
|
||||||
<label className="h-2 w-2 rounded-full bg-primary" />
|
<label className="h-2 w-2 rounded-full bg-blue-500" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-medium text-muted-foreground">{description}</p>
|
<p className="font-medium text-muted-foreground">{description}</p>
|
||||||
|
|||||||
@ -28,7 +28,7 @@ const AppLogs = () => {
|
|||||||
<div className="absolute -top-11 right-2">
|
<div className="absolute -top-11 right-2">
|
||||||
<Button
|
<Button
|
||||||
themes="outline"
|
themes="outline"
|
||||||
className="bg-white dark:bg-secondary/50"
|
className="bg-white"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clipboard.copy(logs.slice(-50) ?? '')
|
clipboard.copy(logs.slice(-50) ?? '')
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ const DeviceSpecs = () => {
|
|||||||
<div className="absolute -top-11 right-2">
|
<div className="absolute -top-11 right-2">
|
||||||
<Button
|
<Button
|
||||||
themes="outline"
|
themes="outline"
|
||||||
className="bg-white dark:bg-secondary/50"
|
className="bg-white"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clipboard.copy(userAgent ?? '')
|
clipboard.copy(userAgent ?? '')
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ const ModalTroubleShooting: React.FC = () => {
|
|||||||
<a
|
<a
|
||||||
href="https://jan.ai/guides/troubleshooting"
|
href="https://jan.ai/guides/troubleshooting"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-blue-600 hover:underline dark:text-blue-300"
|
className="text-blue-600 hover:underline"
|
||||||
>
|
>
|
||||||
troubleshooting guide
|
troubleshooting guide
|
||||||
</a>
|
</a>
|
||||||
@ -65,7 +65,7 @@ const ModalTroubleShooting: React.FC = () => {
|
|||||||
<a
|
<a
|
||||||
href="https://discord.gg/AsJ8krTT3N"
|
href="https://discord.gg/AsJ8krTT3N"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-blue-600 hover:underline dark:text-blue-300"
|
className="text-blue-600 hover:underline"
|
||||||
>
|
>
|
||||||
Discord
|
Discord
|
||||||
</a>
|
</a>
|
||||||
@ -77,8 +77,8 @@ const ModalTroubleShooting: React.FC = () => {
|
|||||||
|
|
||||||
<div className="flex flex-col pt-4">
|
<div className="flex flex-col pt-4">
|
||||||
{/* TODO @faisal replace this once we have better tabs component UI */}
|
{/* TODO @faisal replace this once we have better tabs component UI */}
|
||||||
<div className="relative bg-zinc-100 px-4 py-2 dark:bg-secondary/50">
|
<div className="relative bg-zinc-100 px-4 py-2">
|
||||||
<ul className="inline-flex space-x-2 rounded-lg bg-zinc-200 px-1 dark:bg-secondary">
|
<ul className="inline-flex space-x-2 rounded-lg bg-zinc-200 px-1">
|
||||||
{logOption.map((name, i) => {
|
{logOption.map((name, i) => {
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
@ -89,15 +89,14 @@ const ModalTroubleShooting: React.FC = () => {
|
|||||||
<span
|
<span
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'relative z-50 font-medium text-muted-foreground',
|
'relative z-50 font-medium text-muted-foreground',
|
||||||
isTabActive === i &&
|
isTabActive === i && 'font-bold text-foreground'
|
||||||
'font-bold text-foreground dark:text-black'
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
{isTabActive === i && (
|
{isTabActive === i && (
|
||||||
<m.div
|
<m.div
|
||||||
className="absolute left-0 top-1 h-[calc(100%-8px)] w-full rounded-md bg-background dark:bg-white"
|
className="absolute left-0 top-1 h-[calc(100%-8px)] w-full rounded-md bg-background"
|
||||||
layoutId="log-state-active"
|
layoutId="log-state-active"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -30,12 +30,10 @@ const ModelConfigInput: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="mb-2 flex items-center gap-x-2">
|
<div className="mb-2 flex items-center gap-x-2">
|
||||||
<p className="text-sm font-semibold text-zinc-500 dark:text-gray-300">
|
<p className="text-sm font-semibold text-zinc-500">{title}</p>
|
||||||
{title}
|
|
||||||
</p>
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<InfoIcon size={16} className="flex-shrink-0 dark:text-gray-500" />
|
<InfoIcon size={16} className="flex-shrink-0" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
<TooltipContent side="top" className="max-w-[240px]">
|
<TooltipContent side="top" className="max-w-[240px]">
|
||||||
|
|||||||
@ -33,7 +33,7 @@ const OpenAiKeyInput: React.FC = () => {
|
|||||||
<div className="my-4">
|
<div className="my-4">
|
||||||
<label
|
<label
|
||||||
id="thread-title"
|
id="thread-title"
|
||||||
className="mb-2 inline-block font-bold text-gray-600 dark:text-gray-300"
|
className="mb-2 inline-block font-bold text-gray-600"
|
||||||
>
|
>
|
||||||
API Key
|
API Key
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
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>
|
||||||
}
|
}
|
||||||
|
|||||||
109
web/containers/Providers/ModelImportListener.tsx
Normal file
109
web/containers/Providers/ModelImportListener.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { Fragment, PropsWithChildren, useCallback, useEffect } from 'react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
ImportingModel,
|
||||||
|
LocalImportModelEvent,
|
||||||
|
Model,
|
||||||
|
ModelEvent,
|
||||||
|
events,
|
||||||
|
} from '@janhq/core'
|
||||||
|
import { useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import { snackbar } from '../Toast'
|
||||||
|
|
||||||
|
import {
|
||||||
|
setImportingModelErrorAtom,
|
||||||
|
setImportingModelSuccessAtom,
|
||||||
|
updateImportingModelProgressAtom,
|
||||||
|
} from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
|
const ModelImportListener = ({ children }: PropsWithChildren) => {
|
||||||
|
const updateImportingModelProgress = useSetAtom(
|
||||||
|
updateImportingModelProgressAtom
|
||||||
|
)
|
||||||
|
const setImportingModelSuccess = useSetAtom(setImportingModelSuccessAtom)
|
||||||
|
const setImportingModelFailed = useSetAtom(setImportingModelErrorAtom)
|
||||||
|
|
||||||
|
const onImportModelUpdate = useCallback(
|
||||||
|
async (state: ImportingModel) => {
|
||||||
|
if (!state.importId) return
|
||||||
|
updateImportingModelProgress(state.importId, state.percentage ?? 0)
|
||||||
|
},
|
||||||
|
[updateImportingModelProgress]
|
||||||
|
)
|
||||||
|
|
||||||
|
const onImportModelFailed = useCallback(
|
||||||
|
async (state: ImportingModel) => {
|
||||||
|
if (!state.importId) return
|
||||||
|
setImportingModelFailed(state.importId, state.error ?? '')
|
||||||
|
},
|
||||||
|
[setImportingModelFailed]
|
||||||
|
)
|
||||||
|
|
||||||
|
const onImportModelSuccess = useCallback(
|
||||||
|
(state: ImportingModel) => {
|
||||||
|
if (!state.modelId) return
|
||||||
|
events.emit(ModelEvent.OnModelsUpdate, {})
|
||||||
|
setImportingModelSuccess(state.importId, state.modelId)
|
||||||
|
},
|
||||||
|
[setImportingModelSuccess]
|
||||||
|
)
|
||||||
|
|
||||||
|
const onImportModelFinished = useCallback((importedModels: Model[]) => {
|
||||||
|
const modelText = importedModels.length === 1 ? 'model' : 'models'
|
||||||
|
snackbar({
|
||||||
|
description: `Successfully imported ${importedModels.length} ${modelText}`,
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.debug('ModelImportListener: registering event listeners..')
|
||||||
|
|
||||||
|
events.on(
|
||||||
|
LocalImportModelEvent.onLocalImportModelUpdate,
|
||||||
|
onImportModelUpdate
|
||||||
|
)
|
||||||
|
events.on(
|
||||||
|
LocalImportModelEvent.onLocalImportModelSuccess,
|
||||||
|
onImportModelSuccess
|
||||||
|
)
|
||||||
|
events.on(
|
||||||
|
LocalImportModelEvent.onLocalImportModelFinished,
|
||||||
|
onImportModelFinished
|
||||||
|
)
|
||||||
|
events.on(
|
||||||
|
LocalImportModelEvent.onLocalImportModelFailed,
|
||||||
|
onImportModelFailed
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
console.debug('ModelImportListener: unregistering event listeners...')
|
||||||
|
events.off(
|
||||||
|
LocalImportModelEvent.onLocalImportModelUpdate,
|
||||||
|
onImportModelUpdate
|
||||||
|
)
|
||||||
|
events.off(
|
||||||
|
LocalImportModelEvent.onLocalImportModelSuccess,
|
||||||
|
onImportModelSuccess
|
||||||
|
)
|
||||||
|
events.off(
|
||||||
|
LocalImportModelEvent.onLocalImportModelFinished,
|
||||||
|
onImportModelFinished
|
||||||
|
)
|
||||||
|
events.off(
|
||||||
|
LocalImportModelEvent.onLocalImportModelFailed,
|
||||||
|
onImportModelFailed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
onImportModelUpdate,
|
||||||
|
onImportModelSuccess,
|
||||||
|
onImportModelFinished,
|
||||||
|
onImportModelFailed,
|
||||||
|
])
|
||||||
|
|
||||||
|
return <Fragment>{children}</Fragment>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModelImportListener
|
||||||
@ -6,17 +6,9 @@ import { ThemeProvider } from 'next-themes'
|
|||||||
|
|
||||||
import { motion as m } from 'framer-motion'
|
import { motion as m } from 'framer-motion'
|
||||||
|
|
||||||
import { useBodyClass } from '@/hooks/useBodyClass'
|
|
||||||
|
|
||||||
import { useUserConfigs } from '@/hooks/useUserConfigs'
|
|
||||||
|
|
||||||
export default function ThemeWrapper({ children }: PropsWithChildren) {
|
export default function ThemeWrapper({ children }: PropsWithChildren) {
|
||||||
const [config] = useUserConfigs()
|
|
||||||
|
|
||||||
useBodyClass(config.primaryColor || 'primary-yellow')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider attribute="class" enableSystem>
|
<ThemeProvider attribute="class" forcedTheme="light">
|
||||||
<m.div
|
<m.div
|
||||||
initial={{ opacity: 0, y: -10 }}
|
initial={{ opacity: 0, y: -10 }}
|
||||||
animate={{
|
animate={{
|
||||||
|
|||||||
@ -57,7 +57,7 @@ const ServerLogs = (props: ServerLogsProps) => {
|
|||||||
<div className="absolute -top-11 right-2">
|
<div className="absolute -top-11 right-2">
|
||||||
<Button
|
<Button
|
||||||
themes="outline"
|
themes="outline"
|
||||||
className="bg-white dark:bg-secondary/50"
|
className="bg-white"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clipboard.copy(logs.slice(-100) ?? '')
|
clipboard.copy(logs.slice(-100) ?? '')
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -42,12 +42,10 @@ const SliderRightPanel: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="mb-3 flex items-center gap-x-2">
|
<div className="mb-3 flex items-center gap-x-2">
|
||||||
<p className="text-sm font-semibold text-zinc-500 dark:text-gray-300">
|
<p className="text-sm font-semibold text-zinc-500">{title}</p>
|
||||||
{title}
|
|
||||||
</p>
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<InfoIcon size={16} className="flex-shrink-0 dark:text-gray-500" />
|
<InfoIcon size={16} className="flex-shrink-0" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
<TooltipContent side="top" className="max-w-[240px]">
|
<TooltipContent side="top" className="max-w-[240px]">
|
||||||
|
|||||||
@ -108,11 +108,11 @@ export function toaster(props: Props) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'unset-drag dark:bg-zinc-white relative flex animate-enter items-center gap-x-4 rounded-lg bg-foreground px-4 py-2 text-white dark:border dark:border-border',
|
'unset-drag relative flex animate-enter items-center gap-x-4 rounded-lg bg-foreground px-4 py-2 text-white',
|
||||||
t.visible ? 'animate-enter' : 'animate-leave'
|
t.visible ? 'animate-enter' : 'animate-leave'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-x-3 dark:text-black">
|
<div className="flex items-start gap-x-3">
|
||||||
<div className="mt-1">{renderIcon(type)}</div>
|
<div className="mt-1">{renderIcon(type)}</div>
|
||||||
<div className="pr-4">
|
<div className="pr-4">
|
||||||
<h1 className="font-bold">{title}</h1>
|
<h1 className="font-bold">{title}</h1>
|
||||||
@ -120,7 +120,7 @@ export function toaster(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<XIcon
|
<XIcon
|
||||||
size={24}
|
size={24}
|
||||||
className="absolute right-2 top-2 w-4 cursor-pointer dark:text-black"
|
className="absolute right-2 top-2 w-4 cursor-pointer"
|
||||||
onClick={() => toast.dismiss(t.id)}
|
onClick={() => toast.dismiss(t.id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -138,16 +138,16 @@ export function snackbar(props: Props) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'unset-drag dark:bg-zinc-white relative bottom-2 flex animate-enter items-center gap-x-4 rounded-lg bg-foreground px-4 py-2 text-white dark:border dark:border-border',
|
'unset-drag relative bottom-2 flex animate-enter items-center gap-x-4 rounded-lg bg-foreground px-4 py-2 text-white',
|
||||||
t.visible ? 'animate-enter' : 'animate-leave'
|
t.visible ? 'animate-enter' : 'animate-leave'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-x-3 dark:text-black">
|
<div className="flex items-start gap-x-3">
|
||||||
<div>{renderIcon(type)}</div>
|
<div>{renderIcon(type)}</div>
|
||||||
<p className="pr-4">{description}</p>
|
<p className="pr-4">{description}</p>
|
||||||
<XIcon
|
<XIcon
|
||||||
size={24}
|
size={24}
|
||||||
className="absolute right-2 top-1/2 w-4 -translate-y-1/2 cursor-pointer dark:text-black"
|
className="absolute right-2 top-1/2 w-4 -translate-y-1/2 cursor-pointer"
|
||||||
onClick={() => toast.dismiss(t.id)}
|
onClick={() => toast.dismiss(t.id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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,99 @@ 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 setImportingModelErrorAtom = atom(
|
||||||
|
null,
|
||||||
|
(get, set, importId: string, error: string) => {
|
||||||
|
const model = get(importingModelsAtom).find((x) => x.importId === importId)
|
||||||
|
if (!model) return
|
||||||
|
const newModel: ImportingModel = {
|
||||||
|
...model,
|
||||||
|
status: 'FAILED',
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`Importing model ${model} failed`, error)
|
||||||
|
const newList = get(importingModelsAtom).map((m) =>
|
||||||
|
m.importId === importId ? newModel : m
|
||||||
|
)
|
||||||
|
set(importingModelsAtom, newList)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const setImportingModelSuccessAtom = atom(
|
||||||
|
null,
|
||||||
|
(get, set, importId: string, modelId: string) => {
|
||||||
|
const model = get(importingModelsAtom).find((x) => x.importId === importId)
|
||||||
|
if (!model) return
|
||||||
|
const newModel: ImportingModel = {
|
||||||
|
...model,
|
||||||
|
modelId,
|
||||||
|
status: 'IMPORTED',
|
||||||
|
percentage: 1,
|
||||||
|
}
|
||||||
|
const newList = get(importingModelsAtom).map((x) =>
|
||||||
|
x.importId === importId ? newModel : x
|
||||||
|
)
|
||||||
|
set(importingModelsAtom, newList)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const updateImportingModelAtom = atom(
|
||||||
|
null,
|
||||||
|
(
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
importId: string,
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
tags: string[]
|
||||||
|
) => {
|
||||||
|
const model = get(importingModelsAtom).find((x) => x.importId === importId)
|
||||||
|
if (!model) return
|
||||||
|
const newModel: ImportingModel = {
|
||||||
|
...model,
|
||||||
|
name,
|
||||||
|
importId,
|
||||||
|
description,
|
||||||
|
tags,
|
||||||
|
}
|
||||||
|
const newList = get(importingModelsAtom).map((x) =>
|
||||||
|
x.importId === importId ? newModel : x
|
||||||
|
)
|
||||||
|
set(importingModelsAtom, newList)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
81
web/hooks/useConvertHuggingFaceModel.ts
Normal file
81
web/hooks/useConvertHuggingFaceModel.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { useContext } from 'react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
ExtensionTypeEnum,
|
||||||
|
HuggingFaceExtension,
|
||||||
|
HuggingFaceRepoData,
|
||||||
|
Quantization,
|
||||||
|
} from '@janhq/core'
|
||||||
|
|
||||||
|
import { useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import { FeatureToggleContext } from '@/context/FeatureToggle'
|
||||||
|
|
||||||
|
import { extensionManager } from '@/extension/ExtensionManager'
|
||||||
|
import {
|
||||||
|
conversionStatusAtom,
|
||||||
|
conversionErrorAtom,
|
||||||
|
} from '@/helpers/atoms/HFConverter.atom'
|
||||||
|
|
||||||
|
export const useConvertHuggingFaceModel = () => {
|
||||||
|
const { ignoreSSL, proxy } = useContext(FeatureToggleContext)
|
||||||
|
const setConversionStatus = useSetAtom(conversionStatusAtom)
|
||||||
|
const setConversionError = useSetAtom(conversionErrorAtom)
|
||||||
|
|
||||||
|
const convertHuggingFaceModel = async (
|
||||||
|
repoID: string,
|
||||||
|
repoData: HuggingFaceRepoData,
|
||||||
|
quantization: Quantization
|
||||||
|
) => {
|
||||||
|
const extension = await extensionManager.get<HuggingFaceExtension>(
|
||||||
|
ExtensionTypeEnum.HuggingFace
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
if (extension) {
|
||||||
|
extension.interrupted = false
|
||||||
|
}
|
||||||
|
setConversionStatus('downloading')
|
||||||
|
await extension?.downloadModelFiles(repoID, repoData, {
|
||||||
|
ignoreSSL,
|
||||||
|
proxy,
|
||||||
|
})
|
||||||
|
if (extension?.interrupted) return
|
||||||
|
setConversionStatus('converting')
|
||||||
|
await extension?.convert(repoID)
|
||||||
|
if (extension?.interrupted) return
|
||||||
|
setConversionStatus('quantizing')
|
||||||
|
await extension?.quantize(repoID, quantization)
|
||||||
|
if (extension?.interrupted) return
|
||||||
|
setConversionStatus('generating')
|
||||||
|
await extension?.generateMetadata(repoID, repoData, quantization)
|
||||||
|
setConversionStatus('done')
|
||||||
|
} catch (err) {
|
||||||
|
if (extension?.interrupted) return
|
||||||
|
extension?.cancelConvert(repoID, repoData)
|
||||||
|
if (typeof err === 'number') {
|
||||||
|
setConversionError(new Error(`exit code: ${err}`))
|
||||||
|
} else {
|
||||||
|
setConversionError(err as Error)
|
||||||
|
}
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelConvertHuggingFaceModel = async (
|
||||||
|
repoID: string,
|
||||||
|
repoData: HuggingFaceRepoData
|
||||||
|
) => {
|
||||||
|
const extension = await extensionManager.get<HuggingFaceExtension>(
|
||||||
|
ExtensionTypeEnum.HuggingFace
|
||||||
|
)
|
||||||
|
|
||||||
|
setConversionStatus('stopping')
|
||||||
|
await extension?.cancelConvert(repoID, repoData)
|
||||||
|
setConversionStatus(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
convertHuggingFaceModel,
|
||||||
|
cancelConvertHuggingFaceModel,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,28 +1,32 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
import { ExtensionTypeEnum, ModelExtension, Model } from '@janhq/core'
|
import { 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)
|
||||||
|
|||||||
55
web/hooks/useDropModelBinaries.ts
Normal file
55
web/hooks/useDropModelBinaries.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
|
import { ImportingModel } from '@janhq/core'
|
||||||
|
import { useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
import { snackbar } from '@/containers/Toast'
|
||||||
|
|
||||||
|
import { getFileInfoFromFile } from '@/utils/file'
|
||||||
|
|
||||||
|
import { setImportModelStageAtom } from './useImportModel'
|
||||||
|
|
||||||
|
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
|
export default function useDropModelBinaries() {
|
||||||
|
const setImportingModels = useSetAtom(importingModelsAtom)
|
||||||
|
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
||||||
|
|
||||||
|
const onDropModels = useCallback(
|
||||||
|
async (acceptedFiles: File[]) => {
|
||||||
|
const files = await getFileInfoFromFile(acceptedFiles)
|
||||||
|
|
||||||
|
const unsupportedFiles = files.filter(
|
||||||
|
(file) => !file.path.endsWith('.gguf')
|
||||||
|
)
|
||||||
|
const supportedFiles = files.filter((file) => file.path.endsWith('.gguf'))
|
||||||
|
|
||||||
|
const importingModels: ImportingModel[] = supportedFiles.map((file) => ({
|
||||||
|
importId: uuidv4(),
|
||||||
|
modelId: undefined,
|
||||||
|
name: file.name.replace('.gguf', ''),
|
||||||
|
description: '',
|
||||||
|
path: file.path,
|
||||||
|
tags: [],
|
||||||
|
size: file.size,
|
||||||
|
status: 'PREPARING',
|
||||||
|
format: 'gguf',
|
||||||
|
}))
|
||||||
|
if (unsupportedFiles.length > 0) {
|
||||||
|
snackbar({
|
||||||
|
description: `File has to be a .gguf file`,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (importingModels.length === 0) return
|
||||||
|
|
||||||
|
setImportingModels(importingModels)
|
||||||
|
setImportModelStage('MODEL_SELECTED')
|
||||||
|
},
|
||||||
|
[setImportModelStage, setImportingModels]
|
||||||
|
)
|
||||||
|
|
||||||
|
return { onDropModels }
|
||||||
|
}
|
||||||
29
web/hooks/useGetHFRepoData.ts
Normal file
29
web/hooks/useGetHFRepoData.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import {
|
||||||
|
repoDataAtom,
|
||||||
|
repoIDAtom,
|
||||||
|
loadingAtom,
|
||||||
|
fetchErrorAtom,
|
||||||
|
} from '@/helpers/atoms/HFConverter.atom'
|
||||||
|
|
||||||
|
export const useGetHFRepoData = () => {
|
||||||
|
const repoID = useAtomValue(repoIDAtom)
|
||||||
|
const setRepoData = useSetAtom(repoDataAtom)
|
||||||
|
const setLoading = useSetAtom(loadingAtom)
|
||||||
|
const setFetchError = useSetAtom(fetchErrorAtom)
|
||||||
|
|
||||||
|
const getRepoData = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch(`https://huggingface.co/api/models/${repoID}`)
|
||||||
|
const data = await res.json()
|
||||||
|
setRepoData(data)
|
||||||
|
} catch (err) {
|
||||||
|
setFetchError(err as Error)
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return getRepoData
|
||||||
|
}
|
||||||
70
web/hooks/useImportModel.ts
Normal file
70
web/hooks/useImportModel.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
ExtensionTypeEnum,
|
||||||
|
ImportingModel,
|
||||||
|
Model,
|
||||||
|
ModelExtension,
|
||||||
|
OptionType,
|
||||||
|
} from '@janhq/core'
|
||||||
|
|
||||||
|
import { atom } from 'jotai'
|
||||||
|
|
||||||
|
import { extensionManager } from '@/extension'
|
||||||
|
|
||||||
|
export type ImportModelStage =
|
||||||
|
| 'NONE'
|
||||||
|
| 'SELECTING_MODEL'
|
||||||
|
| 'MODEL_SELECTED'
|
||||||
|
| 'IMPORTING_MODEL'
|
||||||
|
| 'EDIT_MODEL_INFO'
|
||||||
|
| 'CONFIRM_CANCEL'
|
||||||
|
|
||||||
|
const importModelStageAtom = atom<ImportModelStage>('NONE')
|
||||||
|
|
||||||
|
export const getImportModelStageAtom = atom((get) => get(importModelStageAtom))
|
||||||
|
|
||||||
|
export const setImportModelStageAtom = atom(
|
||||||
|
null,
|
||||||
|
(_get, set, stage: ImportModelStage) => {
|
||||||
|
set(importModelStageAtom, stage)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ModelUpdate = {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
tags: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const useImportModel = () => {
|
||||||
|
const importModels = useCallback(
|
||||||
|
(models: ImportingModel[], optionType: OptionType) =>
|
||||||
|
localImportModels(models, optionType),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateModelInfo = useCallback(
|
||||||
|
async (modelInfo: Partial<Model>) => localUpdateModelInfo(modelInfo),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
return { importModels, updateModelInfo }
|
||||||
|
}
|
||||||
|
|
||||||
|
const localImportModels = async (
|
||||||
|
models: ImportingModel[],
|
||||||
|
optionType: OptionType
|
||||||
|
): Promise<void> =>
|
||||||
|
extensionManager
|
||||||
|
.get<ModelExtension>(ExtensionTypeEnum.Model)
|
||||||
|
?.importModels(models, optionType)
|
||||||
|
|
||||||
|
const localUpdateModelInfo = async (
|
||||||
|
modelInfo: Partial<Model>
|
||||||
|
): Promise<Model | undefined> =>
|
||||||
|
extensionManager
|
||||||
|
.get<ModelExtension>(ExtensionTypeEnum.Model)
|
||||||
|
?.updateModelInfo(modelInfo)
|
||||||
|
|
||||||
|
export default useImportModel
|
||||||
@ -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 }
|
|
||||||
}
|
|
||||||
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