Merge branch 'main' into internal-48
This commit is contained in:
commit
2555dae49f
2
.github/workflows/jan-electron-build.yml
vendored
2
.github/workflows/jan-electron-build.yml
vendored
@ -122,7 +122,7 @@ jobs:
|
|||||||
yarn build:core
|
yarn build:core
|
||||||
yarn install
|
yarn install
|
||||||
$env:NITRO_VERSION = Get-Content .\plugins\inference-plugin\nitro\version.txt; echo $env:NITRO_VERSION
|
$env:NITRO_VERSION = Get-Content .\plugins\inference-plugin\nitro\version.txt; echo $env:NITRO_VERSION
|
||||||
yarn build:plugins-windows
|
yarn build:plugins-win32
|
||||||
|
|
||||||
- name: Build and publish app
|
- name: Build and publish app
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@ -95,7 +95,7 @@ jobs:
|
|||||||
yarn build:core
|
yarn build:core
|
||||||
yarn install
|
yarn install
|
||||||
$env:NITRO_VERSION = Get-Content .\plugins\inference-plugin\nitro\version.txt; echo $env:NITRO_VERSION
|
$env:NITRO_VERSION = Get-Content .\plugins\inference-plugin\nitro\version.txt; echo $env:NITRO_VERSION
|
||||||
yarn build:plugins-windows
|
yarn build:plugins-win32
|
||||||
yarn build:test-win32
|
yarn build:test-win32
|
||||||
$env:CI="e2e"
|
$env:CI="e2e"
|
||||||
yarn test
|
yarn test
|
||||||
|
|||||||
74
.github/workflows/jan-plugin-core.yml
vendored
74
.github/workflows/jan-plugin-core.yml
vendored
@ -1,74 +0,0 @@
|
|||||||
name: Plugin Core
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- "core/**"
|
|
||||||
- "!core/package.json"
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- "core/**"
|
|
||||||
- ".github/workflows/jan-plugin-core.yml"
|
|
||||||
- "!core/package.json"
|
|
||||||
jobs:
|
|
||||||
build-and-publish-plugins:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment: production
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: "0"
|
|
||||||
token: ${{ secrets.PAT_SERVICE_ACCOUNT }}
|
|
||||||
|
|
||||||
- name: Install jq
|
|
||||||
uses: dcarbone/install-jq-action@v2.0.1
|
|
||||||
|
|
||||||
- name: "Auto Increase package Version"
|
|
||||||
run: |
|
|
||||||
# Extract current version
|
|
||||||
current_version=$(jq -r '.version' core/package.json)
|
|
||||||
|
|
||||||
# Break the version into its components
|
|
||||||
major_version=$(echo $current_version | cut -d "." -f 1)
|
|
||||||
minor_version=$(echo $current_version | cut -d "." -f 2)
|
|
||||||
patch_version=$(echo $current_version | cut -d "." -f 3)
|
|
||||||
|
|
||||||
# Increment the patch version by one
|
|
||||||
new_patch_version=$((patch_version+1))
|
|
||||||
|
|
||||||
# Construct the new version
|
|
||||||
new_version="$major_version.$minor_version.$new_patch_version"
|
|
||||||
|
|
||||||
# Replace the old version with the new version in package.json
|
|
||||||
jq --arg version "$new_version" '.version = $version' core/package.json > /tmp/package.json && mv /tmp/package.json core/package.json
|
|
||||||
|
|
||||||
# Print the new version
|
|
||||||
echo "Updated package.json version to: $new_version"
|
|
||||||
|
|
||||||
# Setup .npmrc file to publish to npm
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: "20.x"
|
|
||||||
registry-url: "https://registry.npmjs.org"
|
|
||||||
- run: npm install && npm run build
|
|
||||||
working-directory: ./core
|
|
||||||
- run: npm publish --access public
|
|
||||||
if: github.event_name == 'push' && github.event.pull_request.head.repo.full_name != github.repository
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
working-directory: ./core
|
|
||||||
|
|
||||||
- name: "Commit new version to main and create tag"
|
|
||||||
if: github.event_name == 'push' && github.event.pull_request.head.repo.full_name != github.repository
|
|
||||||
run: |
|
|
||||||
version=$(jq -r '.version' core/package.json)
|
|
||||||
git config --global user.email "service@jan.ai"
|
|
||||||
git config --global user.name "Service Account"
|
|
||||||
git add core/package.json
|
|
||||||
git commit -m "${GITHUB_REPOSITORY}: Update tag build $version"
|
|
||||||
git -c http.extraheader="AUTHORIZATION: bearer ${{ secrets.PAT_SERVICE_ACCOUNT }}" push origin HEAD:main
|
|
||||||
git tag -a core-$version -m "${GITHUB_REPOSITORY}: Update tag build $version for core"
|
|
||||||
git -c http.extraheader="AUTHORIZATION: bearer ${{ secrets.PAT_SERVICE_ACCOUNT }}" push origin core-$version
|
|
||||||
163
.github/workflows/jan-plugins.yml
vendored
163
.github/workflows/jan-plugins.yml
vendored
@ -1,163 +0,0 @@
|
|||||||
name: Jan Default Plugins
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- "plugins/**"
|
|
||||||
- "!plugins/*/package.json"
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- "plugins/**"
|
|
||||||
- ".github/workflows/jan-plugins.yml"
|
|
||||||
- "!plugins/*/package.json"
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: macos-latest
|
|
||||||
environment: production
|
|
||||||
outputs:
|
|
||||||
branch_name: ${{ steps.commit_and_tag.outputs.branch_name }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: "0"
|
|
||||||
token: ${{ secrets.PAT_SERVICE_ACCOUNT }}
|
|
||||||
|
|
||||||
- name: Install jq
|
|
||||||
uses: dcarbone/install-jq-action@v2.0.1
|
|
||||||
|
|
||||||
- name: Get Cer for code signing
|
|
||||||
run: base64 -d <<< "$CODE_SIGN_P12_BASE64" > /tmp/codesign.p12
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
CODE_SIGN_P12_BASE64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
|
|
||||||
|
|
||||||
- uses: apple-actions/import-codesign-certs@v2
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
p12-file-base64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
|
|
||||||
p12-password: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Check Path Change
|
|
||||||
run: |
|
|
||||||
git config --global user.email "service@jan.ai"
|
|
||||||
git config --global user.name "Service Account"
|
|
||||||
echo "Changes in these directories trigger the build:"
|
|
||||||
changed_dirs=$(git -c http.extraheader="AUTHORIZATION: bearer ${{ secrets.GITHUB_TOKEN }}" diff --name-only HEAD HEAD~1 | grep '^plugins/' | awk -F/ '{print $2}' | uniq)
|
|
||||||
echo $changed_dirs > /tmp/change_dir.txt
|
|
||||||
|
|
||||||
- name: "Auto Increase package Version"
|
|
||||||
run: |
|
|
||||||
cd plugins
|
|
||||||
for dir in $(cat /tmp/change_dir.txt)
|
|
||||||
do
|
|
||||||
echo "$dir"
|
|
||||||
if [ ! -d "$dir" ]; then
|
|
||||||
echo "Directory $dir does not exist, plugin might be removed, skipping..."
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract current version
|
|
||||||
current_version=$(jq -r '.version' $dir/package.json)
|
|
||||||
|
|
||||||
# Break the version into its components
|
|
||||||
major_version=$(echo $current_version | cut -d "." -f 1)
|
|
||||||
minor_version=$(echo $current_version | cut -d "." -f 2)
|
|
||||||
patch_version=$(echo $current_version | cut -d "." -f 3)
|
|
||||||
|
|
||||||
# Increment the patch version by one
|
|
||||||
new_patch_version=$((patch_version+1))
|
|
||||||
|
|
||||||
# Construct the new version
|
|
||||||
new_version="$major_version.$minor_version.$new_patch_version"
|
|
||||||
|
|
||||||
# Replace the old version with the new version in package.json
|
|
||||||
jq --arg version "$new_version" '.version = $version' $dir/package.json > /tmp/package.json && mv /tmp/package.json $dir/package.json
|
|
||||||
|
|
||||||
# Print the new version
|
|
||||||
echo "Updated $dir package.json version to: $new_version"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Setup .npmrc file to publish to npm
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: "20.x"
|
|
||||||
registry-url: "https://registry.npmjs.org"
|
|
||||||
- name: Build core module
|
|
||||||
run: |
|
|
||||||
cd core
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
- name: Publish npm packages
|
|
||||||
run: |
|
|
||||||
cd plugins
|
|
||||||
for dir in $(cat /tmp/change_dir.txt)
|
|
||||||
do
|
|
||||||
echo $dir
|
|
||||||
if [ ! -d "$dir" ]; then
|
|
||||||
echo "Directory $dir does not exist, plugin might be removed, skipping..."
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
cd $dir
|
|
||||||
npm install
|
|
||||||
if [[ $dir == 'data-plugin' ]]; then
|
|
||||||
npm run build:deps
|
|
||||||
fi
|
|
||||||
npm run postinstall && ../../.github/scripts/auto-sign.sh
|
|
||||||
if [[ $GITHUB_EVENT_NAME == 'push' && $GITHUB_EVENT_PULL_REQUEST_HEAD_REPO_FULL_NAME != $GITHUB_REPOSITORY ]]; then
|
|
||||||
npm publish --access public
|
|
||||||
fi
|
|
||||||
cd ..
|
|
||||||
done
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }}
|
|
||||||
APP_PATH: "."
|
|
||||||
|
|
||||||
- name: "Commit new version to main and create tag"
|
|
||||||
id: commit_and_tag
|
|
||||||
if: github.event_name == 'push' && github.event.pull_request.head.repo.full_name != github.repository
|
|
||||||
run: |
|
|
||||||
rm -rf /tmp/plugin-catalog
|
|
||||||
git clone https://${{ secrets.SERVICE_ACCOUNT_USERNAME }}:${{ secrets.PAT_SERVICE_ACCOUNT }}@github.com/janhq/plugin-catalog.git /tmp/plugin-catalog
|
|
||||||
for dir in $(cat /tmp/change_dir.txt)
|
|
||||||
do
|
|
||||||
echo "$dir"
|
|
||||||
if [ ! -d "$dir" ]; then
|
|
||||||
echo "Directory $dir does not exist, plugin might be removed, skipping..."
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
version=$(jq -r '.version' plugins/$dir/package.json)
|
|
||||||
git config --global user.email "service@jan.ai"
|
|
||||||
git config --global user.name "Service Account"
|
|
||||||
git add plugins/$dir/package.json
|
|
||||||
git commit -m "${GITHUB_REPOSITORY}: Update tag build $version for $dir"
|
|
||||||
git -c http.extraheader="AUTHORIZATION: bearer ${{ secrets.PAT_SERVICE_ACCOUNT }}" push origin HEAD:main
|
|
||||||
git tag -a $dir-$version -m "${GITHUB_REPOSITORY}: Update tag build $version for $dir"
|
|
||||||
git -c http.extraheader="AUTHORIZATION: bearer ${{ secrets.PAT_SERVICE_ACCOUNT }}" push origin $dir-$version
|
|
||||||
plugin_name=$(jq -r '.name | sub("@janhq/"; "")' plugins/$dir/package.json)
|
|
||||||
cp plugins/$dir/package.json /tmp/plugin-catalog/${plugin_name}.json
|
|
||||||
done
|
|
||||||
cd /tmp/plugin-catalog
|
|
||||||
BRANCH_NAME="update-package-$(date +'%Y%m%d%H%M%S')"
|
|
||||||
git checkout -b $BRANCH_NAME
|
|
||||||
git add .
|
|
||||||
git commit -m "Update plugin catalog"
|
|
||||||
git push origin $BRANCH_NAME
|
|
||||||
cd /tmp && rm -rf /tmp/plugin-catalog
|
|
||||||
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
|
|
||||||
echo "::set-output name=branch_name::$BRANCH_NAME"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment: production
|
|
||||||
if: github.event_name == 'push' && github.event.pull_request.head.repo.full_name != github.repository
|
|
||||||
needs: build
|
|
||||||
steps:
|
|
||||||
- run: |
|
|
||||||
gh pr create --title "Update plugin catalog" --body "Update plugin catalog" --base main --head ${{ needs.build.outputs.branch_name }} --repo janhq/plugin-catalog --reviewer louis-jan,hiento09
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.PAT_SERVICE_ACCOUNT }}
|
|
||||||
31
README.md
31
README.md
@ -55,13 +55,34 @@ As Jan is development mode, you might get stuck on a broken build.
|
|||||||
|
|
||||||
To reset your installation:
|
To reset your installation:
|
||||||
|
|
||||||
1. Delete Jan Application from /Applications
|
1. Delete Jan from your `/Applications` folder
|
||||||
|
|
||||||
1. Clear cache:
|
1. Delete Application data:
|
||||||
`rm -rf /Users/$(whoami)/Library/Application\ Support/jan-electron`
|
```sh
|
||||||
OR
|
# Newer versions
|
||||||
`rm -rf /Users/$(whoami)/Library/Application\ Support/jan`
|
rm -rf /Users/$(whoami)/Library/Application\ Support/jan
|
||||||
|
|
||||||
|
# Versions 0.2.0 and older
|
||||||
|
rm -rf /Users/$(whoami)/Library/Application\ Support/jan-electron
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Clear Application cache:
|
||||||
|
```sh
|
||||||
|
rm -rf /Users/$(whoami)/Library/Caches/jan*
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Use the following commands to remove any dangling backend processes:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ps aux | grep nitro
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for processes like "nitro" and "nitro_arm_64," and kill them one by one with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kill -9 <PID>
|
||||||
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) file
|
Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) file
|
||||||
|
|||||||
@ -8,7 +8,7 @@ export { core, deleteFile, invokePluginFunc } from "./core";
|
|||||||
* Core module exports.
|
* Core module exports.
|
||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
export { downloadFile, executeOnMain } from "./core";
|
export { downloadFile, executeOnMain, appDataPath } from "./core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events module exports.
|
* Events module exports.
|
||||||
|
|||||||
@ -7,21 +7,28 @@ sidebar_position: 5
|
|||||||
Please note that 👋Jan is in "development mode," and you might encounter issues. If you need to reset your installation, follow these steps:
|
Please note that 👋Jan is in "development mode," and you might encounter issues. If you need to reset your installation, follow these steps:
|
||||||
|
|
||||||
## Issue 1: Broken Build
|
## Issue 1: Broken Build
|
||||||
1. Delete the Jan Application from your computer.
|
|
||||||
|
|
||||||
2. Clear the cache by running one of the following commands:
|
As Jan is development mode, you might get stuck on a broken build.
|
||||||
|
|
||||||
```sh
|
To reset your installation:
|
||||||
rm -rf /Users/$(whoami)/Library/Application\ Support/jan-electron
|
|
||||||
```
|
|
||||||
|
|
||||||
or
|
1. Delete Jan from your `/Applications` folder
|
||||||
|
|
||||||
```sh
|
1. Delete Application data:
|
||||||
rm -rf /Users/$(whoami)/Library/Application\ Support/jan
|
```sh
|
||||||
```
|
# Newer versions
|
||||||
|
rm -rf /Users/$(whoami)/Library/Application\ Support/jan
|
||||||
|
|
||||||
3. If the above steps fail, use the following commands to find and kill any problematic processes:
|
# Versions 0.2.0 and older
|
||||||
|
rm -rf /Users/$(whoami)/Library/Application\ Support/jan-electron
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Clear Application cache:
|
||||||
|
```sh
|
||||||
|
rm -rf /Users/$(whoami)/Library/Caches/jan*
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Use the following commands to remove any dangling backend processes:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
ps aux | grep nitro
|
ps aux | grep nitro
|
||||||
@ -31,4 +38,5 @@ Please note that 👋Jan is in "development mode," and you might encounter issue
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
kill -9 <PID>
|
kill -9 <PID>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -51,8 +51,8 @@ const config = {
|
|||||||
[
|
[
|
||||||
"posthog-docusaurus",
|
"posthog-docusaurus",
|
||||||
{
|
{
|
||||||
apiKey: process.env.POSTHOG_PROJECT_API_KEY,
|
apiKey: process.env.POSTHOG_PROJECT_API_KEY || "XXX",
|
||||||
appUrl: process.env.POSTHOG_APP_URL, // optional
|
appUrl: process.env.POSTHOG_APP_URL || "XXX", // optional
|
||||||
enableInDevelopment: false, // optional
|
enableInDevelopment: false, // optional
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -93,7 +93,7 @@ const config = {
|
|||||||
},
|
},
|
||||||
// GTM is always inactive in development and only active in production to avoid polluting the analytics statistics.
|
// GTM is always inactive in development and only active in production to avoid polluting the analytics statistics.
|
||||||
googleTagManager: {
|
googleTagManager: {
|
||||||
containerId: process.env.GTM_ID,
|
containerId: process.env.GTM_ID || "XXX",
|
||||||
},
|
},
|
||||||
// Will be passed to @docusaurus/plugin-content-pages (false to disable)
|
// Will be passed to @docusaurus/plugin-content-pages (false to disable)
|
||||||
// pages: {},
|
// pages: {},
|
||||||
@ -162,7 +162,7 @@ const config = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
prism: {
|
prism: {
|
||||||
theme: lightCodeTheme,
|
theme: darkCodeTheme,
|
||||||
darkTheme: darkCodeTheme,
|
darkTheme: darkCodeTheme,
|
||||||
additionalLanguages: ["python"],
|
additionalLanguages: ["python"],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -53,20 +53,27 @@ export default function Dropdown() {
|
|||||||
return match ? match[1] : null;
|
return match ? match[1] : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeDefaultSystem = (systems) => {
|
const changeDefaultSystem = async (systems) => {
|
||||||
const userAgent = navigator.userAgent;
|
const userAgent = navigator.userAgent;
|
||||||
|
|
||||||
|
const arc = await navigator?.userAgentData?.getHighEntropyValues([
|
||||||
|
"architecture",
|
||||||
|
]);
|
||||||
|
|
||||||
if (userAgent.includes("Windows")) {
|
if (userAgent.includes("Windows")) {
|
||||||
// windows user
|
// windows user
|
||||||
setDefaultSystem(systems[2]);
|
setDefaultSystem(systems[2]);
|
||||||
} else if (userAgent.includes("Linux")) {
|
} else if (userAgent.includes("Linux")) {
|
||||||
// linux user
|
// linux user
|
||||||
setDefaultSystem(systems[3]);
|
setDefaultSystem(systems[3]);
|
||||||
} else if (userAgent.includes("Mac OS") && userAgent.includes("Intel")) {
|
} else if (
|
||||||
// mac intel user
|
userAgent.includes("Mac OS") &&
|
||||||
setDefaultSystem(systems[1]);
|
arc &&
|
||||||
} else {
|
arc.architecture === "arm"
|
||||||
// mac user and also default
|
) {
|
||||||
setDefaultSystem(systems[0]);
|
setDefaultSystem(systems[0]);
|
||||||
|
} else {
|
||||||
|
setDefaultSystem(systems[1]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
.custom-toc-title {
|
.custom-toc-title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
margin-top: -20px;
|
margin-top: -20px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,10 +27,10 @@
|
|||||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
|
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply text-sm;
|
@apply text-base;
|
||||||
@apply antialiased;
|
@apply antialiased;
|
||||||
@apply bg-white dark:bg-black;
|
@apply bg-white dark:bg-black;
|
||||||
@apply text-gray-700 dark:text-gray-400;
|
@apply text-gray-800 dark:text-gray-300;
|
||||||
}
|
}
|
||||||
img {
|
img {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
@ -92,9 +92,23 @@
|
|||||||
}
|
}
|
||||||
ul,
|
ul,
|
||||||
ol {
|
ol {
|
||||||
padding-left: 16px;
|
padding-left: 32px;
|
||||||
li {
|
li {
|
||||||
@apply leading-loose;
|
@apply leading-loose;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown h1:first-child,
|
||||||
|
.markdown > p {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-code-block {
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-of-contents {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|||||||
65
electron/handlers/app.ts
Normal file
65
electron/handlers/app.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { app, ipcMain, shell } from "electron";
|
||||||
|
import { ModuleManager } from "../managers/module";
|
||||||
|
import { join } from "path";
|
||||||
|
import { PluginManager } from "../managers/plugin";
|
||||||
|
import { WindowManager } from "../managers/window";
|
||||||
|
|
||||||
|
export function handleAppIPCs() {
|
||||||
|
/**
|
||||||
|
* Retrieves the path to the app data directory using the `coreAPI` object.
|
||||||
|
* If the `coreAPI` object is not available, the function returns `undefined`.
|
||||||
|
* @returns A Promise that resolves with the path to the app data directory, or `undefined` if the `coreAPI` object is not available.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("appDataPath", async (_event) => {
|
||||||
|
return app.getPath("userData");
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the version of the app.
|
||||||
|
* @param _event - The IPC event object.
|
||||||
|
* @returns The version of the app.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("appVersion", async (_event) => {
|
||||||
|
return app.getVersion();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the "openAppDirectory" IPC message by opening the app's user data directory.
|
||||||
|
* The `shell.openPath` method is used to open the directory in the user's default file explorer.
|
||||||
|
* @param _event - The IPC event object.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("openAppDirectory", async (_event) => {
|
||||||
|
shell.openPath(app.getPath("userData"));
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a URL in the user's default browser.
|
||||||
|
* @param _event - The IPC event object.
|
||||||
|
* @param url - The URL to open.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("openExternalUrl", async (_event, url) => {
|
||||||
|
shell.openExternal(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relaunches the app in production - reload window in development.
|
||||||
|
* @param _event - The IPC event object.
|
||||||
|
* @param url - The URL to reload.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("relaunch", async (_event, url) => {
|
||||||
|
ModuleManager.instance.clearImportedModules();
|
||||||
|
|
||||||
|
if (app.isPackaged) {
|
||||||
|
app.relaunch();
|
||||||
|
app.exit();
|
||||||
|
} else {
|
||||||
|
for (const modulePath in ModuleManager.instance.requiredModules) {
|
||||||
|
delete require.cache[
|
||||||
|
require.resolve(join(app.getPath("userData"), "plugins", modulePath))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
PluginManager.instance.setupPlugins();
|
||||||
|
WindowManager.instance.currentWindow?.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
106
electron/handlers/download.ts
Normal file
106
electron/handlers/download.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
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, unlink } from "fs";
|
||||||
|
const progress = require("request-progress");
|
||||||
|
|
||||||
|
export function handleDownloaderIPCs() {
|
||||||
|
/**
|
||||||
|
* Handles the "pauseDownload" IPC message by pausing the download associated with the provided fileName.
|
||||||
|
* @param _event - The IPC event object.
|
||||||
|
* @param fileName - The name of the file being downloaded.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("pauseDownload", async (_event, fileName) => {
|
||||||
|
DownloadManager.instance.networkRequests[fileName]?.pause();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the "resumeDownload" IPC message by resuming the download associated with the provided fileName.
|
||||||
|
* @param _event - The IPC event object.
|
||||||
|
* @param fileName - The name of the file being downloaded.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("resumeDownload", async (_event, fileName) => {
|
||||||
|
DownloadManager.instance.networkRequests[fileName]?.resume();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the "abortDownload" IPC message by aborting the download associated with the provided fileName.
|
||||||
|
* The network request associated with the fileName is then removed from the networkRequests object.
|
||||||
|
* @param _event - The IPC event object.
|
||||||
|
* @param fileName - The name of the file being downloaded.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("abortDownload", async (_event, fileName) => {
|
||||||
|
const rq = DownloadManager.instance.networkRequests[fileName];
|
||||||
|
DownloadManager.instance.networkRequests[fileName] = undefined;
|
||||||
|
const userDataPath = app.getPath("userData");
|
||||||
|
const fullPath = join(userDataPath, fileName);
|
||||||
|
rq?.abort();
|
||||||
|
let result = "NULL";
|
||||||
|
unlink(fullPath, function (err) {
|
||||||
|
if (err && err.code == "ENOENT") {
|
||||||
|
result = `File not exist: ${err}`;
|
||||||
|
} else if (err) {
|
||||||
|
result = `File delete error: ${err}`;
|
||||||
|
} else {
|
||||||
|
result = "File deleted successfully";
|
||||||
|
}
|
||||||
|
console.log(`Delete file ${fileName} from ${fullPath} result: ${result}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a file from a given URL.
|
||||||
|
* @param _event - The IPC event object.
|
||||||
|
* @param url - The URL to download the file from.
|
||||||
|
* @param fileName - The name to give the downloaded file.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("downloadFile", async (_event, url, fileName) => {
|
||||||
|
const userDataPath = app.getPath("userData");
|
||||||
|
const destination = resolve(userDataPath, fileName);
|
||||||
|
const rq = request(url);
|
||||||
|
|
||||||
|
progress(rq, {})
|
||||||
|
.on("progress", function (state: any) {
|
||||||
|
WindowManager?.instance.currentWindow?.webContents.send(
|
||||||
|
"FILE_DOWNLOAD_UPDATE",
|
||||||
|
{
|
||||||
|
...state,
|
||||||
|
fileName,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.on("error", function (err: Error) {
|
||||||
|
WindowManager?.instance.currentWindow?.webContents.send(
|
||||||
|
"FILE_DOWNLOAD_ERROR",
|
||||||
|
{
|
||||||
|
fileName,
|
||||||
|
err,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.on("end", function () {
|
||||||
|
if (DownloadManager.instance.networkRequests[fileName]) {
|
||||||
|
WindowManager?.instance.currentWindow?.webContents.send(
|
||||||
|
"FILE_DOWNLOAD_COMPLETE",
|
||||||
|
{
|
||||||
|
fileName,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
DownloadManager.instance.setRequest(fileName, undefined);
|
||||||
|
} else {
|
||||||
|
WindowManager?.instance.currentWindow?.webContents.send(
|
||||||
|
"FILE_DOWNLOAD_ERROR",
|
||||||
|
{
|
||||||
|
fileName,
|
||||||
|
err: "Download cancelled",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.pipe(createWriteStream(destination));
|
||||||
|
|
||||||
|
DownloadManager.instance.setRequest(fileName, rq);
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -5,7 +5,7 @@ import { join } from "path";
|
|||||||
/**
|
/**
|
||||||
* Handles file system operations.
|
* Handles file system operations.
|
||||||
*/
|
*/
|
||||||
export function handleFs() {
|
export function handleFsIPCs() {
|
||||||
/**
|
/**
|
||||||
* Reads a file from the user data directory.
|
* Reads a file from the user data directory.
|
||||||
* @param event - The event object.
|
* @param event - The event object.
|
||||||
@ -115,4 +115,29 @@ export function handleFs() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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("deleteFile", async (_event, filePath) => {
|
||||||
|
const userDataPath = app.getPath("userData");
|
||||||
|
const fullPath = join(userDataPath, filePath);
|
||||||
|
|
||||||
|
let result = "NULL";
|
||||||
|
fs.unlink(fullPath, function (err) {
|
||||||
|
if (err && err.code == "ENOENT") {
|
||||||
|
result = `File not exist: ${err}`;
|
||||||
|
} else if (err) {
|
||||||
|
result = `File delete error: ${err}`;
|
||||||
|
} else {
|
||||||
|
result = "File deleted successfully";
|
||||||
|
}
|
||||||
|
console.log(`Delete file ${filePath} from ${fullPath} result: ${result}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
118
electron/handlers/plugin.ts
Normal file
118
electron/handlers/plugin.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { app, ipcMain } from "electron";
|
||||||
|
import { readdirSync, rmdir, writeFileSync } from "fs";
|
||||||
|
import { ModuleManager } from "../managers/module";
|
||||||
|
import { join, extname } from "path";
|
||||||
|
import { PluginManager } from "../managers/plugin";
|
||||||
|
import { WindowManager } from "../managers/window";
|
||||||
|
import { manifest, tarball } from "pacote";
|
||||||
|
|
||||||
|
export function handlePluginIPCs() {
|
||||||
|
/**
|
||||||
|
* Invokes a function from a plugin module in main node process.
|
||||||
|
* @param _event - The IPC event object.
|
||||||
|
* @param modulePath - The path to the plugin module.
|
||||||
|
* @param method - The name of the function to invoke.
|
||||||
|
* @param args - The arguments to pass to the function.
|
||||||
|
* @returns The result of the invoked function.
|
||||||
|
*/
|
||||||
|
ipcMain.handle(
|
||||||
|
"invokePluginFunc",
|
||||||
|
async (_event, modulePath, method, ...args) => {
|
||||||
|
const module = require(
|
||||||
|
/* webpackIgnore: true */ join(
|
||||||
|
app.getPath("userData"),
|
||||||
|
"plugins",
|
||||||
|
modulePath
|
||||||
|
)
|
||||||
|
);
|
||||||
|
ModuleManager.instance.setModule(modulePath, module);
|
||||||
|
|
||||||
|
if (typeof module[method] === "function") {
|
||||||
|
return module[method](...args);
|
||||||
|
} else {
|
||||||
|
console.log(module[method]);
|
||||||
|
console.error(`Function "${method}" does not exist in the module.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the paths of the base plugins.
|
||||||
|
* @param _event - The IPC event object.
|
||||||
|
* @returns An array of paths to the base plugins.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("basePlugins", async (_event) => {
|
||||||
|
const basePluginPath = join(
|
||||||
|
__dirname,
|
||||||
|
"../",
|
||||||
|
app.isPackaged
|
||||||
|
? "../../app.asar.unpacked/core/pre-install"
|
||||||
|
: "../core/pre-install"
|
||||||
|
);
|
||||||
|
return readdirSync(basePluginPath)
|
||||||
|
.filter((file) => extname(file) === ".tgz")
|
||||||
|
.map((file) => join(basePluginPath, file));
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path to the user's plugin directory.
|
||||||
|
* @param _event - The IPC event object.
|
||||||
|
* @returns The path to the user's plugin directory.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("pluginPath", async (_event) => {
|
||||||
|
return join(app.getPath("userData"), "plugins");
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the `plugins` directory in the user data path and disposes of required modules.
|
||||||
|
* If the app is packaged, the function relaunches the app and exits.
|
||||||
|
* Otherwise, the function deletes the cached modules and sets up the plugins and reloads the main window.
|
||||||
|
* @param _event - The IPC event object.
|
||||||
|
* @param url - The URL to reload.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("reloadPlugins", async (_event, url) => {
|
||||||
|
const userDataPath = app.getPath("userData");
|
||||||
|
const fullPath = join(userDataPath, "plugins");
|
||||||
|
|
||||||
|
rmdir(fullPath, { recursive: true }, function (err) {
|
||||||
|
if (err) console.log(err);
|
||||||
|
ModuleManager.instance.clearImportedModules();
|
||||||
|
|
||||||
|
// just relaunch if packaged, should launch manually in development mode
|
||||||
|
if (app.isPackaged) {
|
||||||
|
app.relaunch();
|
||||||
|
app.exit();
|
||||||
|
} else {
|
||||||
|
for (const modulePath in ModuleManager.instance.requiredModules) {
|
||||||
|
delete require.cache[
|
||||||
|
require.resolve(
|
||||||
|
join(app.getPath("userData"), "plugins", modulePath)
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
PluginManager.instance.setupPlugins();
|
||||||
|
WindowManager.instance.currentWindow?.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs a remote plugin by downloading its tarball and writing it to a tgz file.
|
||||||
|
* @param _event - The IPC event object.
|
||||||
|
* @param pluginName - The name of the remote plugin to install.
|
||||||
|
* @returns A Promise that resolves to the path of the installed plugin file.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("installRemotePlugin", async (_event, pluginName) => {
|
||||||
|
const destination = join(
|
||||||
|
app.getPath("userData"),
|
||||||
|
pluginName.replace(/^@.*\//, "") + ".tgz"
|
||||||
|
);
|
||||||
|
return manifest(pluginName)
|
||||||
|
.then(async (manifest: any) => {
|
||||||
|
await tarball(manifest._resolved).then((data: Buffer) => {
|
||||||
|
writeFileSync(destination, data);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => destination);
|
||||||
|
});
|
||||||
|
}
|
||||||
27
electron/handlers/theme.ts
Normal file
27
electron/handlers/theme.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { ipcMain, nativeTheme } from "electron";
|
||||||
|
|
||||||
|
export function handleThemesIPCs() {
|
||||||
|
/**
|
||||||
|
* Handles the "setNativeThemeLight" IPC message by setting the native theme source to "light".
|
||||||
|
* This will change the appearance of the app to the light theme.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("setNativeThemeLight", () => {
|
||||||
|
nativeTheme.themeSource = "light";
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the "setNativeThemeDark" IPC message by setting the native theme source to "dark".
|
||||||
|
* This will change the appearance of the app to the dark theme.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("setNativeThemeDark", () => {
|
||||||
|
nativeTheme.themeSource = "dark";
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the "setNativeThemeSystem" IPC message by setting the native theme source to "system".
|
||||||
|
* This will change the appearance of the app to match the system's current theme.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("setNativeThemeSystem", () => {
|
||||||
|
nativeTheme.themeSource = "system";
|
||||||
|
});
|
||||||
|
}
|
||||||
58
electron/handlers/update.ts
Normal file
58
electron/handlers/update.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { app, dialog } from "electron";
|
||||||
|
import { WindowManager } from "../managers/window";
|
||||||
|
import { autoUpdater } from "electron-updater";
|
||||||
|
|
||||||
|
export function handleAppUpdates() {
|
||||||
|
/* Should not check for update during development */
|
||||||
|
if (!app.isPackaged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* New Update Available */
|
||||||
|
autoUpdater.on("update-available", async (_info: any) => {
|
||||||
|
const action = await dialog.showMessageBox({
|
||||||
|
message: `Update available. Do you want to download the latest update?`,
|
||||||
|
buttons: ["Download", "Later"],
|
||||||
|
});
|
||||||
|
if (action.response === 0) await autoUpdater.downloadUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* App Update Completion Message */
|
||||||
|
autoUpdater.on("update-downloaded", async (_info: any) => {
|
||||||
|
WindowManager.instance.currentWindow?.webContents.send(
|
||||||
|
"APP_UPDATE_COMPLETE",
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
const action = await dialog.showMessageBox({
|
||||||
|
message: `Update downloaded. Please restart the application to apply the updates.`,
|
||||||
|
buttons: ["Restart", "Later"],
|
||||||
|
});
|
||||||
|
if (action.response === 0) {
|
||||||
|
autoUpdater.quitAndInstall();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* App Update Error */
|
||||||
|
autoUpdater.on("error", (info: any) => {
|
||||||
|
dialog.showMessageBox({ message: info.message });
|
||||||
|
WindowManager.instance.currentWindow?.webContents.send(
|
||||||
|
"APP_UPDATE_ERROR",
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* App Update Progress */
|
||||||
|
autoUpdater.on("download-progress", (progress: any) => {
|
||||||
|
console.log("app update progress: ", progress.percent);
|
||||||
|
WindowManager.instance.currentWindow?.webContents.send(
|
||||||
|
"APP_UPDATE_PROGRESS",
|
||||||
|
{
|
||||||
|
percent: progress.percent,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
autoUpdater.autoDownload = false;
|
||||||
|
autoUpdater.autoInstallOnAppQuit = true;
|
||||||
|
if (process.env.CI !== "e2e") {
|
||||||
|
autoUpdater.checkForUpdates();
|
||||||
|
}
|
||||||
|
}
|
||||||
446
electron/main.ts
446
electron/main.ts
@ -1,33 +1,28 @@
|
|||||||
import {
|
import { app, BrowserWindow } from "electron";
|
||||||
app,
|
import { join } from "path";
|
||||||
BrowserWindow,
|
|
||||||
ipcMain,
|
|
||||||
dialog,
|
|
||||||
shell,
|
|
||||||
nativeTheme,
|
|
||||||
} from "electron";
|
|
||||||
import { readdirSync, writeFileSync } from "fs";
|
|
||||||
import { resolve, join, extname } from "path";
|
|
||||||
import { rmdir, unlink, createWriteStream } from "fs";
|
|
||||||
import { init } from "./core/plugin/index";
|
|
||||||
import { setupMenu } from "./utils/menu";
|
import { setupMenu } from "./utils/menu";
|
||||||
import { dispose } from "./utils/disposable";
|
import { handleFsIPCs } from "./handlers/fs";
|
||||||
import { handleFs } from "./handlers/fs";
|
|
||||||
|
|
||||||
const pacote = require("pacote");
|
/**
|
||||||
const request = require("request");
|
* Managers
|
||||||
const progress = require("request-progress");
|
**/
|
||||||
const { autoUpdater } = require("electron-updater");
|
import { WindowManager } from "./managers/window";
|
||||||
const Store = require("electron-store");
|
import { ModuleManager } from "./managers/module";
|
||||||
|
import { PluginManager } from "./managers/plugin";
|
||||||
|
|
||||||
let requiredModules: Record<string, any> = {};
|
/**
|
||||||
const networkRequests: Record<string, any> = {};
|
* IPC Handlers
|
||||||
let mainWindow: BrowserWindow | undefined = undefined;
|
**/
|
||||||
|
import { handleDownloaderIPCs } from "./handlers/download";
|
||||||
|
import { handleThemesIPCs } from "./handlers/theme";
|
||||||
|
import { handlePluginIPCs } from "./handlers/plugin";
|
||||||
|
import { handleAppIPCs } from "./handlers/app";
|
||||||
|
import { handleAppUpdates } from "./handlers/update";
|
||||||
|
|
||||||
app
|
app
|
||||||
.whenReady()
|
.whenReady()
|
||||||
.then(migratePlugins)
|
.then(PluginManager.instance.migratePlugins)
|
||||||
.then(setupPlugins)
|
.then(PluginManager.instance.setupPlugins)
|
||||||
.then(setupMenu)
|
.then(setupMenu)
|
||||||
.then(handleIPCs)
|
.then(handleIPCs)
|
||||||
.then(handleAppUpdates)
|
.then(handleAppUpdates)
|
||||||
@ -41,27 +36,18 @@ app
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.on("window-all-closed", () => {
|
app.on("window-all-closed", () => {
|
||||||
clearImportedModules();
|
ModuleManager.instance.clearImportedModules();
|
||||||
app.quit();
|
app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("quit", () => {
|
app.on("quit", () => {
|
||||||
clearImportedModules();
|
ModuleManager.instance.clearImportedModules();
|
||||||
app.quit();
|
app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
function createMainWindow() {
|
function createMainWindow() {
|
||||||
mainWindow = new BrowserWindow({
|
/* Create main window */
|
||||||
width: 1200,
|
const mainWindow = WindowManager.instance.createWindow({
|
||||||
minWidth: 800,
|
|
||||||
height: 800,
|
|
||||||
show: false,
|
|
||||||
trafficLightPosition: {
|
|
||||||
x: 10,
|
|
||||||
y: 15,
|
|
||||||
},
|
|
||||||
titleBarStyle: "hidden",
|
|
||||||
vibrancy: "sidebar",
|
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
preload: join(__dirname, "preload.js"),
|
preload: join(__dirname, "preload.js"),
|
||||||
@ -73,6 +59,7 @@ function createMainWindow() {
|
|||||||
? `file://${join(__dirname, "../renderer/index.html")}`
|
? `file://${join(__dirname, "../renderer/index.html")}`
|
||||||
: "http://localhost:3000";
|
: "http://localhost:3000";
|
||||||
|
|
||||||
|
/* Load frontend app to the window */
|
||||||
mainWindow.loadURL(startURL);
|
mainWindow.loadURL(startURL);
|
||||||
|
|
||||||
mainWindow.once("ready-to-show", () => mainWindow?.show());
|
mainWindow.once("ready-to-show", () => mainWindow?.show());
|
||||||
@ -80,390 +67,17 @@ function createMainWindow() {
|
|||||||
if (process.platform !== "darwin") app.quit();
|
if (process.platform !== "darwin") app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Enable dev tools for development */
|
||||||
if (!app.isPackaged) mainWindow.webContents.openDevTools();
|
if (!app.isPackaged) mainWindow.webContents.openDevTools();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAppUpdates() {
|
|
||||||
/*New Update Available*/
|
|
||||||
autoUpdater.on("update-available", async (_info: any) => {
|
|
||||||
const action = await dialog.showMessageBox({
|
|
||||||
message: `Update available. Do you want to download the latest update?`,
|
|
||||||
buttons: ["Download", "Later"],
|
|
||||||
});
|
|
||||||
if (action.response === 0) await autoUpdater.downloadUpdate();
|
|
||||||
});
|
|
||||||
|
|
||||||
/*App Update Completion Message*/
|
|
||||||
autoUpdater.on("update-downloaded", async (_info: any) => {
|
|
||||||
mainWindow?.webContents.send("APP_UPDATE_COMPLETE", {});
|
|
||||||
const action = await dialog.showMessageBox({
|
|
||||||
message: `Update downloaded. Please restart the application to apply the updates.`,
|
|
||||||
buttons: ["Restart", "Later"],
|
|
||||||
});
|
|
||||||
if (action.response === 0) {
|
|
||||||
autoUpdater.quitAndInstall();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/*App Update Error */
|
|
||||||
autoUpdater.on("error", (info: any) => {
|
|
||||||
dialog.showMessageBox({ message: info.message });
|
|
||||||
mainWindow?.webContents.send("APP_UPDATE_ERROR", {});
|
|
||||||
});
|
|
||||||
|
|
||||||
/*App Update Progress */
|
|
||||||
autoUpdater.on("download-progress", (progress: any) => {
|
|
||||||
console.log("app update progress: ", progress.percent);
|
|
||||||
mainWindow?.webContents.send("APP_UPDATE_PROGRESS", {
|
|
||||||
percent: progress.percent,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
autoUpdater.autoDownload = false;
|
|
||||||
autoUpdater.autoInstallOnAppQuit = true;
|
|
||||||
if (process.env.CI !== "e2e") {
|
|
||||||
autoUpdater.checkForUpdates();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles various IPC messages from the renderer process.
|
* Handles various IPC messages from the renderer process.
|
||||||
*/
|
*/
|
||||||
function handleIPCs() {
|
function handleIPCs() {
|
||||||
handleFs();
|
handleFsIPCs();
|
||||||
/**
|
handleDownloaderIPCs();
|
||||||
* Handles the "setNativeThemeLight" IPC message by setting the native theme source to "light".
|
handleThemesIPCs();
|
||||||
* This will change the appearance of the app to the light theme.
|
handlePluginIPCs();
|
||||||
*/
|
handleAppIPCs();
|
||||||
ipcMain.handle("setNativeThemeLight", () => {
|
|
||||||
nativeTheme.themeSource = "light";
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the "setNativeThemeDark" IPC message by setting the native theme source to "dark".
|
|
||||||
* This will change the appearance of the app to the dark theme.
|
|
||||||
*/
|
|
||||||
ipcMain.handle("setNativeThemeDark", () => {
|
|
||||||
nativeTheme.themeSource = "dark";
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the "setNativeThemeSystem" IPC message by setting the native theme source to "system".
|
|
||||||
* This will change the appearance of the app to match the system's current theme.
|
|
||||||
*/
|
|
||||||
ipcMain.handle("setNativeThemeSystem", () => {
|
|
||||||
nativeTheme.themeSource = "system";
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invokes a function from a plugin module in main node process.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param modulePath - The path to the plugin module.
|
|
||||||
* @param method - The name of the function to invoke.
|
|
||||||
* @param args - The arguments to pass to the function.
|
|
||||||
* @returns The result of the invoked function.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
"invokePluginFunc",
|
|
||||||
async (_event, modulePath, method, ...args) => {
|
|
||||||
const module = require(
|
|
||||||
/* webpackIgnore: true */ join(
|
|
||||||
app.getPath("userData"),
|
|
||||||
"plugins",
|
|
||||||
modulePath
|
|
||||||
)
|
|
||||||
);
|
|
||||||
requiredModules[modulePath] = module;
|
|
||||||
|
|
||||||
if (typeof module[method] === "function") {
|
|
||||||
return module[method](...args);
|
|
||||||
} else {
|
|
||||||
console.log(module[method]);
|
|
||||||
console.error(`Function "${method}" does not exist in the module.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the paths of the base plugins.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @returns An array of paths to the base plugins.
|
|
||||||
*/
|
|
||||||
ipcMain.handle("basePlugins", async (_event) => {
|
|
||||||
const basePluginPath = join(
|
|
||||||
__dirname,
|
|
||||||
"../",
|
|
||||||
app.isPackaged
|
|
||||||
? "../app.asar.unpacked/core/pre-install"
|
|
||||||
: "/core/pre-install"
|
|
||||||
);
|
|
||||||
return readdirSync(basePluginPath)
|
|
||||||
.filter((file) => extname(file) === ".tgz")
|
|
||||||
.map((file) => join(basePluginPath, file));
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the path to the user's plugin directory.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @returns The path to the user's plugin directory.
|
|
||||||
*/
|
|
||||||
ipcMain.handle("pluginPath", async (_event) => {
|
|
||||||
return join(app.getPath("userData"), "plugins");
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the path to the app data directory using the `coreAPI` object.
|
|
||||||
* If the `coreAPI` object is not available, the function returns `undefined`.
|
|
||||||
* @returns A Promise that resolves with the path to the app data directory, or `undefined` if the `coreAPI` object is not available.
|
|
||||||
*/
|
|
||||||
ipcMain.handle("appDataPath", async (_event) => {
|
|
||||||
return app.getPath("userData");
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the version of the app.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @returns The version of the app.
|
|
||||||
*/
|
|
||||||
ipcMain.handle("appVersion", async (_event) => {
|
|
||||||
return app.getVersion();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the "openAppDirectory" IPC message by opening the app's user data directory.
|
|
||||||
* The `shell.openPath` method is used to open the directory in the user's default file explorer.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
*/
|
|
||||||
ipcMain.handle("openAppDirectory", async (_event) => {
|
|
||||||
shell.openPath(app.getPath("userData"));
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens a URL in the user's default browser.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param url - The URL to open.
|
|
||||||
*/
|
|
||||||
ipcMain.handle("openExternalUrl", async (_event, url) => {
|
|
||||||
shell.openExternal(url);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Relaunches the app in production - reload window in development.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param url - The URL to reload.
|
|
||||||
*/
|
|
||||||
ipcMain.handle("relaunch", async (_event, url) => {
|
|
||||||
clearImportedModules();
|
|
||||||
|
|
||||||
if (app.isPackaged) {
|
|
||||||
app.relaunch();
|
|
||||||
app.exit();
|
|
||||||
} else {
|
|
||||||
for (const modulePath in requiredModules) {
|
|
||||||
delete require.cache[
|
|
||||||
require.resolve(join(app.getPath("userData"), "plugins", modulePath))
|
|
||||||
];
|
|
||||||
}
|
|
||||||
setupPlugins();
|
|
||||||
mainWindow?.reload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the `plugins` directory in the user data path and disposes of required modules.
|
|
||||||
* If the app is packaged, the function relaunches the app and exits.
|
|
||||||
* Otherwise, the function deletes the cached modules and sets up the plugins and reloads the main window.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param url - The URL to reload.
|
|
||||||
*/
|
|
||||||
ipcMain.handle("reloadPlugins", async (_event, url) => {
|
|
||||||
const userDataPath = app.getPath("userData");
|
|
||||||
const fullPath = join(userDataPath, "plugins");
|
|
||||||
|
|
||||||
rmdir(fullPath, { recursive: true }, function (err) {
|
|
||||||
if (err) console.log(err);
|
|
||||||
clearImportedModules();
|
|
||||||
|
|
||||||
// just relaunch if packaged, should launch manually in development mode
|
|
||||||
if (app.isPackaged) {
|
|
||||||
app.relaunch();
|
|
||||||
app.exit();
|
|
||||||
} else {
|
|
||||||
for (const modulePath in requiredModules) {
|
|
||||||
delete require.cache[
|
|
||||||
require.resolve(
|
|
||||||
join(app.getPath("userData"), "plugins", modulePath)
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
setupPlugins();
|
|
||||||
mainWindow?.reload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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("deleteFile", async (_event, filePath) => {
|
|
||||||
const userDataPath = app.getPath("userData");
|
|
||||||
const fullPath = join(userDataPath, filePath);
|
|
||||||
|
|
||||||
let result = "NULL";
|
|
||||||
unlink(fullPath, function (err) {
|
|
||||||
if (err && err.code == "ENOENT") {
|
|
||||||
result = `File not exist: ${err}`;
|
|
||||||
} else if (err) {
|
|
||||||
result = `File delete error: ${err}`;
|
|
||||||
} else {
|
|
||||||
result = "File deleted successfully";
|
|
||||||
}
|
|
||||||
console.log(`Delete file ${filePath} from ${fullPath} result: ${result}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Downloads a file from a given URL.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param url - The URL to download the file from.
|
|
||||||
* @param fileName - The name to give the downloaded file.
|
|
||||||
*/
|
|
||||||
ipcMain.handle("downloadFile", async (_event, url, fileName) => {
|
|
||||||
const userDataPath = app.getPath("userData");
|
|
||||||
const destination = resolve(userDataPath, fileName);
|
|
||||||
const rq = request(url);
|
|
||||||
|
|
||||||
progress(rq, {})
|
|
||||||
.on("progress", function (state: any) {
|
|
||||||
mainWindow?.webContents.send("FILE_DOWNLOAD_UPDATE", {
|
|
||||||
...state,
|
|
||||||
fileName,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.on("error", function (err: Error) {
|
|
||||||
mainWindow?.webContents.send("FILE_DOWNLOAD_ERROR", {
|
|
||||||
fileName,
|
|
||||||
err,
|
|
||||||
});
|
|
||||||
networkRequests[fileName] = undefined;
|
|
||||||
})
|
|
||||||
.on("end", function () {
|
|
||||||
if (networkRequests[fileName]) {
|
|
||||||
mainWindow?.webContents.send("FILE_DOWNLOAD_COMPLETE", {
|
|
||||||
fileName,
|
|
||||||
});
|
|
||||||
networkRequests[fileName] = undefined;
|
|
||||||
} else {
|
|
||||||
mainWindow?.webContents.send("FILE_DOWNLOAD_ERROR", {
|
|
||||||
fileName,
|
|
||||||
err: "Download cancelled",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.pipe(createWriteStream(destination));
|
|
||||||
|
|
||||||
networkRequests[fileName] = rq;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the "pauseDownload" IPC message by pausing the download associated with the provided fileName.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param fileName - The name of the file being downloaded.
|
|
||||||
*/
|
|
||||||
ipcMain.handle("pauseDownload", async (_event, fileName) => {
|
|
||||||
networkRequests[fileName]?.pause();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the "resumeDownload" IPC message by resuming the download associated with the provided fileName.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param fileName - The name of the file being downloaded.
|
|
||||||
*/
|
|
||||||
ipcMain.handle("resumeDownload", async (_event, fileName) => {
|
|
||||||
networkRequests[fileName]?.resume();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the "abortDownload" IPC message by aborting the download associated with the provided fileName.
|
|
||||||
* The network request associated with the fileName is then removed from the networkRequests object.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param fileName - The name of the file being downloaded.
|
|
||||||
*/
|
|
||||||
ipcMain.handle("abortDownload", async (_event, fileName) => {
|
|
||||||
const rq = networkRequests[fileName];
|
|
||||||
networkRequests[fileName] = undefined;
|
|
||||||
rq?.abort();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Installs a remote plugin by downloading its tarball and writing it to a tgz file.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param pluginName - The name of the remote plugin to install.
|
|
||||||
* @returns A Promise that resolves to the path of the installed plugin file.
|
|
||||||
*/
|
|
||||||
ipcMain.handle("installRemotePlugin", async (_event, pluginName) => {
|
|
||||||
const destination = join(
|
|
||||||
app.getPath("userData"),
|
|
||||||
pluginName.replace(/^@.*\//, "") + ".tgz"
|
|
||||||
);
|
|
||||||
return pacote
|
|
||||||
.manifest(pluginName)
|
|
||||||
.then(async (manifest: any) => {
|
|
||||||
await pacote.tarball(manifest._resolved).then((data: Buffer) => {
|
|
||||||
writeFileSync(destination, data);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(() => destination);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrates the plugins by deleting the `plugins` 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 `plugins` directory and sets the `migrated_version` key to the current app version.
|
|
||||||
* @returns A Promise that resolves when the migration is complete.
|
|
||||||
*/
|
|
||||||
function migratePlugins() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const store = new Store();
|
|
||||||
if (store.get("migrated_version") !== app.getVersion()) {
|
|
||||||
console.log("start migration:", store.get("migrated_version"));
|
|
||||||
const userDataPath = app.getPath("userData");
|
|
||||||
const fullPath = join(userDataPath, "plugins");
|
|
||||||
|
|
||||||
rmdir(fullPath, { recursive: true }, function (err) {
|
|
||||||
if (err) console.log(err);
|
|
||||||
store.set("migrated_version", app.getVersion());
|
|
||||||
console.log("migrate plugins done");
|
|
||||||
resolve(undefined);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
resolve(undefined);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up the plugins by initializing the `plugins` module with the `confirmInstall` and `pluginsPath` options.
|
|
||||||
* The `confirmInstall` function always returns `true` to allow plugin installation.
|
|
||||||
* The `pluginsPath` option specifies the path to install plugins to.
|
|
||||||
*/
|
|
||||||
function setupPlugins() {
|
|
||||||
init({
|
|
||||||
// Function to check from the main process that user wants to install a plugin
|
|
||||||
confirmInstall: async (_plugins: string[]) => {
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
// Path to install plugin to
|
|
||||||
pluginsPath: join(app.getPath("userData"), "plugins"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearImportedModules() {
|
|
||||||
dispose(requiredModules);
|
|
||||||
requiredModules = {};
|
|
||||||
}
|
}
|
||||||
|
|||||||
24
electron/managers/download.ts
Normal file
24
electron/managers/download.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Request } from "request";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages file downloads and network requests.
|
||||||
|
*/
|
||||||
|
export class DownloadManager {
|
||||||
|
public networkRequests: Record<string, any> = {};
|
||||||
|
|
||||||
|
public static instance: DownloadManager = new DownloadManager();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
if (DownloadManager.instance) {
|
||||||
|
return DownloadManager.instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets a network request for a specific file.
|
||||||
|
* @param {string} fileName - The name of the file.
|
||||||
|
* @param {Request | undefined} request - The network request to set, or undefined to clear the request.
|
||||||
|
*/
|
||||||
|
setRequest(fileName: string, request: Request | undefined) {
|
||||||
|
this.networkRequests[fileName] = request;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
electron/managers/module.ts
Normal file
33
electron/managers/module.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { dispose } from "../utils/disposable";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages imported modules.
|
||||||
|
*/
|
||||||
|
export class ModuleManager {
|
||||||
|
public requiredModules: Record<string, any> = {};
|
||||||
|
|
||||||
|
public static instance: ModuleManager = new ModuleManager();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
if (ModuleManager.instance) {
|
||||||
|
return ModuleManager.instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a module.
|
||||||
|
* @param {string} moduleName - The name of the module.
|
||||||
|
* @param {any | undefined} nodule - The module to set, or undefined to clear the module.
|
||||||
|
*/
|
||||||
|
setModule(moduleName: string, nodule: any | undefined) {
|
||||||
|
this.requiredModules[moduleName] = nodule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all imported modules.
|
||||||
|
*/
|
||||||
|
clearImportedModules() {
|
||||||
|
dispose(this.requiredModules);
|
||||||
|
this.requiredModules = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
60
electron/managers/plugin.ts
Normal file
60
electron/managers/plugin.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { app } from "electron";
|
||||||
|
import { init } from "../core/plugin/index";
|
||||||
|
import { join } from "path";
|
||||||
|
import { rmdir } from "fs";
|
||||||
|
import Store from "electron-store";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages plugin installation and migration.
|
||||||
|
*/
|
||||||
|
export class PluginManager {
|
||||||
|
public static instance: PluginManager = new PluginManager();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
if (PluginManager.instance) {
|
||||||
|
return PluginManager.instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the plugins by initializing the `plugins` module with the `confirmInstall` and `pluginsPath` options.
|
||||||
|
* The `confirmInstall` function always returns `true` to allow plugin installation.
|
||||||
|
* The `pluginsPath` option specifies the path to install plugins to.
|
||||||
|
*/
|
||||||
|
setupPlugins() {
|
||||||
|
init({
|
||||||
|
// Function to check from the main process that user wants to install a plugin
|
||||||
|
confirmInstall: async (_plugins: string[]) => {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
// Path to install plugin to
|
||||||
|
pluginsPath: join(app.getPath("userData"), "plugins"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates the plugins by deleting the `plugins` 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 `plugins` directory and sets the `migrated_version` key to the current app version.
|
||||||
|
* @returns A Promise that resolves when the migration is complete.
|
||||||
|
*/
|
||||||
|
migratePlugins() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const store = new Store();
|
||||||
|
if (store.get("migrated_version") !== app.getVersion()) {
|
||||||
|
console.log("start migration:", store.get("migrated_version"));
|
||||||
|
const userDataPath = app.getPath("userData");
|
||||||
|
const fullPath = join(userDataPath, "plugins");
|
||||||
|
|
||||||
|
rmdir(fullPath, { recursive: true }, function (err) {
|
||||||
|
if (err) console.log(err);
|
||||||
|
store.set("migrated_version", app.getVersion());
|
||||||
|
console.log("migrate plugins done");
|
||||||
|
resolve(undefined);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
37
electron/managers/window.ts
Normal file
37
electron/managers/window.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { BrowserWindow } from "electron";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the current window instance.
|
||||||
|
*/
|
||||||
|
export class WindowManager {
|
||||||
|
public static instance: WindowManager = new WindowManager();
|
||||||
|
public currentWindow?: BrowserWindow;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
if (WindowManager.instance) {
|
||||||
|
return WindowManager.instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new window instance.
|
||||||
|
* @param {Electron.BrowserWindowConstructorOptions} options - The options to create the window with.
|
||||||
|
* @returns The created window instance.
|
||||||
|
*/
|
||||||
|
createWindow(options?: Electron.BrowserWindowConstructorOptions | undefined) {
|
||||||
|
this.currentWindow = new BrowserWindow({
|
||||||
|
width: 1200,
|
||||||
|
minWidth: 800,
|
||||||
|
height: 800,
|
||||||
|
show: false,
|
||||||
|
trafficLightPosition: {
|
||||||
|
x: 10,
|
||||||
|
y: 15,
|
||||||
|
},
|
||||||
|
titleBarStyle: "hidden",
|
||||||
|
vibrancy: "sidebar",
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
return this.currentWindow;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -67,6 +67,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@npmcli/arborist": "^7.1.0",
|
"@npmcli/arborist": "^7.1.0",
|
||||||
|
"@types/request": "^2.48.12",
|
||||||
"@uiball/loaders": "^1.3.0",
|
"@uiball/loaders": "^1.3.0",
|
||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
"electron-updater": "^6.1.4",
|
"electron-updater": "^6.1.4",
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
"outDir": "./build",
|
"outDir": "./build",
|
||||||
"rootDir": "./",
|
"rootDir": "./",
|
||||||
"noEmitOnError": true,
|
"noEmitOnError": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|||||||
17
package.json
17
package.json
@ -35,27 +35,22 @@
|
|||||||
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
||||||
"build:electron": "yarn workspace jan build",
|
"build:electron": "yarn workspace jan build",
|
||||||
"build:electron:test": "yarn workspace jan build:test",
|
"build:electron:test": "yarn workspace jan build:test",
|
||||||
"build:pull-plugins": "rimraf ./electron/core/pre-install/*.tgz && cd ./electron/core/pre-install && npm pack @janhq/inference-plugin @janhq/monitoring-plugin",
|
"build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run build:publish\" \"cd ./plugins/model-plugin && npm install && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run build:publish\"",
|
||||||
"build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install --ignore-scripts && npm run postinstall:dev && npm run build:publish\" \"cd ./plugins/model-plugin && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall && npm run build:publish\"",
|
"build:plugins-win32": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run build:publish-win32\" \"cd ./plugins/model-plugin && npm install && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run build:publish\"",
|
||||||
"build:plugins-windows": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install --ignore-scripts && npm run postinstall:windows && npm run build:publish\" \"cd ./plugins/model-plugin && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall && npm run build:publish\"",
|
"build:plugins-linux": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run build:publish-linux\" \"cd ./plugins/model-plugin && npm install && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run build:publish\"",
|
||||||
"build:plugins-linux": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/model-plugin && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall && npm run build:publish\"",
|
"build:plugins-darwin": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run build:publish-darwin\" \"cd ./plugins/model-plugin && npm install && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run build:publish\"",
|
||||||
"build:plugins-darwin": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run postinstall && ../../.github/scripts/auto-sign.sh && npm run build:publish\" \"cd ./plugins/model-plugin && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall && npm run build:publish\"",
|
|
||||||
"build:plugins-web": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run build:deps && npm run postinstall\" \"cd ./plugins/inference-plugin && npm install && npm run postinstall\" \"cd ./plugins/model-plugin && npm install && npm run postinstall\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall\" && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm run build:publish\" \"cd ./plugins/inference-plugin && npm run build:publish\" \"cd ./plugins/model-plugin && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm run build:publish\"",
|
|
||||||
"build": "yarn build:web && yarn build:electron",
|
|
||||||
"build:test": "yarn build:web && yarn build:electron:test",
|
"build:test": "yarn build:web && yarn build:electron:test",
|
||||||
"build:test-darwin": "yarn build:web && yarn workspace jan build:test-darwin",
|
"build:test-darwin": "yarn build:web && yarn workspace jan build:test-darwin",
|
||||||
"build:test-win32": "yarn build:web && yarn workspace jan build:test-win32",
|
"build:test-win32": "yarn build:web && yarn workspace jan build:test-win32",
|
||||||
"build:test-linux": "yarn build:web && yarn workspace jan build:test-linux",
|
"build:test-linux": "yarn build:web && yarn workspace jan build:test-linux",
|
||||||
|
"build": "yarn build:web && yarn build:electron",
|
||||||
"build:darwin": "yarn build:web && yarn workspace jan build:darwin",
|
"build:darwin": "yarn build:web && yarn workspace jan build:darwin",
|
||||||
"build:win32": "yarn build:web && yarn workspace jan build:win32",
|
"build:win32": "yarn build:web && yarn workspace jan build:win32",
|
||||||
"build:linux": "yarn build:web && yarn workspace jan build:linux",
|
"build:linux": "yarn build:web && yarn workspace jan build:linux",
|
||||||
"build:publish": "yarn build:web && yarn workspace jan build:publish",
|
"build:publish": "yarn build:web && yarn workspace jan build:publish",
|
||||||
"build:publish-darwin": "yarn build:web && yarn workspace jan build:publish-darwin",
|
"build:publish-darwin": "yarn build:web && yarn workspace jan build:publish-darwin",
|
||||||
"build:publish-win32": "yarn build:web && yarn workspace jan build:publish-win32",
|
"build:publish-win32": "yarn build:web && yarn workspace jan build:publish-win32",
|
||||||
"build:publish-linux": "yarn build:web && yarn workspace jan build:publish-linux",
|
"build:publish-linux": "yarn build:web && yarn workspace jan build:publish-linux"
|
||||||
"build:web-plugins": "yarn build:web && yarn build:plugins-web && mkdir -p \"./web/out/plugins/conversational-json\" && cp \"./plugins/conversational-json/dist/index.js\" \"./web/out/plugins/conversational-json\" && mkdir -p \"./web/out/plugins/inference-plugin\" && cp \"./plugins/inference-plugin/dist/index.js\" \"./web/out/plugins/inference-plugin\" && mkdir -p \"./web/out/plugins/model-plugin\" && cp \"./plugins/model-plugin/dist/index.js\" \"./web/out/plugins/model-plugin\" && mkdir -p \"./web/out/plugins/monitoring-plugin\" && cp \"./plugins/monitoring-plugin/dist/index.js\" \"./web/out/plugins/monitoring-plugin\"",
|
|
||||||
"server:prod": "yarn workspace server build && yarn build:web-plugins && cpx \"web/out/**\" \"server/build/renderer/\" && mkdir -p ./server/build/@janhq && cp -r ./plugins/* ./server/build/@janhq",
|
|
||||||
"start:server": "yarn server:prod && node server/build/main.js"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^8.2.1",
|
"concurrently": "^8.2.1",
|
||||||
|
|||||||
@ -10,9 +10,7 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||||
"postinstall": "rimraf *.tgz --glob && npm run build",
|
"build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/core/pre-install"
|
||||||
"build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install",
|
|
||||||
"build:debug": "rimraf *.tgz --glob && npm run build && npm pack"
|
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist/index.js",
|
".": "./dist/index.js",
|
||||||
|
|||||||
@ -11,8 +11,7 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||||
"postinstall": "rimraf *.tgz --glob && npm run build",
|
"build:publish": "rimraf *.tgz --glob npm run build && && npm pack && cpx *.tgz ../../electron/core/pre-install"
|
||||||
"build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install"
|
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist/index.js",
|
".": "./dist/index.js",
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
0.1.6
|
0.1.8
|
||||||
@ -15,14 +15,16 @@
|
|||||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||||
"downloadnitro:linux-cpu": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64.zip -e --strip 1 -o ./nitro/linux-cpu && chmod +x ./nitro/linux-cpu/nitro && chmod +x ./nitro/linux-start.sh ",
|
"downloadnitro:linux-cpu": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64.zip -e --strip 1 -o ./nitro/linux-cpu && chmod +x ./nitro/linux-cpu/nitro && chmod +x ./nitro/linux-start.sh ",
|
||||||
"downloadnitro:linux-cuda": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-cuda.zip -e --strip 1 -o ./nitro/linux-cuda && chmod +x ./nitro/linux-cuda/nitro && chmod +x ./nitro/linux-start.sh",
|
"downloadnitro:linux-cuda": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-cuda.zip -e --strip 1 -o ./nitro/linux-cuda && chmod +x ./nitro/linux-cuda/nitro && chmod +x ./nitro/linux-start.sh",
|
||||||
"downloadnitro:mac-arm64": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-arm64.zip -e --strip 1 -o ./nitro/mac-arm64 && chmod +x ./nitro/mac-arm64/nitro",
|
"downloadnitro:darwin-arm64": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-arm64.zip -e --strip 1 -o ./nitro/mac-arm64 && chmod +x ./nitro/mac-arm64/nitro",
|
||||||
"downloadnitro:mac-x64": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-amd64.zip -e --strip 1 -o ./nitro/mac-x64 && chmod +x ./nitro/mac-x64/nitro",
|
"downloadnitro:darwin-x64": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-amd64.zip -e --strip 1 -o ./nitro/mac-x64 && chmod +x ./nitro/mac-x64/nitro",
|
||||||
"downloadnitro:win-cpu": "download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64.zip -e --strip 1 -o ./nitro/win-cpu",
|
"downloadnitro:win32-cpu": "download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64.zip -e --strip 1 -o ./nitro/win-cpu",
|
||||||
"downloadnitro:win-cuda": "download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-cuda.zip -e --strip 1 -o ./nitro/win-cuda",
|
"downloadnitro:win32-cuda": "download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-cuda.zip -e --strip 1 -o ./nitro/win-cuda",
|
||||||
"postinstall": "rimraf *.tgz --glob && npm run build && npm run downloadnitro:linux-cpu && npm run downloadnitro:linux-cuda && npm run downloadnitro:mac-arm64 && npm run downloadnitro:mac-x64 && rimraf dist/nitro/* && cpx \"nitro/**\" \"dist/nitro\"",
|
"downloadnitro:all": "npm run downloadnitro:darwin-arm64 && npm run downloadnitro:darwin-x64 && downloadnitro:win32-cpu && npm run downloadnitro:win32-cuda && npm run downloadnitro:linux-cpu && npm run downloadnitro:linux-cuda",
|
||||||
"postinstall:dev": "rimraf *.tgz --glob && npm run build && npm run downloadnitro:mac-arm64 && npm run downloadnitro:mac-x64 && rimraf dist/nitro/* && cpx \"nitro/**\" \"dist/nitro\"",
|
"build:publish": "rimraf *.tgz --glob && npm run build && npm run downloadnitro:darwin-arm64 && npm run downloadnitro:darwin-x64 && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install",
|
||||||
"postinstall:windows": "rimraf *.tgz --glob && npm run build && npm run downloadnitro:win-cpu && npm run downloadnitro:win-cuda && rimraf dist/nitro/* && cpx \"nitro/**\" \"dist/nitro\"",
|
"build:publish-darwin": "rimraf *.tgz --glob && npm run build && npm run downloadnitro:darwin-arm64 && npm run downloadnitro:darwin-x64 && ../../.github/scripts/auto-sign.sh && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install",
|
||||||
"build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install"
|
"build:publish-win32": "rimraf *.tgz --glob && npm run build && npm run downloadnitro:win32-cpu && npm run downloadnitro:win32-cuda && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install",
|
||||||
|
"build:publish-linux": "rimraf *.tgz --glob && npm run build && npm run downloadnitro:linux-cpu && npm run downloadnitro:linux-cuda && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install",
|
||||||
|
"build:publish-all": "rimraf *.tgz --glob && npm run build && npm run downloadnitro:all && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist/index.js",
|
".": "./dist/index.js",
|
||||||
@ -37,10 +39,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@janhq/core": "file:../../core",
|
"@janhq/core": "file:../../core",
|
||||||
"download-cli": "^1.1.1",
|
"download-cli": "^1.1.1",
|
||||||
|
"fetch-retry": "^5.0.6",
|
||||||
"kill-port": "^2.0.1",
|
"kill-port": "^2.0.1",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"tcp-port-used": "^1.0.2",
|
"tcp-port-used": "^1.0.2",
|
||||||
"ts-loader": "^9.5.0"
|
"ts-loader": "^9.5.0",
|
||||||
|
"ulid": "^2.3.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
@ -52,6 +57,7 @@
|
|||||||
],
|
],
|
||||||
"bundleDependencies": [
|
"bundleDependencies": [
|
||||||
"tcp-port-used",
|
"tcp-port-used",
|
||||||
"kill-port"
|
"kill-port",
|
||||||
|
"fetch-retry"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
export const generateMessageId = () => {
|
|
||||||
return `m-${Date.now()}`
|
|
||||||
}
|
|
||||||
@ -16,7 +16,9 @@ import {
|
|||||||
} from "@janhq/core";
|
} from "@janhq/core";
|
||||||
import { InferencePlugin } from "@janhq/core/lib/plugins";
|
import { InferencePlugin } from "@janhq/core/lib/plugins";
|
||||||
import { requestInference } from "./helpers/sse";
|
import { requestInference } from "./helpers/sse";
|
||||||
import { generateMessageId } from "./helpers/message";
|
import { ulid } from "ulid";
|
||||||
|
import { join } from "path";
|
||||||
|
import { appDataPath } from "@janhq/core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class that implements the InferencePlugin interface from the @janhq/core package.
|
* A class that implements the InferencePlugin interface from the @janhq/core package.
|
||||||
@ -48,18 +50,19 @@ export default class JanInferencePlugin implements InferencePlugin {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the model with the specified file name.
|
* Initializes the model with the specified file name.
|
||||||
* @param {string} modelFileName - The name of the model file.
|
* @param {string} modelFileName - The file name of the model file.
|
||||||
* @returns {Promise<void>} A promise that resolves when the model is initialized.
|
* @returns {Promise<void>} A promise that resolves when the model is initialized.
|
||||||
*/
|
*/
|
||||||
initModel(modelFileName: string): Promise<void> {
|
async initModel(modelFileName: string): Promise<void> {
|
||||||
return executeOnMain(MODULE, "initModel", modelFileName);
|
const appPath = await appDataPath();
|
||||||
|
return executeOnMain(MODULE, "initModel", join(appPath, modelFileName));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the model.
|
* Stops the model.
|
||||||
* @returns {Promise<void>} A promise that resolves when the model is stopped.
|
* @returns {Promise<void>} A promise that resolves when the model is stopped.
|
||||||
*/
|
*/
|
||||||
stopModel(): Promise<void> {
|
async stopModel(): Promise<void> {
|
||||||
return executeOnMain(MODULE, "killSubprocess");
|
return executeOnMain(MODULE, "killSubprocess");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,13 +115,13 @@ export default class JanInferencePlugin implements InferencePlugin {
|
|||||||
content: data.message,
|
content: data.message,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const recentMessages = await (data.history ?? prompts);
|
const recentMessages = data.history ?? prompts;
|
||||||
const message = {
|
const message = {
|
||||||
...data,
|
...data,
|
||||||
message: "",
|
message: "",
|
||||||
user: "assistant",
|
user: "assistant",
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
_id: generateMessageId(),
|
_id: ulid(),
|
||||||
};
|
};
|
||||||
events.emit(EventName.OnNewMessageResponse, message);
|
events.emit(EventName.OnNewMessageResponse, message);
|
||||||
|
|
||||||
|
|||||||
@ -1,119 +1,251 @@
|
|||||||
const path = require("path");
|
|
||||||
const { app } = require("electron");
|
|
||||||
const { spawn } = require("child_process");
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const tcpPortUsed = require("tcp-port-used");
|
|
||||||
const kill = require("kill-port");
|
const kill = require("kill-port");
|
||||||
|
const path = require("path");
|
||||||
|
const { spawn } = require("child_process");
|
||||||
|
const tcpPortUsed = require("tcp-port-used");
|
||||||
|
const fetchRetry = require("fetch-retry")(global.fetch);
|
||||||
|
|
||||||
|
// The PORT to use for the Nitro subprocess
|
||||||
const PORT = 3928;
|
const PORT = 3928;
|
||||||
|
const LOCAL_HOST = "127.0.0.1";
|
||||||
|
const NITRO_HTTP_SERVER_URL = `http://${LOCAL_HOST}:${PORT}`;
|
||||||
|
const NITRO_HTTP_LOAD_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/loadmodel`;
|
||||||
|
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`;
|
||||||
|
|
||||||
|
// The subprocess instance for Nitro
|
||||||
let subprocess = null;
|
let subprocess = null;
|
||||||
|
let currentModelFile = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The response from the initModel function.
|
||||||
|
* @property error - An error message if the model fails to load.
|
||||||
|
*/
|
||||||
|
interface InitModelResponse {
|
||||||
|
error?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a Nitro subprocess to load a machine learning model.
|
||||||
|
* @param modelFile - The name of the machine learning model file.
|
||||||
|
* @returns A Promise that resolves when the model is loaded successfully, or rejects with an error message if the model is not found or fails to load.
|
||||||
|
* TODO: Should pass absolute of the model file instead of just the name - So we can modurize the module.ts to npm package
|
||||||
|
* TODO: Should it be startModel instead?
|
||||||
|
*/
|
||||||
|
function initModel(modelFile: string): Promise<InitModelResponse> {
|
||||||
|
// 1. Check if the model file exists
|
||||||
|
currentModelFile = modelFile;
|
||||||
|
|
||||||
const initModel = (fileName) => {
|
|
||||||
return (
|
return (
|
||||||
new Promise<void>(async (resolve, reject) => {
|
// 1. Check if the port is used, if used, attempt to unload model / kill nitro process
|
||||||
if (!fileName) {
|
validateModelVersion()
|
||||||
reject("Model not found, please download again.");
|
.then(checkAndUnloadNitro)
|
||||||
}
|
// 2. Spawn the Nitro subprocess
|
||||||
resolve(fileName);
|
.then(spawnNitroProcess)
|
||||||
})
|
// 3. Wait until the port is used (Nitro http server is up)
|
||||||
// Spawn Nitro subprocess to load model
|
|
||||||
.then(() => {
|
|
||||||
return tcpPortUsed.check(PORT, "127.0.0.1").then((inUse) => {
|
|
||||||
if (!inUse) {
|
|
||||||
let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default
|
|
||||||
let binaryName;
|
|
||||||
|
|
||||||
if (process.platform === "win32") {
|
|
||||||
// Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries
|
|
||||||
binaryName = "win-start.bat";
|
|
||||||
} else if (process.platform === "darwin") {
|
|
||||||
// Mac OS platform
|
|
||||||
if (process.arch === "arm64") {
|
|
||||||
binaryFolder = path.join(binaryFolder, "mac-arm64")
|
|
||||||
} else {
|
|
||||||
binaryFolder = path.join(binaryFolder, "mac-x64")
|
|
||||||
}
|
|
||||||
binaryName = "nitro"
|
|
||||||
} else {
|
|
||||||
// Linux
|
|
||||||
// Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries
|
|
||||||
binaryName = "linux-start.sh"; // For other platforms
|
|
||||||
}
|
|
||||||
|
|
||||||
const binaryPath = path.join(binaryFolder, binaryName);
|
|
||||||
|
|
||||||
// Execute the binary
|
|
||||||
subprocess = spawn(binaryPath,["0.0.0.0", PORT], { cwd: binaryFolder });
|
|
||||||
|
|
||||||
// Handle subprocess output
|
|
||||||
subprocess.stdout.on("data", (data) => {
|
|
||||||
console.log(`stdout: ${data}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
subprocess.stderr.on("data", (data) => {
|
|
||||||
console.error(`stderr: ${data}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
subprocess.on("close", (code) => {
|
|
||||||
console.log(`child process exited with code ${code}`);
|
|
||||||
subprocess = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(() => tcpPortUsed.waitUntilUsed(PORT, 300, 30000))
|
.then(() => tcpPortUsed.waitUntilUsed(PORT, 300, 30000))
|
||||||
.then(() => {
|
// 4. Load the model into the Nitro subprocess (HTTP POST request)
|
||||||
const llama_model_path = path.join(appPath(), fileName);
|
.then(loadLLMModel)
|
||||||
|
// 5. Check if the model is loaded successfully
|
||||||
const config = {
|
.then(validateModelStatus)
|
||||||
llama_model_path,
|
|
||||||
ctx_len: 2048,
|
|
||||||
ngl: 100,
|
|
||||||
embedding: true, // Always enable embedding mode on
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load model config
|
|
||||||
return fetch(`http://127.0.0.1:${PORT}/inferences/llamacpp/loadmodel`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(config),
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
if (res.ok) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
throw new Error("Nitro: Model failed to load.");
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
return { error: err };
|
return { error: err };
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
function dispose() {
|
|
||||||
killSubprocess();
|
|
||||||
// clean other registered resources here
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function killSubprocess() {
|
/**
|
||||||
|
* Loads a LLM model into the Nitro subprocess by sending a HTTP POST request.
|
||||||
|
* @returns A Promise that resolves when the model is loaded successfully, or rejects with an error message if the model is not found or fails to load.
|
||||||
|
*/
|
||||||
|
function loadLLMModel(): Promise<Response> {
|
||||||
|
const config = {
|
||||||
|
llama_model_path: currentModelFile,
|
||||||
|
ctx_len: 2048,
|
||||||
|
ngl: 100,
|
||||||
|
embedding: false, // Always enable embedding mode on
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load model config
|
||||||
|
return fetchRetry(NITRO_HTTP_LOAD_MODEL_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(config),
|
||||||
|
retries: 3,
|
||||||
|
retryDelay: 500,
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
// Fetch error, Nitro server might not started properly
|
||||||
|
throw new Error("Model loading failed.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the status of a model.
|
||||||
|
* @returns {Promise<InitModelResponse>} A promise that resolves to an object.
|
||||||
|
* If the model is loaded successfully, the object is empty.
|
||||||
|
* If the model is not loaded successfully, the object contains an error message.
|
||||||
|
*/
|
||||||
|
async function validateModelStatus(): Promise<InitModelResponse> {
|
||||||
|
// Send a GET request to the validation URL.
|
||||||
|
// Retry the request up to 3 times if it fails, with a delay of 500 milliseconds between retries.
|
||||||
|
return fetchRetry(NITRO_HTTP_VALIDATE_MODEL_URL, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
retries: 5,
|
||||||
|
retryDelay: 500,
|
||||||
|
})
|
||||||
|
.then(async (res: Response) => {
|
||||||
|
// If the response is OK, check model_loaded status.
|
||||||
|
if (res.ok) {
|
||||||
|
const body = await res.json();
|
||||||
|
// If the model is loaded, return an empty object.
|
||||||
|
// Otherwise, return an object with an error message.
|
||||||
|
if (body.model_loaded) {
|
||||||
|
return { error: undefined };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { error: "Model is not loaded successfully" };
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
return { error: `Model is not loaded successfully. ${err.message}` };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Terminates the Nitro subprocess.
|
||||||
|
* @returns A Promise that resolves when the subprocess is terminated successfully, or rejects with an error message if the subprocess fails to terminate.
|
||||||
|
*/
|
||||||
|
function killSubprocess(): Promise<void> {
|
||||||
if (subprocess) {
|
if (subprocess) {
|
||||||
subprocess.kill();
|
subprocess.kill();
|
||||||
subprocess = null;
|
subprocess = null;
|
||||||
console.log("Subprocess terminated.");
|
console.log("Subprocess terminated.");
|
||||||
} else {
|
} else {
|
||||||
kill(PORT, "tcp").then(console.log).catch(console.log);
|
return kill(PORT, "tcp").then(console.log).catch(console.log);
|
||||||
console.error("No subprocess is currently running.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function appPath() {
|
/**
|
||||||
if (app) {
|
* Check port is used or not, if used, attempt to unload model
|
||||||
return app.getPath("userData");
|
* If unload failed, kill the port
|
||||||
|
*/
|
||||||
|
function checkAndUnloadNitro() {
|
||||||
|
return tcpPortUsed.check(PORT, LOCAL_HOST).then((inUse) => {
|
||||||
|
// If inUse - try unload or kill process, otherwise do nothing
|
||||||
|
if (inUse) {
|
||||||
|
// Attempt to unload model
|
||||||
|
return fetch(NITRO_HTTP_UNLOAD_MODEL_URL, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
// Fallback to kill the port
|
||||||
|
return killSubprocess();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look for the Nitro binary and execute it
|
||||||
|
* Using child-process to spawn the process
|
||||||
|
* Should run exactly platform specified Nitro binary version
|
||||||
|
*/
|
||||||
|
function spawnNitroProcess() {
|
||||||
|
let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default
|
||||||
|
let binaryName;
|
||||||
|
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
// Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries
|
||||||
|
binaryName = "win-start.bat";
|
||||||
|
} else if (process.platform === "darwin") {
|
||||||
|
// Mac OS platform
|
||||||
|
if (process.arch === "arm64") {
|
||||||
|
binaryFolder = path.join(binaryFolder, "mac-arm64");
|
||||||
|
} else {
|
||||||
|
binaryFolder = path.join(binaryFolder, "mac-x64");
|
||||||
|
}
|
||||||
|
binaryName = "nitro";
|
||||||
|
} else {
|
||||||
|
// Linux
|
||||||
|
// Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries
|
||||||
|
binaryName = "linux-start.sh"; // For other platforms
|
||||||
}
|
}
|
||||||
return process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + "/.local/share");
|
|
||||||
|
const binaryPath = path.join(binaryFolder, binaryName);
|
||||||
|
|
||||||
|
// Execute the binary
|
||||||
|
subprocess = spawn(binaryPath, [1, "0.0.0.0", PORT], {
|
||||||
|
cwd: binaryFolder,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle subprocess output
|
||||||
|
subprocess.stdout.on("data", (data) => {
|
||||||
|
console.log(`stdout: ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
subprocess.stderr.on("data", (data) => {
|
||||||
|
console.error(`stderr: ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
subprocess.on("close", (code) => {
|
||||||
|
console.log(`child process exited with code ${code}`);
|
||||||
|
subprocess = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the model version, if it is GGUFv1, reject the promise
|
||||||
|
* @returns A Promise that resolves when the model is loaded successfully, or rejects with an error message if the model is not found or fails to load.
|
||||||
|
*/
|
||||||
|
function validateModelVersion(): Promise<void> {
|
||||||
|
// Read the file
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.open(currentModelFile, "r", (err, fd) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer to store the byte
|
||||||
|
const buffer = Buffer.alloc(1);
|
||||||
|
|
||||||
|
// Model version will be the 5th byte of the file
|
||||||
|
fs.read(fd, buffer, 0, 1, 4, (err, bytesRead, buffer) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
} else {
|
||||||
|
// Interpret the byte as ASCII
|
||||||
|
if (buffer[0] === 0x01) {
|
||||||
|
// This is GGUFv1, which is deprecated
|
||||||
|
reject("GGUFv1 model is deprecated, please try another model.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the file descriptor
|
||||||
|
fs.close(fd, (err) => {
|
||||||
|
if (err) console.error(err.message);
|
||||||
|
});
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up any registered resources.
|
||||||
|
* Its module specific function, should be called when application is closed
|
||||||
|
*/
|
||||||
|
function dispose() {
|
||||||
|
// clean other registered resources here
|
||||||
|
killSubprocess();
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@ -18,7 +18,10 @@ module.exports = {
|
|||||||
plugins: [
|
plugins: [
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
MODULE: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
|
MODULE: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
|
||||||
INFERENCE_URL: JSON.stringify(process.env.INFERENCE_URL || "http://127.0.0.1:3928/inferences/llamacpp/chat_completion"),
|
INFERENCE_URL: JSON.stringify(
|
||||||
|
process.env.INFERENCE_URL ||
|
||||||
|
"http://127.0.0.1:3928/inferences/llamacpp/chat_completion"
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
output: {
|
output: {
|
||||||
@ -28,6 +31,9 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: [".ts", ".js"],
|
extensions: [".ts", ".js"],
|
||||||
|
fallback: {
|
||||||
|
path: require.resolve("path-browserify"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
minimize: false,
|
minimize: false,
|
||||||
|
|||||||
@ -14,8 +14,7 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||||
"postinstall": "rimraf *.tgz --glob && npm run build",
|
"build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/core/pre-install"
|
||||||
"build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cpx": "^1.5.0",
|
"cpx": "^1.5.0",
|
||||||
|
|||||||
@ -14,8 +14,7 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||||
"postinstall": "rimraf *.tgz --glob && npm run build",
|
"build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/core/pre-install"
|
||||||
"build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
|
|||||||
179
server/main.ts
179
server/main.ts
@ -1,179 +0,0 @@
|
|||||||
import express, { Express, Request, Response, NextFunction } from 'express'
|
|
||||||
import cors from "cors";
|
|
||||||
import { resolve } from "path";
|
|
||||||
const fs = require("fs");
|
|
||||||
const progress = require("request-progress");
|
|
||||||
const path = require("path");
|
|
||||||
const request = require("request");
|
|
||||||
|
|
||||||
// Create app dir
|
|
||||||
const userDataPath = appPath();
|
|
||||||
if (!fs.existsSync(userDataPath)) fs.mkdirSync(userDataPath);
|
|
||||||
|
|
||||||
interface ProgressState {
|
|
||||||
percent?: number;
|
|
||||||
speed?: number;
|
|
||||||
size?: {
|
|
||||||
total: number;
|
|
||||||
transferred: number;
|
|
||||||
};
|
|
||||||
time?: {
|
|
||||||
elapsed: number;
|
|
||||||
remaining: number;
|
|
||||||
};
|
|
||||||
success?: boolean | undefined;
|
|
||||||
fileName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: cors.CorsOptions = { origin: "*" };
|
|
||||||
const requiredModules: Record<string, any> = {};
|
|
||||||
const port = process.env.PORT || 4000;
|
|
||||||
const dataDir = __dirname;
|
|
||||||
type DownloadProgress = Record<string, ProgressState>;
|
|
||||||
const downloadProgress: DownloadProgress = {};
|
|
||||||
const app: Express = express()
|
|
||||||
app.use(express.static(dataDir + '/renderer'))
|
|
||||||
app.use(cors(options))
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a plugin module function via API call
|
|
||||||
*
|
|
||||||
* @param modulePath path to module name to import
|
|
||||||
* @param method function name to execute. The methods "deleteFile" and "downloadFile" will call the server function {@link deleteFile}, {@link downloadFile} instead of the plugin function.
|
|
||||||
* @param args arguments to pass to the function
|
|
||||||
* @returns Promise<any>
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
app.post('/api/v1/invokeFunction', (req: Request, res: Response, next: NextFunction): void => {
|
|
||||||
const method = req.body["method"];
|
|
||||||
const args = req.body["args"];
|
|
||||||
switch (method) {
|
|
||||||
case "deleteFile":
|
|
||||||
deleteFile(args).then(() => res.json(Object())).catch((err: any) => next(err));
|
|
||||||
break;
|
|
||||||
case "downloadFile":
|
|
||||||
downloadFile(args.downloadUrl, args.fileName).then(() => res.json(Object())).catch((err: any) => next(err));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
const result = invokeFunction(req.body["modulePath"], method, args)
|
|
||||||
if (typeof result === "undefined") {
|
|
||||||
res.json(Object())
|
|
||||||
} else {
|
|
||||||
result?.then((result: any) => {
|
|
||||||
res.json(result)
|
|
||||||
}).catch((err: any) => next(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/api/v1/downloadProgress', (req: Request, res: Response): void => {
|
|
||||||
const fileName = req.body["fileName"];
|
|
||||||
if (fileName && downloadProgress[fileName]) {
|
|
||||||
res.json(downloadProgress[fileName])
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
const obj = downloadingFile();
|
|
||||||
if (obj) {
|
|
||||||
res.json(obj)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.json(Object());
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use((err: Error, req: Request, res: Response, next: NextFunction): void => {
|
|
||||||
console.error("ErrorHandler", req.url, req.body, err);
|
|
||||||
res.status(500);
|
|
||||||
res.json({ error: err?.message ?? "Internal Server Error" })
|
|
||||||
});
|
|
||||||
|
|
||||||
app.listen(port, () => console.log(`Application is running on port ${port}`));
|
|
||||||
|
|
||||||
|
|
||||||
async function invokeFunction(modulePath: string, method: string, args: any): Promise<any> {
|
|
||||||
console.log(modulePath, method, args);
|
|
||||||
const module = require(/* webpackIgnore: true */ path.join(
|
|
||||||
dataDir,
|
|
||||||
"",
|
|
||||||
modulePath
|
|
||||||
));
|
|
||||||
requiredModules[modulePath] = module;
|
|
||||||
if (typeof module[method] === "function") {
|
|
||||||
return module[method](...args);
|
|
||||||
} else {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadModel(downloadUrl: string, fileName: string): void {
|
|
||||||
const userDataPath = appPath();
|
|
||||||
const destination = resolve(userDataPath, fileName);
|
|
||||||
console.log("Download file", fileName, "to", destination);
|
|
||||||
progress(request(downloadUrl), {})
|
|
||||||
.on("progress", function (state: any) {
|
|
||||||
downloadProgress[fileName] = {
|
|
||||||
...state,
|
|
||||||
fileName,
|
|
||||||
success: undefined
|
|
||||||
};
|
|
||||||
console.log("downloading file", fileName, (state.percent * 100).toFixed(2) + '%');
|
|
||||||
})
|
|
||||||
.on("error", function (err: Error) {
|
|
||||||
downloadProgress[fileName] = {
|
|
||||||
...downloadProgress[fileName],
|
|
||||||
success: false,
|
|
||||||
fileName: fileName,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.on("end", function () {
|
|
||||||
downloadProgress[fileName] = {
|
|
||||||
success: true,
|
|
||||||
fileName: fileName,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.pipe(fs.createWriteStream(destination));
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteFile(filePath: string): Promise<void> {
|
|
||||||
const userDataPath = appPath();
|
|
||||||
const fullPath = resolve(userDataPath, filePath);
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
fs.unlink(fullPath, function (err: any) {
|
|
||||||
if (err && err.code === "ENOENT") {
|
|
||||||
reject(Error(`File does not exist: ${err}`));
|
|
||||||
} else if (err) {
|
|
||||||
reject(Error(`File delete error: ${err}`));
|
|
||||||
} else {
|
|
||||||
console.log(`Delete file ${filePath} from ${fullPath}`)
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadingFile(): ProgressState | undefined {
|
|
||||||
const obj = Object.values(downloadProgress).find(obj => obj && typeof obj.success === "undefined")
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async function downloadFile(downloadUrl: string, fileName: string): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const obj = downloadingFile();
|
|
||||||
if (obj) {
|
|
||||||
reject(Error(obj.fileName + " is being downloaded!"))
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
(async () => {
|
|
||||||
downloadModel(downloadUrl, fileName);
|
|
||||||
})().catch(e => {
|
|
||||||
console.error("downloadModel", fileName, e);
|
|
||||||
});
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function appPath(): string {
|
|
||||||
return process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + "/.local/share")
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"watch": [
|
|
||||||
"main.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "server",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"main": "index.js",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"electron": "^26.2.1",
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"request": "^2.88.2",
|
|
||||||
"request-progress": "^3.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/cors": "^2.8.14",
|
|
||||||
"@types/express": "^4.17.18",
|
|
||||||
"@types/node": "^20.8.2",
|
|
||||||
"nodemon": "^3.0.1",
|
|
||||||
"ts-node": "^10.9.1",
|
|
||||||
"typescript": "^5.2.2"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc --project ./",
|
|
||||||
"dev": "nodemon main.ts",
|
|
||||||
"prod": "node build/main.js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es5",
|
|
||||||
"module": "commonjs",
|
|
||||||
"noImplicitAny": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"strict": true,
|
|
||||||
"outDir": "./build",
|
|
||||||
"rootDir": "./",
|
|
||||||
"noEmitOnError": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"allowJs": true,
|
|
||||||
"paths": { "*": ["node_modules/*"] },
|
|
||||||
"typeRoots": ["node_modules/@types"],
|
|
||||||
"esModuleInterop": true
|
|
||||||
},
|
|
||||||
"include": ["./**/*.ts"],
|
|
||||||
"exclude": ["core", "build", "dist", "tests"]
|
|
||||||
}
|
|
||||||
@ -2,18 +2,10 @@ import { PropsWithChildren } from 'react'
|
|||||||
|
|
||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next'
|
||||||
|
|
||||||
// import { Inter } from 'next/font/google'
|
|
||||||
|
|
||||||
import Providers from '@/containers/Providers'
|
import Providers from '@/containers/Providers'
|
||||||
|
|
||||||
import '@/styles/main.scss'
|
import '@/styles/main.scss'
|
||||||
|
|
||||||
// const inter = Inter({
|
|
||||||
// subsets: ['latin'],
|
|
||||||
// display: 'swap',
|
|
||||||
// variable: '--font-inter',
|
|
||||||
// })
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Jan',
|
title: 'Jan',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import {
|
|||||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
import { MessageStatus, toChatMessage } from '@/models/ChatMessage'
|
import { MessageStatus, toChatMessage } from '@/models/ChatMessage'
|
||||||
import { pluginManager } from '@/plugin'
|
import { pluginManager } from '@/plugin'
|
||||||
|
import { ChatMessage, Conversation } from '@/types/chatMessage'
|
||||||
|
|
||||||
let currentConversation: Conversation | undefined = undefined
|
let currentConversation: Conversation | undefined = undefined
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { atom } from 'jotai'
|
|||||||
|
|
||||||
import { getActiveConvoIdAtom } from './Conversation.atom'
|
import { getActiveConvoIdAtom } from './Conversation.atom'
|
||||||
|
|
||||||
import { MessageStatus } from '@/models/ChatMessage'
|
import { ChatMessage, MessageStatus } from '@/models/ChatMessage'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores all chat messages for all conversations
|
* Stores all chat messages for all conversations
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { Conversation, ConversationState } from '@/types/chatMessage'
|
||||||
import { atom } from 'jotai'
|
import { atom } from 'jotai'
|
||||||
|
|
||||||
// import { MainViewState, setMainViewStateAtom } from './MainView.atom'
|
// import { MainViewState, setMainViewStateAtom } from './MainView.atom'
|
||||||
|
|||||||
@ -45,14 +45,14 @@ export function useActiveModel() {
|
|||||||
|
|
||||||
const res = await initModel(`models/${modelId}`)
|
const res = await initModel(`models/${modelId}`)
|
||||||
if (res?.error) {
|
if (res?.error) {
|
||||||
const errorMessage = `Failed to init model: ${res.error}`
|
const errorMessage = `${res.error}`
|
||||||
console.error(errorMessage)
|
|
||||||
alert(errorMessage)
|
alert(errorMessage)
|
||||||
setStateModel(() => ({
|
setStateModel(() => ({
|
||||||
state: 'start',
|
state: 'start',
|
||||||
loading: false,
|
loading: false,
|
||||||
model: modelId,
|
model: modelId,
|
||||||
}))
|
}))
|
||||||
|
setActiveModel(undefined)
|
||||||
} else {
|
} else {
|
||||||
console.debug(
|
console.debug(
|
||||||
`Init model ${modelId} successfully!, take ${
|
`Init model ${modelId} successfully!, take ${
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
addNewConversationStateAtom,
|
addNewConversationStateAtom,
|
||||||
} from '@/helpers/atoms/Conversation.atom'
|
} from '@/helpers/atoms/Conversation.atom'
|
||||||
import { pluginManager } from '@/plugin'
|
import { pluginManager } from '@/plugin'
|
||||||
|
import { Conversation } from '@/types/chatMessage'
|
||||||
|
|
||||||
export const useCreateConversation = () => {
|
export const useCreateConversation = () => {
|
||||||
const [userConversations, setUserConversations] = useAtom(
|
const [userConversations, setUserConversations] = useAtom(
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { useActiveModel } from './useActiveModel'
|
|||||||
import { useGetDownloadedModels } from './useGetDownloadedModels'
|
import { useGetDownloadedModels } from './useGetDownloadedModels'
|
||||||
|
|
||||||
import { currentConversationAtom } from '@/helpers/atoms/Conversation.atom'
|
import { currentConversationAtom } from '@/helpers/atoms/Conversation.atom'
|
||||||
|
import { Conversation } from '@/types/chatMessage'
|
||||||
|
|
||||||
export default function useGetInputState() {
|
export default function useGetInputState() {
|
||||||
const [inputState, setInputState] = useState<InputType>('loading')
|
const [inputState, setInputState] = useState<InputType>('loading')
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
} from '@/helpers/atoms/Conversation.atom'
|
} from '@/helpers/atoms/Conversation.atom'
|
||||||
import { toChatMessage } from '@/models/ChatMessage'
|
import { toChatMessage } from '@/models/ChatMessage'
|
||||||
import { pluginManager } from '@/plugin/PluginManager'
|
import { pluginManager } from '@/plugin/PluginManager'
|
||||||
|
import { ChatMessage, ConversationState } from '@/types/chatMessage'
|
||||||
|
|
||||||
const useGetUserConversations = () => {
|
const useGetUserConversations = () => {
|
||||||
const setConversationStates = useSetAtom(conversationStatesAtom)
|
const setConversationStates = useSetAtom(conversationStatesAtom)
|
||||||
|
|||||||
@ -12,9 +12,7 @@ import { Message } from '@janhq/core/lib/types'
|
|||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { currentPromptAtom } from '@/containers/Providers/Jotai'
|
import { currentPromptAtom } from '@/containers/Providers/Jotai'
|
||||||
|
import { ulid } from 'ulid'
|
||||||
import { generateMessageId } from '@/utils/message'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addNewMessageAtom,
|
addNewMessageAtom,
|
||||||
getCurrentChatMessagesAtom,
|
getCurrentChatMessagesAtom,
|
||||||
@ -24,9 +22,10 @@ import {
|
|||||||
updateConversationAtom,
|
updateConversationAtom,
|
||||||
updateConversationWaitingForResponseAtom,
|
updateConversationWaitingForResponseAtom,
|
||||||
} from '@/helpers/atoms/Conversation.atom'
|
} from '@/helpers/atoms/Conversation.atom'
|
||||||
import { toChatMessage } from '@/models/ChatMessage'
|
import { MessageSenderType, toChatMessage } from '@/models/ChatMessage'
|
||||||
|
|
||||||
import { pluginManager } from '@/plugin/PluginManager'
|
import { pluginManager } from '@/plugin/PluginManager'
|
||||||
|
import { ChatMessage, Conversation } from '@/types/chatMessage'
|
||||||
|
|
||||||
export default function useSendChatMessage() {
|
export default function useSendChatMessage() {
|
||||||
const currentConvo = useAtomValue(currentConversationAtom)
|
const currentConvo = useAtomValue(currentConversationAtom)
|
||||||
@ -73,16 +72,13 @@ export default function useSendChatMessage() {
|
|||||||
...updatedConv,
|
...updatedConv,
|
||||||
name: updatedConv.name ?? '',
|
name: updatedConv.name ?? '',
|
||||||
message: updatedConv.lastMessage ?? '',
|
message: updatedConv.lastMessage ?? '',
|
||||||
messages: currentMessages.map<Message>((e: ChatMessage) => {
|
messages: currentMessages.map<Message>((e: ChatMessage) => ({
|
||||||
return {
|
_id: e.id,
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
message: e.text,
|
||||||
_id: e.id,
|
user: e.senderUid,
|
||||||
message: e.text,
|
updatedAt: new Date(e.createdAt).toISOString(),
|
||||||
user: e.senderUid,
|
createdAt: new Date(e.createdAt).toISOString(),
|
||||||
updatedAt: new Date(e.createdAt).toISOString(),
|
})),
|
||||||
createdAt: new Date(e.createdAt).toISOString(),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
@ -98,25 +94,23 @@ export default function useSendChatMessage() {
|
|||||||
|
|
||||||
const prompt = currentPrompt.trim()
|
const prompt = currentPrompt.trim()
|
||||||
const messageHistory: MessageHistory[] = currentMessages
|
const messageHistory: MessageHistory[] = currentMessages
|
||||||
.map((msg) => {
|
.map((msg) => ({
|
||||||
return {
|
role: msg.senderUid,
|
||||||
role: msg.senderUid === 'user' ? 'user' : 'assistant',
|
content: msg.text ?? '',
|
||||||
content: msg.text ?? '',
|
}))
|
||||||
}
|
|
||||||
})
|
|
||||||
.reverse()
|
.reverse()
|
||||||
.concat([
|
.concat([
|
||||||
{
|
{
|
||||||
role: 'user',
|
role: MessageSenderType.User,
|
||||||
content: prompt,
|
content: prompt,
|
||||||
} as MessageHistory,
|
} as MessageHistory,
|
||||||
])
|
])
|
||||||
const newMessage: NewMessageRequest = {
|
const newMessage: NewMessageRequest = {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
_id: generateMessageId(),
|
_id: ulid(),
|
||||||
conversationId: convoId,
|
conversationId: convoId,
|
||||||
message: prompt,
|
message: prompt,
|
||||||
user: 'user',
|
user: MessageSenderType.User,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
history: messageHistory,
|
history: messageHistory,
|
||||||
}
|
}
|
||||||
@ -124,6 +118,11 @@ export default function useSendChatMessage() {
|
|||||||
const newChatMessage = toChatMessage(newMessage)
|
const newChatMessage = toChatMessage(newMessage)
|
||||||
addNewMessage(newChatMessage)
|
addNewMessage(newChatMessage)
|
||||||
|
|
||||||
|
// delay randomly from 50 - 100ms
|
||||||
|
// to prevent duplicate message id
|
||||||
|
const delay = Math.floor(Math.random() * 50) + 50
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||||
|
|
||||||
events.emit(EventName.OnNewMessageRequest, newMessage)
|
events.emit(EventName.OnNewMessageRequest, newMessage)
|
||||||
if (!currentConvo?.summary && currentConvo) {
|
if (!currentConvo?.summary && currentConvo) {
|
||||||
const updatedConv: Conversation = {
|
const updatedConv: Conversation = {
|
||||||
|
|||||||
@ -38,6 +38,7 @@
|
|||||||
"tailwind-merge": "^2.0.0",
|
"tailwind-merge": "^2.0.0",
|
||||||
"tailwindcss": "3.3.5",
|
"tailwindcss": "3.3.5",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
|
"ulid": "^2.3.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import React, { forwardRef } from 'react'
|
import React, { forwardRef } from 'react'
|
||||||
|
|
||||||
import SimpleTextMessage from '../SimpleTextMessage'
|
import SimpleTextMessage from '../SimpleTextMessage'
|
||||||
|
import { ChatMessage } from '@/types/chatMessage'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
message: ChatMessage
|
message: ChatMessage
|
||||||
@ -8,20 +9,18 @@ type Props = {
|
|||||||
|
|
||||||
type Ref = HTMLDivElement
|
type Ref = HTMLDivElement
|
||||||
|
|
||||||
const ChatItem = forwardRef<Ref, Props>(({ message }, ref) => {
|
const ChatItem = forwardRef<Ref, Props>(({ message }, ref) => (
|
||||||
return (
|
<div ref={ref} className="py-4 even:bg-secondary dark:even:bg-secondary/20">
|
||||||
<div ref={ref} className="py-4 even:bg-secondary dark:even:bg-secondary/20">
|
<SimpleTextMessage
|
||||||
<SimpleTextMessage
|
status={message.status}
|
||||||
status={message.status}
|
key={message.id}
|
||||||
key={message.id}
|
avatarUrl={message.senderAvatarUrl}
|
||||||
avatarUrl={message.senderAvatarUrl}
|
senderName={message.senderName}
|
||||||
senderName={message.senderName}
|
createdAt={message.createdAt}
|
||||||
createdAt={message.createdAt}
|
senderType={message.messageSenderType}
|
||||||
senderType={message.messageSenderType}
|
text={message.text}
|
||||||
text={message.text}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
))
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default ChatItem
|
export default ChatItem
|
||||||
|
|||||||
@ -56,7 +56,7 @@ const ChatScreen = () => {
|
|||||||
const conversations = useAtomValue(userConversationsAtom)
|
const conversations = useAtomValue(userConversationsAtom)
|
||||||
const isEnableChat = (currentConvo && activeModel) || conversations.length > 0
|
const isEnableChat = (currentConvo && activeModel) || conversations.length > 0
|
||||||
const [isModelAvailable, setIsModelAvailable] = useState(
|
const [isModelAvailable, setIsModelAvailable] = useState(
|
||||||
downloadedModels.some((x) => x.name !== currentConvo?.name)
|
downloadedModels.some((x) => x._id === currentConvo?.modelId)
|
||||||
)
|
)
|
||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
@ -72,9 +72,8 @@ const ChatScreen = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsModelAvailable(
|
setIsModelAvailable(
|
||||||
downloadedModels.some((x) => x.name !== currentConvo?.name)
|
downloadedModels.some((x) => x._id === currentConvo?.modelId)
|
||||||
)
|
)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [currentConvo, downloadedModels])
|
}, [currentConvo, downloadedModels])
|
||||||
|
|
||||||
const handleSendMessage = async () => {
|
const handleSendMessage = async () => {
|
||||||
@ -131,10 +130,10 @@ const ChatScreen = () => {
|
|||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'flex items-center space-x-3',
|
'flex items-center space-x-3',
|
||||||
isModelAvailable && '-mt-1'
|
!isModelAvailable && '-mt-1'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isModelAvailable && (
|
{!isModelAvailable && (
|
||||||
<Button
|
<Button
|
||||||
themes="secondary"
|
themes="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@ -64,11 +64,12 @@
|
|||||||
border-radius: 0.4rem;
|
border-radius: 0.4rem;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
white-space: normal;
|
||||||
}
|
}
|
||||||
pre > code {
|
pre > code {
|
||||||
display: block;
|
display: block;
|
||||||
text-indent: 0;
|
text-indent: 0;
|
||||||
white-space: inherit;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hljs-emphasis {
|
.hljs-emphasis {
|
||||||
|
|||||||
2
web/types/chatMessage.d.ts
vendored
2
web/types/chatMessage.d.ts
vendored
@ -6,7 +6,7 @@ enum MessageType {
|
|||||||
Error = 'Error',
|
Error = 'Error',
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MessageSenderType {
|
export enum MessageSenderType {
|
||||||
Ai = 'assistant',
|
Ai = 'assistant',
|
||||||
User = 'user',
|
User = 'user',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,3 @@ export function mergeAndRemoveDuplicates(
|
|||||||
|
|
||||||
return result.reverse()
|
return result.reverse()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateMessageId = () => {
|
|
||||||
return `m-${Date.now()}`
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user