Merge branch 'docs/add-guides' of https://github.com/janhq/jan into docs/add-guides
18
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
about: Create a report to help us improve Jan
|
||||
title: 'bug: [DESCRIPTION]'
|
||||
labels: 'type: bug'
|
||||
assignees: ''
|
||||
@ -10,7 +10,7 @@ assignees: ''
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
**Steps to reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
@ -21,12 +21,14 @@ Steps to reproduce the behavior:
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
If applicable, add screenshots to help explain your issue.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
**Environment details**
|
||||
- Operating System: [Specify your OS. e.g., MacOS Sonoma 14.2.1, Windows 11, Ubuntu 22, etc]
|
||||
- Jan Version: [e.g., 0.1.3]
|
||||
- Processor: [e.g., Apple M1, Intel Core i7, AMD Ryzen 5, etc]
|
||||
- RAM: [e.g., 8GB, 16GB]
|
||||
- Any additional relevant hardware specifics: [e.g., Graphics card, SSD/HDD]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
Add any other context or information that could be helpful in diagnosing the problem.
|
||||
|
||||
23
.github/ISSUE_TEMPLATE/epic-request.md
vendored
@ -7,22 +7,19 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Motivation**
|
||||
## Motivation
|
||||
-
|
||||
|
||||
**Specs & Designs**
|
||||
## Specs
|
||||
-
|
||||
|
||||
**In Scope**
|
||||
## Designs
|
||||
[Figma](link)
|
||||
|
||||
## Tasklist
|
||||
- [ ]
|
||||
|
||||
## Not in Scope
|
||||
-
|
||||
|
||||
**Not in Scope**
|
||||
-
|
||||
|
||||
**Tasklist**
|
||||
> Note: All issues need to share the same `milestone` as this epic
|
||||
-
|
||||
|
||||
**Related Milestones**
|
||||
- Past
|
||||
- Future
|
||||
## Appendix
|
||||
|
||||
1
Makefile
@ -18,6 +18,7 @@ ifeq ($(OS),Windows_NT)
|
||||
yarn config set network-timeout 300000
|
||||
endif
|
||||
yarn build:core
|
||||
yarn build:server
|
||||
yarn install
|
||||
yarn build:extensions
|
||||
|
||||
|
||||
63
README.md
@ -70,7 +70,7 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
|
||||
<tr style="text-align: center">
|
||||
<td style="text-align:center"><b>Experimental (Nighlty Build)</b></td>
|
||||
<td style="text-align:center" colspan="4">
|
||||
<a href='https://github.com/janhq/jan/actions/runs/7324039788'>
|
||||
<a href='https://github.com/janhq/jan/actions/runs/7372465396'>
|
||||
<b>Github action artifactory</b>
|
||||
</a>
|
||||
</td>
|
||||
@ -81,16 +81,14 @@ Download the latest version of Jan at https://jan.ai/ or visit the **[GitHub Rel
|
||||
|
||||
## Demo
|
||||
|
||||
<p align="center">
|
||||
<video src="https://github.com/janhq/jan/assets/89722390/43adfddc-7980-4ae6-b544-719f04660dd7">
|
||||
</video>
|
||||
</p>
|
||||

|
||||
|
||||
|
||||
_Video: Jan v0.4.0 on Mac Air M2, 16GB Ventura_
|
||||
_Realtime Video: Jan v0.4.3-nightly on a Mac M1, 16GB Sonoma 14_
|
||||
|
||||
## Quicklinks
|
||||
|
||||
#### Jan
|
||||
|
||||
- [Jan website](https://jan.ai/)
|
||||
- [Jan Github](https://github.com/janhq/jan)
|
||||
- [User Guides](https://jan.ai/docs)
|
||||
@ -98,8 +96,10 @@ _Video: Jan v0.4.0 on Mac Air M2, 16GB Ventura_
|
||||
- [API reference](https://jan.ai/api-reference/)
|
||||
- [Specs](https://jan.ai/specs/)
|
||||
|
||||
#### Nitro:
|
||||
Nitro is a high-efficiency C++ inference engine for edge computing, powering Jan. It is lightweight and embeddable, ideal for product integration.
|
||||
#### Nitro
|
||||
|
||||
Nitro is a high-efficiency C++ inference engine for edge computing. It is lightweight and embeddable, and can be used on its own within your own projects.
|
||||
|
||||
- [Nitro Website](https://nitro.jan.ai)
|
||||
- [Nitro Github](https://github.com/janhq/nitro)
|
||||
- [Documentation](https://nitro.jan.ai/docs)
|
||||
@ -118,21 +118,22 @@ To reset your installation:
|
||||
```
|
||||
|
||||
This will remove all build artifacts and cached files:
|
||||
|
||||
- Delete Jan from your `/Applications` folder
|
||||
- Clear Application cache in `/Users/$(whoami)/Library/Caches/jan`
|
||||
|
||||
2. Use the following commands to remove any dangling backend processes:
|
||||
|
||||
```sh
|
||||
ps aux | grep nitro
|
||||
```
|
||||
```sh
|
||||
ps aux | grep nitro
|
||||
```
|
||||
|
||||
Look for processes like "nitro" and "nitro_arm_64," and kill them one by one with:
|
||||
Look for processes like "nitro" and "nitro_arm_64," and kill them one by one with:
|
||||
|
||||
```sh
|
||||
kill -9 <PID>
|
||||
```
|
||||
|
||||
```sh
|
||||
kill -9 <PID>
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) file
|
||||
@ -145,15 +146,15 @@ Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) fi
|
||||
|
||||
### Instructions
|
||||
|
||||
1. **Clone the Repository:**
|
||||
1. **Clone the repository and prepare:**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/janhq/jan
|
||||
git checkout DESIRED_BRANCH
|
||||
cd jan
|
||||
git checkout -b DESIRED_BRANCH
|
||||
```
|
||||
|
||||
2. **Run development and Using Jan Desktop**
|
||||
2. **Run development and use Jan Desktop**
|
||||
|
||||
```
|
||||
make dev
|
||||
@ -164,10 +165,7 @@ Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) fi
|
||||
### For production build
|
||||
|
||||
```bash
|
||||
# Do step 1 and 2 in previous section
|
||||
git clone https://github.com/janhq/jan
|
||||
cd jan
|
||||
|
||||
# Do steps 1 and 2 in the previous section
|
||||
# Build the app
|
||||
make build
|
||||
```
|
||||
@ -176,19 +174,22 @@ This will build the app MacOS m1/m2 for production (with code signing already do
|
||||
|
||||
## Nightly Build
|
||||
|
||||
Nightly build is a process where the software is built automatically every night. This helps in detecting and fixing bugs early in the development cycle. The process for this project is defined in [`.github/workflows/jan-electron-build-nightly.yml`](.github/workflows/jan-electron-build-nightly.yml)
|
||||
|
||||
You can join our Discord server [here](https://discord.gg/FTk2MvZwJH) and go to channel [github-jan](https://discordapp.com/channels/1107178041848909847/1148534730359308298) to monitor the build process.
|
||||
Our nightly build process for this project is defined in [`.github/workflows/jan-electron-build-nightly.yml`](.github/workflows/jan-electron-build-nightly.yml)
|
||||
|
||||
The nightly build is triggered at 2:00 AM UTC every day.
|
||||
|
||||
The nightly build can be downloaded from the url notified in the Discord channel. Please access the url from the browser and download the build artifacts from there.
|
||||
Getting on Nightly:
|
||||
|
||||
1. Join our Discord server [here](https://discord.gg/FTk2MvZwJH) and go to channel [github-jan](https://discordapp.com/channels/1107178041848909847/1148534730359308298).
|
||||
2. Download the build artifacts from the channel.
|
||||
3. Subsequently, to get the latest nightly, just quit and restart the app.
|
||||
4. Upon app restart, you will be automatically prompted to update to the latest nightly build.
|
||||
|
||||
## Manual Build
|
||||
|
||||
Manual build is a process where the software is built manually by the developers. This is usually done when a new feature is implemented or a bug is fixed. The process for this project is defined in [`.github/workflows/jan-electron-build-nightly.yml`](.github/workflows/jan-electron-build-nightly.yml)
|
||||
Stable releases are triggered by manual builds. This is usually done for new features or a bug fixes.
|
||||
|
||||
It is similar to the nightly build process, except that it is triggered manually by the developers.
|
||||
The process for this project is defined in [`.github/workflows/jan-electron-build-nightly.yml`](.github/workflows/jan-electron-build-nightly.yml)
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
@ -199,7 +200,7 @@ Jan builds on top of other open-source projects:
|
||||
|
||||
## Contact
|
||||
|
||||
- Bugs & requests: file a Github ticket
|
||||
- Bugs & requests: file a GitHub ticket
|
||||
- For discussion: join our Discord [here](https://discord.gg/FTk2MvZwJH)
|
||||
- For business inquiries: email hello@jan.ai
|
||||
- For jobs: please email hr@jan.ai
|
||||
|
||||
@ -22,6 +22,27 @@
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
},
|
||||
"exports": {
|
||||
".": "./dist/core.umd.js",
|
||||
"./sdk": "./dist/core.umd.js",
|
||||
"./node": "./dist/node/index.cjs.js"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
".": [
|
||||
"./dist/core.es5.js.map",
|
||||
"./dist/types/index.d.ts"
|
||||
],
|
||||
"sdk": [
|
||||
"./dist/core.es5.js.map",
|
||||
"./dist/types/index.d.ts"
|
||||
],
|
||||
"node": [
|
||||
"./dist/node/index.cjs.js.map",
|
||||
"./dist/types/node/index.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'",
|
||||
"prebuild": "rimraf dist",
|
||||
|
||||
@ -8,30 +8,69 @@ const pkg = require('./package.json')
|
||||
|
||||
const libraryName = 'core'
|
||||
|
||||
export default {
|
||||
input: `src/index.ts`,
|
||||
output: [
|
||||
{ file: pkg.main, name: libraryName, format: 'umd', sourcemap: true },
|
||||
{ file: pkg.module, format: 'es', sourcemap: true },
|
||||
],
|
||||
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
|
||||
external: [],
|
||||
watch: {
|
||||
include: 'src/**',
|
||||
},
|
||||
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(),
|
||||
export default [
|
||||
{
|
||||
input: `src/index.ts`,
|
||||
output: [
|
||||
{ file: pkg.main, name: libraryName, format: 'umd', sourcemap: true },
|
||||
{ file: pkg.module, format: 'es', sourcemap: true },
|
||||
],
|
||||
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
|
||||
external: ['path'],
|
||||
watch: {
|
||||
include: 'src/**',
|
||||
},
|
||||
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(),
|
||||
|
||||
// Resolve source maps to the original source
|
||||
sourceMaps(),
|
||||
],
|
||||
}
|
||||
// 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: [
|
||||
'fs/promises',
|
||||
'path',
|
||||
'pacote',
|
||||
'@types/pacote',
|
||||
'@npmcli/arborist',
|
||||
'ulid',
|
||||
'node-fetch',
|
||||
'fs',
|
||||
'request',
|
||||
'crypto',
|
||||
'url',
|
||||
'http',
|
||||
],
|
||||
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(),
|
||||
|
||||
// Resolve source maps to the original source
|
||||
sourceMaps(),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@ -5,11 +5,12 @@
|
||||
export enum AppRoute {
|
||||
appDataPath = 'appDataPath',
|
||||
appVersion = 'appVersion',
|
||||
getResourcePath = 'getResourcePath',
|
||||
openExternalUrl = 'openExternalUrl',
|
||||
openAppDirectory = 'openAppDirectory',
|
||||
openFileExplore = 'openFileExplorer',
|
||||
relaunch = 'relaunch',
|
||||
joinPath = 'joinPath',
|
||||
baseName = 'baseName',
|
||||
}
|
||||
|
||||
export enum AppEvent {
|
||||
@ -40,20 +41,20 @@ export enum ExtensionRoute {
|
||||
uninstallExtension = 'uninstallExtension',
|
||||
}
|
||||
export enum FileSystemRoute {
|
||||
appendFile = 'appendFile',
|
||||
copyFile = 'copyFile',
|
||||
syncFile = 'syncFile',
|
||||
deleteFile = 'deleteFile',
|
||||
exists = 'exists',
|
||||
getResourcePath = 'getResourcePath',
|
||||
appendFileSync = 'appendFileSync',
|
||||
copyFileSync = 'copyFileSync',
|
||||
unlinkSync = 'unlinkSync',
|
||||
existsSync = 'existsSync',
|
||||
readdirSync = 'readdirSync',
|
||||
mkdirSync = 'mkdirSync',
|
||||
readFileSync = 'readFileSync',
|
||||
rmdirSync = 'rmdirSync',
|
||||
writeFileSync = 'writeFileSync',
|
||||
}
|
||||
export enum FileManagerRoute {
|
||||
synceFile = 'syncFile',
|
||||
getUserSpace = 'getUserSpace',
|
||||
isDirectory = 'isDirectory',
|
||||
listFiles = 'listFiles',
|
||||
mkdir = 'mkdir',
|
||||
readFile = 'readFile',
|
||||
readLineByLine = 'readLineByLine',
|
||||
rmdir = 'rmdir',
|
||||
writeFile = 'writeFile',
|
||||
getResourcePath = 'getResourcePath',
|
||||
}
|
||||
|
||||
export type ApiFunction = (...args: any[]) => any
|
||||
@ -82,17 +83,23 @@ export type FileSystemRouteFunctions = {
|
||||
[K in FileSystemRoute]: ApiFunction
|
||||
}
|
||||
|
||||
export type FileManagerRouteFunctions = {
|
||||
[K in FileManagerRoute]: ApiFunction
|
||||
}
|
||||
|
||||
export type APIFunctions = AppRouteFunctions &
|
||||
AppEventFunctions &
|
||||
DownloadRouteFunctions &
|
||||
DownloadEventFunctions &
|
||||
ExtensionRouteFunctions &
|
||||
FileSystemRouteFunctions
|
||||
FileSystemRouteFunctions &
|
||||
FileManagerRoute
|
||||
|
||||
export const APIRoutes = [
|
||||
...Object.values(AppRoute),
|
||||
...Object.values(DownloadRoute),
|
||||
...Object.values(ExtensionRoute),
|
||||
...Object.values(FileSystemRoute),
|
||||
...Object.values(FileManagerRoute),
|
||||
]
|
||||
export const APIEvents = [...Object.values(AppEvent), ...Object.values(DownloadEvent)]
|
||||
|
||||
@ -44,6 +44,34 @@ const getUserSpace = (): Promise<string> => global.core.api?.getUserSpace()
|
||||
const openFileExplorer: (path: string) => Promise<any> = (path) =>
|
||||
global.core.api?.openFileExplorer(path)
|
||||
|
||||
/**
|
||||
* Joins multiple paths together.
|
||||
* @param paths - The paths to join.
|
||||
* @returns {Promise<string>} A promise that resolves with the joined path.
|
||||
*/
|
||||
const joinPath: (paths: string[]) => Promise<string> = (paths) => global.core.api?.joinPath(paths)
|
||||
|
||||
/**
|
||||
* Retrive the basename from an url.
|
||||
* @param path - The path to retrieve.
|
||||
* @returns {Promise<string>} A promise that resolves with the basename.
|
||||
*/
|
||||
const baseName: (paths: string[]) => Promise<string> = (path) => global.core.api?.baseName(path)
|
||||
|
||||
/**
|
||||
* Opens an external URL in the default web browser.
|
||||
*
|
||||
* @param {string} url - The URL to open.
|
||||
* @returns {Promise<any>} - A promise that resolves when the URL has been successfully opened.
|
||||
*/
|
||||
const openExternalUrl: (url: string) => Promise<any> = (url) =>
|
||||
global.core.api?.openExternalUrl(url)
|
||||
|
||||
/**
|
||||
* Gets the resource path of the application.
|
||||
*
|
||||
* @returns {Promise<string>} - A promise that resolves with the resource path.
|
||||
*/
|
||||
const getResourcePath: () => Promise<string> = () => global.core.api?.getResourcePath()
|
||||
|
||||
/**
|
||||
@ -66,4 +94,7 @@ export {
|
||||
getUserSpace,
|
||||
openFileExplorer,
|
||||
getResourcePath,
|
||||
joinPath,
|
||||
openExternalUrl,
|
||||
baseName,
|
||||
}
|
||||
|
||||
@ -1,89 +1,74 @@
|
||||
/**
|
||||
* Writes data to a file at the specified path.
|
||||
* @param {string} path - The path to the file.
|
||||
* @param {string} data - The data to write to the file.
|
||||
* @returns {Promise<any>} A Promise that resolves when the file is written successfully.
|
||||
*/
|
||||
const writeFile: (path: string, data: string) => Promise<any> = (path, data) =>
|
||||
global.core.api?.writeFile(path, data)
|
||||
|
||||
/**
|
||||
* Checks whether the path is a directory.
|
||||
* @param path - The path to check.
|
||||
* @returns {boolean} A boolean indicating whether the path is a directory.
|
||||
*/
|
||||
const isDirectory = (path: string): Promise<boolean> => global.core.api?.isDirectory(path)
|
||||
const writeFileSync = (...args: any[]) => global.core.api?.writeFileSync(...args)
|
||||
|
||||
/**
|
||||
* Reads the contents of a file at the specified path.
|
||||
* @param {string} path - The path of the file to read.
|
||||
* @returns {Promise<any>} A Promise that resolves with the contents of the file.
|
||||
*/
|
||||
const readFile: (path: string) => Promise<any> = (path) => global.core.api?.readFile(path)
|
||||
const readFileSync = (...args: any[]) => global.core.api?.readFileSync(...args)
|
||||
/**
|
||||
* Check whether the file exists
|
||||
* @param {string} path
|
||||
* @returns {boolean} A boolean indicating whether the path is a file.
|
||||
*/
|
||||
const exists = (path: string): Promise<boolean> => global.core.api?.exists(path)
|
||||
const existsSync = (...args: any[]) => global.core.api?.existsSync(...args)
|
||||
/**
|
||||
* List the directory files
|
||||
* @param {string} path - The path of the directory to list files.
|
||||
* @returns {Promise<any>} A Promise that resolves with the contents of the directory.
|
||||
*/
|
||||
const listFiles: (path: string) => Promise<any> = (path) => global.core.api?.listFiles(path)
|
||||
const readdirSync = (...args: any[]) => global.core.api?.readdirSync(...args)
|
||||
/**
|
||||
* Creates a directory at the specified path.
|
||||
* @param {string} path - The path of the directory to create.
|
||||
* @returns {Promise<any>} A Promise that resolves when the directory is created successfully.
|
||||
*/
|
||||
const mkdir: (path: string) => Promise<any> = (path) => global.core.api?.mkdir(path)
|
||||
const mkdirSync = (...args: any[]) => global.core.api?.mkdirSync(...args)
|
||||
|
||||
/**
|
||||
* Removes a directory at the specified path.
|
||||
* @param {string} path - The path of the directory to remove.
|
||||
* @returns {Promise<any>} A Promise that resolves when the directory is removed successfully.
|
||||
*/
|
||||
const rmdir: (path: string) => Promise<any> = (path) => global.core.api?.rmdir(path)
|
||||
const rmdirSync = (...args: any[]) =>
|
||||
global.core.api?.rmdirSync(...args, { recursive: true, force: true })
|
||||
/**
|
||||
* Deletes a file from the local file system.
|
||||
* @param {string} path - The path of the file to delete.
|
||||
* @returns {Promise<any>} A Promise that resolves when the file is deleted.
|
||||
*/
|
||||
const deleteFile: (path: string) => Promise<any> = (path) => global.core.api?.deleteFile(path)
|
||||
const unlinkSync = (...args: any[]) => global.core.api?.unlinkSync(...args)
|
||||
|
||||
/**
|
||||
* Appends data to a file at the specified path.
|
||||
* @param path path to the file
|
||||
* @param data data to append
|
||||
*/
|
||||
const appendFile: (path: string, data: string) => Promise<any> = (path, data) =>
|
||||
global.core.api?.appendFile(path, data)
|
||||
|
||||
const copyFile: (src: string, dest: string) => Promise<any> = (src, dest) =>
|
||||
global.core.api?.copyFile(src, dest)
|
||||
const appendFileSync = (...args: any[]) => global.core.api?.appendFileSync(...args)
|
||||
|
||||
/**
|
||||
* Synchronizes a file from a source path to a destination path.
|
||||
* @param {string} src - The source path of the file to be synchronized.
|
||||
* @param {string} dest - The destination path where the file will be synchronized to.
|
||||
* @returns {Promise<any>} - A promise that resolves when the file has been successfully synchronized.
|
||||
*/
|
||||
const syncFile: (src: string, dest: string) => Promise<any> = (src, dest) =>
|
||||
global.core.api?.syncFile(src, dest)
|
||||
/**
|
||||
* Reads a file line by line.
|
||||
* @param {string} path - The path of the file to read.
|
||||
* @returns {Promise<any>} A promise that resolves to the lines of the file.
|
||||
*/
|
||||
const readLineByLine: (path: string) => Promise<any> = (path) =>
|
||||
global.core.api?.readLineByLine(path)
|
||||
|
||||
/**
|
||||
* Copy file sync.
|
||||
*/
|
||||
const copyFileSync = (...args: any[]) => global.core.api?.copyFileSync(...args)
|
||||
|
||||
// TODO: Export `dummy` fs functions automatically
|
||||
// Currently adding these manually
|
||||
export const fs = {
|
||||
isDirectory,
|
||||
writeFile,
|
||||
readFile,
|
||||
exists,
|
||||
listFiles,
|
||||
mkdir,
|
||||
rmdir,
|
||||
deleteFile,
|
||||
appendFile,
|
||||
readLineByLine,
|
||||
copyFile,
|
||||
writeFileSync,
|
||||
readFileSync,
|
||||
existsSync,
|
||||
readdirSync,
|
||||
mkdirSync,
|
||||
rmdirSync,
|
||||
unlinkSync,
|
||||
appendFileSync,
|
||||
copyFileSync,
|
||||
syncFile,
|
||||
}
|
||||
|
||||
8
core/src/node/api/HttpServer.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface HttpServer {
|
||||
post: (route: string, handler: (req: any, res: any) => Promise<any>) => void
|
||||
get: (route: string, handler: (req: any, res: any) => Promise<any>) => void
|
||||
patch: (route: string, handler: (req: any, res: any) => Promise<any>) => void
|
||||
put: (route: string, handler: (req: any, res: any) => Promise<any>) => void
|
||||
delete: (route: string, handler: (req: any, res: any) => Promise<any>) => void
|
||||
register: (router: any, opts?: any) => void
|
||||
}
|
||||
335
core/src/node/api/common/builder.ts
Normal file
@ -0,0 +1,335 @@
|
||||
import fs from 'fs'
|
||||
import { JanApiRouteConfiguration, RouteConfiguration } from './configuration'
|
||||
import { join } from 'path'
|
||||
import { Model, ThreadMessage } from './../../../index'
|
||||
|
||||
import fetch from 'node-fetch'
|
||||
import { ulid } from 'ulid'
|
||||
import request from 'request'
|
||||
|
||||
const progress = require('request-progress')
|
||||
const os = require('os')
|
||||
|
||||
const path = join(os.homedir(), 'jan')
|
||||
|
||||
export const getBuilder = async (configuration: RouteConfiguration) => {
|
||||
const directoryPath = join(path, configuration.dirName)
|
||||
|
||||
try {
|
||||
if (!(await fs.existsSync(directoryPath))) {
|
||||
console.debug('model folder not found')
|
||||
return []
|
||||
}
|
||||
|
||||
const files: string[] = await fs.readdirSync(directoryPath)
|
||||
|
||||
const allDirectories: string[] = []
|
||||
for (const file of files) {
|
||||
if (file === '.DS_Store') continue
|
||||
allDirectories.push(file)
|
||||
}
|
||||
|
||||
const readJsonPromises = allDirectories.map(async (dirName) => {
|
||||
const jsonPath = join(directoryPath, dirName, configuration.metadataFileName)
|
||||
return await readModelMetadata(jsonPath)
|
||||
})
|
||||
|
||||
const results = await Promise.all(readJsonPromises)
|
||||
const modelData = results
|
||||
.map((result: any) => {
|
||||
try {
|
||||
return JSON.parse(result)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
.filter((e: any) => !!e)
|
||||
|
||||
return modelData
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const readModelMetadata = async (path: string) => {
|
||||
return fs.readFileSync(path, 'utf-8')
|
||||
}
|
||||
|
||||
export const retrieveBuilder = async (configuration: RouteConfiguration, id: string) => {
|
||||
const data = await getBuilder(configuration)
|
||||
const filteredData = data.filter((d: any) => d.id === id)[0]
|
||||
|
||||
if (!filteredData) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return filteredData
|
||||
}
|
||||
|
||||
export const deleteBuilder = async (configuration: RouteConfiguration, id: string) => {
|
||||
if (configuration.dirName === 'assistants' && id === 'jan') {
|
||||
return {
|
||||
message: 'Cannot delete Jan assistant',
|
||||
}
|
||||
}
|
||||
|
||||
const directoryPath = join(path, configuration.dirName)
|
||||
try {
|
||||
const data = await retrieveBuilder(configuration, id)
|
||||
if (!data || !data.keys) {
|
||||
return {
|
||||
message: 'Not found',
|
||||
}
|
||||
}
|
||||
|
||||
const myPath = join(directoryPath, id)
|
||||
fs.rmdirSync(myPath, { recursive: true })
|
||||
return {
|
||||
id: id,
|
||||
object: configuration.delete.object,
|
||||
deleted: true,
|
||||
}
|
||||
} catch (ex) {
|
||||
console.error(ex)
|
||||
}
|
||||
}
|
||||
|
||||
export const getMessages = async (threadId: string) => {
|
||||
const threadDirPath = join(path, 'threads', threadId)
|
||||
const messageFile = 'messages.jsonl'
|
||||
try {
|
||||
const files: string[] = await fs.readdirSync(threadDirPath)
|
||||
if (!files.includes(messageFile)) {
|
||||
throw Error(`${threadDirPath} not contains message file`)
|
||||
}
|
||||
|
||||
const messageFilePath = join(threadDirPath, messageFile)
|
||||
|
||||
const lines = fs
|
||||
.readFileSync(messageFilePath, 'utf-8')
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter((line: any) => line !== '')
|
||||
|
||||
const messages: ThreadMessage[] = []
|
||||
lines.forEach((line: string) => {
|
||||
messages.push(JSON.parse(line) as ThreadMessage)
|
||||
})
|
||||
return messages
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const retrieveMesasge = async (threadId: string, messageId: string) => {
|
||||
const messages = await getMessages(threadId)
|
||||
const filteredMessages = messages.filter((m) => m.id === messageId)
|
||||
if (!filteredMessages || filteredMessages.length === 0) {
|
||||
return {
|
||||
message: 'Not found',
|
||||
}
|
||||
}
|
||||
|
||||
return filteredMessages[0]
|
||||
}
|
||||
|
||||
export const createThread = async (thread: any) => {
|
||||
const threadMetadataFileName = 'thread.json'
|
||||
// TODO: add validation
|
||||
if (!thread.assistants || thread.assistants.length === 0) {
|
||||
return {
|
||||
message: 'Thread must have at least one assistant',
|
||||
}
|
||||
}
|
||||
|
||||
const threadId = generateThreadId(thread.assistants[0].assistant_id)
|
||||
try {
|
||||
const updatedThread = {
|
||||
...thread,
|
||||
id: threadId,
|
||||
created: Date.now(),
|
||||
updated: Date.now(),
|
||||
}
|
||||
const threadDirPath = join(path, 'threads', updatedThread.id)
|
||||
const threadJsonPath = join(threadDirPath, threadMetadataFileName)
|
||||
|
||||
if (!fs.existsSync(threadDirPath)) {
|
||||
fs.mkdirSync(threadDirPath)
|
||||
}
|
||||
|
||||
await fs.writeFileSync(threadJsonPath, JSON.stringify(updatedThread, null, 2))
|
||||
return updatedThread
|
||||
} catch (err) {
|
||||
return {
|
||||
error: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const updateThread = async (threadId: string, thread: any) => {
|
||||
const threadMetadataFileName = 'thread.json'
|
||||
const currentThreadData = await retrieveBuilder(JanApiRouteConfiguration.threads, threadId)
|
||||
if (!currentThreadData) {
|
||||
return {
|
||||
message: 'Thread not found',
|
||||
}
|
||||
}
|
||||
// we don't want to update the id and object
|
||||
delete thread.id
|
||||
delete thread.object
|
||||
|
||||
const updatedThread = {
|
||||
...currentThreadData,
|
||||
...thread,
|
||||
updated: Date.now(),
|
||||
}
|
||||
try {
|
||||
const threadDirPath = join(path, 'threads', updatedThread.id)
|
||||
const threadJsonPath = join(threadDirPath, threadMetadataFileName)
|
||||
|
||||
await fs.writeFileSync(threadJsonPath, JSON.stringify(updatedThread, null, 2))
|
||||
return updatedThread
|
||||
} catch (err) {
|
||||
return {
|
||||
message: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const generateThreadId = (assistantId: string) => {
|
||||
return `${assistantId}_${(Date.now() / 1000).toFixed(0)}`
|
||||
}
|
||||
|
||||
export const createMessage = async (threadId: string, message: any) => {
|
||||
const threadMessagesFileName = 'messages.jsonl'
|
||||
// TODO: add validation
|
||||
try {
|
||||
const msgId = ulid()
|
||||
const createdAt = Date.now()
|
||||
const threadMessage: ThreadMessage = {
|
||||
...message,
|
||||
id: msgId,
|
||||
thread_id: threadId,
|
||||
created: createdAt,
|
||||
updated: createdAt,
|
||||
object: 'thread.message',
|
||||
}
|
||||
|
||||
const threadDirPath = join(path, 'threads', threadId)
|
||||
const threadMessagePath = join(threadDirPath, threadMessagesFileName)
|
||||
|
||||
if (!fs.existsSync(threadDirPath)) {
|
||||
fs.mkdirSync(threadDirPath)
|
||||
}
|
||||
fs.appendFileSync(threadMessagePath, JSON.stringify(threadMessage) + '\n')
|
||||
return threadMessage
|
||||
} catch (err) {
|
||||
return {
|
||||
message: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const downloadModel = async (modelId: string) => {
|
||||
const model = await retrieveBuilder(JanApiRouteConfiguration.models, modelId)
|
||||
if (!model || model.object !== 'model') {
|
||||
return {
|
||||
message: 'Model not found',
|
||||
}
|
||||
}
|
||||
|
||||
const directoryPath = join(path, 'models', modelId)
|
||||
if (!fs.existsSync(directoryPath)) {
|
||||
fs.mkdirSync(directoryPath)
|
||||
}
|
||||
|
||||
// path to model binary
|
||||
const modelBinaryPath = join(directoryPath, modelId)
|
||||
const rq = request(model.source_url)
|
||||
|
||||
progress(rq, {})
|
||||
.on('progress', function (state: any) {
|
||||
console.log('progress', JSON.stringify(state, null, 2))
|
||||
})
|
||||
.on('error', function (err: Error) {
|
||||
console.error('error', err)
|
||||
})
|
||||
.on('end', function () {
|
||||
console.log('end')
|
||||
})
|
||||
.pipe(fs.createWriteStream(modelBinaryPath))
|
||||
|
||||
return {
|
||||
message: `Starting download ${modelId}`,
|
||||
}
|
||||
}
|
||||
|
||||
export const chatCompletions = async (request: any, reply: any) => {
|
||||
const modelList = await getBuilder(JanApiRouteConfiguration.models)
|
||||
const modelId = request.body.model
|
||||
|
||||
const matchedModels = modelList.filter((model: Model) => model.id === modelId)
|
||||
if (matchedModels.length === 0) {
|
||||
const error = {
|
||||
error: {
|
||||
message: `The model ${request.body.model} does not exist`,
|
||||
type: 'invalid_request_error',
|
||||
param: null,
|
||||
code: 'model_not_found',
|
||||
},
|
||||
}
|
||||
reply.code(404).send(error)
|
||||
return
|
||||
}
|
||||
|
||||
const requestedModel = matchedModels[0]
|
||||
const engineConfiguration = await getEngineConfiguration(requestedModel.engine)
|
||||
|
||||
let apiKey: string | undefined = undefined
|
||||
let apiUrl: string = 'http://127.0.0.1:3928/inferences/llamacpp/chat_completion' // default nitro url
|
||||
|
||||
if (engineConfiguration) {
|
||||
apiKey = engineConfiguration.api_key
|
||||
apiUrl = engineConfiguration.full_url
|
||||
}
|
||||
|
||||
reply.raw.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
})
|
||||
|
||||
const headers: Record<string, any> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if (apiKey) {
|
||||
headers['Authorization'] = `Bearer ${apiKey}`
|
||||
headers['api-key'] = apiKey
|
||||
}
|
||||
console.log(apiUrl)
|
||||
console.log(JSON.stringify(headers))
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(request.body),
|
||||
})
|
||||
if (response.status !== 200) {
|
||||
console.error(response)
|
||||
return
|
||||
} else {
|
||||
response.body.pipe(reply.raw)
|
||||
}
|
||||
}
|
||||
|
||||
const getEngineConfiguration = async (engineId: string) => {
|
||||
if (engineId !== 'openai') {
|
||||
return undefined
|
||||
}
|
||||
const directoryPath = join(path, 'engines')
|
||||
const filePath = join(directoryPath, `${engineId}.json`)
|
||||
const data = await fs.readFileSync(filePath, 'utf-8')
|
||||
return JSON.parse(data)
|
||||
}
|
||||
31
core/src/node/api/common/configuration.ts
Normal file
@ -0,0 +1,31 @@
|
||||
export const JanApiRouteConfiguration: Record<string, RouteConfiguration> = {
|
||||
models: {
|
||||
dirName: 'models',
|
||||
metadataFileName: 'model.json',
|
||||
delete: {
|
||||
object: 'model',
|
||||
},
|
||||
},
|
||||
assistants: {
|
||||
dirName: 'assistants',
|
||||
metadataFileName: 'assistant.json',
|
||||
delete: {
|
||||
object: 'assistant',
|
||||
},
|
||||
},
|
||||
threads: {
|
||||
dirName: 'threads',
|
||||
metadataFileName: 'thread.json',
|
||||
delete: {
|
||||
object: 'thread',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export type RouteConfiguration = {
|
||||
dirName: string
|
||||
metadataFileName: string
|
||||
delete: {
|
||||
object: string
|
||||
}
|
||||
}
|
||||
2
core/src/node/api/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './HttpServer'
|
||||
export * from './routes'
|
||||
46
core/src/node/api/routes/common.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { AppRoute } from '../../../api'
|
||||
import { HttpServer } from '../HttpServer'
|
||||
import { basename, join } from 'path'
|
||||
import {
|
||||
chatCompletions,
|
||||
deleteBuilder,
|
||||
downloadModel,
|
||||
getBuilder,
|
||||
retrieveBuilder,
|
||||
} from '../common/builder'
|
||||
|
||||
import { JanApiRouteConfiguration } from '../common/configuration'
|
||||
|
||||
export const commonRouter = async (app: HttpServer) => {
|
||||
// Common Routes
|
||||
Object.keys(JanApiRouteConfiguration).forEach((key) => {
|
||||
app.get(`/${key}`, async (_request) => getBuilder(JanApiRouteConfiguration[key]))
|
||||
|
||||
app.get(`/${key}/:id`, async (request: any) =>
|
||||
retrieveBuilder(JanApiRouteConfiguration[key], request.params.id),
|
||||
)
|
||||
|
||||
app.delete(`/${key}/:id`, async (request: any) =>
|
||||
deleteBuilder(JanApiRouteConfiguration[key], request.params.id),
|
||||
)
|
||||
})
|
||||
|
||||
// Download Model Routes
|
||||
app.get(`/models/download/:modelId`, async (request: any) =>
|
||||
downloadModel(request.params.modelId),
|
||||
)
|
||||
|
||||
// Chat Completion Routes
|
||||
app.post(`/chat/completions`, async (request: any, reply: any) => chatCompletions(request, reply))
|
||||
|
||||
// App Routes
|
||||
app.post(`/app/${AppRoute.joinPath}`, async (request: any, reply: any) => {
|
||||
const args = JSON.parse(request.body) as any[]
|
||||
reply.send(JSON.stringify(join(...args[0])))
|
||||
})
|
||||
|
||||
app.post(`/app/${AppRoute.baseName}`, async (request: any, reply: any) => {
|
||||
const args = JSON.parse(request.body) as any[]
|
||||
reply.send(JSON.stringify(basename(args[0])))
|
||||
})
|
||||
}
|
||||
54
core/src/node/api/routes/download.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { DownloadRoute } from '../../../api'
|
||||
import { join } from 'path'
|
||||
import { userSpacePath, DownloadManager, HttpServer } from '../../index'
|
||||
import { createWriteStream } from 'fs'
|
||||
|
||||
const request = require('request')
|
||||
const progress = require('request-progress')
|
||||
|
||||
export const downloadRouter = async (app: HttpServer) => {
|
||||
app.post(`/${DownloadRoute.downloadFile}`, async (req, res) => {
|
||||
const body = JSON.parse(req.body as any)
|
||||
const normalizedArgs = body.map((arg: any) => {
|
||||
if (typeof arg === 'string' && arg.includes('file:/')) {
|
||||
return join(userSpacePath, arg.replace('file:/', ''))
|
||||
}
|
||||
return arg
|
||||
})
|
||||
|
||||
const localPath = normalizedArgs[1]
|
||||
const fileName = localPath.split('/').pop() ?? ''
|
||||
|
||||
const rq = request(normalizedArgs[0])
|
||||
progress(rq, {})
|
||||
.on('progress', function (state: any) {
|
||||
console.log('download onProgress', state)
|
||||
})
|
||||
.on('error', function (err: Error) {
|
||||
console.log('download onError', err)
|
||||
})
|
||||
.on('end', function () {
|
||||
console.log('download onEnd')
|
||||
})
|
||||
.pipe(createWriteStream(normalizedArgs[1]))
|
||||
|
||||
DownloadManager.instance.setRequest(fileName, rq)
|
||||
})
|
||||
|
||||
app.post(`/${DownloadRoute.abortDownload}`, async (req, res) => {
|
||||
const body = JSON.parse(req.body as any)
|
||||
const normalizedArgs = body.map((arg: any) => {
|
||||
if (typeof arg === 'string' && arg.includes('file:/')) {
|
||||
return join(userSpacePath, arg.replace('file:/', ''))
|
||||
}
|
||||
return arg
|
||||
})
|
||||
|
||||
const localPath = normalizedArgs[0]
|
||||
const fileName = localPath.split('/').pop() ?? ''
|
||||
console.debug('fileName', fileName)
|
||||
const rq = DownloadManager.instance.networkRequests[fileName]
|
||||
DownloadManager.instance.networkRequests[fileName] = undefined
|
||||
rq?.abort()
|
||||
})
|
||||
}
|
||||
51
core/src/node/api/routes/extension.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { join, extname } from 'path'
|
||||
import { ExtensionRoute } from '../../../api'
|
||||
import {
|
||||
userSpacePath,
|
||||
ModuleManager,
|
||||
getActiveExtensions,
|
||||
installExtensions,
|
||||
HttpServer,
|
||||
} from '../../index'
|
||||
import { readdirSync } from 'fs'
|
||||
|
||||
export const extensionRouter = async (app: HttpServer) => {
|
||||
// TODO: Share code between node projects
|
||||
app.post(`/${ExtensionRoute.getActiveExtensions}`, async (req, res) => {
|
||||
const activeExtensions = await getActiveExtensions()
|
||||
res.status(200).send(activeExtensions)
|
||||
})
|
||||
|
||||
app.post(`/${ExtensionRoute.baseExtensions}`, async (req, res) => {
|
||||
const baseExtensionPath = join(__dirname, '..', '..', '..', 'pre-install')
|
||||
const extensions = readdirSync(baseExtensionPath)
|
||||
.filter((file) => extname(file) === '.tgz')
|
||||
.map((file) => join(baseExtensionPath, file))
|
||||
|
||||
res.status(200).send(extensions)
|
||||
})
|
||||
|
||||
app.post(`/${ExtensionRoute.installExtension}`, async (req, res) => {
|
||||
const extensions = req.body as any
|
||||
const installed = await installExtensions(JSON.parse(extensions)[0])
|
||||
return JSON.parse(JSON.stringify(installed))
|
||||
})
|
||||
|
||||
app.post(`/${ExtensionRoute.invokeExtensionFunc}`, async (req, res) => {
|
||||
const args = JSON.parse(req.body as any)
|
||||
console.debug(args)
|
||||
const module = await import(join(userSpacePath, 'extensions', args[0]))
|
||||
|
||||
ModuleManager.instance.setModule(args[0], module)
|
||||
const method = args[1]
|
||||
if (typeof module[method] === 'function') {
|
||||
// remove first item from args
|
||||
const newArgs = args.slice(2)
|
||||
console.log(newArgs)
|
||||
return module[method](...args.slice(2))
|
||||
} else {
|
||||
console.debug(module[method])
|
||||
console.error(`Function "${method}" does not exist in the module.`)
|
||||
}
|
||||
})
|
||||
}
|
||||
27
core/src/node/api/routes/fs.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { FileSystemRoute } from '../../../api'
|
||||
import { join } from 'path'
|
||||
import { HttpServer, userSpacePath } from '../../index'
|
||||
|
||||
export const fsRouter = async (app: HttpServer) => {
|
||||
const moduleName = 'fs'
|
||||
// Generate handlers for each fs route
|
||||
Object.values(FileSystemRoute).forEach((route) => {
|
||||
app.post(`/${route}`, async (req, res) => {
|
||||
const body = JSON.parse(req.body as any)
|
||||
try {
|
||||
const result = await import(moduleName).then((mdl) => {
|
||||
return mdl[route](
|
||||
...body.map((arg: any) =>
|
||||
typeof arg === 'string' && arg.includes('file:/')
|
||||
? join(userSpacePath, arg.replace('file:/', ''))
|
||||
: arg,
|
||||
),
|
||||
)
|
||||
})
|
||||
res.status(200).send(result)
|
||||
} catch (ex) {
|
||||
console.log(ex)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
6
core/src/node/api/routes/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from './download'
|
||||
export * from './extension'
|
||||
export * from './fs'
|
||||
export * from './thread'
|
||||
export * from './common'
|
||||
export * from './v1'
|
||||
30
core/src/node/api/routes/thread.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { HttpServer } from '../HttpServer'
|
||||
import {
|
||||
createMessage,
|
||||
createThread,
|
||||
getMessages,
|
||||
retrieveMesasge,
|
||||
updateThread,
|
||||
} from '../common/builder'
|
||||
|
||||
export const threadRouter = async (app: HttpServer) => {
|
||||
// create thread
|
||||
app.post(`/`, async (req, res) => createThread(req.body))
|
||||
|
||||
app.get(`/:threadId/messages`, async (req, res) => getMessages(req.params.threadId))
|
||||
|
||||
// retrieve message
|
||||
app.get(`/:threadId/messages/:messageId`, async (req, res) =>
|
||||
retrieveMesasge(req.params.threadId, req.params.messageId),
|
||||
)
|
||||
|
||||
// create message
|
||||
app.post(`/:threadId/messages`, async (req, res) =>
|
||||
createMessage(req.params.threadId as any, req.body as any),
|
||||
)
|
||||
|
||||
// modify thread
|
||||
app.patch(`/:threadId`, async (request: any) =>
|
||||
updateThread(request.params.threadId, request.body),
|
||||
)
|
||||
}
|
||||
21
core/src/node/api/routes/v1.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { HttpServer } from '../HttpServer'
|
||||
import { commonRouter, threadRouter, fsRouter, extensionRouter, downloadRouter } from './index'
|
||||
|
||||
export const v1Router = async (app: HttpServer) => {
|
||||
// MARK: External Routes
|
||||
app.register(commonRouter)
|
||||
app.register(threadRouter, {
|
||||
prefix: '/thread',
|
||||
})
|
||||
|
||||
// MARK: Internal Application Routes
|
||||
app.register(fsRouter, {
|
||||
prefix: '/fs',
|
||||
})
|
||||
app.register(extensionRouter, {
|
||||
prefix: '/extension',
|
||||
})
|
||||
app.register(downloadRouter, {
|
||||
prefix: '/download',
|
||||
})
|
||||
}
|
||||
@ -1,14 +1,14 @@
|
||||
import { rmdir } from 'fs/promises'
|
||||
import { rmdirSync } from 'fs'
|
||||
import { resolve, join } from 'path'
|
||||
import { manifest, extract } from 'pacote'
|
||||
import * as Arborist from '@npmcli/arborist'
|
||||
import { ExtensionManager } from './../managers/extension'
|
||||
import { ExtensionManager } from './manager'
|
||||
|
||||
/**
|
||||
* An NPM package that can be used as an extension.
|
||||
* Used to hold all the information and functions necessary to handle the extension lifecycle.
|
||||
*/
|
||||
class Extension {
|
||||
export default class Extension {
|
||||
/**
|
||||
* @property {string} origin Original specification provided to fetch the package.
|
||||
* @property {Object} installOptions Options provided to pacote when fetching the manifest.
|
||||
@ -56,10 +56,7 @@ class Extension {
|
||||
* @type {string}
|
||||
*/
|
||||
get specifier() {
|
||||
return (
|
||||
this.origin +
|
||||
(this.installOptions.version ? '@' + this.installOptions.version : '')
|
||||
)
|
||||
return this.origin + (this.installOptions.version ? '@' + this.installOptions.version : '')
|
||||
}
|
||||
|
||||
/**
|
||||
@ -85,9 +82,7 @@ class Extension {
|
||||
this.main = mnf.main
|
||||
this.description = mnf.description
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Package ${this.origin} does not contain a valid manifest: ${error}`
|
||||
)
|
||||
throw new Error(`Package ${this.origin} does not contain a valid manifest: ${error}`)
|
||||
}
|
||||
|
||||
return true
|
||||
@ -107,7 +102,7 @@ class Extension {
|
||||
await extract(
|
||||
this.specifier,
|
||||
join(ExtensionManager.instance.extensionsPath ?? '', this.name ?? ''),
|
||||
this.installOptions
|
||||
this.installOptions,
|
||||
)
|
||||
|
||||
// Set the url using the custom extensions protocol
|
||||
@ -180,11 +175,8 @@ class Extension {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async uninstall() {
|
||||
const extPath = resolve(
|
||||
ExtensionManager.instance.extensionsPath ?? '',
|
||||
this.name ?? ''
|
||||
)
|
||||
await rmdir(extPath, { recursive: true })
|
||||
const extPath = resolve(ExtensionManager.instance.extensionsPath ?? '', this.name ?? '')
|
||||
await rmdirSync(extPath, { recursive: true })
|
||||
|
||||
this.emitUpdate()
|
||||
}
|
||||
@ -200,5 +192,3 @@ class Extension {
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
export default Extension
|
||||
@ -1,5 +1,5 @@
|
||||
import { readFileSync } from 'fs'
|
||||
import { protocol } from 'electron'
|
||||
|
||||
import { normalize } from 'path'
|
||||
|
||||
import Extension from './extension'
|
||||
@ -12,18 +12,8 @@ import {
|
||||
getActiveExtensions,
|
||||
addExtension,
|
||||
} from './store'
|
||||
import { ExtensionManager } from './../managers/extension'
|
||||
import { ExtensionManager } from './manager'
|
||||
|
||||
/**
|
||||
* Sets up the required communication between the main and renderer processes.
|
||||
* Additionally sets the extensions up using {@link useExtensions} if a extensionsPath is provided.
|
||||
* @param {Object} options configuration for setting up the renderer facade.
|
||||
* @param {confirmInstall} [options.confirmInstall] Function to validate that a extension should be installed.
|
||||
* @param {Boolean} [options.useFacade=true] Whether to make a facade to the extensions available in the renderer.
|
||||
* @param {string} [options.extensionsPath] Optional path to the extensions folder.
|
||||
* @returns {extensionManager|Object} A set of functions used to manage the extension lifecycle if useExtensions is provided.
|
||||
* @function
|
||||
*/
|
||||
export function init(options: any) {
|
||||
// Create extensions protocol to serve extensions to renderer
|
||||
registerExtensionProtocol()
|
||||
@ -41,13 +31,24 @@ export function init(options: any) {
|
||||
* @private
|
||||
* @returns {boolean} Whether the protocol registration was successful
|
||||
*/
|
||||
function registerExtensionProtocol() {
|
||||
return protocol.registerFileProtocol('extension', (request, callback) => {
|
||||
const entry = request.url.substr('extension://'.length - 1)
|
||||
async function registerExtensionProtocol() {
|
||||
let electron: any = undefined
|
||||
|
||||
const url = normalize(ExtensionManager.instance.extensionsPath + entry)
|
||||
callback({ path: url })
|
||||
})
|
||||
try {
|
||||
const moduleName = "electron"
|
||||
electron = await import(moduleName)
|
||||
} catch (err) {
|
||||
console.error('Electron is not available')
|
||||
}
|
||||
|
||||
if (electron) {
|
||||
return electron.protocol.registerFileProtocol('extension', (request: any, callback: any) => {
|
||||
const entry = request.url.substr('extension://'.length - 1)
|
||||
|
||||
const url = normalize(ExtensionManager.instance.extensionsPath + entry)
|
||||
callback({ path: url })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -57,8 +58,7 @@ function registerExtensionProtocol() {
|
||||
* @returns {extensionManager} A set of functions used to manage the extension lifecycle.
|
||||
*/
|
||||
export function useExtensions(extensionsPath: string) {
|
||||
if (!extensionsPath)
|
||||
throw Error('A path to the extensions folder is required to use extensions')
|
||||
if (!extensionsPath) throw Error('A path to the extensions folder is required to use extensions')
|
||||
// Store the path to the extensions folder
|
||||
ExtensionManager.instance.setExtensionsPath(extensionsPath)
|
||||
|
||||
@ -69,7 +69,7 @@ export function useExtensions(extensionsPath: string) {
|
||||
|
||||
// Read extension list from extensions folder
|
||||
const extensions = JSON.parse(
|
||||
readFileSync(ExtensionManager.instance.getExtensionsFile(), 'utf-8')
|
||||
readFileSync(ExtensionManager.instance.getExtensionsFile(), 'utf-8'),
|
||||
)
|
||||
try {
|
||||
// Create and store a Extension instance for each extension in list
|
||||
@ -82,7 +82,7 @@ export function useExtensions(extensionsPath: string) {
|
||||
throw new Error(
|
||||
'Could not successfully rebuild list of installed extensions.\n' +
|
||||
error +
|
||||
'\nPlease check the extensions.json file in the extensions folder.'
|
||||
'\nPlease check the extensions.json file in the extensions folder.',
|
||||
)
|
||||
}
|
||||
|
||||
@ -111,7 +111,6 @@ function loadExtension(ext: any) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
addExtension(extension, false)
|
||||
extension.subscribe('pe-persist', persistExtensions)
|
||||
}
|
||||
@ -123,7 +122,7 @@ function loadExtension(ext: any) {
|
||||
export function getStore() {
|
||||
if (!ExtensionManager.instance.extensionsPath) {
|
||||
throw new Error(
|
||||
'The extension path has not yet been set up. Please run useExtensions before accessing the store'
|
||||
'The extension path has not yet been set up. Please run useExtensions before accessing the store',
|
||||
)
|
||||
}
|
||||
|
||||
@ -134,4 +133,4 @@ export function getStore() {
|
||||
getActiveExtensions,
|
||||
removeExtension,
|
||||
}
|
||||
}
|
||||
}
|
||||
61
core/src/node/extension/manager.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { join, resolve } from "path";
|
||||
|
||||
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
||||
import { init } from "./index";
|
||||
import { homedir } from "os"
|
||||
/**
|
||||
* Manages extension installation and migration.
|
||||
*/
|
||||
|
||||
export const userSpacePath = join(homedir(), "jan");
|
||||
|
||||
export class ExtensionManager {
|
||||
public static instance: ExtensionManager = new ExtensionManager();
|
||||
|
||||
extensionsPath: string | undefined = join(userSpacePath, "extensions");
|
||||
|
||||
constructor() {
|
||||
if (ExtensionManager.instance) {
|
||||
return ExtensionManager.instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the extensions by initializing the `extensions` module with the `confirmInstall` and `extensionsPath` options.
|
||||
* The `confirmInstall` function always returns `true` to allow extension installation.
|
||||
* The `extensionsPath` option specifies the path to install extensions to.
|
||||
*/
|
||||
setupExtensions() {
|
||||
init({
|
||||
// Function to check from the main process that user wants to install a extension
|
||||
confirmInstall: async (_extensions: string[]) => {
|
||||
return true;
|
||||
},
|
||||
// Path to install extension to
|
||||
extensionsPath: join(userSpacePath, "extensions"),
|
||||
});
|
||||
}
|
||||
|
||||
setExtensionsPath(extPath: string) {
|
||||
// Create folder if it does not exist
|
||||
let extDir;
|
||||
try {
|
||||
extDir = resolve(extPath);
|
||||
if (extDir.length < 2) throw new Error();
|
||||
|
||||
if (!existsSync(extDir)) mkdirSync(extDir);
|
||||
|
||||
const extensionsJson = join(extDir, "extensions.json");
|
||||
if (!existsSync(extensionsJson))
|
||||
writeFileSync(extensionsJson, "{}", "utf8");
|
||||
|
||||
this.extensionsPath = extDir;
|
||||
} catch (error) {
|
||||
throw new Error("Invalid path provided to the extensions folder");
|
||||
}
|
||||
}
|
||||
|
||||
getExtensionsFile() {
|
||||
return join(this.extensionsPath ?? "", "extensions.json");
|
||||
}
|
||||
}
|
||||
@ -1,16 +1,6 @@
|
||||
/**
|
||||
* Provides access to the extensions stored by Extension Store
|
||||
* @typedef {Object} extensionManager
|
||||
* @prop {getExtension} getExtension
|
||||
* @prop {getAllExtensions} getAllExtensions
|
||||
* @prop {getActiveExtensions} getActiveExtensions
|
||||
* @prop {installExtensions} installExtensions
|
||||
* @prop {removeExtension} removeExtension
|
||||
*/
|
||||
|
||||
import { writeFileSync } from 'fs'
|
||||
import Extension from './extension'
|
||||
import { ExtensionManager } from './../managers/extension'
|
||||
import { writeFileSync } from "fs";
|
||||
import Extension from "./extension";
|
||||
import { ExtensionManager } from "./manager";
|
||||
|
||||
/**
|
||||
* @module store
|
||||
@ -21,7 +11,7 @@ import { ExtensionManager } from './../managers/extension'
|
||||
* Register of installed extensions
|
||||
* @type {Object.<string, Extension>} extension - List of installed extensions
|
||||
*/
|
||||
const extensions: Record<string, Extension> = {}
|
||||
const extensions: Record<string, Extension> = {};
|
||||
|
||||
/**
|
||||
* Get a extension from the stored extensions.
|
||||
@ -31,10 +21,10 @@ const extensions: Record<string, Extension> = {}
|
||||
*/
|
||||
export function getExtension(name: string) {
|
||||
if (!Object.prototype.hasOwnProperty.call(extensions, name)) {
|
||||
throw new Error(`Extension ${name} does not exist`)
|
||||
throw new Error(`Extension ${name} does not exist`);
|
||||
}
|
||||
|
||||
return extensions[name]
|
||||
return extensions[name];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,7 +33,7 @@ export function getExtension(name: string) {
|
||||
* @alias extensionManager.getAllExtensions
|
||||
*/
|
||||
export function getAllExtensions() {
|
||||
return Object.values(extensions)
|
||||
return Object.values(extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -52,7 +42,7 @@ export function getAllExtensions() {
|
||||
* @alias extensionManager.getActiveExtensions
|
||||
*/
|
||||
export function getActiveExtensions() {
|
||||
return Object.values(extensions).filter((extension) => extension.active)
|
||||
return Object.values(extensions).filter((extension) => extension.active);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -63,9 +53,9 @@ export function getActiveExtensions() {
|
||||
* @alias extensionManager.removeExtension
|
||||
*/
|
||||
export function removeExtension(name: string, persist = true) {
|
||||
const del = delete extensions[name]
|
||||
if (persist) persistExtensions()
|
||||
return del
|
||||
const del = delete extensions[name];
|
||||
if (persist) persistExtensions();
|
||||
return del;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -75,10 +65,10 @@ export function removeExtension(name: string, persist = true) {
|
||||
* @returns {void}
|
||||
*/
|
||||
export function addExtension(extension: Extension, persist = true) {
|
||||
if (extension.name) extensions[extension.name] = extension
|
||||
if (extension.name) extensions[extension.name] = extension;
|
||||
if (persist) {
|
||||
persistExtensions()
|
||||
extension.subscribe('pe-persist', persistExtensions)
|
||||
persistExtensions();
|
||||
extension.subscribe("pe-persist", persistExtensions);
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,15 +77,15 @@ export function addExtension(extension: Extension, persist = true) {
|
||||
* @returns {void}
|
||||
*/
|
||||
export function persistExtensions() {
|
||||
const persistData: Record<string, Extension> = {}
|
||||
const persistData: Record<string, Extension> = {};
|
||||
for (const name in extensions) {
|
||||
persistData[name] = extensions[name]
|
||||
persistData[name] = extensions[name];
|
||||
}
|
||||
writeFileSync(
|
||||
ExtensionManager.instance.getExtensionsFile(),
|
||||
JSON.stringify(persistData),
|
||||
'utf8'
|
||||
)
|
||||
"utf8"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -106,25 +96,25 @@ export function persistExtensions() {
|
||||
* @alias extensionManager.installExtensions
|
||||
*/
|
||||
export async function installExtensions(extensions: any, store = true) {
|
||||
const installed: Extension[] = []
|
||||
const installed: Extension[] = [];
|
||||
for (const ext of extensions) {
|
||||
// Set install options and activation based on input type
|
||||
const isObject = typeof ext === 'object'
|
||||
const spec = isObject ? [ext.specifier, ext] : [ext]
|
||||
const activate = isObject ? ext.activate !== false : true
|
||||
const isObject = typeof ext === "object";
|
||||
const spec = isObject ? [ext.specifier, ext] : [ext];
|
||||
const activate = isObject ? ext.activate !== false : true;
|
||||
|
||||
// Install and possibly activate extension
|
||||
const extension = new Extension(...spec)
|
||||
await extension._install()
|
||||
if (activate) extension.setActive(true)
|
||||
const extension = new Extension(...spec);
|
||||
await extension._install();
|
||||
if (activate) extension.setActive(true);
|
||||
|
||||
// Add extension to store if needed
|
||||
if (store) addExtension(extension)
|
||||
installed.push(extension)
|
||||
if (store) addExtension(extension);
|
||||
installed.push(extension);
|
||||
}
|
||||
|
||||
// Return list of all installed extensions
|
||||
return installed
|
||||
return installed;
|
||||
}
|
||||
|
||||
/**
|
||||
7
core/src/node/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export * from './extension/index'
|
||||
export * from './extension/extension'
|
||||
export * from './extension/manager'
|
||||
export * from './extension/store'
|
||||
export * from './download'
|
||||
export * from './module'
|
||||
export * from './api'
|
||||
@ -1,16 +1,14 @@
|
||||
import { dispose } from "./../utils/disposable";
|
||||
|
||||
/**
|
||||
* Manages imported modules.
|
||||
*/
|
||||
export class ModuleManager {
|
||||
public requiredModules: Record<string, any> = {};
|
||||
public requiredModules: Record<string, any> = {}
|
||||
|
||||
public static instance: ModuleManager = new ModuleManager();
|
||||
public static instance: ModuleManager = new ModuleManager()
|
||||
|
||||
constructor() {
|
||||
if (ModuleManager.instance) {
|
||||
return ModuleManager.instance;
|
||||
return ModuleManager.instance
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,14 +18,13 @@ export class ModuleManager {
|
||||
* @param {any | undefined} nodule - The module to set, or undefined to clear the module.
|
||||
*/
|
||||
setModule(moduleName: string, nodule: any | undefined) {
|
||||
this.requiredModules[moduleName] = nodule;
|
||||
this.requiredModules[moduleName] = nodule
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all imported modules.
|
||||
*/
|
||||
clearImportedModules() {
|
||||
dispose(this.requiredModules);
|
||||
this.requiredModules = {};
|
||||
this.requiredModules = {}
|
||||
}
|
||||
}
|
||||
@ -67,13 +67,6 @@ export type Model = {
|
||||
*/
|
||||
description: string
|
||||
|
||||
/**
|
||||
* The model state.
|
||||
* Default: "to_download"
|
||||
* Enum: "to_download" "downloading" "ready" "running"
|
||||
*/
|
||||
state?: ModelState
|
||||
|
||||
/**
|
||||
* The model settings.
|
||||
*/
|
||||
@ -101,15 +94,6 @@ export type ModelMetadata = {
|
||||
cover?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The Model transition states.
|
||||
*/
|
||||
export enum ModelState {
|
||||
Downloading = 'downloading',
|
||||
Ready = 'ready',
|
||||
Running = 'running',
|
||||
}
|
||||
|
||||
/**
|
||||
* The available model settings.
|
||||
*/
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"target": "es5",
|
||||
"module": "es2015",
|
||||
"module": "ES2020",
|
||||
"lib": ["es2015", "es2016", "es2017", "dom"],
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
|
||||
@ -58,16 +58,16 @@ To reset your installation
|
||||
|
||||
```bash
|
||||
# Newer versions
|
||||
rm -rf /Users/$(whoami)/Library/Application\ Support/jan
|
||||
rm -rf ~/Library/Application\ Support/jan
|
||||
|
||||
# Versions 0.2.0 and older
|
||||
rm -rf /Users/$(whoami)/Library/Application\ Support/jan-electron
|
||||
rm -rf ~/Library/Application\ Support/jan-electron
|
||||
```
|
||||
|
||||
3. Clear Application cache
|
||||
|
||||
```bash
|
||||
rm -rf /Users/$(whoami)/Library/Caches/jan*
|
||||
rm -rf ~/Library/Caches/jan*
|
||||
```
|
||||
|
||||
4. Use the following commands to remove any dangling backend processes:
|
||||
@ -76,7 +76,7 @@ rm -rf /Users/$(whoami)/Library/Caches/jan*
|
||||
ps aux | grep nitro
|
||||
```
|
||||
|
||||
Look for processes like "nitro" and "nitro_arm_64," and kill them one by one with:
|
||||
Look for processes like "nitro" and "nitro_arm_64", and kill them one by one with:
|
||||
|
||||
```bash
|
||||
kill -9 <PID>
|
||||
|
||||
@ -51,13 +51,13 @@ C:\Users\{username}\AppData\Local\Programs\Jan
|
||||
|
||||
## Uninstalling Jan
|
||||
|
||||
To uninstall Jan in Windows, use the [Windows Control Panel](https://support.microsoft.com/en-us/windows/uninstall-or-remove-apps-and-programs-in-windows-4b55f974-2cc6-2d2b-d092-5905080eaf98).
|
||||
To uninstall Jan on Windows, use the [Windows Control Panel](https://support.microsoft.com/en-us/windows/uninstall-or-remove-apps-and-programs-in-windows-4b55f974-2cc6-2d2b-d092-5905080eaf98).
|
||||
|
||||
To remove all user data associated with Jan, you can delete the `/Jan` directory in Windows' [AppData directory](https://superuser.com/questions/632891/what-is-appdata).
|
||||
To remove all user data associated with Jan, you can delete the `/jan` directory in Windows' [AppData directory](https://superuser.com/questions/632891/what-is-appdata).
|
||||
|
||||
```shell
|
||||
# Jan's User Data directory
|
||||
%AppData%\Jan
|
||||
```bash
|
||||
cd C:\Users\%USERNAME%\AppData\Roaming
|
||||
rmdir /S jan
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@ -49,4 +49,4 @@ sudo apt-get remove jan
|
||||
# where jan is the name of Jan package
|
||||
```
|
||||
|
||||
In case you wish to completely remove all user data associated with Jan after uninstallation, you can delete the user data folders located at `$HOME/.config/Jan` and ~/.jan. This will return your system to its state prior to the installation of Jan. This method can also be used to reset all settings if you are experiencing any issues with Jan.
|
||||
In case you wish to completely remove all user data associated with Jan after uninstallation, you can delete the user data folders located at ~/jan. This will return your system to its state prior to the installation of Jan. This method can also be used to reset all settings if you are experiencing any issues with Jan.
|
||||
|
||||
@ -17,11 +17,13 @@ keywords:
|
||||
]
|
||||
---
|
||||
|
||||
A thread is the conversations between an assistant and a user. This tutorial will walk you through the process of starting a thread on Jan.
|
||||
Jan persists your app usage locally on your filesystem, so your data never leaves your computer. This guide will walk you through how to direct manage your application files and start a thread.
|
||||
|
||||
### Setting Thread Title
|
||||
|
||||
A thread title acts as a name for your thread. It appears on the left side of the chat window, helping you easily navigate through your interactions. If you do not set a thread title, Jan will set it to "New Thread" as default.
|
||||
Naming your thread is a good way to keep your conversations organized. It appears on the right side of chat window. Without a thread title, Jan will set it to `New Thread` as default.
|
||||
|
||||

|
||||
|
||||
### Setting Assistant Instructions
|
||||
|
||||
@ -31,10 +33,10 @@ On Jan, you can set assistant instructions that guide the responses of your assi
|
||||
|
||||
Jan offers a variety of models to suit your conversational needs. Each model has its unique characteristics and capabilities. You can choose a model that best fits your needs via the hub and download your preferred model. Then, you can set the model for your thread.
|
||||
|
||||

|
||||
|
||||
### Customizing Model Params
|
||||
|
||||
You can customize the model params for your thread e.g. setting a customized value of max_tokens value for your thread, etc.
|
||||
You can customize parameters for your thread, e.g., `max_tokens`. If you do not set the model params, Jan will set it to the default values as default.
|
||||
|
||||
<br></br>
|
||||
|
||||

|
||||

|
||||
|
||||
@ -14,3 +14,9 @@ keywords:
|
||||
large language model,
|
||||
]
|
||||
---
|
||||
|
||||
:::caution
|
||||
This is currently under development.
|
||||
:::
|
||||
|
||||
A quickstart on how to upload docs to Jan.
|
||||
@ -14,3 +14,9 @@ keywords:
|
||||
large language model,
|
||||
]
|
||||
---
|
||||
|
||||
:::caution
|
||||
This is currently under development.
|
||||
:::
|
||||
|
||||
A quickstart on how to upload images to Jan.
|
||||
@ -26,18 +26,31 @@ Jan offers a convenient and private way to interact with a conversational AI loc
|
||||
4. Scroll up and down to view the entire chat history in the selected thread.
|
||||
|
||||
<br></br>
|
||||

|
||||

|
||||
|
||||
## Managing Threads via Folders
|
||||
|
||||
This feature allows you to directly manage your thread history and configurations.
|
||||
|
||||
1. Navigate to the Thread that you want to manage via the list of threads on the left side of the dashboard.
|
||||
2. Click on the three dots (⋮) on the `Thread` section on the right side of the dashboard. There are two options:
|
||||
|
||||
- `Reveal in Finder` will open the folder containing the thread history and configurations.
|
||||
- `View as JSON` will open the thread.json file in your default browser.
|
||||
|
||||
<br></br>
|
||||

|
||||
|
||||
## Clean Thread
|
||||
|
||||
To streamline your conservation view, click on the three dots (⋮) on the thread you want to clean, then select `Clean Thread`. It will remove all messages from the thread. It is useful if you want to keep the thread settings, but want to remove the messages from the chat window.
|
||||
|
||||
<br></br>
|
||||

|
||||

|
||||
|
||||
## Delete Thread
|
||||
|
||||
To delete a thread, click on the three dots (⋮) on the thread you want to delete, then select `Delete Thread`. It will remove the thread from the list of threads.
|
||||
|
||||
<br></br>
|
||||

|
||||

|
||||
|
||||
BIN
docs/docs/guides/03-chatting/assets/choose-model.png
Normal file
|
After Width: | Height: | Size: 360 KiB |
BIN
docs/docs/guides/03-chatting/assets/clean-thread.gif
Normal file
|
After Width: | Height: | Size: 8.5 MiB |
BIN
docs/docs/guides/03-chatting/assets/customize-model-params.png
Normal file
|
After Width: | Height: | Size: 342 KiB |
|
Before Width: | Height: | Size: 10 MiB After Width: | Height: | Size: 10 MiB |
|
Before Width: | Height: | Size: 7.1 MiB |
|
After Width: | Height: | Size: 18 MiB |
|
After Width: | Height: | Size: 333 KiB |
BIN
docs/docs/guides/03-chatting/assets/setting-thread-title.png
Normal file
|
After Width: | Height: | Size: 342 KiB |
|
Before Width: | Height: | Size: 11 MiB After Width: | Height: | Size: 11 MiB |
@ -1,24 +1,42 @@
|
||||
---
|
||||
title: Import Models Manually
|
||||
slug: /guides/using-models/import-manually
|
||||
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
|
||||
keywords:
|
||||
[
|
||||
Jan AI,
|
||||
Jan,
|
||||
ChatGPT alternative,
|
||||
local AI,
|
||||
private AI,
|
||||
conversational AI,
|
||||
no-subscription fee,
|
||||
large language model,
|
||||
import-models-manually,
|
||||
]
|
||||
---
|
||||
|
||||
:::caution
|
||||
This is currently under development.
|
||||
:::
|
||||
|
||||
{/* Imports */}
|
||||
import Tabs from "@theme/Tabs";
|
||||
import TabItem from "@theme/TabItem";
|
||||
|
||||
Jan is compatible with all GGUF models.
|
||||
|
||||
If you don't see the model you want in the Hub, or if you have a custom model, you can add it to Jan.
|
||||
If you can not find the model you want in the Hub or have a custom model you want to use, you can import it manually.
|
||||
|
||||
In this guide we will use our latest model, [Trinity](https://huggingface.co/janhq/trinity-v1-GGUF), as an example.
|
||||
In this guide, we will show you how to import a GGUF model from [HuggingFace](https://huggingface.co/), using our lastest model, [Trinity](https://huggingface.co/janhq/trinity-v1-GGUF), as an example.
|
||||
|
||||
> We are fast shipping a UI to make this easier, but it's a bit manual for now. Apologies.
|
||||
|
||||
## 1. Create a model folder
|
||||
## Steps to Manually Import a Model
|
||||
|
||||
Navigate to the `~/jan/models` folder on your computer.
|
||||
### 1. Create a Model Folder
|
||||
|
||||
In `App Settings`, go to `Advanced`, then `Open App Directory`.
|
||||
Navigate to the `~/jan/models` folder. You can find this folder by going to `App Settings` > `Advanced` > `Open App Directory`.
|
||||
|
||||
<Tabs groupId="operating-systems">
|
||||
<TabItem value="mac" label="macOS">
|
||||
@ -70,11 +88,11 @@ In the `models` folder, create a folder with the name of the model.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## 2. Create a model JSON
|
||||
### 2. Create a Model JSON
|
||||
|
||||
Jan follows a folder-based, [standard model template](/specs/models) called a `model.json` to persist the model configurations on your local filesystem.
|
||||
Jan follows a folder-based, [standard model template](/docs/engineering/models) called a `model.json` to persist the model configurations on your local filesystem.
|
||||
|
||||
This means you can easily & transparently reconfigure your models and export and share your preferences.
|
||||
This means that you can easily reconfigure your models, export them, and share your preferences transparently.
|
||||
|
||||
<Tabs groupId="operating-systems">
|
||||
<TabItem value="mac" label="macOS">
|
||||
@ -89,7 +107,7 @@ This means you can easily & transparently reconfigure your models and export and
|
||||
|
||||
```sh
|
||||
cd trinity-v1-7b
|
||||
touch model.json
|
||||
echo {} > model.json
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@ -103,45 +121,53 @@ This means you can easily & transparently reconfigure your models and export and
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Copy the following configurations into the `model.json`.
|
||||
Edit `model.json` and include the following configurations:
|
||||
|
||||
1. Make sure the `id` property is the same as the folder name you created.
|
||||
2. Make sure the `source_url` property is the direct binary download link ending in `.gguf`. In HuggingFace, you can find the directl links in `Files and versions` tab.
|
||||
3. Ensure you are using the correct `prompt_template`. This is usually provided in the HuggingFace model's description page.
|
||||
- Ensure the filename must be `model.json`.
|
||||
- Ensure the `id` property matches the folder name you created.
|
||||
- Ensure the GGUF filename should match the `id` property exactly.
|
||||
- Ensure the `source_url` property is the direct binary download link ending in `.gguf`. In HuggingFace, you can find the direct links in `Files and versions` tab.
|
||||
- Ensure you are using the correct `prompt_template`. This is usually provided in the HuggingFace model's description page.
|
||||
- Ensure the `state` property is set to `ready`.
|
||||
|
||||
```js
|
||||
{
|
||||
// highlight-start
|
||||
"source_url": "https://huggingface.co/janhq/trinity-v1-GGUF/resolve/main/trinity-v1.Q4_K_M.gguf",
|
||||
"id": "trinity-v1-7b",
|
||||
// highlight-end
|
||||
"object": "model",
|
||||
"name": "Trinity 7B Q4",
|
||||
"name": "Trinity-v1 7B Q4",
|
||||
"version": "1.0",
|
||||
"description": "Trinity is an experimental model merge of GreenNodeLM & LeoScorpius using the Slerp method. Recommended for daily assistance purposes.",
|
||||
"format": "gguf",
|
||||
"settings": {
|
||||
"ctx_len": 2048,
|
||||
"prompt_template": "<|im_start|>system\n{system_message}<|im_end|>\n<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant"
|
||||
"ctx_len": 4096,
|
||||
// highlight-next-line
|
||||
"prompt_template": "{system_message}\n### Instruction:\n{prompt}\n### Response:"
|
||||
},
|
||||
"parameters": {
|
||||
"max_tokens": 2048
|
||||
"max_tokens": 4096
|
||||
},
|
||||
"metadata": {
|
||||
"author": "Jan",
|
||||
"tags": ["7B", "Merged", "Featured"],
|
||||
"tags": ["7B", "Merged"],
|
||||
"size": 4370000000
|
||||
},
|
||||
// highlight-next-line
|
||||
"state": "ready",
|
||||
"engine": "nitro"
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Download your model
|
||||
### 3. Download the Model
|
||||
|
||||
Restart the Jan application and look for your model in the Hub.
|
||||
Restart Jan and navigate to the Hub. Locate your model and click the `Download` button to download the model binary.
|
||||
|
||||
Click the green `download` button to download your actual model binary. This pulls from the `source_url` you provided above.
|
||||

|
||||
|
||||

|
||||
Your model is now ready to use in Jan.
|
||||
|
||||
There you go! You are ready to use your model.
|
||||
## Assistance and Support
|
||||
|
||||
If you have any questions or want to request for more preconfigured GGUF models, please message us in [Discord](https://discord.gg/Dt7MxDyNNZ).
|
||||
If you have questions or are looking for more preconfigured GGUF models, please feel free to join our [Discord community](https://discord.gg/Dt7MxDyNNZ) for support, updates, and discussions.
|
||||
|
||||
BIN
docs/docs/guides/04-using-models/assets/download-model.png
Normal file
|
After Width: | Height: | Size: 378 KiB |
@ -27,6 +27,6 @@ Jan runs on port `1337` by default, but this can be changed in Settings.
|
||||
Check out the [API Reference](/api-reference) for more information on the API endpoints.
|
||||
|
||||
```
|
||||
curl https://localhost:1337/v1/chat/completions
|
||||
curl http://localhost:1337/v1/chat/completions
|
||||
|
||||
```
|
||||
|
||||
@ -32,7 +32,7 @@ The following steps will help you troubleshoot and resolve issues related to bro
|
||||
</TabItem>
|
||||
<TabItem value="win" label="Windows">
|
||||
|
||||
To uninstall Jan in Windows, use the [Windows Control Panel](https://support.microsoft.com/en-us/windows/uninstall-or-remove-apps-and-programs-in-windows-4b55f974-2cc6-2d2b-d092-5905080eaf98).
|
||||
To uninstall Jan on Windows, use the [Windows Control Panel](https://support.microsoft.com/en-us/windows/uninstall-or-remove-apps-and-programs-in-windows-4b55f974-2cc6-2d2b-d092-5905080eaf98).
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="linux" label="Linux">
|
||||
@ -53,15 +53,15 @@ The following steps will help you troubleshoot and resolve issues related to bro
|
||||
```bash
|
||||
# Step 1: Delete the application data
|
||||
## Newer versions
|
||||
rm -rf /Users/$(whoami)/Library/Application\ Support/jan
|
||||
rm -rf ~/Library/Application\ Support/jan
|
||||
## Versions 0.2.0 and older
|
||||
rm -rf /Users/$(whoami)/Library/Application\ Support/jan-electron
|
||||
rm -rf ~/Library/Application\ Support/jan-electron
|
||||
|
||||
# Step 2: Clear application cache
|
||||
rm -rf /Users/$(whoami)/Library/Caches/jan*
|
||||
rm -rf ~/Library/Caches/jan*
|
||||
|
||||
# Step 3: Remove all user data
|
||||
rm -rf ./jan
|
||||
rm -rf ~/jan
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@ -70,16 +70,15 @@ The following steps will help you troubleshoot and resolve issues related to bro
|
||||
```bash
|
||||
# You can delete the `/Jan` directory in Windows's AppData Directory by visiting the following path `%APPDATA%\Jan`
|
||||
cd C:\Users\%USERNAME%\AppData\Roaming
|
||||
rm -rf ./Jan
|
||||
rmdir /S jan
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="linux" label="Linux">
|
||||
|
||||
```bash
|
||||
# You can delete the user data folders located at the following `$HOME/.config/jan` and `~/.jan`
|
||||
rm -r $HOME/.config/jan
|
||||
rm -r ~/.jan
|
||||
# You can delete the user data folders located at the following `~/jan`
|
||||
rm -rf ~/jan
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@ -119,3 +118,4 @@ The following steps will help you troubleshoot and resolve issues related to bro
|
||||
</Tabs>
|
||||
|
||||
4. Download the latest version from via our homepage, [https://jan.ai/](https://jan.ai/).
|
||||
> Note: If Jan is installed on multiple user accounts on your device, ensure it's completely removed from all shared space before reinstalling.
|
||||
@ -14,7 +14,7 @@ license:
|
||||
name: AGPLv3
|
||||
url: "https://github.com/janhq/nitro/blob/main/LICENSE"
|
||||
servers:
|
||||
- url: "https://localhost:1337/v1/"
|
||||
- url: "http://localhost:1337/v1/"
|
||||
tags:
|
||||
- name: Models
|
||||
description: List and describe the various models available in the API.
|
||||
@ -100,7 +100,7 @@ paths:
|
||||
x-codeSamples:
|
||||
- lang: cURL
|
||||
source: |
|
||||
curl https://localhost:1337/v1/models
|
||||
curl http://localhost:1337/v1/models
|
||||
post:
|
||||
operationId: downloadModel
|
||||
tags:
|
||||
@ -118,7 +118,7 @@ paths:
|
||||
x-codeSamples:
|
||||
- lang: cURL
|
||||
source: |
|
||||
curl -X POST https://localhost:1337/v1/models
|
||||
curl -X POST http://localhost:1337/v1/models
|
||||
"/models/{model_id}":
|
||||
get:
|
||||
operationId: retrieveModel
|
||||
@ -149,7 +149,7 @@ paths:
|
||||
x-codeSamples:
|
||||
- lang: cURL
|
||||
source: |
|
||||
curl https://localhost:1337/v1/models/{model_id}
|
||||
curl http://localhost:1337/v1/models/{model_id}
|
||||
delete:
|
||||
operationId: deleteModel
|
||||
tags:
|
||||
@ -178,7 +178,7 @@ paths:
|
||||
x-codeSamples:
|
||||
- lang: cURL
|
||||
source: |
|
||||
curl -X DELETE https://localhost:1337/v1/models/{model_id}
|
||||
curl -X DELETE http://localhost:1337/v1/models/{model_id}
|
||||
"/models/{model_id}/start":
|
||||
put:
|
||||
operationId: startModel
|
||||
@ -206,7 +206,7 @@ paths:
|
||||
x-codeSamples:
|
||||
- lang: cURL
|
||||
source: |
|
||||
curl -X PUT https://localhost:1337/v1/models/{model_id}/start
|
||||
curl -X PUT http://localhost:1337/v1/models/{model_id}/start
|
||||
"/models/{model_id}/stop":
|
||||
put:
|
||||
operationId: stopModel
|
||||
@ -233,7 +233,7 @@ paths:
|
||||
x-codeSamples:
|
||||
- lang: cURL
|
||||
source: |
|
||||
curl -X PUT https://localhost:1337/v1/models/{model_id}/stop
|
||||
curl -X PUT http://localhost:1337/v1/models/{model_id}/stop
|
||||
/threads:
|
||||
post:
|
||||
operationId: createThread
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import { app, ipcMain, shell, nativeTheme } from 'electron'
|
||||
import { ModuleManager } from './../managers/module'
|
||||
import { join } from 'path'
|
||||
import { ExtensionManager } from './../managers/extension'
|
||||
import { join, basename } from 'path'
|
||||
import { WindowManager } from './../managers/window'
|
||||
import { userSpacePath } from './../utils/path'
|
||||
import { AppRoute } from '@janhq/core'
|
||||
import { getResourcePath } from './../utils/path'
|
||||
import { ExtensionManager, ModuleManager } from '@janhq/core/node'
|
||||
|
||||
export function handleAppIPCs() {
|
||||
/**
|
||||
@ -26,10 +24,6 @@ export function handleAppIPCs() {
|
||||
shell.openPath(userSpacePath)
|
||||
})
|
||||
|
||||
ipcMain.handle(AppRoute.getResourcePath, async (_event) => {
|
||||
return getResourcePath()
|
||||
})
|
||||
|
||||
/**
|
||||
* Opens a URL in the user's default browser.
|
||||
* @param _event - The IPC event object.
|
||||
@ -48,6 +42,20 @@ export function handleAppIPCs() {
|
||||
shell.openPath(url)
|
||||
})
|
||||
|
||||
/**
|
||||
* Joins multiple paths together, respect to the current OS.
|
||||
*/
|
||||
ipcMain.handle(AppRoute.joinPath, async (_event, paths: string[]) =>
|
||||
join(...paths)
|
||||
)
|
||||
|
||||
/**
|
||||
* Retrieve basename from given path, respect to the current OS.
|
||||
*/
|
||||
ipcMain.handle(AppRoute.baseName, async (_event, path: string) =>
|
||||
basename(path)
|
||||
)
|
||||
|
||||
/**
|
||||
* Relaunches the app in production - reload window in development.
|
||||
* @param _event - The IPC event object.
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { app, ipcMain } from 'electron'
|
||||
import { DownloadManager } from './../managers/download'
|
||||
import { resolve, join } from 'path'
|
||||
import { WindowManager } from './../managers/window'
|
||||
import request from 'request'
|
||||
import { createWriteStream } from 'fs'
|
||||
import { createWriteStream, renameSync } from 'fs'
|
||||
import { DownloadEvent, DownloadRoute } from '@janhq/core'
|
||||
const progress = require('request-progress')
|
||||
import { DownloadManager } from '@janhq/core/node'
|
||||
|
||||
export function handleDownloaderIPCs() {
|
||||
/**
|
||||
@ -46,8 +46,16 @@ export function handleDownloaderIPCs() {
|
||||
*/
|
||||
ipcMain.handle(DownloadRoute.downloadFile, async (_event, url, fileName) => {
|
||||
const userDataPath = join(app.getPath('home'), 'jan')
|
||||
if (
|
||||
typeof fileName === 'string' &&
|
||||
(fileName.includes('file:/') || fileName.includes('file:\\'))
|
||||
) {
|
||||
fileName = fileName.replace('file:/', '').replace('file:\\', '')
|
||||
}
|
||||
const destination = resolve(userDataPath, fileName)
|
||||
const rq = request(url)
|
||||
// downloading file to a temp file first
|
||||
const downloadingTempFile = `${destination}.download`
|
||||
|
||||
progress(rq, {})
|
||||
.on('progress', function (state: any) {
|
||||
@ -70,6 +78,9 @@ export function handleDownloaderIPCs() {
|
||||
})
|
||||
.on('end', function () {
|
||||
if (DownloadManager.instance.networkRequests[fileName]) {
|
||||
// Finished downloading, rename temp file to actual file
|
||||
renameSync(downloadingTempFile, destination)
|
||||
|
||||
WindowManager?.instance.currentWindow?.webContents.send(
|
||||
DownloadEvent.onFileDownloadSuccess,
|
||||
{
|
||||
@ -87,7 +98,7 @@ export function handleDownloaderIPCs() {
|
||||
)
|
||||
}
|
||||
})
|
||||
.pipe(createWriteStream(destination))
|
||||
.pipe(createWriteStream(downloadingTempFile))
|
||||
|
||||
DownloadManager.instance.setRequest(fileName, rq)
|
||||
})
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import { ipcMain, webContents } from 'electron'
|
||||
import { readdirSync } from 'fs'
|
||||
import { ModuleManager } from './../managers/module'
|
||||
import { join, extname } from 'path'
|
||||
|
||||
import {
|
||||
getActiveExtensions,
|
||||
getAllExtensions,
|
||||
installExtensions,
|
||||
} from './../extension/store'
|
||||
import { getExtension } from './../extension/store'
|
||||
import { removeExtension } from './../extension/store'
|
||||
import Extension from './../extension/extension'
|
||||
getExtension,
|
||||
removeExtension,
|
||||
getActiveExtensions,
|
||||
ModuleManager
|
||||
} from '@janhq/core/node'
|
||||
|
||||
import { getResourcePath, userSpacePath } from './../utils/path'
|
||||
import { ExtensionRoute } from '@janhq/core'
|
||||
|
||||
@ -81,7 +81,7 @@ export function handleExtensionIPCs() {
|
||||
ExtensionRoute.updateExtension,
|
||||
async (e, extensions, reload) => {
|
||||
// Update all provided extensions
|
||||
const updated: Extension[] = []
|
||||
const updated: any[] = []
|
||||
for (const ext of extensions) {
|
||||
const extension = getExtension(ext)
|
||||
const res = await extension.update()
|
||||
|
||||
37
electron/handlers/fileManager.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { ipcMain } from 'electron'
|
||||
// @ts-ignore
|
||||
import reflect from '@alumna/reflect'
|
||||
|
||||
import { FileManagerRoute } from '@janhq/core'
|
||||
import { userSpacePath, getResourcePath } from './../utils/path'
|
||||
|
||||
/**
|
||||
* Handles file system extensions operations.
|
||||
*/
|
||||
export function handleFileMangerIPCs() {
|
||||
// Handles the 'synceFile' IPC event. This event is triggered to synchronize a file from a source path to a destination path.
|
||||
ipcMain.handle(
|
||||
FileManagerRoute.synceFile,
|
||||
async (_event, src: string, dest: string) => {
|
||||
return reflect({
|
||||
src,
|
||||
dest,
|
||||
recursive: true,
|
||||
delete: false,
|
||||
overwrite: true,
|
||||
errorOnExist: false,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// Handles the 'getUserSpace' IPC event. This event is triggered to get the user space path.
|
||||
ipcMain.handle(
|
||||
FileManagerRoute.getUserSpace,
|
||||
(): Promise<string> => Promise.resolve(userSpacePath)
|
||||
)
|
||||
|
||||
// Handles the 'getResourcePath' IPC event. This event is triggered to get the resource path.
|
||||
ipcMain.handle(FileManagerRoute.getResourcePath, async (_event) => {
|
||||
return getResourcePath()
|
||||
})
|
||||
}
|
||||
@ -1,238 +1,32 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import * as fs from 'fs'
|
||||
import fse from 'fs-extra'
|
||||
import { join } from 'path'
|
||||
import readline from 'readline'
|
||||
import { userSpacePath } from './../utils/path'
|
||||
import { FileSystemRoute } from '@janhq/core'
|
||||
const reflect = require('@alumna/reflect')
|
||||
|
||||
import { FileSystemRoute } from '@janhq/core'
|
||||
import { userSpacePath } from '../utils/path'
|
||||
import { join } from 'path'
|
||||
/**
|
||||
* Handles file system operations.
|
||||
*/
|
||||
export function handleFsIPCs() {
|
||||
/**
|
||||
* Gets the path to the user data directory.
|
||||
* @param event - The event object.
|
||||
* @returns A promise that resolves with the path to the user data directory.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.getUserSpace,
|
||||
(): Promise<string> => Promise.resolve(userSpacePath)
|
||||
)
|
||||
|
||||
/**
|
||||
* Checks whether the path is a directory.
|
||||
* @param event - The event object.
|
||||
* @param path - The path to check.
|
||||
* @returns A promise that resolves with a boolean indicating whether the path is a directory.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.isDirectory,
|
||||
(_event, path: string): Promise<boolean> => {
|
||||
const fullPath = join(userSpacePath, path)
|
||||
return Promise.resolve(
|
||||
fs.existsSync(fullPath) && fs.lstatSync(fullPath).isDirectory()
|
||||
const moduleName = 'fs'
|
||||
Object.values(FileSystemRoute).forEach((route) => {
|
||||
ipcMain.handle(route, async (event, ...args) => {
|
||||
return import(moduleName).then((mdl) =>
|
||||
mdl[route](
|
||||
...args.map((arg) =>
|
||||
typeof arg === 'string' &&
|
||||
(arg.includes(`file:/`) || arg.includes(`file:\\`))
|
||||
? join(
|
||||
userSpacePath,
|
||||
arg
|
||||
.replace(`file://`, '')
|
||||
.replace(`file:/`, '')
|
||||
.replace(`file:\\\\`, '')
|
||||
.replace(`file:\\`, '')
|
||||
)
|
||||
: arg
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Reads a file from the user data directory.
|
||||
* @param event - The event object.
|
||||
* @param path - The path of the file to read.
|
||||
* @returns A promise that resolves with the contents of the file.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.readFile,
|
||||
async (event, path: string): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(join(userSpacePath, path), 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(data)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Checks whether a file exists in the user data directory.
|
||||
* @param event - The event object.
|
||||
* @param path - The path of the file to check.
|
||||
* @returns A promise that resolves with a boolean indicating whether the file exists.
|
||||
*/
|
||||
ipcMain.handle(FileSystemRoute.exists, async (_event, path: string) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fullPath = join(userSpacePath, path)
|
||||
fs.existsSync(fullPath) ? resolve(true) : resolve(false)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Writes data to a file in the user data directory.
|
||||
* @param event - The event object.
|
||||
* @param path - The path of the file to write to.
|
||||
* @param data - The data to write to the file.
|
||||
* @returns A promise that resolves when the file has been written.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.writeFile,
|
||||
async (event, path: string, data: string): Promise<void> => {
|
||||
try {
|
||||
await fs.writeFileSync(join(userSpacePath, path), data, 'utf8')
|
||||
} catch (err) {
|
||||
console.error(`writeFile ${path} result: ${err}`)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a directory in the user data directory.
|
||||
* @param event - The event object.
|
||||
* @param path - The path of the directory to create.
|
||||
* @returns A promise that resolves when the directory has been created.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.mkdir,
|
||||
async (event, path: string): Promise<void> => {
|
||||
try {
|
||||
fs.mkdirSync(join(userSpacePath, path), { recursive: true })
|
||||
} catch (err) {
|
||||
console.error(`mkdir ${path} result: ${err}`)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Removes a directory in the user data directory.
|
||||
* @param event - The event object.
|
||||
* @param path - The path of the directory to remove.
|
||||
* @returns A promise that resolves when the directory is removed successfully.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.rmdir,
|
||||
async (event, path: string): Promise<void> => {
|
||||
try {
|
||||
await fs.rmSync(join(userSpacePath, path), { recursive: true })
|
||||
} catch (err) {
|
||||
console.error(`rmdir ${path} result: ${err}`)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Lists the files in a directory in the user data directory.
|
||||
* @param event - The event object.
|
||||
* @param path - The path of the directory to list files from.
|
||||
* @returns A promise that resolves with an array of file names.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.listFiles,
|
||||
async (event, path: string): Promise<string[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readdir(join(userSpacePath, path), (err, files) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(files)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Deletes a file from the user data folder.
|
||||
* @param _event - The IPC event object.
|
||||
* @param filePath - The path to the file to delete.
|
||||
* @returns A string indicating the result of the operation.
|
||||
*/
|
||||
ipcMain.handle(FileSystemRoute.deleteFile, async (_event, filePath) => {
|
||||
try {
|
||||
await fs.unlinkSync(join(userSpacePath, filePath))
|
||||
} catch (err) {
|
||||
console.error(`unlink ${filePath} result: ${err}`)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Appends data to a file in the user data directory.
|
||||
* @param event - The event object.
|
||||
* @param path - The path of the file to append to.
|
||||
* @param data - The data to append to the file.
|
||||
* @returns A promise that resolves when the file has been written.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.appendFile,
|
||||
async (_event, path: string, data: string) => {
|
||||
try {
|
||||
await fs.appendFileSync(join(userSpacePath, path), data, 'utf8')
|
||||
} catch (err) {
|
||||
console.error(`appendFile ${path} result: ${err}`)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.syncFile,
|
||||
async (_event, src: string, dest: string) => {
|
||||
console.debug(`Copying file from ${src} to ${dest}`)
|
||||
|
||||
return reflect({
|
||||
src,
|
||||
dest,
|
||||
recursive: true,
|
||||
delete: false,
|
||||
overwrite: true,
|
||||
errorOnExist: false,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.copyFile,
|
||||
async (_event, src: string, dest: string) => {
|
||||
console.debug(`Copying file from ${src} to ${dest}`)
|
||||
|
||||
return fse.copySync(src, dest, {
|
||||
overwrite: false,
|
||||
recursive: true,
|
||||
errorOnExist: false,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Reads a file line by line.
|
||||
* @param event - The event object.
|
||||
* @param path - The path of the file to read.
|
||||
* @returns A promise that resolves with the contents of the file.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.readLineByLine,
|
||||
async (_event, path: string) => {
|
||||
const fullPath = join(userSpacePath, path)
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
try {
|
||||
const readInterface = readline.createInterface({
|
||||
input: fs.createReadStream(fullPath),
|
||||
})
|
||||
const lines: any = []
|
||||
readInterface
|
||||
.on('line', function (line) {
|
||||
lines.push(line)
|
||||
})
|
||||
.on('close', function () {
|
||||
res(lines)
|
||||
})
|
||||
} catch (err) {
|
||||
rej(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -7,27 +7,34 @@ import { createUserSpace } from './utils/path'
|
||||
* Managers
|
||||
**/
|
||||
import { WindowManager } from './managers/window'
|
||||
import { ModuleManager } from './managers/module'
|
||||
import { ExtensionManager } from './managers/extension'
|
||||
import { ExtensionManager, ModuleManager } from '@janhq/core/node'
|
||||
|
||||
/**
|
||||
* IPC Handlers
|
||||
**/
|
||||
import { handleDownloaderIPCs } from './handlers/download'
|
||||
import { handleExtensionIPCs } from './handlers/extension'
|
||||
import { handleFileMangerIPCs } from './handlers/fileManager'
|
||||
import { handleAppIPCs } from './handlers/app'
|
||||
import { handleAppUpdates } from './handlers/update'
|
||||
import { handleFsIPCs } from './handlers/fs'
|
||||
import { migrateExtensions } from './utils/migration'
|
||||
|
||||
/**
|
||||
* Server
|
||||
*/
|
||||
import { startServer } from '@janhq/server'
|
||||
|
||||
app
|
||||
.whenReady()
|
||||
.then(createUserSpace)
|
||||
.then(ExtensionManager.instance.migrateExtensions)
|
||||
.then(migrateExtensions)
|
||||
.then(ExtensionManager.instance.setupExtensions)
|
||||
.then(setupMenu)
|
||||
.then(handleIPCs)
|
||||
.then(handleAppUpdates)
|
||||
.then(createMainWindow)
|
||||
.then(startServer)
|
||||
.then(() => {
|
||||
app.on('activate', () => {
|
||||
if (!BrowserWindow.getAllWindows().length) {
|
||||
@ -80,4 +87,5 @@ function handleIPCs() {
|
||||
handleDownloaderIPCs()
|
||||
handleExtensionIPCs()
|
||||
handleAppIPCs()
|
||||
handleFileMangerIPCs()
|
||||
}
|
||||
|
||||
@ -1,85 +0,0 @@
|
||||
import { app } from 'electron'
|
||||
import { init } from './../extension'
|
||||
import { join, resolve } from 'path'
|
||||
import { rmdir } from 'fs'
|
||||
import Store from 'electron-store'
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'fs'
|
||||
import { userSpacePath } from './../utils/path'
|
||||
/**
|
||||
* Manages extension installation and migration.
|
||||
*/
|
||||
export class ExtensionManager {
|
||||
public static instance: ExtensionManager = new ExtensionManager()
|
||||
|
||||
extensionsPath: string | undefined = undefined
|
||||
|
||||
constructor() {
|
||||
if (ExtensionManager.instance) {
|
||||
return ExtensionManager.instance
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the extensions by initializing the `extensions` module with the `confirmInstall` and `extensionsPath` options.
|
||||
* The `confirmInstall` function always returns `true` to allow extension installation.
|
||||
* The `extensionsPath` option specifies the path to install extensions to.
|
||||
*/
|
||||
setupExtensions() {
|
||||
init({
|
||||
// Function to check from the main process that user wants to install a extension
|
||||
confirmInstall: async (_extensions: string[]) => {
|
||||
return true
|
||||
},
|
||||
// Path to install extension to
|
||||
extensionsPath: join(userSpacePath, 'extensions'),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates the extensions by deleting the `extensions` directory in the user data path.
|
||||
* If the `migrated_version` key in the `Store` object does not match the current app version,
|
||||
* the function deletes the `extensions` directory and sets the `migrated_version` key to the current app version.
|
||||
* @returns A Promise that resolves when the migration is complete.
|
||||
*/
|
||||
migrateExtensions() {
|
||||
return new Promise((resolve) => {
|
||||
const store = new Store()
|
||||
if (store.get('migrated_version') !== app.getVersion()) {
|
||||
console.debug('start migration:', store.get('migrated_version'))
|
||||
const fullPath = join(userSpacePath, 'extensions')
|
||||
|
||||
rmdir(fullPath, { recursive: true }, function (err) {
|
||||
if (err) console.error(err)
|
||||
store.set('migrated_version', app.getVersion())
|
||||
console.debug('migrate extensions done')
|
||||
resolve(undefined)
|
||||
})
|
||||
} else {
|
||||
resolve(undefined)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setExtensionsPath(extPath: string) {
|
||||
// Create folder if it does not exist
|
||||
let extDir
|
||||
try {
|
||||
extDir = resolve(extPath)
|
||||
if (extDir.length < 2) throw new Error()
|
||||
|
||||
if (!existsSync(extDir)) mkdirSync(extDir)
|
||||
|
||||
const extensionsJson = join(extDir, 'extensions.json')
|
||||
if (!existsSync(extensionsJson))
|
||||
writeFileSync(extensionsJson, '{}', 'utf8')
|
||||
|
||||
this.extensionsPath = extDir
|
||||
} catch (error) {
|
||||
throw new Error('Invalid path provided to the extensions folder')
|
||||
}
|
||||
}
|
||||
|
||||
getExtensionsFile() {
|
||||
return join(this.extensionsPath ?? '', 'extensions.json')
|
||||
}
|
||||
}
|
||||
@ -72,15 +72,20 @@
|
||||
"dependencies": {
|
||||
"@alumna/reflect": "^1.1.3",
|
||||
"@janhq/core": "link:./core",
|
||||
"@janhq/server": "link:./server",
|
||||
"@npmcli/arborist": "^7.1.0",
|
||||
"@types/request": "^2.48.12",
|
||||
"@uiball/loaders": "^1.3.0",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
"fs-extra": "^11.2.0",
|
||||
"node-fetch": "2",
|
||||
"pacote": "^17.0.4",
|
||||
"request": "^2.88.2",
|
||||
"request-progress": "^3.0.0",
|
||||
"rimraf": "^5.0.5",
|
||||
"typescript": "^5.3.3",
|
||||
"ulid": "^2.3.0",
|
||||
"use-debounce": "^9.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -15,6 +15,9 @@
|
||||
"paths": { "*": ["node_modules/*"] },
|
||||
"typeRoots": ["node_modules/@types"]
|
||||
},
|
||||
"ts-node": {
|
||||
"esm": true
|
||||
},
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["core", "build", "dist", "tests", "node_modules"]
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// @ts-nocheck
|
||||
const { app, Menu, dialog } = require("electron");
|
||||
import { app, Menu, dialog, shell } from "electron";
|
||||
const isMac = process.platform === "darwin";
|
||||
const { autoUpdater } = require("electron-updater");
|
||||
import { compareSemanticVersions } from "./versionDiff";
|
||||
@ -97,7 +97,6 @@ const template: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = [
|
||||
{
|
||||
label: "Learn More",
|
||||
click: async () => {
|
||||
const { shell } = require("electron");
|
||||
await shell.openExternal("https://jan.ai/");
|
||||
},
|
||||
},
|
||||
|
||||
30
electron/utils/migration.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { app } from 'electron'
|
||||
import { join } from 'path'
|
||||
|
||||
import { rmdir } from 'fs'
|
||||
import Store from 'electron-store'
|
||||
import { userSpacePath } from './path'
|
||||
/**
|
||||
* Migrates the extensions by deleting the `extensions` directory in the user data path.
|
||||
* If the `migrated_version` key in the `Store` object does not match the current app version,
|
||||
* the function deletes the `extensions` directory and sets the `migrated_version` key to the current app version.
|
||||
* @returns A Promise that resolves when the migration is complete.
|
||||
*/
|
||||
export function migrateExtensions() {
|
||||
return new Promise((resolve) => {
|
||||
const store = new Store()
|
||||
if (store.get('migrated_version') !== app.getVersion()) {
|
||||
console.debug('start migration:', store.get('migrated_version'))
|
||||
const fullPath = join(userSpacePath, 'extensions')
|
||||
|
||||
rmdir(fullPath, { recursive: true }, function (err) {
|
||||
if (err) console.error(err)
|
||||
store.set('migrated_version', app.getVersion())
|
||||
console.debug('migrate extensions done')
|
||||
resolve(undefined)
|
||||
})
|
||||
} else {
|
||||
resolve(undefined)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -3,17 +3,18 @@ import { AssistantExtension } from "@janhq/core";
|
||||
import { join } from "path";
|
||||
|
||||
export default class JanAssistantExtension implements AssistantExtension {
|
||||
private static readonly _homeDir = "assistants";
|
||||
private static readonly _homeDir = "file://assistants";
|
||||
|
||||
type(): ExtensionType {
|
||||
return ExtensionType.Assistant;
|
||||
}
|
||||
|
||||
onLoad(): void {
|
||||
async onLoad() {
|
||||
// making the assistant directory
|
||||
fs.mkdir(JanAssistantExtension._homeDir).then(() => {
|
||||
this.createJanAssistant();
|
||||
});
|
||||
if (!(await fs.existsSync(JanAssistantExtension._homeDir)))
|
||||
fs.mkdirSync(JanAssistantExtension._homeDir).then(() => {
|
||||
this.createJanAssistant();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -23,12 +24,12 @@ export default class JanAssistantExtension implements AssistantExtension {
|
||||
|
||||
async createAssistant(assistant: Assistant): Promise<void> {
|
||||
const assistantDir = join(JanAssistantExtension._homeDir, assistant.id);
|
||||
await fs.mkdir(assistantDir);
|
||||
if (!(await fs.existsSync(assistantDir))) await fs.mkdirSync(assistantDir);
|
||||
|
||||
// store the assistant metadata json
|
||||
const assistantMetadataPath = join(assistantDir, "assistant.json");
|
||||
try {
|
||||
await fs.writeFile(
|
||||
await fs.writeFileSync(
|
||||
assistantMetadataPath,
|
||||
JSON.stringify(assistant, null, 2)
|
||||
);
|
||||
@ -41,18 +42,14 @@ export default class JanAssistantExtension implements AssistantExtension {
|
||||
// get all the assistant directories
|
||||
// get all the assistant metadata json
|
||||
const results: Assistant[] = [];
|
||||
const allFileName: string[] = await fs.listFiles(
|
||||
const allFileName: string[] = await fs.readdirSync(
|
||||
JanAssistantExtension._homeDir
|
||||
);
|
||||
for (const fileName of allFileName) {
|
||||
const filePath = join(JanAssistantExtension._homeDir, fileName);
|
||||
const isDirectory = await fs.isDirectory(filePath);
|
||||
if (!isDirectory) {
|
||||
// if not a directory, ignore
|
||||
continue;
|
||||
}
|
||||
|
||||
const jsonFiles: string[] = (await fs.listFiles(filePath)).filter(
|
||||
if (filePath.includes(".DS_Store")) continue;
|
||||
const jsonFiles: string[] = (await fs.readdirSync(filePath)).filter(
|
||||
(file: string) => file === "assistant.json"
|
||||
);
|
||||
|
||||
@ -61,9 +58,12 @@ export default class JanAssistantExtension implements AssistantExtension {
|
||||
continue;
|
||||
}
|
||||
|
||||
const assistant: Assistant = JSON.parse(
|
||||
await fs.readFile(join(filePath, jsonFiles[0]))
|
||||
const content = await fs.readFileSync(
|
||||
join(filePath, jsonFiles[0]),
|
||||
"utf-8"
|
||||
);
|
||||
const assistant: Assistant =
|
||||
typeof content === "object" ? content : JSON.parse(content);
|
||||
|
||||
results.push(assistant);
|
||||
}
|
||||
@ -78,7 +78,7 @@ export default class JanAssistantExtension implements AssistantExtension {
|
||||
|
||||
// remove the directory
|
||||
const assistantDir = join(JanAssistantExtension._homeDir, assistant.id);
|
||||
await fs.rmdir(assistantDir);
|
||||
await fs.rmdirSync(assistantDir);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { ExtensionType, fs } from '@janhq/core'
|
||||
import { ExtensionType, fs, joinPath } from '@janhq/core'
|
||||
import { ConversationalExtension } from '@janhq/core'
|
||||
import { Thread, ThreadMessage } from '@janhq/core'
|
||||
import { join } from 'path'
|
||||
|
||||
/**
|
||||
* JSONConversationalExtension is a ConversationalExtension implementation that provides
|
||||
@ -10,7 +9,7 @@ import { join } from 'path'
|
||||
export default class JSONConversationalExtension
|
||||
implements ConversationalExtension
|
||||
{
|
||||
private static readonly _homeDir = 'threads'
|
||||
private static readonly _homeDir = 'file://threads'
|
||||
private static readonly _threadInfoFileName = 'thread.json'
|
||||
private static readonly _threadMessagesFileName = 'messages.jsonl'
|
||||
|
||||
@ -24,8 +23,9 @@ export default class JSONConversationalExtension
|
||||
/**
|
||||
* Called when the extension is loaded.
|
||||
*/
|
||||
onLoad() {
|
||||
fs.mkdir(JSONConversationalExtension._homeDir)
|
||||
async onLoad() {
|
||||
if (!(await fs.existsSync(JSONConversationalExtension._homeDir)))
|
||||
await fs.mkdirSync(JSONConversationalExtension._homeDir)
|
||||
console.debug('JSONConversationalExtension loaded')
|
||||
}
|
||||
|
||||
@ -48,7 +48,9 @@ export default class JSONConversationalExtension
|
||||
const convos = promiseResults
|
||||
.map((result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
return JSON.parse(result.value) as Thread
|
||||
return typeof result.value === 'object'
|
||||
? result.value
|
||||
: JSON.parse(result.value)
|
||||
}
|
||||
})
|
||||
.filter((convo) => convo != null)
|
||||
@ -69,16 +71,19 @@ export default class JSONConversationalExtension
|
||||
*/
|
||||
async saveThread(thread: Thread): Promise<void> {
|
||||
try {
|
||||
const threadDirPath = join(
|
||||
const threadDirPath = await joinPath([
|
||||
JSONConversationalExtension._homeDir,
|
||||
thread.id
|
||||
)
|
||||
const threadJsonPath = join(
|
||||
thread.id,
|
||||
])
|
||||
const threadJsonPath = await joinPath([
|
||||
threadDirPath,
|
||||
JSONConversationalExtension._threadInfoFileName
|
||||
)
|
||||
await fs.mkdir(threadDirPath)
|
||||
await fs.writeFile(threadJsonPath, JSON.stringify(thread, null, 2))
|
||||
JSONConversationalExtension._threadInfoFileName,
|
||||
])
|
||||
if (!(await fs.existsSync(threadDirPath))) {
|
||||
await fs.mkdirSync(threadDirPath)
|
||||
}
|
||||
|
||||
await fs.writeFileSync(threadJsonPath, JSON.stringify(thread))
|
||||
Promise.resolve()
|
||||
} catch (err) {
|
||||
Promise.reject(err)
|
||||
@ -89,22 +94,26 @@ export default class JSONConversationalExtension
|
||||
* Delete a thread with the specified ID.
|
||||
* @param threadId The ID of the thread to delete.
|
||||
*/
|
||||
deleteThread(threadId: string): Promise<void> {
|
||||
return fs.rmdir(join(JSONConversationalExtension._homeDir, `${threadId}`))
|
||||
async deleteThread(threadId: string): Promise<void> {
|
||||
return fs.rmdirSync(
|
||||
await joinPath([JSONConversationalExtension._homeDir, `${threadId}`]),
|
||||
{ recursive: true }
|
||||
)
|
||||
}
|
||||
|
||||
async addNewMessage(message: ThreadMessage): Promise<void> {
|
||||
try {
|
||||
const threadDirPath = join(
|
||||
const threadDirPath = await joinPath([
|
||||
JSONConversationalExtension._homeDir,
|
||||
message.thread_id
|
||||
)
|
||||
const threadMessagePath = join(
|
||||
message.thread_id,
|
||||
])
|
||||
const threadMessagePath = await joinPath([
|
||||
threadDirPath,
|
||||
JSONConversationalExtension._threadMessagesFileName
|
||||
)
|
||||
await fs.mkdir(threadDirPath)
|
||||
await fs.appendFile(threadMessagePath, JSON.stringify(message) + '\n')
|
||||
JSONConversationalExtension._threadMessagesFileName,
|
||||
])
|
||||
if (!(await fs.existsSync(threadDirPath)))
|
||||
await fs.mkdirSync(threadDirPath)
|
||||
await fs.appendFileSync(threadMessagePath, JSON.stringify(message) + '\n')
|
||||
Promise.resolve()
|
||||
} catch (err) {
|
||||
Promise.reject(err)
|
||||
@ -116,13 +125,17 @@ export default class JSONConversationalExtension
|
||||
messages: ThreadMessage[]
|
||||
): Promise<void> {
|
||||
try {
|
||||
const threadDirPath = join(JSONConversationalExtension._homeDir, threadId)
|
||||
const threadMessagePath = join(
|
||||
const threadDirPath = await joinPath([
|
||||
JSONConversationalExtension._homeDir,
|
||||
threadId,
|
||||
])
|
||||
const threadMessagePath = await joinPath([
|
||||
threadDirPath,
|
||||
JSONConversationalExtension._threadMessagesFileName
|
||||
)
|
||||
await fs.mkdir(threadDirPath)
|
||||
await fs.writeFile(
|
||||
JSONConversationalExtension._threadMessagesFileName,
|
||||
])
|
||||
if (!(await fs.existsSync(threadDirPath)))
|
||||
await fs.mkdirSync(threadDirPath)
|
||||
await fs.writeFileSync(
|
||||
threadMessagePath,
|
||||
messages.map((msg) => JSON.stringify(msg)).join('\n') +
|
||||
(messages.length ? '\n' : '')
|
||||
@ -139,12 +152,13 @@ export default class JSONConversationalExtension
|
||||
* @returns data of the thread
|
||||
*/
|
||||
private async readThread(threadDirName: string): Promise<any> {
|
||||
return fs.readFile(
|
||||
join(
|
||||
return fs.readFileSync(
|
||||
await joinPath([
|
||||
JSONConversationalExtension._homeDir,
|
||||
threadDirName,
|
||||
JSONConversationalExtension._threadInfoFileName
|
||||
)
|
||||
JSONConversationalExtension._threadInfoFileName,
|
||||
]),
|
||||
'utf-8'
|
||||
)
|
||||
}
|
||||
|
||||
@ -153,23 +167,19 @@ export default class JSONConversationalExtension
|
||||
* @private
|
||||
*/
|
||||
private async getValidThreadDirs(): Promise<string[]> {
|
||||
const fileInsideThread: string[] = await fs.listFiles(
|
||||
const fileInsideThread: string[] = await fs.readdirSync(
|
||||
JSONConversationalExtension._homeDir
|
||||
)
|
||||
|
||||
const threadDirs: string[] = []
|
||||
for (let i = 0; i < fileInsideThread.length; i++) {
|
||||
const path = join(
|
||||
if (fileInsideThread[i].includes('.DS_Store')) continue
|
||||
const path = await joinPath([
|
||||
JSONConversationalExtension._homeDir,
|
||||
fileInsideThread[i]
|
||||
)
|
||||
const isDirectory = await fs.isDirectory(path)
|
||||
if (!isDirectory) {
|
||||
console.debug(`Ignore ${path} because it is not a directory`)
|
||||
continue
|
||||
}
|
||||
fileInsideThread[i],
|
||||
])
|
||||
|
||||
const isHavingThreadInfo = (await fs.listFiles(path)).includes(
|
||||
const isHavingThreadInfo = (await fs.readdirSync(path)).includes(
|
||||
JSONConversationalExtension._threadInfoFileName
|
||||
)
|
||||
if (!isHavingThreadInfo) {
|
||||
@ -184,25 +194,31 @@ export default class JSONConversationalExtension
|
||||
|
||||
async getAllMessages(threadId: string): Promise<ThreadMessage[]> {
|
||||
try {
|
||||
const threadDirPath = join(JSONConversationalExtension._homeDir, threadId)
|
||||
const isDir = await fs.isDirectory(threadDirPath)
|
||||
if (!isDir) {
|
||||
throw Error(`${threadDirPath} is not directory`)
|
||||
}
|
||||
const threadDirPath = await joinPath([
|
||||
JSONConversationalExtension._homeDir,
|
||||
threadId,
|
||||
])
|
||||
|
||||
const files: string[] = await fs.listFiles(threadDirPath)
|
||||
const files: string[] = await fs.readdirSync(threadDirPath)
|
||||
if (
|
||||
!files.includes(JSONConversationalExtension._threadMessagesFileName)
|
||||
) {
|
||||
throw Error(`${threadDirPath} not contains message file`)
|
||||
}
|
||||
|
||||
const messageFilePath = join(
|
||||
const messageFilePath = await joinPath([
|
||||
threadDirPath,
|
||||
JSONConversationalExtension._threadMessagesFileName
|
||||
)
|
||||
JSONConversationalExtension._threadMessagesFileName,
|
||||
])
|
||||
|
||||
const result = await fs.readLineByLine(messageFilePath)
|
||||
const result = await fs
|
||||
.readFileSync(messageFilePath, 'utf-8')
|
||||
.then((content) =>
|
||||
content
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter((line) => line !== '')
|
||||
)
|
||||
|
||||
const messages: ThreadMessage[] = []
|
||||
result.forEach((line: string) => {
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Check if nvidia-smi exists and is executable
|
||||
if ! command -v nvidia-smi &> /dev/null; then
|
||||
echo "nvidia-smi not found, proceeding with CPU version..."
|
||||
cd linux-cpu
|
||||
./nitro "$@"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# Find the GPU with the highest VRAM
|
||||
readarray -t gpus < <(nvidia-smi --query-gpu=index,memory.total --format=csv,noheader,nounits)
|
||||
maxMemory=0
|
||||
selectedGpuId=0
|
||||
|
||||
for gpu in "${gpus[@]}"; do
|
||||
IFS=, read -ra gpuInfo <<< "$gpu"
|
||||
gpuId=${gpuInfo[0]}
|
||||
gpuMemory=${gpuInfo[1]}
|
||||
if (( gpuMemory > maxMemory )); then
|
||||
maxMemory=$gpuMemory
|
||||
selectedGpuId=$gpuId
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Selected GPU: $selectedGpuId"
|
||||
export CUDA_VISIBLE_DEVICES=$selectedGpuId
|
||||
|
||||
# Attempt to run nitro_linux_amd64_cuda
|
||||
cd linux-cuda
|
||||
if ./nitro "$@"; then
|
||||
exit $?
|
||||
else
|
||||
echo "nitro_linux_amd64_cuda encountered an error, attempting to run nitro_linux_amd64..."
|
||||
cd ../linux-cpu
|
||||
./nitro "$@"
|
||||
exit $?
|
||||
fi
|
||||
@ -1,44 +0,0 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set "maxMemory=0"
|
||||
set "gpuId="
|
||||
|
||||
rem check if nvidia-smi command exist or not
|
||||
where nvidia-smi >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo nvidia-smi not found, proceeding with CPU version...
|
||||
cd win-cuda
|
||||
goto RunCpuVersion
|
||||
)
|
||||
|
||||
set "tempFile=%temp%\nvidia_smi_output.txt"
|
||||
nvidia-smi --query-gpu=index,memory.total --format=csv,noheader,nounits > "%tempFile%"
|
||||
|
||||
for /f "usebackq tokens=1-2 delims=, " %%a in ("%tempFile%") do (
|
||||
set /a memorySize=%%b
|
||||
if !memorySize! gtr !maxMemory! (
|
||||
set "maxMemory=!memorySize!"
|
||||
set "gpuId=%%a"
|
||||
)
|
||||
)
|
||||
|
||||
rem Echo the selected GPU
|
||||
echo Selected GPU: !gpuId!
|
||||
|
||||
rem Set the GPU with the highest VRAM as the visible CUDA device
|
||||
set CUDA_VISIBLE_DEVICES=!gpuId!
|
||||
|
||||
rem Attempt to run nitro_windows_amd64_cuda.exe
|
||||
cd win-cuda
|
||||
nitro.exe %*
|
||||
if %errorlevel% neq 0 goto RunCpuVersion
|
||||
goto End
|
||||
|
||||
:RunCpuVersion
|
||||
rem Run nitro_windows_amd64.exe...
|
||||
cd ..\win-cpu
|
||||
nitro.exe %*
|
||||
|
||||
:End
|
||||
endlocal
|
||||
@ -8,7 +8,7 @@
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||
"downloadnitro:linux": "NITRO_VERSION=$(cat ./bin/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64.tar.gz -e --strip 1 -o ./bin/linux-cpu && chmod +x ./bin/linux-cpu/nitro && chmod +x ./bin/linux-start.sh && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-cuda.tar.gz -e --strip 1 -o ./bin/linux-cuda && chmod +x ./bin/linux-cuda/nitro && chmod +x ./bin/linux-start.sh",
|
||||
"downloadnitro:linux": "NITRO_VERSION=$(cat ./bin/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64.tar.gz -e --strip 1 -o ./bin/linux-cpu && chmod +x ./bin/linux-cpu/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-cuda.tar.gz -e --strip 1 -o ./bin/linux-cuda && chmod +x ./bin/linux-cuda/nitro",
|
||||
"downloadnitro:darwin": "NITRO_VERSION=$(cat ./bin/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-arm64.tar.gz -e --strip 1 -o ./bin/mac-arm64 && chmod +x ./bin/mac-arm64/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-amd64.tar.gz -e --strip 1 -o ./bin/mac-x64 && chmod +x ./bin/mac-x64/nitro",
|
||||
"downloadnitro:win32": "download.bat",
|
||||
"downloadnitro": "run-script-os",
|
||||
|
||||
@ -17,9 +17,9 @@ import {
|
||||
ThreadMessage,
|
||||
events,
|
||||
executeOnMain,
|
||||
getUserSpace,
|
||||
fs,
|
||||
Model,
|
||||
joinPath,
|
||||
} from "@janhq/core";
|
||||
import { InferenceExtension } from "@janhq/core";
|
||||
import { requestInference } from "./helpers/sse";
|
||||
@ -32,7 +32,8 @@ import { join } from "path";
|
||||
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
|
||||
*/
|
||||
export default class JanInferenceNitroExtension implements InferenceExtension {
|
||||
private static readonly _homeDir = "engines";
|
||||
private static readonly _homeDir = "file://engines";
|
||||
private static readonly _settingsDir = "file://settings";
|
||||
private static readonly _engineMetadataFileName = "nitro.json";
|
||||
|
||||
private static _currentModel: Model;
|
||||
@ -58,8 +59,15 @@ export default class JanInferenceNitroExtension implements InferenceExtension {
|
||||
/**
|
||||
* Subscribes to events emitted by the @janhq/core package.
|
||||
*/
|
||||
onLoad(): void {
|
||||
fs.mkdir(JanInferenceNitroExtension._homeDir);
|
||||
async onLoad() {
|
||||
if (!(await fs.existsSync(JanInferenceNitroExtension._homeDir))) {
|
||||
await fs
|
||||
.mkdirSync(JanInferenceNitroExtension._homeDir)
|
||||
.catch((err) => console.debug(err));
|
||||
}
|
||||
|
||||
if (!(await fs.existsSync(JanInferenceNitroExtension._settingsDir)))
|
||||
await fs.mkdirSync(JanInferenceNitroExtension._settingsDir);
|
||||
this.writeDefaultEngineSettings();
|
||||
|
||||
// Events subscription
|
||||
@ -78,6 +86,9 @@ export default class JanInferenceNitroExtension implements InferenceExtension {
|
||||
events.on(EventName.OnInferenceStopped, () => {
|
||||
JanInferenceNitroExtension.handleInferenceStopped(this);
|
||||
});
|
||||
|
||||
// Attempt to fetch nvidia info
|
||||
await executeOnMain(MODULE, "updateNvidiaInfo", {});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,12 +102,12 @@ export default class JanInferenceNitroExtension implements InferenceExtension {
|
||||
JanInferenceNitroExtension._homeDir,
|
||||
JanInferenceNitroExtension._engineMetadataFileName
|
||||
);
|
||||
if (await fs.exists(engineFile)) {
|
||||
JanInferenceNitroExtension._engineSettings = JSON.parse(
|
||||
await fs.readFile(engineFile)
|
||||
);
|
||||
if (await fs.existsSync(engineFile)) {
|
||||
const engine = await fs.readFileSync(engineFile, "utf-8");
|
||||
JanInferenceNitroExtension._engineSettings =
|
||||
typeof engine === "object" ? engine : JSON.parse(engine);
|
||||
} else {
|
||||
await fs.writeFile(
|
||||
await fs.writeFileSync(
|
||||
engineFile,
|
||||
JSON.stringify(JanInferenceNitroExtension._engineSettings, null, 2)
|
||||
);
|
||||
@ -110,8 +121,7 @@ export default class JanInferenceNitroExtension implements InferenceExtension {
|
||||
if (model.engine !== "nitro") {
|
||||
return;
|
||||
}
|
||||
const userSpacePath = await getUserSpace();
|
||||
const modelFullPath = join(userSpacePath, "models", model.id, model.id);
|
||||
const modelFullPath = await joinPath(["models", model.id]);
|
||||
|
||||
const nitroInitResult = await executeOnMain(MODULE, "initModel", {
|
||||
modelFullPath: modelFullPath,
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
const fs = require("fs");
|
||||
const fsPromises = fs.promises;
|
||||
const path = require("path");
|
||||
const { spawn } = require("child_process");
|
||||
const { exec, spawn } = require("child_process");
|
||||
const tcpPortUsed = require("tcp-port-used");
|
||||
const fetchRetry = require("fetch-retry")(global.fetch);
|
||||
const si = require("systeminformation");
|
||||
const { readFileSync, writeFileSync, existsSync } = require("fs");
|
||||
|
||||
// The PORT to use for the Nitro subprocess
|
||||
const PORT = 3928;
|
||||
@ -13,10 +15,32 @@ const NITRO_HTTP_LOAD_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/
|
||||
const NITRO_HTTP_UNLOAD_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/unloadModel`;
|
||||
const NITRO_HTTP_VALIDATE_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/modelstatus`;
|
||||
const NITRO_HTTP_KILL_URL = `${NITRO_HTTP_SERVER_URL}/processmanager/destroy`;
|
||||
const SUPPORTED_MODEL_FORMAT = ".gguf";
|
||||
const NVIDIA_INFO_FILE = path.join(
|
||||
require("os").homedir(),
|
||||
"jan",
|
||||
"settings",
|
||||
"settings.json"
|
||||
);
|
||||
|
||||
const DEFALT_SETTINGS = {
|
||||
"notify": true,
|
||||
"run_mode": "cpu",
|
||||
"nvidia_driver": {
|
||||
"exist": false,
|
||||
"version": ""
|
||||
},
|
||||
"cuda": {
|
||||
"exist": false,
|
||||
"version": ""
|
||||
},
|
||||
"gpus": [],
|
||||
"gpu_highest_vram": ""
|
||||
}
|
||||
|
||||
// The subprocess instance for Nitro
|
||||
let subprocess = undefined;
|
||||
let currentModelFile = undefined;
|
||||
let currentModelFile: string = undefined;
|
||||
let currentSettings = undefined;
|
||||
|
||||
/**
|
||||
@ -28,6 +52,125 @@ function stopModel(): Promise<void> {
|
||||
return killSubprocess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate nvidia and cuda for linux and windows
|
||||
*/
|
||||
async function updateNvidiaDriverInfo(): Promise<void> {
|
||||
exec(
|
||||
"nvidia-smi --query-gpu=driver_version --format=csv,noheader",
|
||||
(error, stdout) => {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf8"));
|
||||
} catch (error) {
|
||||
data = DEFALT_SETTINGS;
|
||||
}
|
||||
|
||||
if (!error) {
|
||||
const firstLine = stdout.split("\n")[0].trim();
|
||||
data["nvidia_driver"].exist = true;
|
||||
data["nvidia_driver"].version = firstLine;
|
||||
} else {
|
||||
data["nvidia_driver"].exist = false;
|
||||
}
|
||||
|
||||
writeFileSync(NVIDIA_INFO_FILE, JSON.stringify(data, null, 2));
|
||||
Promise.resolve();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function checkFileExistenceInPaths(file: string, paths: string[]): boolean {
|
||||
return paths.some((p) => existsSync(path.join(p, file)));
|
||||
}
|
||||
|
||||
function updateCudaExistence() {
|
||||
let files: string[];
|
||||
let paths: string[];
|
||||
|
||||
if (process.platform === "win32") {
|
||||
files = ["cublas64_12.dll", "cudart64_12.dll", "cublasLt64_12.dll"];
|
||||
paths = process.env.PATH ? process.env.PATH.split(path.delimiter) : [];
|
||||
const nitro_cuda_path = path.join(__dirname, "bin", "win-cuda");
|
||||
paths.push(nitro_cuda_path);
|
||||
} else {
|
||||
files = ["libcudart.so.12", "libcublas.so.12", "libcublasLt.so.12"];
|
||||
paths = process.env.LD_LIBRARY_PATH
|
||||
? process.env.LD_LIBRARY_PATH.split(path.delimiter)
|
||||
: [];
|
||||
const nitro_cuda_path = path.join(__dirname, "bin", "linux-cuda");
|
||||
paths.push(nitro_cuda_path);
|
||||
paths.push("/usr/lib/x86_64-linux-gnu/");
|
||||
}
|
||||
|
||||
let cudaExists = files.every(
|
||||
(file) => existsSync(file) || checkFileExistenceInPaths(file, paths)
|
||||
);
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf8"));
|
||||
} catch (error) {
|
||||
data = DEFALT_SETTINGS;
|
||||
}
|
||||
|
||||
data["cuda"].exist = cudaExists;
|
||||
if (cudaExists) {
|
||||
data.run_mode = "gpu";
|
||||
}
|
||||
writeFileSync(NVIDIA_INFO_FILE, JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
async function updateGpuInfo(): Promise<void> {
|
||||
exec(
|
||||
"nvidia-smi --query-gpu=index,memory.total --format=csv,noheader,nounits",
|
||||
(error, stdout) => {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf8"));
|
||||
} catch (error) {
|
||||
data = DEFALT_SETTINGS;
|
||||
}
|
||||
|
||||
if (!error) {
|
||||
// Get GPU info and gpu has higher memory first
|
||||
let highestVram = 0;
|
||||
let highestVramId = "0";
|
||||
let gpus = stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => {
|
||||
let [id, vram] = line.split(", ");
|
||||
vram = vram.replace(/\r/g, "");
|
||||
if (parseFloat(vram) > highestVram) {
|
||||
highestVram = parseFloat(vram);
|
||||
highestVramId = id;
|
||||
}
|
||||
return { id, vram };
|
||||
});
|
||||
|
||||
data["gpus"] = gpus;
|
||||
data["gpu_highest_vram"] = highestVramId;
|
||||
} else {
|
||||
data["gpus"] = [];
|
||||
}
|
||||
|
||||
writeFileSync(NVIDIA_INFO_FILE, JSON.stringify(data, null, 2));
|
||||
Promise.resolve();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function updateNvidiaInfo() {
|
||||
if (process.platform !== "darwin") {
|
||||
await Promise.all([
|
||||
updateNvidiaDriverInfo(),
|
||||
updateCudaExistence(),
|
||||
updateGpuInfo(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a Nitro subprocess to load a machine learning model.
|
||||
* @param wrapper - The model wrapper.
|
||||
@ -37,6 +180,21 @@ function stopModel(): Promise<void> {
|
||||
*/
|
||||
async function initModel(wrapper: any): Promise<ModelOperationResponse> {
|
||||
currentModelFile = wrapper.modelFullPath;
|
||||
const janRoot = path.join(require("os").homedir(), "jan");
|
||||
if (!currentModelFile.includes(janRoot)) {
|
||||
currentModelFile = path.join(janRoot, currentModelFile);
|
||||
}
|
||||
const files: string[] = fs.readdirSync(currentModelFile);
|
||||
|
||||
// Look for GGUF model file
|
||||
const ggufBinFile = files.find(
|
||||
(file) =>
|
||||
file === path.basename(currentModelFile) ||
|
||||
file.toLowerCase().includes(SUPPORTED_MODEL_FORMAT)
|
||||
);
|
||||
|
||||
currentModelFile = path.join(currentModelFile, ggufBinFile);
|
||||
|
||||
if (wrapper.model.engine !== "nitro") {
|
||||
return Promise.resolve({ error: "Not a nitro model" });
|
||||
} else {
|
||||
@ -66,15 +224,31 @@ async function initModel(wrapper: any): Promise<ModelOperationResponse> {
|
||||
async function loadModel(nitroResourceProbe: any | undefined) {
|
||||
// Gather system information for CPU physical cores and memory
|
||||
if (!nitroResourceProbe) nitroResourceProbe = await getResourcesInfo();
|
||||
return killSubprocess()
|
||||
.then(() => spawnNitroProcess(nitroResourceProbe))
|
||||
.then(() => loadLLMModel(currentSettings))
|
||||
.then(validateModelStatus)
|
||||
.catch((err) => {
|
||||
console.error("error: ", err);
|
||||
// TODO: Broadcast error so app could display proper error message
|
||||
return { error: err, currentModelFile };
|
||||
});
|
||||
return (
|
||||
killSubprocess()
|
||||
.then(() => tcpPortUsed.waitUntilFree(PORT, 300, 5000))
|
||||
// wait for 500ms to make sure the port is free for windows platform
|
||||
.then(() => {
|
||||
if (process.platform === "win32") {
|
||||
return sleep(500);
|
||||
} else {
|
||||
return sleep(0);
|
||||
}
|
||||
})
|
||||
.then(() => spawnNitroProcess(nitroResourceProbe))
|
||||
.then(() => loadLLMModel(currentSettings))
|
||||
.then(validateModelStatus)
|
||||
.catch((err) => {
|
||||
console.error("error: ", err);
|
||||
// TODO: Broadcast error so app could display proper error message
|
||||
return { error: err, currentModelFile };
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Add function sleep
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function promptTemplateConverter(promptTemplate) {
|
||||
@ -190,14 +364,26 @@ async function killSubprocess(): Promise<void> {
|
||||
* Using child-process to spawn the process
|
||||
* Should run exactly platform specified Nitro binary version
|
||||
*/
|
||||
/**
|
||||
* Spawns a Nitro subprocess.
|
||||
* @param nitroResourceProbe - The Nitro resource probe.
|
||||
* @returns A promise that resolves when the Nitro subprocess is started.
|
||||
*/
|
||||
function spawnNitroProcess(nitroResourceProbe: any): Promise<any> {
|
||||
console.debug("Starting Nitro subprocess...");
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let binaryFolder = path.join(__dirname, "bin"); // Current directory by default
|
||||
let cudaVisibleDevices = "";
|
||||
let binaryName;
|
||||
|
||||
if (process.platform === "win32") {
|
||||
binaryName = "win-start.bat";
|
||||
let nvida_info = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf8"));
|
||||
if (nvida_info["run_mode"] === "cpu") {
|
||||
binaryFolder = path.join(binaryFolder, "win-cpu");
|
||||
} else {
|
||||
binaryFolder = path.join(binaryFolder, "win-cuda");
|
||||
cudaVisibleDevices = nvida_info["gpu_highest_vram"];
|
||||
}
|
||||
binaryName = "nitro.exe";
|
||||
} else if (process.platform === "darwin") {
|
||||
if (process.arch === "arm64") {
|
||||
binaryFolder = path.join(binaryFolder, "mac-arm64");
|
||||
@ -206,13 +392,24 @@ function spawnNitroProcess(nitroResourceProbe: any): Promise<any> {
|
||||
}
|
||||
binaryName = "nitro";
|
||||
} else {
|
||||
binaryName = "linux-start.sh";
|
||||
let nvida_info = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf8"));
|
||||
if (nvida_info["run_mode"] === "cpu") {
|
||||
binaryFolder = path.join(binaryFolder, "linux-cpu");
|
||||
} else {
|
||||
binaryFolder = path.join(binaryFolder, "linux-cuda");
|
||||
cudaVisibleDevices = nvida_info["gpu_highest_vram"];
|
||||
}
|
||||
binaryName = "nitro";
|
||||
}
|
||||
|
||||
const binaryPath = path.join(binaryFolder, binaryName);
|
||||
// Execute the binary
|
||||
subprocess = spawn(binaryPath, [1, LOCAL_HOST, PORT], {
|
||||
cwd: binaryFolder,
|
||||
env: {
|
||||
...process.env,
|
||||
CUDA_VISIBLE_DEVICES: cudaVisibleDevices,
|
||||
},
|
||||
});
|
||||
|
||||
// Handle subprocess output
|
||||
@ -244,11 +441,11 @@ function spawnNitroProcess(nitroResourceProbe: any): Promise<any> {
|
||||
function getResourcesInfo(): Promise<ResourcesInfo> {
|
||||
return new Promise(async (resolve) => {
|
||||
const cpu = await si.cpu();
|
||||
const mem = await si.mem();
|
||||
// const mem = await si.mem();
|
||||
|
||||
const response = {
|
||||
const response: ResourcesInfo = {
|
||||
numCpuPhysicalCore: cpu.physicalCores,
|
||||
memAvailable: mem.available,
|
||||
memAvailable: 0,
|
||||
};
|
||||
resolve(response);
|
||||
});
|
||||
@ -264,4 +461,5 @@ module.exports = {
|
||||
stopModel,
|
||||
killSubprocess,
|
||||
dispose,
|
||||
updateNvidiaInfo,
|
||||
};
|
||||
|
||||
@ -29,7 +29,7 @@ import { join } from "path";
|
||||
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
|
||||
*/
|
||||
export default class JanInferenceOpenAIExtension implements InferenceExtension {
|
||||
private static readonly _homeDir = "engines";
|
||||
private static readonly _homeDir = "file://engines";
|
||||
private static readonly _engineMetadataFileName = "openai.json";
|
||||
|
||||
private static _currentModel: OpenAIModel;
|
||||
@ -53,8 +53,13 @@ export default class JanInferenceOpenAIExtension implements InferenceExtension {
|
||||
/**
|
||||
* Subscribes to events emitted by the @janhq/core package.
|
||||
*/
|
||||
onLoad(): void {
|
||||
fs.mkdir(JanInferenceOpenAIExtension._homeDir);
|
||||
async onLoad() {
|
||||
if (!(await fs.existsSync(JanInferenceOpenAIExtension._homeDir))) {
|
||||
await fs
|
||||
.mkdirSync(JanInferenceOpenAIExtension._homeDir)
|
||||
.catch((err) => console.debug(err));
|
||||
}
|
||||
|
||||
JanInferenceOpenAIExtension.writeDefaultEngineSettings();
|
||||
|
||||
// Events subscription
|
||||
@ -85,12 +90,12 @@ export default class JanInferenceOpenAIExtension implements InferenceExtension {
|
||||
JanInferenceOpenAIExtension._homeDir,
|
||||
JanInferenceOpenAIExtension._engineMetadataFileName
|
||||
);
|
||||
if (await fs.exists(engineFile)) {
|
||||
JanInferenceOpenAIExtension._engineSettings = JSON.parse(
|
||||
await fs.readFile(engineFile)
|
||||
);
|
||||
if (await fs.existsSync(engineFile)) {
|
||||
const engine = await fs.readFileSync(engineFile, "utf-8");
|
||||
JanInferenceOpenAIExtension._engineSettings =
|
||||
typeof engine === "object" ? engine : JSON.parse(engine);
|
||||
} else {
|
||||
await fs.writeFile(
|
||||
await fs.writeFileSync(
|
||||
engineFile,
|
||||
JSON.stringify(JanInferenceOpenAIExtension._engineSettings, null, 2)
|
||||
);
|
||||
|
||||
@ -34,7 +34,7 @@ import { EngineSettings } from "./@types/global";
|
||||
export default class JanInferenceTritonTrtLLMExtension
|
||||
implements InferenceExtension
|
||||
{
|
||||
private static readonly _homeDir = "engines";
|
||||
private static readonly _homeDir = "file://engines";
|
||||
private static readonly _engineMetadataFileName = "triton_trtllm.json";
|
||||
|
||||
static _currentModel: Model;
|
||||
@ -57,9 +57,9 @@ export default class JanInferenceTritonTrtLLMExtension
|
||||
/**
|
||||
* Subscribes to events emitted by the @janhq/core package.
|
||||
*/
|
||||
onLoad(): void {
|
||||
fs.mkdir(JanInferenceTritonTrtLLMExtension._homeDir);
|
||||
JanInferenceTritonTrtLLMExtension.writeDefaultEngineSettings();
|
||||
async onLoad() {
|
||||
if (!(await fs.existsSync(JanInferenceTritonTrtLLMExtension._homeDir)))
|
||||
JanInferenceTritonTrtLLMExtension.writeDefaultEngineSettings();
|
||||
|
||||
// Events subscription
|
||||
events.on(EventName.OnMessageSent, (data) =>
|
||||
@ -98,12 +98,12 @@ export default class JanInferenceTritonTrtLLMExtension
|
||||
JanInferenceTritonTrtLLMExtension._homeDir,
|
||||
JanInferenceTritonTrtLLMExtension._engineMetadataFileName
|
||||
);
|
||||
if (await fs.exists(engine_json)) {
|
||||
JanInferenceTritonTrtLLMExtension._engineSettings = JSON.parse(
|
||||
await fs.readFile(engine_json)
|
||||
);
|
||||
if (await fs.existsSync(engine_json)) {
|
||||
const engine = await fs.readFileSync(engine_json, "utf-8");
|
||||
JanInferenceTritonTrtLLMExtension._engineSettings =
|
||||
typeof engine === "object" ? engine : JSON.parse(engine);
|
||||
} else {
|
||||
await fs.writeFile(
|
||||
await fs.writeFileSync(
|
||||
engine_json,
|
||||
JSON.stringify(
|
||||
JanInferenceTritonTrtLLMExtension._engineSettings,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@janhq/model-extension",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.17",
|
||||
"description": "Model Management Extension provides model exploration and seamless downloads",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/module.js",
|
||||
|
||||
@ -5,16 +5,21 @@ import {
|
||||
abortDownload,
|
||||
getResourcePath,
|
||||
getUserSpace,
|
||||
InferenceEngine,
|
||||
joinPath,
|
||||
} from '@janhq/core'
|
||||
import { ModelExtension, Model, ModelState } from '@janhq/core'
|
||||
import { join } from 'path'
|
||||
import { ModelExtension, Model } from '@janhq/core'
|
||||
import { baseName } from '@janhq/core/.'
|
||||
|
||||
/**
|
||||
* A extension for models
|
||||
*/
|
||||
export default class JanModelExtension implements ModelExtension {
|
||||
private static readonly _homeDir = 'models'
|
||||
private static readonly _homeDir = 'file://models'
|
||||
private static readonly _modelMetadataFileName = 'model.json'
|
||||
private static readonly _supportedModelFormat = '.gguf'
|
||||
private static readonly _incompletedModelFileName = '.download'
|
||||
private static readonly _offlineInferenceEngine = InferenceEngine.nitro
|
||||
|
||||
/**
|
||||
* Implements type from JanExtension.
|
||||
@ -29,7 +34,7 @@ export default class JanModelExtension implements ModelExtension {
|
||||
* Called when the extension is loaded.
|
||||
* @override
|
||||
*/
|
||||
onLoad(): void {
|
||||
async onLoad() {
|
||||
this.copyModelsToHomeDir()
|
||||
}
|
||||
|
||||
@ -41,11 +46,11 @@ export default class JanModelExtension implements ModelExtension {
|
||||
|
||||
private async copyModelsToHomeDir() {
|
||||
try {
|
||||
if (
|
||||
localStorage.getItem(`${EXTENSION_NAME}-version`) === VERSION &&
|
||||
(await fs.exists(JanModelExtension._homeDir))
|
||||
) {
|
||||
console.debug('Model already migrated')
|
||||
// list all of the files under the home directory
|
||||
|
||||
if (await fs.existsSync(JanModelExtension._homeDir)) {
|
||||
// ignore if the model is already downloaded
|
||||
console.debug('Models already persisted.')
|
||||
return
|
||||
}
|
||||
|
||||
@ -54,10 +59,10 @@ export default class JanModelExtension implements ModelExtension {
|
||||
|
||||
// copy models folder from resources to home directory
|
||||
const resourePath = await getResourcePath()
|
||||
const srcPath = join(resourePath, 'models')
|
||||
const srcPath = await joinPath([resourePath, 'models'])
|
||||
|
||||
const userSpace = await getUserSpace()
|
||||
const destPath = join(userSpace, JanModelExtension._homeDir)
|
||||
const destPath = await joinPath([userSpace, 'models'])
|
||||
|
||||
await fs.syncFile(srcPath, destPath)
|
||||
|
||||
@ -88,11 +93,18 @@ export default class JanModelExtension implements ModelExtension {
|
||||
*/
|
||||
async downloadModel(model: Model): Promise<void> {
|
||||
// create corresponding directory
|
||||
const directoryPath = join(JanModelExtension._homeDir, model.id)
|
||||
await fs.mkdir(directoryPath)
|
||||
const modelDirPath = await joinPath([JanModelExtension._homeDir, model.id])
|
||||
if (!(await fs.existsSync(modelDirPath))) await fs.mkdirSync(modelDirPath)
|
||||
|
||||
// path to model binary
|
||||
const path = join(directoryPath, model.id)
|
||||
// try to retrieve the download file name from the source url
|
||||
// if it fails, use the model ID as the file name
|
||||
const extractedFileName = await model.source_url.split('/').pop()
|
||||
const fileName = extractedFileName
|
||||
.toLowerCase()
|
||||
.endsWith(JanModelExtension._supportedModelFormat)
|
||||
? extractedFileName
|
||||
: model.id
|
||||
const path = await joinPath([modelDirPath, fileName])
|
||||
downloadFile(model.source_url, path)
|
||||
}
|
||||
|
||||
@ -103,9 +115,11 @@ export default class JanModelExtension implements ModelExtension {
|
||||
*/
|
||||
async cancelModelDownload(modelId: string): Promise<void> {
|
||||
return abortDownload(
|
||||
join(JanModelExtension._homeDir, modelId, modelId)
|
||||
).then(() => {
|
||||
fs.deleteFile(join(JanModelExtension._homeDir, modelId, modelId))
|
||||
await joinPath([JanModelExtension._homeDir, modelId, modelId])
|
||||
).then(async () => {
|
||||
fs.unlinkSync(
|
||||
await joinPath([JanModelExtension._homeDir, modelId, modelId])
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@ -116,27 +130,16 @@ export default class JanModelExtension implements ModelExtension {
|
||||
*/
|
||||
async deleteModel(modelId: string): Promise<void> {
|
||||
try {
|
||||
const dirPath = join(JanModelExtension._homeDir, modelId)
|
||||
const dirPath = await joinPath([JanModelExtension._homeDir, modelId])
|
||||
|
||||
// remove all files under dirPath except model.json
|
||||
const files = await fs.listFiles(dirPath)
|
||||
const deletePromises = files.map((fileName: string) => {
|
||||
const files = await fs.readdirSync(dirPath)
|
||||
const deletePromises = files.map(async (fileName: string) => {
|
||||
if (fileName !== JanModelExtension._modelMetadataFileName) {
|
||||
return fs.deleteFile(join(dirPath, fileName))
|
||||
return fs.unlinkSync(await joinPath([dirPath, fileName]))
|
||||
}
|
||||
})
|
||||
await Promise.allSettled(deletePromises)
|
||||
|
||||
// update the state as default
|
||||
const jsonFilePath = join(
|
||||
dirPath,
|
||||
JanModelExtension._modelMetadataFileName
|
||||
)
|
||||
const json = await fs.readFile(jsonFilePath)
|
||||
const model = JSON.parse(json) as Model
|
||||
delete model.state
|
||||
|
||||
await fs.writeFile(jsonFilePath, JSON.stringify(model, null, 2))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
@ -148,24 +151,14 @@ export default class JanModelExtension implements ModelExtension {
|
||||
* @returns A Promise that resolves when the model is saved.
|
||||
*/
|
||||
async saveModel(model: Model): Promise<void> {
|
||||
const jsonFilePath = join(
|
||||
const jsonFilePath = await joinPath([
|
||||
JanModelExtension._homeDir,
|
||||
model.id,
|
||||
JanModelExtension._modelMetadataFileName
|
||||
)
|
||||
JanModelExtension._modelMetadataFileName,
|
||||
])
|
||||
|
||||
try {
|
||||
await fs.writeFile(
|
||||
jsonFilePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
...model,
|
||||
state: ModelState.Ready,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
await fs.writeFileSync(jsonFilePath, JSON.stringify(model, null, 2))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
@ -176,43 +169,70 @@ export default class JanModelExtension implements ModelExtension {
|
||||
* @returns A Promise that resolves with an array of all models.
|
||||
*/
|
||||
async getDownloadedModels(): Promise<Model[]> {
|
||||
const models = await this.getModelsMetadata()
|
||||
return models.filter((model) => model.state === ModelState.Ready)
|
||||
return await this.getModelsMetadata(
|
||||
async (modelDir: string, model: Model) => {
|
||||
if (model.engine !== JanModelExtension._offlineInferenceEngine) {
|
||||
return true
|
||||
}
|
||||
return await fs
|
||||
.readdirSync(await joinPath([JanModelExtension._homeDir, modelDir]))
|
||||
.then((files: string[]) => {
|
||||
// or model binary exists in the directory
|
||||
// model binary name can match model ID or be a .gguf file and not be an incompleted model file
|
||||
return (
|
||||
files.includes(modelDir) ||
|
||||
files.some(
|
||||
(file) =>
|
||||
file
|
||||
.toLowerCase()
|
||||
.includes(JanModelExtension._supportedModelFormat) &&
|
||||
!file.endsWith(JanModelExtension._incompletedModelFileName)
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private async getModelsMetadata(): Promise<Model[]> {
|
||||
private async getModelsMetadata(
|
||||
selector?: (path: string, model: Model) => Promise<boolean>
|
||||
): Promise<Model[]> {
|
||||
try {
|
||||
const filesUnderJanRoot = await fs.listFiles('')
|
||||
if (!filesUnderJanRoot.includes(JanModelExtension._homeDir)) {
|
||||
if (!(await fs.existsSync(JanModelExtension._homeDir))) {
|
||||
console.debug('model folder not found')
|
||||
return []
|
||||
}
|
||||
|
||||
const files: string[] = await fs.listFiles(JanModelExtension._homeDir)
|
||||
const files: string[] = await fs.readdirSync(JanModelExtension._homeDir)
|
||||
|
||||
const allDirectories: string[] = []
|
||||
for (const file of files) {
|
||||
const isDirectory = await fs.isDirectory(
|
||||
join(JanModelExtension._homeDir, file)
|
||||
)
|
||||
if (isDirectory) {
|
||||
allDirectories.push(file)
|
||||
}
|
||||
if (file === '.DS_Store') continue
|
||||
allDirectories.push(file)
|
||||
}
|
||||
|
||||
const readJsonPromises = allDirectories.map((dirName) => {
|
||||
const jsonPath = join(
|
||||
const readJsonPromises = allDirectories.map(async (dirName) => {
|
||||
// filter out directories that don't match the selector
|
||||
|
||||
// read model.json
|
||||
const jsonPath = await joinPath([
|
||||
JanModelExtension._homeDir,
|
||||
dirName,
|
||||
JanModelExtension._modelMetadataFileName
|
||||
)
|
||||
return this.readModelMetadata(jsonPath)
|
||||
JanModelExtension._modelMetadataFileName,
|
||||
])
|
||||
let model = await this.readModelMetadata(jsonPath)
|
||||
model = typeof model === 'object' ? model : JSON.parse(model)
|
||||
|
||||
if (selector && !(await selector?.(dirName, model))) {
|
||||
return
|
||||
}
|
||||
return model
|
||||
})
|
||||
const results = await Promise.allSettled(readJsonPromises)
|
||||
const modelData = results.map((result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
try {
|
||||
return JSON.parse(result.value) as Model
|
||||
return result.value as Model
|
||||
} catch {
|
||||
console.debug(`Unable to parse model metadata: ${result.value}`)
|
||||
return undefined
|
||||
@ -222,6 +242,7 @@ export default class JanModelExtension implements ModelExtension {
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
|
||||
return modelData.filter((e) => !!e)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
@ -230,7 +251,7 @@ export default class JanModelExtension implements ModelExtension {
|
||||
}
|
||||
|
||||
private readModelMetadata(path: string) {
|
||||
return fs.readFile(join(path))
|
||||
return fs.readFileSync(path, 'utf-8')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -18,7 +18,7 @@ export default class JanMonitoringExtension implements MonitoringExtension {
|
||||
/**
|
||||
* Called when the extension is loaded.
|
||||
*/
|
||||
onLoad(): void {}
|
||||
async onLoad() {}
|
||||
|
||||
/**
|
||||
* Called when the extension is unloaded.
|
||||
|
||||
@ -4,11 +4,11 @@ const getResourcesInfo = async () =>
|
||||
new Promise(async (resolve) => {
|
||||
const cpu = await si.cpu();
|
||||
const mem = await si.mem();
|
||||
const gpu = await si.graphics();
|
||||
// const gpu = await si.graphics();
|
||||
const response = {
|
||||
cpu,
|
||||
mem,
|
||||
gpu,
|
||||
// gpu,
|
||||
};
|
||||
resolve(response);
|
||||
});
|
||||
|
||||
23
models/codeninja-1.0-7b/model.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"source_url": "https://huggingface.co/beowolx/CodeNinja-1.0-OpenChat-7B-GGUF/resolve/main/codeninja-1.0-openchat-7b.Q4_K_M.gguf",
|
||||
"id": "codeninja-1.0-7b",
|
||||
"object": "model",
|
||||
"name": "CodeNinja 7B Q4",
|
||||
"version": "1.0",
|
||||
"description": "CodeNinja is finetuned on openchat/openchat-3.5-1210. It is good for codding tasks",
|
||||
"format": "gguf",
|
||||
"settings": {
|
||||
"ctx_len": 4096,
|
||||
"prompt_template": "GPT4 Correct User: {prompt}<|end_of_turn|>GPT4 Correct Assistant:"
|
||||
},
|
||||
"parameters": {
|
||||
"max_tokens": 4096
|
||||
},
|
||||
"metadata": {
|
||||
"author": "Beowolx",
|
||||
"tags": ["7B", "Finetuned"],
|
||||
"size": 4370000000
|
||||
},
|
||||
"engine": "nitro"
|
||||
}
|
||||
|
||||
23
models/magicoder-s-ds-7b/model.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"source_url": "https://huggingface.co/TheBloke/Magicoder-S-DS-6.7B-GGUF/resolve/main/magicoder-s-ds-6.7b.Q4_K_M.gguf",
|
||||
"id": "magicoder-s-ds7b",
|
||||
"object": "model",
|
||||
"name": "Magicoder 7B Q4",
|
||||
"version": "1.0",
|
||||
"description": "Magicoder is a model family, a novel approach to enlightening LLMs with open-source code snippets for generating low-bias and high-quality instruction data for code.",
|
||||
"format": "gguf",
|
||||
"settings": {
|
||||
"ctx_len": 4096,
|
||||
"prompt_template": "@@ Instruction\n{prompt}\n@@ Response"
|
||||
},
|
||||
"parameters": {
|
||||
"max_tokens": 4096
|
||||
},
|
||||
"metadata": {
|
||||
"author": "Ise-uiuc",
|
||||
"tags": ["7B", "Code"],
|
||||
"size": 4080000000
|
||||
},
|
||||
"engine": "nitro"
|
||||
}
|
||||
|
||||
25
models/stealth-v1.2-7b/model.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"source_url": "https://huggingface.co/janhq/stealth-v1.2-GGUF/resolve/main/stealth-v1.2.Q4_K_M.gguf",
|
||||
"id": "stealth-v1.2-7b",
|
||||
"object": "model",
|
||||
"name": "Stealth-v1.2 7B Q4",
|
||||
"version": "1.0",
|
||||
"description": "This is a new experimental family designed to enhance Mathematical and Logical abilities.",
|
||||
"format": "gguf",
|
||||
"settings": {
|
||||
"ctx_len": 4096,
|
||||
"prompt_template": "<|im_start|>system\n{system_message}<|im_end|>\n<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant"
|
||||
},
|
||||
"parameters": {
|
||||
"max_tokens": 4096
|
||||
},
|
||||
"metadata": {
|
||||
"author": "Jan",
|
||||
"tags": [
|
||||
"7B",
|
||||
"Merged"
|
||||
],
|
||||
"size": 4370000000
|
||||
},
|
||||
"engine": "nitro"
|
||||
}
|
||||
@ -4,7 +4,7 @@
|
||||
"object": "model",
|
||||
"name": "Trinity-v1 7B Q4",
|
||||
"version": "1.0",
|
||||
"description": "Trinity is an experimental model merge of GreenNodeLM & LeoScorpius using the Slerp method. Recommended for daily assistance purposes.",
|
||||
"description": "Please use the latest version Trinity v1.2 for the best experience. Trinity is an experimental model merge of GreenNodeLM & LeoScorpius using the Slerp method. Recommended for daily assistance purposes.",
|
||||
"format": "gguf",
|
||||
"settings": {
|
||||
"ctx_len": 4096,
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"uikit",
|
||||
"core",
|
||||
"electron",
|
||||
"web"
|
||||
"web",
|
||||
"server"
|
||||
],
|
||||
"nohoist": [
|
||||
"uikit",
|
||||
@ -16,7 +17,9 @@
|
||||
"electron",
|
||||
"electron/**",
|
||||
"web",
|
||||
"web/**"
|
||||
"web/**",
|
||||
"server",
|
||||
"server/**"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
@ -28,6 +31,7 @@
|
||||
"test-local": "yarn lint && yarn build:test && yarn test",
|
||||
"dev:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit dev",
|
||||
"build:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit build",
|
||||
"build:server": "cd server && yarn install && yarn run build",
|
||||
"build:core": "cd core && yarn install && yarn run build",
|
||||
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
||||
"build:electron": "cpx \"models/**\" \"electron/models/\" && yarn workspace jan build",
|
||||
|
||||
|
Before Width: | Height: | Size: 38 KiB |
60
server/index.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import fastify from "fastify";
|
||||
import dotenv from "dotenv";
|
||||
import { v1Router } from "@janhq/core/node";
|
||||
import path from "path";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const JAN_API_HOST = process.env.JAN_API_HOST || "0.0.0.0";
|
||||
const JAN_API_PORT = Number.parseInt(process.env.JAN_API_PORT || "1337");
|
||||
|
||||
const server = fastify();
|
||||
server.register(require("@fastify/cors"), {});
|
||||
server.register(require("@fastify/swagger"), {
|
||||
mode: "static",
|
||||
specification: {
|
||||
path: "./../docs/openapi/jan.yaml",
|
||||
baseDir: "./../docs/openapi",
|
||||
},
|
||||
});
|
||||
server.register(require("@fastify/swagger-ui"), {
|
||||
routePrefix: "/docs",
|
||||
baseDir: path.join(__dirname, "../..", "./docs/openapi"),
|
||||
uiConfig: {
|
||||
docExpansion: "full",
|
||||
deepLinking: false,
|
||||
},
|
||||
staticCSP: true,
|
||||
transformSpecificationClone: true,
|
||||
});
|
||||
server.register(
|
||||
(childContext, _, done) => {
|
||||
childContext.register(require("@fastify/static"), {
|
||||
root:
|
||||
process.env.EXTENSION_ROOT ||
|
||||
path.join(require("os").homedir(), "jan", "extensions"),
|
||||
wildcard: false,
|
||||
});
|
||||
|
||||
done();
|
||||
},
|
||||
{ prefix: "extensions" }
|
||||
);
|
||||
server.register(v1Router, { prefix: "/v1" });
|
||||
|
||||
export const startServer = () => {
|
||||
server
|
||||
.listen({
|
||||
port: JAN_API_PORT,
|
||||
host: JAN_API_HOST,
|
||||
})
|
||||
.then(() => {
|
||||
console.log(
|
||||
`JAN API listening at: http://${JAN_API_HOST}:${JAN_API_PORT}`
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const stopServer = () => {
|
||||
server.close();
|
||||
};
|
||||
@ -1,19 +1,3 @@
|
||||
import fastify from 'fastify'
|
||||
import dotenv from 'dotenv'
|
||||
import v1API from './v1'
|
||||
const server = fastify()
|
||||
|
||||
dotenv.config()
|
||||
server.register(v1API, {prefix: "/api/v1"})
|
||||
|
||||
|
||||
const JAN_API_PORT = Number.parseInt(process.env.JAN_API_PORT || '1337')
|
||||
const JAN_API_HOST = process.env.JAN_API_HOST || "0.0.0.0"
|
||||
|
||||
server.listen({
|
||||
port: JAN_API_PORT,
|
||||
host: JAN_API_HOST
|
||||
}).then(() => {
|
||||
console.log(`JAN API listening at: http://${JAN_API_HOST}:${JAN_API_PORT}`);
|
||||
})
|
||||
import { startServer } from "./index";
|
||||
|
||||
startServer();
|
||||
|
||||
@ -1,32 +1,39 @@
|
||||
{
|
||||
"name": "jan-server",
|
||||
"name": "@janhq/server",
|
||||
"version": "0.1.3",
|
||||
"main": "./build/main.js",
|
||||
"main": "build/index.js",
|
||||
"types": "build/index.d.ts",
|
||||
"author": "Jan <service@jan.ai>",
|
||||
"license": "AGPL-3.0",
|
||||
"homepage": "https://jan.ai",
|
||||
"description": "Use offline LLMs with your own data. Run open source models like Llama2 or Falcon on your internal computers/servers.",
|
||||
"build": "",
|
||||
"files": [
|
||||
"build/**"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
||||
"test:e2e": "playwright test --workers=1",
|
||||
"dev": "nodemon .",
|
||||
"dev": "tsc --watch & node --watch build/main.js",
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^8.4.2",
|
||||
"@fastify/static": "^6.12.0",
|
||||
"@fastify/swagger": "^8.13.0",
|
||||
"@fastify/swagger-ui": "^2.0.1",
|
||||
"@janhq/core": "link:./core",
|
||||
"dotenv": "^16.3.1",
|
||||
"fastify": "^4.24.3",
|
||||
"request": "^2.88.2",
|
||||
"request-progress": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/body-parser": "^1.19.5",
|
||||
"@types/npmcli__arborist": "^5.6.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||
"@typescript-eslint/parser": "^6.7.3",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"fastify": "^4.24.3",
|
||||
"nodemon": "^3.0.1",
|
||||
"run-script-os": "^1.1.6"
|
||||
},
|
||||
"installConfig": {
|
||||
"hoistingLimits": "workspaces"
|
||||
"run-script-os": "^1.1.6",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,10 +13,12 @@
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": { "*": ["node_modules/*"] },
|
||||
"typeRoots": ["node_modules/@types"]
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"ignoreDeprecations": "5.0",
|
||||
"declaration": true
|
||||
},
|
||||
// "sourceMap": true,
|
||||
|
||||
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["core", "build", "dist", "tests", "node_modules"]
|
||||
}
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
import { FastifyInstance, FastifyPluginAsync, FastifyPluginOptions } from 'fastify'
|
||||
|
||||
const router: FastifyPluginAsync = async (app: FastifyInstance, opts: FastifyPluginOptions) => {
|
||||
//TODO: Add controllers for assistants here
|
||||
// app.get("/", controller)
|
||||
// app.post("/", controller)
|
||||
}
|
||||
export default router;
|
||||
@ -1,11 +0,0 @@
|
||||
import { FastifyInstance, FastifyPluginAsync, FastifyPluginOptions } from 'fastify'
|
||||
|
||||
const router: FastifyPluginAsync = async (app: FastifyInstance, opts: FastifyPluginOptions) => {
|
||||
//TODO: Add controllers for here
|
||||
// app.get("/", controller)
|
||||
|
||||
app.post("/", (req, res) => {
|
||||
req.body
|
||||
})
|
||||
}
|
||||
export default router;
|
||||
@ -1,37 +0,0 @@
|
||||
import assistantsAPI from './assistants'
|
||||
import chatCompletionAPI from './chat'
|
||||
import modelsAPI from './models'
|
||||
import threadsAPI from './threads'
|
||||
|
||||
import { FastifyInstance, FastifyPluginAsync } from 'fastify'
|
||||
|
||||
const router: FastifyPluginAsync = async (app: FastifyInstance, opts) => {
|
||||
app.register(
|
||||
assistantsAPI,
|
||||
{
|
||||
prefix: "/assistants"
|
||||
}
|
||||
)
|
||||
|
||||
app.register(
|
||||
chatCompletionAPI,
|
||||
{
|
||||
prefix: "/chat/completion"
|
||||
}
|
||||
)
|
||||
|
||||
app.register(
|
||||
modelsAPI,
|
||||
{
|
||||
prefix: "/models"
|
||||
}
|
||||
)
|
||||
|
||||
app.register(
|
||||
threadsAPI,
|
||||
{
|
||||
prefix: "/threads"
|
||||
}
|
||||
)
|
||||
}
|
||||
export default router;
|
||||
@ -1,23 +0,0 @@
|
||||
import { RouteHandlerMethod, FastifyRequest, FastifyReply } from 'fastify'
|
||||
import { MODEL_FOLDER_PATH } from "./index"
|
||||
import fs from 'fs/promises'
|
||||
|
||||
const controller: RouteHandlerMethod = async (req: FastifyRequest, res: FastifyReply) => {
|
||||
//TODO: download models impl
|
||||
//Mirror logic from JanModelExtension.downloadModel?
|
||||
let model = req.body.model;
|
||||
|
||||
// Fetching logic
|
||||
// const directoryPath = join(MODEL_FOLDER_PATH, model.id)
|
||||
// await fs.mkdir(directoryPath)
|
||||
|
||||
// const path = join(directoryPath, model.id)
|
||||
// downloadFile(model.source_url, path)
|
||||
// TODO: Different model downloader from different model vendor
|
||||
|
||||
res.status(200).send({
|
||||
status: "Ok"
|
||||
})
|
||||
}
|
||||
|
||||
export default controller;
|
||||
@ -1,61 +0,0 @@
|
||||
|
||||
export const MODEL_FOLDER_PATH = "./data/models"
|
||||
export const _modelMetadataFileName = 'model.json'
|
||||
|
||||
import fs from 'fs/promises'
|
||||
import { Model } from '@janhq/core'
|
||||
import { join } from 'path'
|
||||
|
||||
// map string => model object
|
||||
let modelIndex = new Map<String, Model>();
|
||||
async function buildModelIndex(){
|
||||
let modelIds = await fs.readdir(MODEL_FOLDER_PATH);
|
||||
// TODO: read modelFolders to get model info, mirror JanModelExtension?
|
||||
try{
|
||||
for(let modelId in modelIds){
|
||||
let path = join(MODEL_FOLDER_PATH, modelId)
|
||||
let fileData = await fs.readFile(join(path, _modelMetadataFileName))
|
||||
modelIndex.set(modelId, JSON.parse(fileData.toString("utf-8")) as Model)
|
||||
}
|
||||
}
|
||||
catch(err){
|
||||
console.error("build model index failed. ", err);
|
||||
}
|
||||
}
|
||||
buildModelIndex()
|
||||
|
||||
import { FastifyInstance, FastifyPluginAsync, FastifyPluginOptions } from 'fastify'
|
||||
import downloadModelController from './downloadModel'
|
||||
import { startModel, stopModel } from './modelOp'
|
||||
|
||||
const router: FastifyPluginAsync = async (app: FastifyInstance, opts: FastifyPluginOptions) => {
|
||||
//TODO: Add controllers declaration here
|
||||
|
||||
///////////// CRUD ////////////////
|
||||
// Model listing
|
||||
app.get("/", async (req, res) => {
|
||||
res.status(200).send(
|
||||
modelIndex.values()
|
||||
)
|
||||
})
|
||||
|
||||
// Retrieve model info
|
||||
app.get("/:id", (req, res) => {
|
||||
res.status(200).send(
|
||||
modelIndex.get(req.params.id)
|
||||
)
|
||||
})
|
||||
|
||||
// Delete model
|
||||
app.delete("/:id", (req, res) => {
|
||||
modelIndex.delete(req.params)
|
||||
|
||||
// TODO: delete on disk
|
||||
})
|
||||
|
||||
///////////// Other ops ////////////////
|
||||
app.post("/", downloadModelController)
|
||||
app.put("/start", startModel)
|
||||
app.put("/stop", stopModel)
|
||||
}
|
||||
export default router;
|
||||
@ -1,11 +0,0 @@
|
||||
import {FastifyRequest, FastifyReply} from 'fastify'
|
||||
|
||||
export async function startModel(req: FastifyRequest, res: FastifyReply): Promise<void> {
|
||||
|
||||
|
||||
}
|
||||
|
||||
export async function stopModel(req: FastifyRequest, res: FastifyReply): Promise<void> {
|
||||
|
||||
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
import { FastifyInstance, FastifyPluginAsync, FastifyPluginOptions } from 'fastify'
|
||||
|
||||
const router: FastifyPluginAsync = async (app: FastifyInstance, opts: FastifyPluginOptions) => {
|
||||
//TODO: Add controllers declaration here
|
||||
|
||||
// app.get()
|
||||
}
|
||||
export default router;
|
||||
@ -29,6 +29,13 @@ export default function CardSidebar({
|
||||
|
||||
useClickOutside(() => setMore(false), null, [menu, toggle])
|
||||
|
||||
let openFolderTitle: string = 'Open Containing Folder'
|
||||
if (isMac) {
|
||||
openFolderTitle = 'Reveal in Finder'
|
||||
} else if (isWindows) {
|
||||
openFolderTitle = 'Reveal in File Explorer'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
@ -74,7 +81,7 @@ export default function CardSidebar({
|
||||
>
|
||||
<FolderOpenIcon size={16} className="text-muted-foreground" />
|
||||
<span className="text-bold text-black dark:text-muted-foreground">
|
||||
Reveal in Finder
|
||||
{openFolderTitle}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@ -1,48 +1,33 @@
|
||||
import { FieldValues, UseFormRegister } from 'react-hook-form'
|
||||
import React from 'react'
|
||||
|
||||
import { ModelRuntimeParams } from '@janhq/core'
|
||||
import { Switch } from '@janhq/uikit'
|
||||
|
||||
import { useAtomValue } from 'jotai'
|
||||
|
||||
import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
|
||||
|
||||
import {
|
||||
getActiveThreadIdAtom,
|
||||
getActiveThreadModelRuntimeParamsAtom,
|
||||
} from '@/helpers/atoms/Thread.atom'
|
||||
import { getActiveThreadIdAtom } from '@/helpers/atoms/Thread.atom'
|
||||
|
||||
type Props = {
|
||||
name: string
|
||||
title: string
|
||||
checked: boolean
|
||||
register: UseFormRegister<FieldValues>
|
||||
}
|
||||
|
||||
const Checkbox: React.FC<Props> = ({ name, title, checked, register }) => {
|
||||
const Checkbox: React.FC<Props> = ({ name, title, checked }) => {
|
||||
const { updateModelParameter } = useUpdateModelParameters()
|
||||
const threadId = useAtomValue(getActiveThreadIdAtom)
|
||||
const activeModelParams = useAtomValue(getActiveThreadModelRuntimeParamsAtom)
|
||||
|
||||
const onCheckedChange = (checked: boolean) => {
|
||||
if (!threadId || !activeModelParams) return
|
||||
if (!threadId) return
|
||||
|
||||
const updatedModelParams: ModelRuntimeParams = {
|
||||
...activeModelParams,
|
||||
[name]: checked,
|
||||
}
|
||||
|
||||
updateModelParameter(threadId, updatedModelParams)
|
||||
updateModelParameter(threadId, name, checked)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-between">
|
||||
<label>{title}</label>
|
||||
<Switch
|
||||
checked={checked}
|
||||
{...register(name)}
|
||||
onCheckedChange={onCheckedChange}
|
||||
/>
|
||||
<p className="mb-2 text-sm font-semibold text-gray-600">{title}</p>
|
||||
<Switch checked={checked} onCheckedChange={onCheckedChange} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -9,10 +9,6 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
TooltipArrow,
|
||||
Input,
|
||||
} from '@janhq/uikit'
|
||||
|
||||
@ -32,14 +28,22 @@ import useRecommendedModel from '@/hooks/useRecommendedModel'
|
||||
|
||||
import { toGigabytes } from '@/utils/converter'
|
||||
|
||||
import { activeThreadAtom, threadStatesAtom } from '@/helpers/atoms/Thread.atom'
|
||||
import {
|
||||
activeThreadAtom,
|
||||
getActiveThreadIdAtom,
|
||||
setThreadModelParamsAtom,
|
||||
threadStatesAtom,
|
||||
} from '@/helpers/atoms/Thread.atom'
|
||||
|
||||
export const selectedModelAtom = atom<Model | undefined>(undefined)
|
||||
|
||||
export default function DropdownListSidebar() {
|
||||
const setSelectedModel = useSetAtom(selectedModelAtom)
|
||||
const threadStates = useAtomValue(threadStatesAtom)
|
||||
const activeThreadId = useAtomValue(getActiveThreadIdAtom)
|
||||
const activeThread = useAtomValue(activeThreadAtom)
|
||||
const threadStates = useAtomValue(threadStatesAtom)
|
||||
const setSelectedModel = useSetAtom(selectedModelAtom)
|
||||
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
|
||||
|
||||
const [selected, setSelected] = useState<Model | undefined>()
|
||||
const { setMainViewState } = useMainViewState()
|
||||
const [openAISettings, setOpenAISettings] = useState<
|
||||
@ -58,83 +62,93 @@ export default function DropdownListSidebar() {
|
||||
useEffect(() => {
|
||||
setSelected(recommendedModel)
|
||||
setSelectedModel(recommendedModel)
|
||||
}, [recommendedModel, setSelectedModel])
|
||||
|
||||
if (activeThread) {
|
||||
const finishInit = threadStates[activeThread.id].isFinishInit ?? true
|
||||
if (finishInit) return
|
||||
const modelParams = {
|
||||
...recommendedModel?.parameters,
|
||||
...recommendedModel?.settings,
|
||||
}
|
||||
setThreadModelParams(activeThread.id, modelParams)
|
||||
}
|
||||
}, [
|
||||
recommendedModel,
|
||||
activeThread,
|
||||
setSelectedModel,
|
||||
setThreadModelParams,
|
||||
threadStates,
|
||||
])
|
||||
|
||||
const onValueSelected = useCallback(
|
||||
(modelId: string) => {
|
||||
const model = downloadedModels.find((m) => m.id === modelId)
|
||||
setSelected(model)
|
||||
setSelectedModel(model)
|
||||
|
||||
if (activeThreadId) {
|
||||
const modelParams = {
|
||||
...model?.parameters,
|
||||
...model?.settings,
|
||||
}
|
||||
setThreadModelParams(activeThreadId, modelParams)
|
||||
}
|
||||
},
|
||||
[downloadedModels, setSelectedModel]
|
||||
[downloadedModels, activeThreadId, setSelectedModel, setThreadModelParams]
|
||||
)
|
||||
|
||||
if (!activeThread) {
|
||||
return null
|
||||
}
|
||||
const finishInit = threadStates[activeThread.id].isFinishInit ?? true
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="w-full">
|
||||
<Select
|
||||
disabled={finishInit}
|
||||
value={selected?.id}
|
||||
onValueChange={finishInit ? undefined : onValueSelected}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Choose model to start">
|
||||
{downloadedModels.filter((x) => x.id === selected?.id)[0]?.name}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="right-5 block w-full min-w-[300px] pr-0">
|
||||
<div className="flex w-full items-center space-x-2 px-4 py-2">
|
||||
<MonitorIcon size={20} className="text-muted-foreground" />
|
||||
<span>Local</span>
|
||||
<>
|
||||
<Select value={selected?.id} onValueChange={onValueSelected}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Choose model to start">
|
||||
{downloadedModels.filter((x) => x.id === selected?.id)[0]?.name}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="right-5 block w-full min-w-[300px] pr-0">
|
||||
<div className="flex w-full items-center space-x-2 px-4 py-2">
|
||||
<MonitorIcon size={20} className="text-muted-foreground" />
|
||||
<span>Local</span>
|
||||
</div>
|
||||
<div className="border-b border-border" />
|
||||
{downloadedModels.length === 0 ? (
|
||||
<div className="px-4 py-2">
|
||||
<p>{`Oops, you don't have a model yet.`}</p>
|
||||
</div>
|
||||
<div className="border-b border-border" />
|
||||
{downloadedModels.length === 0 ? (
|
||||
<div className="px-4 py-2">
|
||||
<p>{`Oops, you don't have a model yet.`}</p>
|
||||
</div>
|
||||
) : (
|
||||
<SelectGroup>
|
||||
{downloadedModels.map((x, i) => (
|
||||
<SelectItem
|
||||
key={i}
|
||||
value={x.id}
|
||||
className={twMerge(x.id === selected?.id && 'bg-secondary')}
|
||||
>
|
||||
<div className="flex w-full justify-between">
|
||||
<span className="line-clamp-1 block">{x.name}</span>
|
||||
<span className="font-bold text-muted-foreground">
|
||||
{toGigabytes(x.metadata.size)}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
)}
|
||||
<div className="border-b border-border" />
|
||||
<div className="w-full px-4 py-2">
|
||||
<Button
|
||||
block
|
||||
className="bg-blue-100 font-bold text-blue-600 hover:bg-blue-100 hover:text-blue-600"
|
||||
onClick={() => setMainViewState(MainViewState.Hub)}
|
||||
>
|
||||
Explore The Hub
|
||||
</Button>
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TooltipTrigger>
|
||||
|
||||
{finishInit && (
|
||||
<TooltipContent sideOffset={10}>
|
||||
<span>Start a new thread to change the model</span>
|
||||
<TooltipArrow />
|
||||
</TooltipContent>
|
||||
)}
|
||||
) : (
|
||||
<SelectGroup>
|
||||
{downloadedModels.map((x, i) => (
|
||||
<SelectItem
|
||||
key={i}
|
||||
value={x.id}
|
||||
className={twMerge(x.id === selected?.id && 'bg-secondary')}
|
||||
>
|
||||
<div className="flex w-full justify-between">
|
||||
<span className="line-clamp-1 block">{x.name}</span>
|
||||
<span className="font-bold text-muted-foreground">
|
||||
{toGigabytes(x.metadata.size)}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
)}
|
||||
<div className="border-b border-border" />
|
||||
<div className="w-full px-4 py-2">
|
||||
<Button
|
||||
block
|
||||
className="bg-blue-100 font-bold text-blue-600 hover:bg-blue-100 hover:text-blue-600"
|
||||
onClick={() => setMainViewState(MainViewState.Hub)}
|
||||
>
|
||||
Explore The Hub
|
||||
</Button>
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{selected?.engine === InferenceEngine.openai && (
|
||||
<div className="mt-4">
|
||||
@ -154,6 +168,6 @@ export default function DropdownListSidebar() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
84
web/containers/GPUDriverPromptModal/index.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import React from 'react'
|
||||
|
||||
import { openExternalUrl } from '@janhq/core'
|
||||
|
||||
import {
|
||||
ModalClose,
|
||||
ModalFooter,
|
||||
ModalContent,
|
||||
Modal,
|
||||
ModalTitle,
|
||||
ModalHeader,
|
||||
Button,
|
||||
} from '@janhq/uikit'
|
||||
|
||||
import { useAtom } from 'jotai'
|
||||
|
||||
import { isShowNotificationAtom, useSettings } from '@/hooks/useSettings'
|
||||
|
||||
const GPUDriverPrompt: React.FC = () => {
|
||||
const [showNotification, setShowNotification] = useAtom(
|
||||
isShowNotificationAtom
|
||||
)
|
||||
|
||||
const { saveSettings } = useSettings()
|
||||
const onDoNotShowAgainChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const isChecked = !e.target.checked
|
||||
saveSettings({ notify: isChecked })
|
||||
}
|
||||
|
||||
const openChanged = () => {
|
||||
setShowNotification(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal open={showNotification} onOpenChange={openChanged}>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Missing Nvidia Driver and Cuda Toolkit</ModalTitle>
|
||||
</ModalHeader>
|
||||
<p>
|
||||
It seems like you are missing Nvidia Driver or Cuda Toolkit or both.
|
||||
Please follow the instructions on the{' '}
|
||||
<span
|
||||
className="cursor-pointer text-blue-600"
|
||||
onClick={() =>
|
||||
openExternalUrl('https://developer.nvidia.com/cuda-toolkit')
|
||||
}
|
||||
>
|
||||
NVidia Cuda Toolkit Installation Page
|
||||
</span>{' '}
|
||||
and the{' '}
|
||||
<span
|
||||
className="cursor-pointer text-blue-600"
|
||||
onClick={() =>
|
||||
openExternalUrl('https://www.nvidia.com/Download/index.aspx')
|
||||
}
|
||||
>
|
||||
Nvidia Driver Installation Page
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
id="default-checkbox"
|
||||
type="checkbox"
|
||||
onChange={onDoNotShowAgainChange}
|
||||
className="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600"
|
||||
/>
|
||||
<span>Don't show again</span>
|
||||
</div>
|
||||
<ModalFooter>
|
||||
<div className="flex gap-x-2">
|
||||
<ModalClose asChild>
|
||||
<Button themes="ghost">OK</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default GPUDriverPrompt
|
||||