diff --git a/README.md b/README.md
index 425ea69be..4d07ad55c 100644
--- a/README.md
+++ b/README.md
@@ -43,31 +43,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
If you are like me, always wanting your own ChatGPT and have sufficient coding knowledge, you would watch open sourced @janframework by @0xSage like a "my-own-ai" hawk Still under development, the architecture is really futuristic. The desktop app for Windows, Mac, Linux are… pic.twitter.com/0HrNquhBsL
— Umesh = EG = Educated Guess - NGI doing AI (@trading_indian) January 11, 2024
+
+
+
+
came across @janframework yesterday and it's my fav native Apple Silicon LLM app yet. Love that I can switch to GPT 4 API and offline LLM models seamlessly. Looks promising! https://t.co/gyOX9gHbKQ
i just ran some ai models locally on my laptop using @janhq_ and can't believe how easy and cool it is. so, now i can have the same experience as with ChatGPT, but offline and without any data concerns
+
+Please share your love for Jan on Twitter and tag us [@janframework](https://twitter.com/janframework)! We would love to hear from you!
+
+## YouTube
+
+Watch these amazing videos to see how Jan is being used and loved by the community!
+
+### Run Any Chatbot FREE Locally on Your Computer
+
+
+
+
+
+
+
+### Jan AI: Run Open Source LLM 100% Local with OpenAI endpoints
+
+
+
+
+
+
+
+### Setup Tutorial on Jan.ai. JAN AI: Run open source LLM on local Windows PC. 100% offline LLM and AI.
+
+
+
+
+
+
+
+### Jan.ai: Like Offline ChatGPT on Your Computer 💡
+
+
+
+
+
+
+
+### Jan: Bring AI to your Desktop With 100% Offline AI
+
+
+
+
+
+
+
+### AI on Your Local PC: Install JanAI (ChatGPT alternative) for Enhanced Privacy
+
+
+
+
+
+
+
+### Install Jan to Run LLM Offline and Local First
+
+
+
+
diff --git a/docs/sidebars.js b/docs/sidebars.js
index 02ea7589f..5f69301be 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -51,6 +51,7 @@ const sidebars = {
"how-we-work/website-docs/website-docs",
],
},
+ "acknowledgements",
],
productSidebar: [
{
diff --git a/docs/src/containers/Footer/index.js b/docs/src/containers/Footer/index.js
index 7cd648149..3e62f579a 100644
--- a/docs/src/containers/Footer/index.js
+++ b/docs/src/containers/Footer/index.js
@@ -86,6 +86,10 @@ const menus = [
path: "https://janai.bamboohr.com/careers",
external: true,
},
+ {
+ menu: "Newsletter",
+ path: "/community#newsletter",
+ }
],
},
];
diff --git a/docs/src/styles/tweaks/markdown.scss b/docs/src/styles/tweaks/markdown.scss
index 1093f2318..ade07e35b 100644
--- a/docs/src/styles/tweaks/markdown.scss
+++ b/docs/src/styles/tweaks/markdown.scss
@@ -1,4 +1,10 @@
.theme-doc-markdown {
+ a,
+ p,
+ span,
+ li {
+ @apply leading-loose;
+ }
a {
@apply text-blue-600 dark:text-blue-400;
}
@@ -10,9 +16,9 @@
}
ul,
ol {
- padding-left: 16px;
+ padding-left: 28px;
li {
- @apply leading-normal;
+ @apply leading-loose;
p {
margin-bottom: 0;
}
diff --git a/docs/src/styles/tweaks/sidebar.scss b/docs/src/styles/tweaks/sidebar.scss
index 5508a3bfa..02fed8ce8 100644
--- a/docs/src/styles/tweaks/sidebar.scss
+++ b/docs/src/styles/tweaks/sidebar.scss
@@ -1,12 +1,12 @@
// * Classname from Docusaurus template
// * We just overide the styling with applied class from tailwind
-[class*="docSidebarContainer_"] {
+[class*='docSidebarContainer_'] {
margin-top: 0 !important;
@apply dark:border-gray-800 border-gray-300;
}
-[class*="sidebar_"] {
+[class*='sidebar_'] {
padding-top: 0px !important;
}
@@ -14,32 +14,40 @@
padding-top: 20px !important;
}
-[class*="sidebarViewport_"] {
+[class*='sidebarViewport_'] {
top: 80px !important;
// height: unset !important;
}
-[class*="docItemCol_"] {
+[class*='docItemCol_'] {
@apply lg:px-8;
}
// * Including custom sidebar table of content
.table-of-contents {
- @apply text-base py-0 dark:border-gray-800 border-gray-300;
+ @apply text-sm py-0 dark:border-gray-800 border-gray-300;
}
.menu__caret:before {
background: var(--ifm-menu-link-sublist-icon) 50% / 1.5rem 1.5rem;
}
-[class*="codeBlockContainer_"] {
+[class*='codeBlockContainer_'] {
margin: 4px;
}
-[class*="codeBlockTitle_"] {
+[class*='codeBlockTitle_'] {
border-bottom: 1px solid #52525a !important;
}
-[class*="iconExternalLink_"] {
+[class*='iconExternalLink_'] {
display: none;
}
+
+[class*='docMainContainer'] {
+ @media (min-width: 1440px) {
+ .container {
+ max-width: var(--ifm-container-width-xl);
+ }
+ }
+}
diff --git a/electron/handlers/native.ts b/electron/handlers/native.ts
index 14ead07bd..79fa994bf 100644
--- a/electron/handlers/native.ts
+++ b/electron/handlers/native.ts
@@ -83,4 +83,22 @@ export function handleAppIPCs() {
return filePaths[0]
}
})
+
+ ipcMain.handle(NativeRoute.selectModelFiles, async () => {
+ const mainWindow = WindowManager.instance.currentWindow
+ if (!mainWindow) {
+ console.error('No main window found')
+ return
+ }
+ const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
+ title: 'Select model files',
+ buttonLabel: 'Select',
+ properties: ['openFile', 'multiSelections'],
+ })
+ if (canceled) {
+ return
+ } else {
+ return filePaths
+ }
+ })
}
diff --git a/electron/package.json b/electron/package.json
index a89803077..7cdb98360 100644
--- a/electron/package.json
+++ b/electron/package.json
@@ -15,12 +15,14 @@
"build/**/*.{js,map}",
"pre-install",
"models/**/*",
- "docs/**/*"
+ "docs/**/*",
+ "scripts/**/*"
],
"asarUnpack": [
"pre-install",
"models",
- "docs"
+ "docs",
+ "scripts"
],
"publish": [
{
diff --git a/electron/utils/dev.ts b/electron/utils/dev.ts
index b2a492886..16e5241b6 100644
--- a/electron/utils/dev.ts
+++ b/electron/utils/dev.ts
@@ -8,10 +8,9 @@ export const setupReactDevTool = async () => {
) // Don't use import on top level, since the installer package is dev-only
try {
const name = await installExtension(REACT_DEVELOPER_TOOLS)
- console.log(`Added Extension: ${name}`)
+ console.debug(`Added Extension: ${name}`)
} catch (err) {
- console.log('An error occurred while installing devtools:')
- console.error(err)
+ console.error('An error occurred while installing devtools:', err)
// Only log the error and don't throw it because it's not critical
}
}
diff --git a/electron/utils/log.ts b/electron/utils/log.ts
index 84c185d75..9dcd4563b 100644
--- a/electron/utils/log.ts
+++ b/electron/utils/log.ts
@@ -35,7 +35,7 @@ export function cleanLogs(
console.error('Error deleting log file:', err)
return
}
- console.log(
+ console.debug(
`Deleted log file due to exceeding size limit: ${filePath}`
)
})
@@ -52,7 +52,7 @@ export function cleanLogs(
console.error('Error deleting log file:', err)
return
}
- console.log(`Deleted old log file: ${filePath}`)
+ console.debug(`Deleted old log file: ${filePath}`)
})
}
}
diff --git a/extensions/huggingface-extension/.gitignore b/extensions/huggingface-extension/.gitignore
new file mode 100644
index 000000000..bdf39cc7f
--- /dev/null
+++ b/extensions/huggingface-extension/.gitignore
@@ -0,0 +1,3 @@
+bin
+scripts/convert*
+scripts/gguf-py
diff --git a/extensions/huggingface-extension/.prettierrc b/extensions/huggingface-extension/.prettierrc
new file mode 100644
index 000000000..46f1abcb0
--- /dev/null
+++ b/extensions/huggingface-extension/.prettierrc
@@ -0,0 +1,8 @@
+{
+ "semi": false,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "es5",
+ "endOfLine": "auto",
+ "plugins": ["prettier-plugin-tailwindcss"]
+}
diff --git a/extensions/huggingface-extension/README.md b/extensions/huggingface-extension/README.md
new file mode 100644
index 000000000..ae70eb4ec
--- /dev/null
+++ b/extensions/huggingface-extension/README.md
@@ -0,0 +1,73 @@
+# Create a Jan Plugin using Typescript
+
+Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀
+
+## Create Your Own Plugin
+
+To create your own plugin, you can use this repository as a template! Just follow the below instructions:
+
+1. Click the Use this template button at the top of the repository
+2. Select Create a new repository
+3. Select an owner and name for your new repository
+4. Click Create repository
+5. Clone your new repository
+
+## Initial Setup
+
+After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin.
+
+> [!NOTE]
+>
+> You'll need to have a reasonably modern version of
+> [Node.js](https://nodejs.org) handy. If you are using a version manager like
+> [`nodenv`](https://github.com/nodenv/nodenv) or
+> [`nvm`](https://github.com/nvm-sh/nvm), you can run `nodenv install` in the
+> root of your repository to install the version specified in
+> [`package.json`](./package.json). Otherwise, 20.x or later should work!
+
+1. :hammer_and_wrench: Install the dependencies
+
+ ```bash
+ npm install
+ ```
+
+1. :building_construction: Package the TypeScript for distribution
+
+ ```bash
+ npm run bundle
+ ```
+
+1. :white_check_mark: Check your artifact
+
+ There will be a tgz file in your plugin directory now
+
+## Update the Plugin Metadata
+
+The [`package.json`](package.json) file defines metadata about your plugin, such as
+plugin name, main entry, description and version.
+
+When you copy this repository, update `package.json` with the name, description for your plugin.
+
+## Update the Plugin Code
+
+The [`src/`](./src/) directory is the heart of your plugin! This contains the
+source code that will be run when your plugin extension functions are invoked. You can replace the
+contents of this directory with your own code.
+
+There are a few things to keep in mind when writing your plugin code:
+
+- Most Jan Plugin Extension functions are processed asynchronously.
+ In `index.ts`, you will see that the extension function will return a `Promise`.
+
+ ```typescript
+ import { core } from "@janhq/core";
+
+ function onStart(): Promise {
+ return core.invokePluginFunc(MODULE_PATH, "run", 0);
+ }
+ ```
+
+ For more information about the Jan Plugin Core module, see the
+ [documentation](https://github.com/janhq/jan/blob/main/core/README.md).
+
+So, what are you waiting for? Go ahead and start customizing your plugin!
diff --git a/extensions/huggingface-extension/bin/mac-arm64/quantize b/extensions/huggingface-extension/bin/mac-arm64/quantize
new file mode 100755
index 000000000..f8a149b10
Binary files /dev/null and b/extensions/huggingface-extension/bin/mac-arm64/quantize differ
diff --git a/extensions/huggingface-extension/download.bat b/extensions/huggingface-extension/download.bat
new file mode 100644
index 000000000..de055cb80
--- /dev/null
+++ b/extensions/huggingface-extension/download.bat
@@ -0,0 +1,3 @@
+@echo off
+set /p LLAMA_CPP_VERSION=<./scripts/version.txt
+.\node_modules\.bin\download https://github.com/ggerganov/llama.cpp/archive/refs/tags/%LLAMA_CPP_VERSION%.tar.gz -o . --filename ./scripts/llama.cpp.tar.gz && tar -xzf .\scripts\llama.cpp.tar.gz "llama.cpp-%LLAMA_CPP_VERSION%/convert.py" "llama.cpp-%LLAMA_CPP_VERSION%/convert-hf-to-gguf.py" "llama.cpp-%LLAMA_CPP_VERSION%/gguf-py" && cpx "./llama.cpp-%LLAMA_CPP_VERSION%/**" "scripts" && rimraf "./scripts/llama.cpp.tar.gz" && rimraf "./llama.cpp-%LLAMA_CPP_VERSION%"
\ No newline at end of file
diff --git a/extensions/huggingface-extension/package.json b/extensions/huggingface-extension/package.json
new file mode 100644
index 000000000..e71dc7406
--- /dev/null
+++ b/extensions/huggingface-extension/package.json
@@ -0,0 +1,57 @@
+{
+ "name": "@janhq/huggingface-extension",
+ "version": "1.0.0",
+ "description": "Hugging Face extension for converting HF models to GGUF",
+ "main": "dist/index.js",
+ "node": "dist/node/index.cjs.js",
+ "author": "Jan ",
+ "license": "AGPL-3.0",
+ "scripts": {
+ "build": "tsc --module commonjs && rollup -c rollup.config.ts --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs",
+ "download:llama": "run-script-os",
+ "download:llama:linux": "LLAMA_CPP_VERSION=$(cat ./scripts/version.txt) && download https://github.com/ggerganov/llama.cpp/archive/refs/tags/${LLAMA_CPP_VERSION}.tar.gz -o . --filename ./scripts/llama.cpp.tar.gz && tar -xzf ./scripts/llama.cpp.tar.gz --wildcards '*/convert.py' '*/convert-hf-to-gguf.py' '*/gguf-py' && cpx \"./llama.cpp-$LLAMA_CPP_VERSION/**\" \"scripts\" && rimraf \"./scripts/llama.cpp.tar.gz\" && rimraf \"./llama.cpp-$LLAMA_CPP_VERSION\"",
+ "download:llama:darwin": "LLAMA_CPP_VERSION=$(cat ./scripts/version.txt) && download https://github.com/ggerganov/llama.cpp/archive/refs/tags/${LLAMA_CPP_VERSION}.tar.gz -o . --filename ./scripts/llama.cpp.tar.gz && tar -xzf ./scripts/llama.cpp.tar.gz '*/convert.py' '*/convert-hf-to-gguf.py' '*/gguf-py' && cpx \"./llama.cpp-$LLAMA_CPP_VERSION/**\" \"scripts\" && rimraf \"./scripts/llama.cpp.tar.gz\" && rimraf \"./llama.cpp-$LLAMA_CPP_VERSION\"",
+ "download:llama:win32": "download.bat",
+ "build:publish:linux": "rimraf *.tgz --glob && npm run build && npm run download:llama && cpx \"scripts/**\" \"dist/scripts\" && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../pre-install",
+ "build:publish:darwin": "rimraf *.tgz --glob && npm run build && npm run download:llama && cpx \"scripts/**\" \"dist/scripts\" && cpx \"bin/**\" \"dist/bin\" && ../../.github/scripts/auto-sign.sh && npm pack && cpx *.tgz ../../pre-install",
+ "build:publish:win32": "rimraf *.tgz --glob && npm run build && npm run download:llama && cpx \"scripts/**\" \"dist/scripts\" && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../pre-install",
+ "build:publish": "run-script-os"
+ },
+ "exports": {
+ ".": "./dist/index.js",
+ "./main": "./dist/node/index.cjs.js"
+ },
+ "devDependencies": {
+ "@rollup/plugin-commonjs": "^25.0.7",
+ "@rollup/plugin-json": "^6.1.0",
+ "@rollup/plugin-node-resolve": "^15.2.3",
+ "@rollup/plugin-replace": "^5.0.5",
+ "@rollup/plugin-typescript": "^11.1.6",
+ "@types/node": "^20.11.16",
+ "cpx": "^1.5.0",
+ "download-cli": "^1.1.1",
+ "rimraf": "^5.0.5",
+ "rollup": "^4.9.6",
+ "rollup-plugin-sourcemaps": "^0.6.3",
+ "rollup-plugin-typescript2": "^0.36.0",
+ "run-script-os": "^1.1.6",
+ "typescript": "^5.3.3"
+ },
+ "dependencies": {
+ "@janhq/core": "file:../../core",
+ "hyllama": "^0.1.2",
+ "python-shell": "^5.0.0",
+ "ts-loader": "^9.5.0"
+ },
+ "bundledDependencies": [
+ "python-shell"
+ ],
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "files": [
+ "dist/*",
+ "package.json",
+ "README.md"
+ ]
+}
diff --git a/extensions/huggingface-extension/rollup.config.ts b/extensions/huggingface-extension/rollup.config.ts
new file mode 100644
index 000000000..7ae2c5781
--- /dev/null
+++ b/extensions/huggingface-extension/rollup.config.ts
@@ -0,0 +1,72 @@
+import resolve from '@rollup/plugin-node-resolve'
+import commonjs from '@rollup/plugin-commonjs'
+import sourceMaps from 'rollup-plugin-sourcemaps'
+import typescript from 'rollup-plugin-typescript2'
+import json from '@rollup/plugin-json'
+import replace from '@rollup/plugin-replace'
+
+const packageJson = require('./package.json')
+
+export default [
+ {
+ input: `src/index.ts`,
+ output: [{ file: packageJson.main, format: 'es', sourcemap: true }],
+ // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
+ external: [],
+ watch: {
+ include: 'src/**',
+ },
+ plugins: [
+ replace({
+ EXTENSION_NAME: JSON.stringify(packageJson.name),
+ NODE_MODULE_PATH: JSON.stringify(
+ `${packageJson.name}/${packageJson.node}`
+ ),
+ }),
+ // Allow json resolution
+ json(),
+ // Compile TypeScript files
+ typescript({ useTsconfigDeclarationDir: true }),
+ // Compile TypeScript files
+ // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
+ commonjs(),
+ // Allow node_modules resolution, so you can use 'external' to control
+ // which external modules to include in the bundle
+ // https://github.com/rollup/rollup-plugin-node-resolve#usage
+ resolve({
+ extensions: ['.js', '.ts'],
+ }),
+
+ // Resolve source maps to the original source
+ sourceMaps(),
+ ],
+ },
+ {
+ input: `src/node/index.ts`,
+ output: [
+ { file: 'dist/node/index.cjs.js', format: 'cjs', sourcemap: true },
+ ],
+ // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
+ external: [],
+ watch: {
+ include: 'src/node/**',
+ },
+ plugins: [
+ // Allow json resolution
+ json(),
+ // Compile TypeScript files
+ typescript({ useTsconfigDeclarationDir: true }),
+ // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
+ commonjs(),
+ // Allow node_modules resolution, so you can use 'external' to control
+ // which external modules to include in the bundle
+ // https://github.com/rollup/rollup-plugin-node-resolve#usage
+ resolve({
+ extensions: ['.ts', '.js', '.json'],
+ }),
+
+ // Resolve source maps to the original source
+ sourceMaps(),
+ ],
+ },
+]
diff --git a/extensions/huggingface-extension/scripts/install_deps.py b/extensions/huggingface-extension/scripts/install_deps.py
new file mode 100644
index 000000000..2dfabed07
--- /dev/null
+++ b/extensions/huggingface-extension/scripts/install_deps.py
@@ -0,0 +1,14 @@
+import subprocess
+import sys
+
+deps = [
+ 'numpy~=1.24.4',
+ 'sentencepiece~=0.1.98',
+ 'transformers>=4.35.2,<5.0.0',
+ 'gguf>=0.1.0',
+ 'protobuf>=4.21.0,<5.0.0',
+ 'torch~=2.1.1',
+ 'packaging>=20.0',
+ 'tiktoken~=0.5.0'
+]
+subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--upgrade', '--force-reinstall', *deps])
diff --git a/extensions/huggingface-extension/scripts/version.txt b/extensions/huggingface-extension/scripts/version.txt
new file mode 100644
index 000000000..f743d6c4a
--- /dev/null
+++ b/extensions/huggingface-extension/scripts/version.txt
@@ -0,0 +1 @@
+b2106
\ No newline at end of file
diff --git a/extensions/huggingface-extension/src/@types/global.d.ts b/extensions/huggingface-extension/src/@types/global.d.ts
new file mode 100644
index 000000000..495ecf00e
--- /dev/null
+++ b/extensions/huggingface-extension/src/@types/global.d.ts
@@ -0,0 +1,2 @@
+declare const EXTENSION_NAME: string
+declare const NODE_MODULE_PATH: string
diff --git a/extensions/huggingface-extension/src/index.ts b/extensions/huggingface-extension/src/index.ts
new file mode 100644
index 000000000..d8f755080
--- /dev/null
+++ b/extensions/huggingface-extension/src/index.ts
@@ -0,0 +1,396 @@
+import {
+ fs,
+ downloadFile,
+ abortDownload,
+ joinPath,
+ HuggingFaceExtension,
+ HuggingFaceRepoData,
+ executeOnMain,
+ Quantization,
+ Model,
+ InferenceEngine,
+ getJanDataFolderPath,
+ events,
+ DownloadEvent,
+ log,
+} from '@janhq/core'
+import { ggufMetadata } from 'hyllama'
+
+declare global {
+ interface Window {
+ electronAPI?: any
+ }
+}
+
+/**
+ * A extension for models
+ */
+export default class JanHuggingFaceExtension extends HuggingFaceExtension {
+ private static readonly _safetensorsRegexs = [
+ /model\.safetensors$/,
+ /model-[0-9]+-of-[0-9]+\.safetensors$/,
+ ]
+ private static readonly _pytorchRegexs = [
+ /pytorch_model\.bin$/,
+ /consolidated\.[0-9]+\.pth$/,
+ /pytorch_model-[0-9]+-of-[0-9]+\.bin$/,
+ /.*\.pt$/,
+ ]
+ interrupted = false
+
+ /**
+ * Called when the extension is loaded.
+ * @override
+ */
+ onLoad() {}
+
+ /**
+ * Called when the extension is unloaded.
+ * @override
+ */
+ onUnload(): void {}
+
+ private getFileList(repoData: HuggingFaceRepoData): string[] {
+ // SafeTensors first, if not, then PyTorch
+ const modelFiles = repoData.siblings
+ .map((file) => file.rfilename)
+ .filter((file) =>
+ JanHuggingFaceExtension._safetensorsRegexs.some((regex) =>
+ regex.test(file)
+ )
+ )
+ if (modelFiles.length === 0) {
+ repoData.siblings.forEach((file) => {
+ if (
+ JanHuggingFaceExtension._pytorchRegexs.some((regex) =>
+ regex.test(file.rfilename)
+ )
+ ) {
+ modelFiles.push(file.rfilename)
+ }
+ })
+ }
+
+ const vocabFiles = [
+ 'tokenizer.model',
+ 'vocab.json',
+ 'tokenizer.json',
+ ].filter((file) =>
+ repoData.siblings.some((sibling) => sibling.rfilename === file)
+ )
+
+ const etcFiles = repoData.siblings
+ .map((file) => file.rfilename)
+ .filter(
+ (file) =>
+ (file.endsWith('.json') && !vocabFiles.includes(file)) ||
+ file.endsWith('.txt') ||
+ file.endsWith('.py') ||
+ file.endsWith('.tiktoken')
+ )
+
+ return [...modelFiles, ...vocabFiles, ...etcFiles]
+ }
+
+ private async getModelDirPath(repoID: string): Promise {
+ const modelName = repoID.split('/').slice(1).join('/')
+ return joinPath([await getJanDataFolderPath(), 'models', modelName])
+ }
+ private async getConvertedModelPath(repoID: string): Promise {
+ const modelName = repoID.split('/').slice(1).join('/')
+ const modelDirPath = await this.getModelDirPath(repoID)
+ return joinPath([modelDirPath, modelName + '.gguf'])
+ }
+ private async getQuantizedModelPath(
+ repoID: string,
+ quantization: Quantization
+ ): Promise {
+ const modelName = repoID.split('/').slice(1).join('/')
+ const modelDirPath = await this.getModelDirPath(repoID)
+ return joinPath([
+ modelDirPath,
+ modelName + `-${quantization.toLowerCase()}.gguf`,
+ ])
+ }
+ private getCtxLength(config: {
+ max_sequence_length?: number
+ max_position_embeddings?: number
+ n_ctx?: number
+ }): number {
+ if (config.max_sequence_length) return config.max_sequence_length
+ if (config.max_position_embeddings) return config.max_position_embeddings
+ if (config.n_ctx) return config.n_ctx
+ return 4096
+ }
+
+ /**
+ * Downloads a Hugging Face model.
+ * @param repoID - The repo ID of the model to convert.
+ * @param repoData - The repo data of the model to convert.
+ * @param network - Optional object to specify proxy/whether to ignore SSL certificates.
+ * @returns A promise that resolves when the download is complete.
+ */
+ async downloadModelFiles(
+ repoID: string,
+ repoData: HuggingFaceRepoData,
+ network?: { ignoreSSL?: boolean; proxy?: string }
+ ): Promise {
+ if (this.interrupted) return
+ const modelDirPath = await this.getModelDirPath(repoID)
+ if (!(await fs.existsSync(modelDirPath))) await fs.mkdirSync(modelDirPath)
+ const files = this.getFileList(repoData)
+ const filePaths: string[] = []
+
+ for (const file of files) {
+ const filePath = file
+ const localPath = await joinPath([modelDirPath, filePath])
+ const url = `https://huggingface.co/${repoID}/resolve/main/${filePath}`
+
+ if (this.interrupted) return
+ if (!(await fs.existsSync(localPath))) {
+ downloadFile(url, localPath, network)
+ filePaths.push(filePath)
+ }
+ }
+
+ await new Promise((resolve, reject) => {
+ if (filePaths.length === 0) resolve()
+ const onDownloadSuccess = async ({ fileName }: { fileName: string }) => {
+ if (filePaths.includes(fileName)) {
+ filePaths.splice(filePaths.indexOf(fileName), 1)
+ if (filePaths.length === 0) {
+ events.off(DownloadEvent.onFileDownloadSuccess, onDownloadSuccess)
+ events.off(DownloadEvent.onFileDownloadError, onDownloadError)
+ resolve()
+ }
+ }
+ }
+
+ const onDownloadError = async ({
+ fileName,
+ error,
+ }: {
+ fileName: string
+ error: Error
+ }) => {
+ if (filePaths.includes(fileName)) {
+ this.cancelConvert(repoID, repoData)
+ events.off(DownloadEvent.onFileDownloadSuccess, onDownloadSuccess)
+ events.off(DownloadEvent.onFileDownloadError, onDownloadError)
+ reject(error)
+ }
+ }
+
+ events.on(DownloadEvent.onFileDownloadSuccess, onDownloadSuccess)
+ events.on(DownloadEvent.onFileDownloadError, onDownloadError)
+ })
+ }
+
+ /**
+ * Converts a Hugging Face model to GGUF.
+ * @param repoID - The repo ID of the model to convert.
+ * @returns A promise that resolves when the conversion is complete.
+ */
+ async convert(repoID: string): Promise {
+ if (this.interrupted) return
+ const modelDirPath = await this.getModelDirPath(repoID)
+ const modelOutPath = await this.getConvertedModelPath(repoID)
+ if (!(await fs.existsSync(modelDirPath))) {
+ throw new Error('Model dir not found')
+ }
+ if (await fs.existsSync(modelOutPath)) return
+
+ await executeOnMain(NODE_MODULE_PATH, 'installDeps')
+ if (this.interrupted) return
+
+ try {
+ await executeOnMain(
+ NODE_MODULE_PATH,
+ 'convertHf',
+ modelDirPath,
+ modelOutPath + '.temp'
+ )
+ } catch (err) {
+ log(`[Conversion]::Debug: Error using hf-to-gguf.py, trying convert.py`)
+
+ let ctx = 4096
+ try {
+ const config = await fs.readFileSync(
+ await joinPath([modelDirPath, 'config.json']),
+ 'utf8'
+ )
+ const configParsed = JSON.parse(config)
+ ctx = this.getCtxLength(configParsed)
+ configParsed.max_sequence_length = ctx
+ await fs.writeFileSync(
+ await joinPath([modelDirPath, 'config.json']),
+ JSON.stringify(configParsed, null, 2)
+ )
+ } catch (err) {
+ log(`${err}`)
+ // ignore missing config.json
+ }
+
+ const bpe = await fs.existsSync(
+ await joinPath([modelDirPath, 'vocab.json'])
+ )
+
+ await executeOnMain(
+ NODE_MODULE_PATH,
+ 'convert',
+ modelDirPath,
+ modelOutPath + '.temp',
+ {
+ ctx,
+ bpe,
+ }
+ )
+ }
+ await executeOnMain(
+ NODE_MODULE_PATH,
+ 'renameSync',
+ modelOutPath + '.temp',
+ modelOutPath
+ )
+
+ for (const file of await fs.readdirSync(modelDirPath)) {
+ if (
+ modelOutPath.endsWith(file) ||
+ (file.endsWith('config.json') && !file.endsWith('_config.json'))
+ )
+ continue
+ await fs.unlinkSync(await joinPath([modelDirPath, file]))
+ }
+ }
+
+ /**
+ * Quantizes a GGUF model.
+ * @param repoID - The repo ID of the model to quantize.
+ * @param quantization - The quantization to use.
+ * @returns A promise that resolves when the quantization is complete.
+ */
+ async quantize(repoID: string, quantization: Quantization): Promise {
+ if (this.interrupted) return
+ const modelDirPath = await this.getModelDirPath(repoID)
+ const modelOutPath = await this.getQuantizedModelPath(repoID, quantization)
+ if (!(await fs.existsSync(modelDirPath))) {
+ throw new Error('Model dir not found')
+ }
+ if (await fs.existsSync(modelOutPath)) return
+
+ await executeOnMain(
+ NODE_MODULE_PATH,
+ 'quantize',
+ await this.getConvertedModelPath(repoID),
+ modelOutPath + '.temp',
+ quantization
+ )
+ await executeOnMain(
+ NODE_MODULE_PATH,
+ 'renameSync',
+ modelOutPath + '.temp',
+ modelOutPath
+ )
+
+ await fs.unlinkSync(await this.getConvertedModelPath(repoID))
+ }
+
+ /**
+ * Generates Jan model metadata from a Hugging Face model.
+ * @param repoID - The repo ID of the model to generate metadata for.
+ * @param repoData - The repo data of the model to generate metadata for.
+ * @param quantization - The quantization of the model.
+ * @returns A promise that resolves when the model metadata generation is complete.
+ */
+ async generateMetadata(
+ repoID: string,
+ repoData: HuggingFaceRepoData,
+ quantization: Quantization
+ ): Promise {
+ const modelName = repoID.split('/').slice(1).join('/')
+ const filename = `${modelName}-${quantization.toLowerCase()}.gguf`
+ const modelDirPath = await this.getModelDirPath(repoID)
+ const modelPath = await this.getQuantizedModelPath(repoID, quantization)
+ const modelConfigPath = await joinPath([modelDirPath, 'model.json'])
+ if (!(await fs.existsSync(modelPath))) {
+ throw new Error('Model not found')
+ }
+
+ const size = await executeOnMain(NODE_MODULE_PATH, 'getSize', modelPath)
+ let ctx = 4096
+ try {
+ const config = await fs.readFileSync(
+ await joinPath([modelDirPath, 'config.json']),
+ 'utf8'
+ )
+ ctx = this.getCtxLength(JSON.parse(config))
+ fs.unlinkSync(await joinPath([modelDirPath, 'config.json']))
+ } catch (err) {
+ // ignore missing config.json
+ }
+ // maybe later, currently it's gonna use too much memory
+ // const buffer = await fs.readFileSync(quantizedModelPath)
+ // const ggufData = ggufMetadata(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength))
+
+ const metadata: Model = {
+ object: 'model',
+ version: 1,
+ format: 'gguf',
+ sources: [
+ {
+ url: `https://huggingface.co/${repoID}`, // i think this is just for download but not sure,
+ filename,
+ },
+ ],
+ id: modelName,
+ name: modelName,
+ created: Date.now(),
+ description: `Auto converted from Hugging Face model: ${repoID}`,
+ settings: {
+ ctx_len: ctx,
+ prompt_template: '',
+ llama_model_path: modelName,
+ },
+ parameters: {
+ temperature: 0.7,
+ top_p: 0.95,
+ stream: true,
+ max_tokens: 4096,
+ // stop: [''], seems like we dont really need this..?
+ frequency_penalty: 0,
+ presence_penalty: 0,
+ },
+ metadata: {
+ author: repoData.author,
+ tags: repoData.tags,
+ size,
+ },
+ engine: InferenceEngine.nitro,
+ }
+
+ await fs.writeFileSync(modelConfigPath, JSON.stringify(metadata, null, 2))
+ }
+
+ /**
+ * Cancels the convert of current Hugging Face model.
+ * @param repoID - The repository ID to cancel.
+ * @param repoData - The repository data to cancel.
+ * @returns {Promise} A promise that resolves when the download has been cancelled.
+ */
+ async cancelConvert(
+ repoID: string,
+ repoData: HuggingFaceRepoData
+ ): Promise {
+ this.interrupted = true
+ const modelDirPath = await this.getModelDirPath(repoID)
+ const files = this.getFileList(repoData)
+ for (const file of files) {
+ const filePath = file
+ const localPath = await joinPath([modelDirPath, filePath])
+ await abortDownload(localPath)
+ }
+ // ;(await fs.existsSync(modelDirPath)) && (await fs.rmdirSync(modelDirPath))
+
+ executeOnMain(NODE_MODULE_PATH, 'killProcesses')
+ }
+}
diff --git a/extensions/huggingface-extension/src/node/index.ts b/extensions/huggingface-extension/src/node/index.ts
new file mode 100644
index 000000000..cd36c1ab9
--- /dev/null
+++ b/extensions/huggingface-extension/src/node/index.ts
@@ -0,0 +1,187 @@
+import { PythonShell } from 'python-shell'
+import { spawn, ChildProcess } from 'child_process'
+import { resolve as presolve, join as pjoin } from 'path'
+import type { Quantization } from '@janhq/core'
+import { log } from '@janhq/core/node'
+import { statSync } from 'fs'
+export { renameSync } from 'fs'
+
+let pythonShell: PythonShell | undefined = undefined
+let quantizeProcess: ChildProcess | undefined = undefined
+
+export const getSize = (path: string): number => statSync(path).size
+
+export const killProcesses = () => {
+ if (pythonShell) {
+ pythonShell.kill()
+ pythonShell = undefined
+ }
+ if (quantizeProcess) {
+ quantizeProcess.kill()
+ quantizeProcess = undefined
+ }
+}
+
+export const getQuantizeExecutable = (): string => {
+ let binaryFolder = pjoin(__dirname, '..', 'bin') // Current directory by default
+ let binaryName = 'quantize'
+ /**
+ * The binary folder is different for each platform.
+ */
+ if (process.platform === 'win32') {
+ binaryFolder = pjoin(binaryFolder, 'win')
+ binaryName = 'quantize.exe'
+ } else if (process.platform === 'darwin') {
+ /**
+ * For MacOS: mac-arm64 (Silicon), mac-x64 (InteL)
+ */
+ if (process.arch === 'arm64') {
+ binaryFolder = pjoin(binaryFolder, 'mac-arm64')
+ } else {
+ binaryFolder = pjoin(binaryFolder, 'mac-x64')
+ }
+ } else {
+ binaryFolder = pjoin(binaryFolder, 'linux-cpu')
+ }
+ return pjoin(binaryFolder, binaryName)
+}
+
+export const installDeps = (): Promise => {
+ return new Promise((resolve, reject) => {
+ const _pythonShell = new PythonShell(
+ presolve(__dirname, '..', 'scripts', 'install_deps.py')
+ )
+ _pythonShell.on('message', (message) => {
+ log(`[Install Deps]::Debug: ${message}`)
+ })
+ _pythonShell.on('stderr', (stderr) => {
+ log(`[Install Deps]::Error: ${stderr}`)
+ })
+ _pythonShell.on('error', (err) => {
+ pythonShell = undefined
+ log(`[Install Deps]::Error: ${err}`)
+ reject(err)
+ })
+ _pythonShell.on('close', () => {
+ const exitCode = _pythonShell.exitCode
+ pythonShell = undefined
+ log(
+ `[Install Deps]::Debug: Deps installation exited with code: ${exitCode}`
+ )
+ exitCode === 0 ? resolve() : reject(exitCode)
+ })
+ })
+}
+
+export const convertHf = async (
+ modelDirPath: string,
+ outPath: string
+): Promise => {
+ return await new Promise((resolve, reject) => {
+ const _pythonShell = new PythonShell(
+ presolve(__dirname, '..', 'scripts', 'convert-hf-to-gguf.py'),
+ {
+ args: [modelDirPath, '--outfile', outPath],
+ }
+ )
+ pythonShell = _pythonShell
+ _pythonShell.on('message', (message) => {
+ log(`[Conversion]::Debug: ${message}`)
+ })
+ _pythonShell.on('stderr', (stderr) => {
+ log(`[Conversion]::Error: ${stderr}`)
+ })
+ _pythonShell.on('error', (err) => {
+ pythonShell = undefined
+ log(`[Conversion]::Error: ${err}`)
+ reject(err)
+ })
+ _pythonShell.on('close', () => {
+ const exitCode = _pythonShell.exitCode
+ pythonShell = undefined
+ if (exitCode !== 0) {
+ log(`[Conversion]::Debug: Conversion exited with code: ${exitCode}`)
+ reject(exitCode)
+ } else {
+ resolve()
+ }
+ })
+ })
+}
+
+export const convert = async (
+ modelDirPath: string,
+ outPath: string,
+ { ctx, bpe }: { ctx?: number; bpe?: boolean }
+): Promise => {
+ const args = [modelDirPath, '--outfile', outPath]
+ if (ctx) {
+ args.push('--ctx')
+ args.push(ctx.toString())
+ }
+ if (bpe) {
+ args.push('--vocab-type')
+ args.push('bpe')
+ }
+ return await new Promise((resolve, reject) => {
+ const _pythonShell = new PythonShell(
+ presolve(__dirname, '..', 'scripts', 'convert.py'),
+ {
+ args,
+ }
+ )
+ _pythonShell.on('message', (message) => {
+ log(`[Conversion]::Debug: ${message}`)
+ })
+ _pythonShell.on('stderr', (stderr) => {
+ log(`[Conversion]::Error: ${stderr}`)
+ })
+ _pythonShell.on('error', (err) => {
+ pythonShell = undefined
+ log(`[Conversion]::Error: ${err}`)
+ reject(err)
+ })
+ _pythonShell.on('close', () => {
+ const exitCode = _pythonShell.exitCode
+ pythonShell = undefined
+ if (exitCode !== 0) {
+ log(`[Conversion]::Debug: Conversion exited with code: ${exitCode}`)
+ reject(exitCode)
+ } else {
+ resolve()
+ }
+ })
+ })
+}
+
+export const quantize = async (
+ modelPath: string,
+ outPath: string,
+ quantization: Quantization
+): Promise => {
+ return await new Promise((resolve, reject) => {
+ const quantizeExecutable = getQuantizeExecutable()
+ const _quantizeProcess = spawn(quantizeExecutable, [
+ modelPath,
+ outPath,
+ quantization,
+ ])
+ quantizeProcess = _quantizeProcess
+
+ _quantizeProcess.stdout?.on('data', (data) => {
+ log(`[Quantization]::Debug: ${data}`)
+ })
+ _quantizeProcess.stderr?.on('data', (data) => {
+ log(`[Quantization]::Error: ${data}`)
+ })
+
+ _quantizeProcess.on('close', (code) => {
+ if (code !== 0) {
+ log(`[Quantization]::Debug: Quantization exited with code: ${code}`)
+ reject(code)
+ } else {
+ resolve()
+ }
+ })
+ })
+}
diff --git a/extensions/huggingface-extension/tsconfig.json b/extensions/huggingface-extension/tsconfig.json
new file mode 100644
index 000000000..a42f31602
--- /dev/null
+++ b/extensions/huggingface-extension/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "moduleResolution": "node",
+ "target": "es2020",
+ "module": "ES2020",
+ "lib": ["es2015", "es2016", "es2017", "dom"],
+ "strict": true,
+ "sourceMap": true,
+ "declaration": true,
+ "allowSyntheticDefaultImports": true,
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true,
+ "declarationDir": "dist/types",
+ "outDir": "dist",
+ "importHelpers": true,
+ "typeRoots": ["node_modules/@types"],
+ "resolveJsonModule": true,
+ },
+ "include": ["src"],
+}
diff --git a/extensions/inference-nitro-extension/bin/version.txt b/extensions/inference-nitro-extension/bin/version.txt
index 0b9c01996..e4737652c 100644
--- a/extensions/inference-nitro-extension/bin/version.txt
+++ b/extensions/inference-nitro-extension/bin/version.txt
@@ -1 +1 @@
-0.3.12
+0.3.13
diff --git a/extensions/inference-nitro-extension/src/node/accelerator.ts b/extensions/inference-nitro-extension/src/node/accelerator.ts
index 972f88681..1ffdbc5bd 100644
--- a/extensions/inference-nitro-extension/src/node/accelerator.ts
+++ b/extensions/inference-nitro-extension/src/node/accelerator.ts
@@ -23,10 +23,7 @@ const DEFALT_SETTINGS = {
gpus_in_use: [],
is_initial: true,
// TODO: This needs to be set based on user toggle in settings
- vulkan: {
- enabled: true,
- gpu_in_use: '1',
- },
+ vulkan: false
}
/**
@@ -152,7 +149,7 @@ export function updateCudaExistence(
data['cuda'].exist = cudaExists
data['cuda'].version = cudaVersion
- console.log(data['is_initial'], data['gpus_in_use'])
+ console.debug(data['is_initial'], data['gpus_in_use'])
if (cudaExists && data['is_initial'] && data['gpus_in_use'].length > 0) {
data.run_mode = 'gpu'
}
diff --git a/extensions/inference-nitro-extension/src/node/execute.ts b/extensions/inference-nitro-extension/src/node/execute.ts
index 08baba0d5..f9a668507 100644
--- a/extensions/inference-nitro-extension/src/node/execute.ts
+++ b/extensions/inference-nitro-extension/src/node/execute.ts
@@ -67,7 +67,7 @@ export const executableNitroFile = (): NitroExecutableOptions => {
if (gpuInfo['vulkan'] === true) {
binaryFolder = path.join(__dirname, '..', 'bin')
- binaryFolder = path.join(binaryFolder, 'win-vulkan')
+ binaryFolder = path.join(binaryFolder, 'linux-vulkan')
vkVisibleDevices = gpuInfo['gpus_in_use'].toString()
}
}
diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts
index 926e65ee5..fb1f26885 100644
--- a/extensions/model-extension/src/index.ts
+++ b/extensions/model-extension/src/index.ts
@@ -13,6 +13,10 @@ import {
DownloadRoute,
ModelEvent,
DownloadState,
+ OptionType,
+ ImportingModel,
+ LocalImportModelEvent,
+ baseName,
} from '@janhq/core'
import { extractFileName } from './helpers/path'
@@ -158,18 +162,18 @@ export default class JanModelExtension extends ModelExtension {
/**
* Cancels the download of a specific machine learning model.
+ *
* @param {string} modelId - The ID of the model whose download is to be cancelled.
* @returns {Promise} A promise that resolves when the download has been cancelled.
*/
async cancelModelDownload(modelId: string): Promise {
- const model = await this.getConfiguredModels()
- return abortDownload(
- await joinPath([JanModelExtension._homeDir, modelId, modelId])
- ).then(async () => {
- fs.unlinkSync(
- await joinPath([JanModelExtension._homeDir, modelId, modelId])
- )
- })
+ const path = await joinPath([JanModelExtension._homeDir, modelId, modelId])
+ try {
+ await abortDownload(path)
+ await fs.unlinkSync(path)
+ } catch (e) {
+ console.error(e)
+ }
}
/**
@@ -180,6 +184,20 @@ export default class JanModelExtension extends ModelExtension {
async deleteModel(modelId: string): Promise {
try {
const dirPath = await joinPath([JanModelExtension._homeDir, modelId])
+ const jsonFilePath = await joinPath([
+ dirPath,
+ JanModelExtension._modelMetadataFileName,
+ ])
+ const modelInfo = JSON.parse(
+ await this.readModelMetadata(jsonFilePath)
+ ) as Model
+
+ const isUserImportModel =
+ modelInfo.metadata?.author?.toLowerCase() === 'user'
+ if (isUserImportModel) {
+ // just delete the folder
+ return fs.rmdirSync(dirPath)
+ }
// remove all files under dirPath except model.json
const files = await fs.readdirSync(dirPath)
@@ -389,7 +407,7 @@ export default class JanModelExtension extends ModelExtension {
llama_model_path: binaryFileName,
},
created: Date.now(),
- description: `${dirName} - user self import model`,
+ description: '',
metadata: {
size: binaryFileSize,
author: 'User',
@@ -455,4 +473,189 @@ export default class JanModelExtension extends ModelExtension {
)
}
}
+
+ private async importModelSymlink(
+ modelBinaryPath: string,
+ modelFolderName: string,
+ modelFolderPath: string
+ ): Promise {
+ const fileStats = await fs.fileStat(modelBinaryPath, true)
+ const binaryFileSize = fileStats.size
+
+ // Just need to generate model.json there
+ const defaultModel = (await this.getDefaultModel()) as Model
+ if (!defaultModel) {
+ console.error('Unable to find default model')
+ return
+ }
+
+ const binaryFileName = await baseName(modelBinaryPath)
+
+ const model: Model = {
+ ...defaultModel,
+ id: modelFolderName,
+ name: modelFolderName,
+ sources: [
+ {
+ url: modelBinaryPath,
+ filename: binaryFileName,
+ },
+ ],
+ settings: {
+ ...defaultModel.settings,
+ llama_model_path: binaryFileName,
+ },
+ created: Date.now(),
+ description: '',
+ metadata: {
+ size: binaryFileSize,
+ author: 'User',
+ tags: [],
+ },
+ }
+
+ const modelFilePath = await joinPath([
+ modelFolderPath,
+ JanModelExtension._modelMetadataFileName,
+ ])
+
+ await fs.writeFileSync(modelFilePath, JSON.stringify(model, null, 2))
+
+ return model
+ }
+
+ async updateModelInfo(modelInfo: Partial): Promise {
+ const modelId = modelInfo.id
+ if (modelInfo.id == null) throw new Error('Model ID is required')
+
+ const janDataFolderPath = await getJanDataFolderPath()
+ const jsonFilePath = await joinPath([
+ janDataFolderPath,
+ 'models',
+ modelId,
+ JanModelExtension._modelMetadataFileName,
+ ])
+ const model = JSON.parse(
+ await this.readModelMetadata(jsonFilePath)
+ ) as Model
+
+ const updatedModel: Model = {
+ ...model,
+ ...modelInfo,
+ metadata: {
+ ...model.metadata,
+ tags: modelInfo.metadata?.tags ?? [],
+ },
+ }
+
+ await fs.writeFileSync(jsonFilePath, JSON.stringify(updatedModel, null, 2))
+ return updatedModel
+ }
+
+ private async importModel(
+ model: ImportingModel,
+ optionType: OptionType
+ ): Promise {
+ const binaryName = (await baseName(model.path)).replace(/\s/g, '')
+
+ let modelFolderName = binaryName
+ if (binaryName.endsWith(JanModelExtension._supportedModelFormat)) {
+ modelFolderName = binaryName.replace(
+ JanModelExtension._supportedModelFormat,
+ ''
+ )
+ }
+
+ const modelFolderPath = await this.getModelFolderName(modelFolderName)
+ await fs.mkdirSync(modelFolderPath)
+
+ const uniqueFolderName = await baseName(modelFolderPath)
+ const modelBinaryFile = binaryName.endsWith(
+ JanModelExtension._supportedModelFormat
+ )
+ ? binaryName
+ : `${binaryName}${JanModelExtension._supportedModelFormat}`
+
+ const binaryPath = await joinPath([modelFolderPath, modelBinaryFile])
+
+ if (optionType === 'SYMLINK') {
+ return this.importModelSymlink(
+ model.path,
+ uniqueFolderName,
+ modelFolderPath
+ )
+ }
+
+ const srcStat = await fs.fileStat(model.path, true)
+
+ // interval getting the file size to calculate the percentage
+ const interval = setInterval(async () => {
+ const destStats = await fs.fileStat(binaryPath, true)
+ const percentage = destStats.size / srcStat.size
+ events.emit(LocalImportModelEvent.onLocalImportModelUpdate, {
+ ...model,
+ percentage,
+ })
+ }, 1000)
+
+ await fs.copyFile(model.path, binaryPath)
+
+ clearInterval(interval)
+
+ // generate model json
+ return this.generateModelMetadata(uniqueFolderName)
+ }
+
+ private async getModelFolderName(
+ modelFolderName: string,
+ count?: number
+ ): Promise {
+ const newModelFolderName = count
+ ? `${modelFolderName}-${count}`
+ : modelFolderName
+
+ const janDataFolderPath = await getJanDataFolderPath()
+ const modelFolderPath = await joinPath([
+ janDataFolderPath,
+ 'models',
+ newModelFolderName,
+ ])
+
+ const isFolderExist = await fs.existsSync(modelFolderPath)
+ if (!isFolderExist) {
+ return modelFolderPath
+ } else {
+ const newCount = (count ?? 0) + 1
+ return this.getModelFolderName(modelFolderName, newCount)
+ }
+ }
+
+ async importModels(
+ models: ImportingModel[],
+ optionType: OptionType
+ ): Promise {
+ const importedModels: Model[] = []
+
+ for (const model of models) {
+ events.emit(LocalImportModelEvent.onLocalImportModelUpdate, model)
+ try {
+ const importedModel = await this.importModel(model, optionType)
+ events.emit(LocalImportModelEvent.onLocalImportModelSuccess, {
+ ...model,
+ modelId: importedModel.id,
+ })
+ importedModels.push(importedModel)
+ } catch (err) {
+ events.emit(LocalImportModelEvent.onLocalImportModelFailed, {
+ ...model,
+ error: err,
+ })
+ }
+ }
+
+ events.emit(
+ LocalImportModelEvent.onLocalImportModelFinished,
+ importedModels
+ )
+ }
}
diff --git a/server/middleware/s3.ts b/server/middleware/s3.ts
index 28971a42b..3024285a3 100644
--- a/server/middleware/s3.ts
+++ b/server/middleware/s3.ts
@@ -38,7 +38,7 @@ export const s3 = (req: any, reply: any, done: any) => {
reply.status(200).send(result)
return
} catch (ex) {
- console.log(ex)
+ console.error(ex)
}
}
}
diff --git a/uikit/src/button/styles.scss b/uikit/src/button/styles.scss
index 003df5b4d..c97bec9e0 100644
--- a/uikit/src/button/styles.scss
+++ b/uikit/src/button/styles.scss
@@ -5,11 +5,11 @@
@apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400;
&-primary {
- @apply bg-primary hover:bg-primary/90 text-white;
+ @apply bg-blue-600 text-white hover:bg-blue-600/90;
}
&-secondary-blue {
- @apply bg-blue-200 text-blue-600 hover:bg-blue-300/50 dark:hover:bg-blue-200/80;
+ @apply bg-blue-200 text-blue-600 hover:bg-blue-300/50;
}
&-danger {
@@ -17,7 +17,7 @@
}
&-secondary-danger {
- @apply bg-red-200 text-red-600 hover:bg-red-300/50 dark:hover:bg-red-200/80;
+ @apply bg-red-200 text-red-600 hover:bg-red-300/50;
}
&-outline {
@@ -66,7 +66,7 @@
[type='reset'],
[type='submit'] {
&.btn-primary {
- @apply bg-primary hover:bg-primary/90;
+ @apply bg-blue-600 hover:bg-blue-600/90;
@apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400;
}
&.btn-secondary {
diff --git a/uikit/src/checkbox/styles.scss b/uikit/src/checkbox/styles.scss
index 33610f837..cf35ed5ca 100644
--- a/uikit/src/checkbox/styles.scss
+++ b/uikit/src/checkbox/styles.scss
@@ -1,5 +1,5 @@
.checkbox {
- @apply border-border data-[state=checked]:bg-primary h-5 w-5 flex-shrink-0 rounded-md border data-[state=checked]:text-white;
+ @apply border-border h-5 w-5 flex-shrink-0 rounded-md border data-[state=checked]:bg-blue-600 data-[state=checked]:text-white;
&--icon {
@apply h-4 w-4;
diff --git a/uikit/src/circular-progress/styles.scss b/uikit/src/circular-progress/styles.scss
new file mode 100644
index 000000000..093cd435f
--- /dev/null
+++ b/uikit/src/circular-progress/styles.scss
@@ -0,0 +1,66 @@
+/*
+ * react-circular-progressbar styles
+ * All of the styles in this file are configurable!
+ */
+
+.CircularProgressbar {
+ /*
+ * This fixes an issue where the CircularProgressbar svg has
+ * 0 width inside a "display: flex" container, and thus not visible.
+ */
+ width: 100%;
+ /*
+ * This fixes a centering issue with CircularProgressbarWithChildren:
+ * https://github.com/kevinsqi/react-circular-progressbar/issues/94
+ */
+ vertical-align: middle;
+}
+
+.CircularProgressbar .CircularProgressbar-path {
+ stroke: #3e98c7;
+ stroke-linecap: round;
+ transition: stroke-dashoffset 0.5s ease 0s;
+}
+
+.CircularProgressbar .CircularProgressbar-trail {
+ stroke: #d6d6d6;
+ /* Used when trail is not full diameter, i.e. when props.circleRatio is set */
+ stroke-linecap: round;
+}
+
+.CircularProgressbar .CircularProgressbar-text {
+ fill: #3e98c7;
+ font-size: 20px;
+ dominant-baseline: middle;
+ text-anchor: middle;
+}
+
+.CircularProgressbar .CircularProgressbar-background {
+ fill: #d6d6d6;
+}
+
+/*
+ * Sample background styles. Use these with e.g.:
+ *
+ *
+ */
+.CircularProgressbar.CircularProgressbar-inverted
+ .CircularProgressbar-background {
+ fill: #3e98c7;
+}
+
+.CircularProgressbar.CircularProgressbar-inverted .CircularProgressbar-text {
+ fill: #fff;
+}
+
+.CircularProgressbar.CircularProgressbar-inverted .CircularProgressbar-path {
+ stroke: #fff;
+}
+
+.CircularProgressbar.CircularProgressbar-inverted .CircularProgressbar-trail {
+ stroke: transparent;
+}
diff --git a/uikit/src/input/styles.scss b/uikit/src/input/styles.scss
index e649f494d..51efd8e57 100644
--- a/uikit/src/input/styles.scss
+++ b/uikit/src/input/styles.scss
@@ -1,6 +1,6 @@
.input {
@apply border-border placeholder:text-muted-foreground flex h-9 w-full rounded-lg border bg-transparent px-3 py-1 transition-colors;
- @apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100 disabled:dark:bg-zinc-800 disabled:dark:text-zinc-600;
+ @apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100;
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1;
@apply file:border-0 file:bg-transparent file:font-medium;
}
diff --git a/uikit/src/main.scss b/uikit/src/main.scss
index c1326ba19..f3294e12e 100644
--- a/uikit/src/main.scss
+++ b/uikit/src/main.scss
@@ -17,6 +17,7 @@
@import './select/styles.scss';
@import './slider/styles.scss';
@import './checkbox/styles.scss';
+@import './circular-progress/styles.scss';
.animate-spin {
animation: spin 1s linear infinite;
diff --git a/uikit/src/modal/index.tsx b/uikit/src/modal/index.tsx
index c41909843..1c0586637 100644
--- a/uikit/src/modal/index.tsx
+++ b/uikit/src/modal/index.tsx
@@ -19,7 +19,7 @@ const ModalOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
))
diff --git a/uikit/src/progress/styles.scss b/uikit/src/progress/styles.scss
index 0b7078f48..1a8483c47 100644
--- a/uikit/src/progress/styles.scss
+++ b/uikit/src/progress/styles.scss
@@ -1,7 +1,7 @@
.progress {
- @apply bg-secondary relative h-4 w-full overflow-hidden rounded-full;
+ @apply relative h-4 w-full overflow-hidden rounded-full bg-gray-100;
&-indicator {
- @apply bg-primary h-full w-full flex-1 transition-all;
+ @apply h-full w-full flex-1 bg-blue-600 transition-all;
}
}
diff --git a/uikit/src/select/styles.scss b/uikit/src/select/styles.scss
index 90485723a..99db49766 100644
--- a/uikit/src/select/styles.scss
+++ b/uikit/src/select/styles.scss
@@ -1,6 +1,6 @@
.select {
@apply placeholder:text-muted-foreground border-border flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm disabled:cursor-not-allowed [&>span]:line-clamp-1;
- @apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100 disabled:dark:bg-zinc-800 disabled:dark:text-zinc-600;
+ @apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100;
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1;
&-caret {
diff --git a/uikit/src/slider/styles.scss b/uikit/src/slider/styles.scss
index 718972efb..465392419 100644
--- a/uikit/src/slider/styles.scss
+++ b/uikit/src/slider/styles.scss
@@ -2,7 +2,7 @@
@apply relative flex w-full touch-none select-none items-center;
&-track {
- @apply relative h-1.5 w-full grow overflow-hidden rounded-full bg-gray-200 dark:bg-gray-800;
+ @apply relative h-1.5 w-full grow overflow-hidden rounded-full bg-gray-200;
[data-disabled] {
@apply cursor-not-allowed opacity-50;
}
@@ -13,6 +13,6 @@
}
&-thumb {
- @apply border-primary/50 bg-background focus-visible:ring-ring block h-4 w-4 rounded-full border shadow transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50;
+ @apply bg-background focus-visible:ring-ring block h-4 w-4 rounded-full border border-blue-600/50 shadow transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50;
}
}
diff --git a/uikit/src/switch/styles.scss b/uikit/src/switch/styles.scss
index c8a12cdf5..57fa128ba 100644
--- a/uikit/src/switch/styles.scss
+++ b/uikit/src/switch/styles.scss
@@ -1,7 +1,7 @@
.switch {
@apply inline-flex h-[20px] w-[36px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent;
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
- @apply data-[state=checked]:bg-primary data-[state=unchecked]:bg-input;
+ @apply data-[state=unchecked]:bg-input data-[state=checked]:bg-blue-600;
@apply disabled:cursor-not-allowed disabled:opacity-50;
&-toggle {
diff --git a/uikit/src/tooltip/styles.scss b/uikit/src/tooltip/styles.scss
index 8ae645cee..169e081b7 100644
--- a/uikit/src/tooltip/styles.scss
+++ b/uikit/src/tooltip/styles.scss
@@ -1,6 +1,6 @@
.tooltip {
- @apply dark:bg-input dark:text-foreground z-50 overflow-hidden rounded-md bg-gray-950 px-2 py-1.5 text-xs font-medium text-gray-200 shadow-md;
+ @apply z-50 overflow-hidden rounded-md bg-gray-950 px-2 py-1.5 text-xs font-medium text-gray-200 shadow-md;
&-arrow {
- @apply dark:fill-input fill-gray-950;
+ @apply fill-gray-950;
}
}
diff --git a/web/.prettierrc b/web/.prettierrc
new file mode 100644
index 000000000..46f1abcb0
--- /dev/null
+++ b/web/.prettierrc
@@ -0,0 +1,8 @@
+{
+ "semi": false,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "es5",
+ "endOfLine": "auto",
+ "plugins": ["prettier-plugin-tailwindcss"]
+}
diff --git a/web/app/layout.tsx b/web/app/layout.tsx
index 6c6fc65ab..37bcdf53e 100644
--- a/web/app/layout.tsx
+++ b/web/app/layout.tsx
@@ -15,7 +15,7 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: PropsWithChildren) {
return (
-
+
{children}
diff --git a/web/app/page.tsx b/web/app/page.tsx
index 92d654528..ab619f061 100644
--- a/web/app/page.tsx
+++ b/web/app/page.tsx
@@ -1,19 +1,21 @@
'use client'
+import { useAtomValue } from 'jotai'
+
import BaseLayout from '@/containers/Layout'
import { MainViewState } from '@/constants/screens'
-import { useMainViewState } from '@/hooks/useMainViewState'
-
import ChatScreen from '@/screens/Chat'
import ExploreModelsScreen from '@/screens/ExploreModels'
import LocalServerScreen from '@/screens/LocalServer'
import SettingsScreen from '@/screens/Settings'
+import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
+
export default function Page() {
- const { mainViewState } = useMainViewState()
+ const mainViewState = useAtomValue(mainViewStateAtom)
let children = null
switch (mainViewState) {
diff --git a/web/containers/CardSidebar/index.tsx b/web/containers/CardSidebar/index.tsx
index 89ff60e66..3013360e9 100644
--- a/web/containers/CardSidebar/index.tsx
+++ b/web/containers/CardSidebar/index.tsx
@@ -38,14 +38,14 @@ export default function CardSidebar({
const [menu, setMenu] = useState(null)
const [toggle, setToggle] = useState(null)
const activeThread = useAtomValue(activeThreadAtom)
- const { onReviewInFinder, onViewJson } = usePath()
+ const { onRevealInFinder, onViewJson } = usePath()
useClickOutside(() => setMore(false), null, [menu, toggle])
return (