mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
making it better and refactoring, digging through the AI generated slop
This commit is contained in:
parent
678acb48b8
commit
d1b76772be
37 changed files with 2706 additions and 1924 deletions
71
.github/RELEASE.md
vendored
Normal file
71
.github/RELEASE.md
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
# Release Workflow
|
||||||
|
|
||||||
|
This GitHub Action workflow automatically builds and releases FriendlyKobold for macOS, Windows, and Linux.
|
||||||
|
|
||||||
|
## How to Create a Release
|
||||||
|
|
||||||
|
### Method 1: Tag-based Release (Recommended)
|
||||||
|
|
||||||
|
1. Create and push a new tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag v1.0.0
|
||||||
|
git push origin v1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
2. The workflow will automatically:
|
||||||
|
- Build the app for all platforms
|
||||||
|
- Run type checking and linting
|
||||||
|
- Create a GitHub release
|
||||||
|
- Upload the built files as release assets
|
||||||
|
|
||||||
|
### Method 2: Manual Release
|
||||||
|
|
||||||
|
1. Go to the "Actions" tab in your GitHub repository
|
||||||
|
2. Select the "Release" workflow
|
||||||
|
3. Click "Run workflow"
|
||||||
|
4. Enter the tag version (e.g., `v1.0.0`)
|
||||||
|
5. Click "Run workflow"
|
||||||
|
|
||||||
|
## Generated Files
|
||||||
|
|
||||||
|
The workflow creates the following files:
|
||||||
|
|
||||||
|
- **macOS**: `FriendlyKobold-{version}.dmg`
|
||||||
|
- **Windows**: `FriendlyKobold Setup {version}.exe`
|
||||||
|
- **Linux**: `FriendlyKobold-{version}.AppImage`
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- The repository must have a `GITHUB_TOKEN` (automatically provided by GitHub)
|
||||||
|
- Node.js 20 and npm for building
|
||||||
|
- All dependencies must be properly defined in `package.json`
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
If the build fails:
|
||||||
|
|
||||||
|
1. Check that all dependencies are installed correctly
|
||||||
|
2. Ensure the build script works locally: `npm run build`
|
||||||
|
3. Check the workflow logs for specific error messages
|
||||||
|
4. Verify that the version tag follows semantic versioning (e.g., `v1.0.0`)
|
||||||
|
|
||||||
|
## Adding Icons
|
||||||
|
|
||||||
|
To add custom icons for your releases:
|
||||||
|
|
||||||
|
1. Create an `assets` folder in the project root
|
||||||
|
2. Add the following icon files:
|
||||||
|
- `icon.icns` for macOS
|
||||||
|
- `icon.ico` for Windows
|
||||||
|
- `icon.png` for Linux
|
||||||
|
3. Update the `build` section in `package.json` to reference these icons
|
||||||
|
|
||||||
|
## Customizing the Release
|
||||||
|
|
||||||
|
You can customize the release by editing `.github/workflows/release.yml`:
|
||||||
|
|
||||||
|
- Change the supported platforms in the `matrix.os` array
|
||||||
|
- Modify the release notes template
|
||||||
|
- Add additional build steps or checks
|
||||||
|
- Change the artifact upload logic
|
||||||
36
.github/workflows/ci.yml
vendored
Normal file
36
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run type check
|
||||||
|
run: npm run type-check
|
||||||
|
|
||||||
|
- name: Run linter
|
||||||
|
run: npm run lint:check
|
||||||
|
|
||||||
|
- name: Run spell check
|
||||||
|
run: npm run spell-check
|
||||||
|
|
||||||
|
- name: Test build
|
||||||
|
run: npm run build:electron
|
||||||
154
.github/workflows/release.yml
vendored
Normal file
154
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Tag version to release (e.g., v1.0.0)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run type check
|
||||||
|
run: npm run type-check
|
||||||
|
|
||||||
|
- name: Run linter
|
||||||
|
run: npm run lint:check
|
||||||
|
|
||||||
|
- name: Build Electron app
|
||||||
|
run: npm run build
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Upload artifacts (macOS)
|
||||||
|
if: matrix.os == 'macos-latest'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: macos-release
|
||||||
|
path: |
|
||||||
|
release/*.dmg
|
||||||
|
release/latest-mac.yml
|
||||||
|
|
||||||
|
- name: Upload artifacts (Windows)
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: windows-release
|
||||||
|
path: |
|
||||||
|
release/*.exe
|
||||||
|
release/latest.yml
|
||||||
|
|
||||||
|
- name: Upload artifacts (Linux)
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: linux-release
|
||||||
|
path: |
|
||||||
|
release/*.AppImage
|
||||||
|
release/latest-linux.yml
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: artifacts
|
||||||
|
|
||||||
|
- name: Display structure of downloaded files
|
||||||
|
run: ls -la artifacts/**/*
|
||||||
|
|
||||||
|
- name: Determine tag name
|
||||||
|
id: tag
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||||
|
echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
run: |
|
||||||
|
gh release create ${{ steps.tag.outputs.tag }} \
|
||||||
|
--title "FriendlyKobold ${{ steps.tag.outputs.tag }}" \
|
||||||
|
--notes "## What's New
|
||||||
|
|
||||||
|
Release ${{ steps.tag.outputs.tag }} of FriendlyKobold
|
||||||
|
|
||||||
|
### Downloads
|
||||||
|
- **macOS**: Download the \`.dmg\` file
|
||||||
|
- **Windows**: Download the \`.exe\` installer
|
||||||
|
- **Linux**: Download the \`.AppImage\` file
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
- **macOS**: Open the \`.dmg\` file and drag FriendlyKobold to Applications
|
||||||
|
- **Windows**: Run the \`.exe\` installer
|
||||||
|
- **Linux**: Make the \`.AppImage\` executable and run it
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For more information, visit our [repository](https://github.com/${{ github.repository }})."
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Upload macOS Release Asset
|
||||||
|
run: |
|
||||||
|
for file in artifacts/macos-release/*.dmg; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
filename=$(basename "$file")
|
||||||
|
gh release upload ${{ steps.tag.outputs.tag }} "$file" --clobber
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Upload Windows Release Asset
|
||||||
|
run: |
|
||||||
|
for file in artifacts/windows-release/*.exe; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
filename=$(basename "$file")
|
||||||
|
gh release upload ${{ steps.tag.outputs.tag }} "$file" --clobber
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Upload Linux Release Asset
|
||||||
|
run: |
|
||||||
|
for file in artifacts/linux-release/*.AppImage; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
filename=$(basename "$file")
|
||||||
|
gh release upload ${{ steps.tag.outputs.tag }} "$file" --clobber
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
141
LICENSE
141
LICENSE
|
|
@ -1,5 +1,5 @@
|
||||||
GNU GENERAL PUBLIC LICENSE
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
|
@ -7,17 +7,15 @@
|
||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
software and other kinds of works.
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
share and change all versions of a program--to make sure it remains free
|
share and change all versions of a program--to make sure it remains free
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
software for all its users.
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its authors. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
|
@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
||||||
want it, that you can change the software or use pieces of it in new
|
want it, that you can change the software or use pieces of it in new
|
||||||
free programs, and that you know you can do these things.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
Developers that use our General Public Licenses protect your rights
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
you this License which gives you legal permission to copy, distribute
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
and/or modify the software.
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
A secondary benefit of defending all users' freedom is that
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
improvements made in alternate versions of the program, if they
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
receive widespread use, become available for other developers to
|
||||||
or can get the source code. And you must show them these terms so they
|
incorporate. Many developers of free software are heartened and
|
||||||
know their rights.
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
The GNU Affero General Public License is designed specifically to
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
ensure that, in such cases, the modified source code becomes available
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
An older license, called the Affero General Public License and
|
||||||
that there is no warranty for this free software. For both users' and
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
changed, so that their problems will not be attributed erroneously to
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
authors of previous versions.
|
this license.
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
|
|
@ -72,7 +60,7 @@ modification follow.
|
||||||
|
|
||||||
0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
works, such as semiconductor masks.
|
works, such as semiconductor masks.
|
||||||
|
|
@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
||||||
the Program, the only way you could satisfy both those terms and this
|
the Program, the only way you could satisfy both those terms and this
|
||||||
License would be to refrain entirely from conveying the Program.
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
under version 3 of the GNU General Public License into a single
|
||||||
combined work, and to convey the resulting work. The terms of this
|
combined work, and to convey the resulting work. The terms of this
|
||||||
License will continue to apply to the part which is the covered work,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the special requirements of the GNU Affero General Public License,
|
but the work with which it is combined will remain governed by version
|
||||||
section 13, concerning interaction through a network will apply to the
|
3 of the GNU General Public License.
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
the GNU General Public License from time to time. Such new versions will
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
Each version is given a distinguishing version number. If the
|
||||||
Program specifies that a certain numbered version of the GNU General
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
Public License "or any later version" applies to it, you have the
|
Public License "or any later version" applies to it, you have the
|
||||||
option of following the terms and conditions either of that numbered
|
option of following the terms and conditions either of that numbered
|
||||||
version or of any later version published by the Free Software
|
version or of any later version published by the Free Software
|
||||||
Foundation. If the Program does not specify a version number of the
|
Foundation. If the Program does not specify a version number of the
|
||||||
GNU General Public License, you may choose any version ever published
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
by the Free Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Program specifies that a proxy can decide which future
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
public statement of acceptance of a version permanently authorizes you
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
|
@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU General Public License for more details.
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
If your software can interact with users remotely through a computer
|
||||||
notice like this when it starts in an interactive mode:
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
<program> Copyright (C) <year> <name of author>
|
interface could display a "Source" link that leads users to an archive
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
of the code. There are many ways you could offer source, and different
|
||||||
This is free software, and you are welcome to redistribute it
|
solutions will be better for different programs; see section 13 for the
|
||||||
under certain conditions; type `show c' for details.
|
specific requirements.
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
<https://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
||||||
|
|
|
||||||
|
|
@ -24,4 +24,4 @@ A koboldcpp manager.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
GPL3 License - see LICENSE file for details
|
AGPL v3 License - see LICENSE file for details
|
||||||
|
|
|
||||||
12
build/entitlements.mac.plist
Normal file
12
build/entitlements.mac.plist
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
381
cspell.json
381
cspell.json
|
|
@ -2,227 +2,224 @@
|
||||||
"version": "0.2",
|
"version": "0.2",
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"words": [
|
"words": [
|
||||||
// Project specific terms
|
"addEventListener",
|
||||||
"kobold",
|
"admin",
|
||||||
"koboldcpp",
|
"allowRunningInsecureContent",
|
||||||
"friendlykobold",
|
|
||||||
"kcpps",
|
|
||||||
"kcppt",
|
|
||||||
// Technology terms
|
|
||||||
"vite",
|
|
||||||
"vitejs",
|
|
||||||
"vitest",
|
|
||||||
"tailwindcss",
|
|
||||||
"postcss",
|
|
||||||
"eslint",
|
|
||||||
"sonarjs",
|
|
||||||
"typescript",
|
|
||||||
"tsx",
|
|
||||||
"jsx",
|
|
||||||
"husky",
|
|
||||||
"linted",
|
|
||||||
"prettierrc",
|
|
||||||
"gitignore",
|
|
||||||
"tsconfig",
|
|
||||||
"treemap",
|
|
||||||
"cuda",
|
|
||||||
"rocm",
|
|
||||||
"openblas",
|
|
||||||
"nvidia",
|
|
||||||
"geforce",
|
|
||||||
"radeon",
|
|
||||||
"amdgpu",
|
"amdgpu",
|
||||||
"wmic",
|
"api",
|
||||||
"oldpc",
|
"apis",
|
||||||
"nocuda",
|
"appendChild",
|
||||||
"cooldown",
|
"arg",
|
||||||
"togglefullscreen",
|
"args",
|
||||||
"libvk",
|
"asar",
|
||||||
"swiftshader",
|
"async",
|
||||||
"icns",
|
"auth",
|
||||||
"nsis",
|
"await",
|
||||||
|
"babel",
|
||||||
|
"basename",
|
||||||
|
"bgcolor",
|
||||||
|
"browserWindow",
|
||||||
|
"bundler",
|
||||||
|
"bundling",
|
||||||
|
"can",
|
||||||
|
"classList",
|
||||||
|
"className",
|
||||||
|
"cloneNode",
|
||||||
"codeinterface",
|
"codeinterface",
|
||||||
"tabler",
|
"componentDidCatch",
|
||||||
"deps",
|
"componentDidMount",
|
||||||
"devs",
|
"componentDidUpdate",
|
||||||
"repo",
|
"componentWillUnmount",
|
||||||
"repos",
|
|
||||||
"config",
|
"config",
|
||||||
"configs",
|
"configs",
|
||||||
|
"contextBridge",
|
||||||
|
"contextIsolation",
|
||||||
|
"contextsize",
|
||||||
|
"cooldown",
|
||||||
|
"couldn",
|
||||||
|
"createContext",
|
||||||
|
"createElement",
|
||||||
|
"createRef",
|
||||||
|
"css",
|
||||||
|
"cuda",
|
||||||
|
"dataset",
|
||||||
|
"deps",
|
||||||
|
"destructuring",
|
||||||
|
"devs",
|
||||||
|
"dirname",
|
||||||
|
"doesn",
|
||||||
|
"don",
|
||||||
|
"Egor",
|
||||||
|
"env",
|
||||||
|
"envs",
|
||||||
|
"eot",
|
||||||
|
"eslint",
|
||||||
"filename",
|
"filename",
|
||||||
"filenames",
|
"filenames",
|
||||||
"filepath",
|
"filepath",
|
||||||
"filepaths",
|
"filepaths",
|
||||||
"pathname",
|
|
||||||
"pathnames",
|
|
||||||
"dirname",
|
|
||||||
"basename",
|
|
||||||
"readonly",
|
|
||||||
"inline",
|
|
||||||
"async",
|
|
||||||
"await",
|
|
||||||
"destructuring",
|
|
||||||
"refactor",
|
|
||||||
"refactoring",
|
|
||||||
"typeof",
|
|
||||||
"instanceof",
|
|
||||||
"addEventListener",
|
|
||||||
"removeEventListener",
|
|
||||||
"preventDefault",
|
|
||||||
"stopPropagation",
|
|
||||||
"querySelector",
|
|
||||||
"querySelectorAll",
|
|
||||||
"innerHTML",
|
|
||||||
"textContent",
|
|
||||||
"classList",
|
|
||||||
"className",
|
|
||||||
"dataset",
|
|
||||||
"setAttribute",
|
|
||||||
"getAttribute",
|
|
||||||
"removeAttribute",
|
|
||||||
"createElement",
|
|
||||||
"appendChild",
|
|
||||||
"removeChild",
|
|
||||||
"insertBefore",
|
|
||||||
"cloneNode",
|
|
||||||
// Electron specific
|
|
||||||
"preload",
|
|
||||||
"webContents",
|
|
||||||
"browserWindow",
|
|
||||||
"mainWindow",
|
|
||||||
"ipcMain",
|
|
||||||
"ipcRenderer",
|
|
||||||
"contextBridge",
|
|
||||||
"nodeIntegration",
|
|
||||||
"contextIsolation",
|
|
||||||
"webSecurity",
|
|
||||||
"allowRunningInsecureContent",
|
|
||||||
// React specific
|
|
||||||
"useEffect",
|
|
||||||
"useState",
|
|
||||||
"useContext",
|
|
||||||
"useReducer",
|
|
||||||
"useMemo",
|
|
||||||
"useCallback",
|
|
||||||
"useRef",
|
|
||||||
"forwardRef",
|
|
||||||
"createContext",
|
|
||||||
"createRef",
|
|
||||||
"componentDidMount",
|
|
||||||
"componentDidUpdate",
|
|
||||||
"componentWillUnmount",
|
|
||||||
"shouldComponentUpdate",
|
|
||||||
"getDerivedStateFromProps",
|
|
||||||
"getSnapshotBeforeUpdate",
|
|
||||||
"componentDidCatch",
|
|
||||||
"getDerivedStateFromError",
|
|
||||||
// CSS/Styling
|
|
||||||
"flexbox",
|
"flexbox",
|
||||||
"flexdir",
|
"flexdir",
|
||||||
"flexwrap",
|
"flexwrap",
|
||||||
"gridcol",
|
|
||||||
"gridrow",
|
|
||||||
"bgcolor",
|
|
||||||
"textcolor",
|
|
||||||
"fontsize",
|
"fontsize",
|
||||||
"fontweight",
|
"fontweight",
|
||||||
"lineheight",
|
"forwardRef",
|
||||||
|
"friendlykobold",
|
||||||
|
"geforce",
|
||||||
|
"getAttribute",
|
||||||
|
"getDerivedStateFromError",
|
||||||
|
"getDerivedStateFromProps",
|
||||||
|
"getSnapshotBeforeUpdate",
|
||||||
|
"gguf",
|
||||||
|
"GGUF",
|
||||||
|
"gif",
|
||||||
|
"gitignore",
|
||||||
|
"gpulayers",
|
||||||
|
"gridcol",
|
||||||
|
"gridrow",
|
||||||
|
"hostname",
|
||||||
|
"html",
|
||||||
|
"http",
|
||||||
|
"https",
|
||||||
|
"husky",
|
||||||
|
"icns",
|
||||||
|
"ico",
|
||||||
|
"impl",
|
||||||
|
"impls",
|
||||||
|
"inline",
|
||||||
|
"innerHTML",
|
||||||
|
"insertBefore",
|
||||||
|
"instanceof",
|
||||||
|
"ipcMain",
|
||||||
|
"ipcRenderer",
|
||||||
|
"jpeg",
|
||||||
|
"jpg",
|
||||||
|
"json",
|
||||||
|
"jsx",
|
||||||
|
"kcpps",
|
||||||
|
"kcppt",
|
||||||
|
"kobold",
|
||||||
|
"KOBOLDAI",
|
||||||
|
"koboldcpp",
|
||||||
|
"less",
|
||||||
"letterspacing",
|
"letterspacing",
|
||||||
"wordspacing",
|
"libvk",
|
||||||
"textalign",
|
"lineheight",
|
||||||
"textdecoration",
|
"linted",
|
||||||
"texttransform",
|
"localhost",
|
||||||
"whitespace",
|
"mainWindow",
|
||||||
"wordbreak",
|
"minified",
|
||||||
"wordwrap",
|
"minify",
|
||||||
|
"moz",
|
||||||
|
"namespace",
|
||||||
|
"namespaces",
|
||||||
|
"newpackagename",
|
||||||
|
"nocuda",
|
||||||
|
"nodeIntegration",
|
||||||
|
"nsis",
|
||||||
|
"nvidia",
|
||||||
|
"oldpc",
|
||||||
|
"openblas",
|
||||||
|
"otf",
|
||||||
"overflow",
|
"overflow",
|
||||||
"overflowx",
|
"overflowx",
|
||||||
"overflowy",
|
"overflowy",
|
||||||
"scrollbar",
|
"param",
|
||||||
"webkit",
|
"params",
|
||||||
"moz",
|
"parcel",
|
||||||
// Build tools
|
"pathname",
|
||||||
"sourcemap",
|
"pathnames",
|
||||||
"sourcemaps",
|
"Philippov",
|
||||||
"minify",
|
"png",
|
||||||
"minified",
|
|
||||||
"bundler",
|
|
||||||
"bundling",
|
|
||||||
"treeshake",
|
|
||||||
"treeshaking",
|
|
||||||
"polyfill",
|
"polyfill",
|
||||||
"polyfills",
|
"polyfills",
|
||||||
|
"postcss",
|
||||||
|
"preload",
|
||||||
|
"prettierrc",
|
||||||
|
"preventDefault",
|
||||||
|
"querySelector",
|
||||||
|
"querySelectorAll",
|
||||||
|
"radeon",
|
||||||
|
"readonly",
|
||||||
|
"refactor",
|
||||||
|
"refactoring",
|
||||||
|
"removeAttribute",
|
||||||
|
"removeChild",
|
||||||
|
"removeEventListener",
|
||||||
|
"repo",
|
||||||
|
"repos",
|
||||||
|
"rocm",
|
||||||
|
"rollup",
|
||||||
|
"sass",
|
||||||
|
"scrollbar",
|
||||||
|
"scss",
|
||||||
|
"setAttribute",
|
||||||
|
"shouldComponentUpdate",
|
||||||
|
"shouldn",
|
||||||
|
"sonarjs",
|
||||||
|
"sourcemap",
|
||||||
|
"sourcemaps",
|
||||||
|
"spec",
|
||||||
|
"specs",
|
||||||
|
"stopPropagation",
|
||||||
|
"subdomain",
|
||||||
|
"svg",
|
||||||
|
"swiftshader",
|
||||||
|
"tabler",
|
||||||
|
"temp",
|
||||||
|
"textalign",
|
||||||
|
"textcolor",
|
||||||
|
"textContent",
|
||||||
|
"textdecoration",
|
||||||
|
"texttransform",
|
||||||
|
"tmp",
|
||||||
|
"togglefullscreen",
|
||||||
|
"toml",
|
||||||
"transpile",
|
"transpile",
|
||||||
"transpiled",
|
"transpiled",
|
||||||
"transpiling",
|
"transpiling",
|
||||||
"babel",
|
"treemap",
|
||||||
"rollup",
|
"treeshake",
|
||||||
"webpack",
|
"treeshaking",
|
||||||
"parcel",
|
"tsconfig",
|
||||||
// Common abbreviations
|
"tsx",
|
||||||
"utils",
|
"ttf",
|
||||||
"util",
|
"typeof",
|
||||||
"impl",
|
"typescript",
|
||||||
"impls",
|
|
||||||
"spec",
|
|
||||||
"specs",
|
|
||||||
"param",
|
|
||||||
"params",
|
|
||||||
"arg",
|
|
||||||
"args",
|
|
||||||
"env",
|
|
||||||
"envs",
|
|
||||||
"var",
|
|
||||||
"vars",
|
|
||||||
"tmp",
|
|
||||||
"temp",
|
|
||||||
"auth",
|
|
||||||
"admin",
|
|
||||||
"api",
|
|
||||||
"apis",
|
|
||||||
"url",
|
|
||||||
"urls",
|
|
||||||
"uri",
|
"uri",
|
||||||
"uris",
|
"uris",
|
||||||
"http",
|
"url",
|
||||||
"https",
|
"urls",
|
||||||
"localhost",
|
"useCallback",
|
||||||
"hostname",
|
"useContext",
|
||||||
"subdomain",
|
"useEffect",
|
||||||
"namespace",
|
"useMemo",
|
||||||
"namespaces",
|
"useReducer",
|
||||||
// File extensions
|
"useRef",
|
||||||
"json",
|
"useState",
|
||||||
"yaml",
|
"util",
|
||||||
"toml",
|
"utils",
|
||||||
"xml",
|
"var",
|
||||||
"html",
|
"vars",
|
||||||
"css",
|
"vite",
|
||||||
"scss",
|
"vitejs",
|
||||||
"sass",
|
"vitest",
|
||||||
"less",
|
"webContents",
|
||||||
"svg",
|
"webkit",
|
||||||
"png",
|
|
||||||
"jpg",
|
|
||||||
"jpeg",
|
|
||||||
"gif",
|
|
||||||
"webp",
|
"webp",
|
||||||
"ico",
|
"webpack",
|
||||||
|
"webSecurity",
|
||||||
|
"whitespace",
|
||||||
|
"wmic",
|
||||||
"woff",
|
"woff",
|
||||||
"woff2",
|
"woff2",
|
||||||
"ttf",
|
|
||||||
"otf",
|
|
||||||
"eot",
|
|
||||||
// Common contractions and informal words
|
|
||||||
"doesn",
|
|
||||||
"don",
|
|
||||||
"won",
|
"won",
|
||||||
"can",
|
"wordbreak",
|
||||||
"couldn",
|
"wordspacing",
|
||||||
"shouldn",
|
"wordwrap",
|
||||||
"wouldn",
|
"wouldn",
|
||||||
// Documentation examples
|
"xml",
|
||||||
"newpackagename"
|
"yaml"
|
||||||
],
|
],
|
||||||
"ignorePaths": [
|
"ignorePaths": [
|
||||||
"node_modules/**",
|
"node_modules/**",
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,11 @@ import { visualizer } from 'rollup-plugin-visualizer';
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
main: {
|
main: {
|
||||||
plugins: [externalizeDepsPlugin()],
|
plugins: [externalizeDepsPlugin()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
define: {
|
define: {
|
||||||
'process.env.VITE_DEV_SERVER_URL': JSON.stringify(
|
'process.env.VITE_DEV_SERVER_URL': JSON.stringify(
|
||||||
process.env.VITE_DEV_SERVER_URL
|
process.env.VITE_DEV_SERVER_URL
|
||||||
|
|
@ -14,6 +19,11 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
preload: {
|
preload: {
|
||||||
plugins: [externalizeDepsPlugin()],
|
plugins: [externalizeDepsPlugin()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
renderer: {
|
renderer: {
|
||||||
root: '.',
|
root: '.',
|
||||||
|
|
|
||||||
108
eslint.config.ts
108
eslint.config.ts
|
|
@ -1,37 +1,35 @@
|
||||||
import js from '@eslint/js';
|
import js from '@eslint/js';
|
||||||
import globals from 'globals';
|
import globals from 'globals';
|
||||||
|
import tseslint from '@typescript-eslint/eslint-plugin';
|
||||||
|
import tsParser from '@typescript-eslint/parser';
|
||||||
import reactHooks from 'eslint-plugin-react-hooks';
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh';
|
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||||
import react from 'eslint-plugin-react';
|
import react from 'eslint-plugin-react';
|
||||||
import importPlugin from 'eslint-plugin-import';
|
import importPlugin from 'eslint-plugin-import';
|
||||||
import tseslint from '@typescript-eslint/eslint-plugin';
|
|
||||||
import tsParser from '@typescript-eslint/parser';
|
|
||||||
import sonarjs from 'eslint-plugin-sonarjs';
|
import sonarjs from 'eslint-plugin-sonarjs';
|
||||||
import security from 'eslint-plugin-security';
|
|
||||||
import cspell from '@cspell/eslint-plugin';
|
import cspell from '@cspell/eslint-plugin';
|
||||||
import type { Linter } from 'eslint';
|
|
||||||
|
|
||||||
const config: Linter.Config[] = [
|
const config = [
|
||||||
|
js.configs.recommended,
|
||||||
{
|
{
|
||||||
ignores: ['dist', 'dist-electron', 'out', 'electron', 'scripts'],
|
ignores: ['dist', 'dist-electron', 'out', 'electron', 'scripts'],
|
||||||
},
|
},
|
||||||
js.configs.recommended,
|
|
||||||
sonarjs.configs.recommended,
|
|
||||||
security.configs.recommended,
|
|
||||||
{
|
{
|
||||||
files: ['**/*.{ts,tsx}'],
|
files: ['**/*.{js,mjs,cjs,ts,tsx}'],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
globals: {
|
|
||||||
...globals.browser,
|
|
||||||
...globals.node,
|
|
||||||
},
|
|
||||||
parser: tsParser,
|
parser: tsParser,
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module',
|
||||||
ecmaFeatures: {
|
ecmaFeatures: {
|
||||||
jsx: true,
|
jsx: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
'@typescript-eslint': tseslint,
|
'@typescript-eslint': tseslint,
|
||||||
|
|
@ -39,18 +37,36 @@ const config: Linter.Config[] = [
|
||||||
'react-refresh': reactRefresh,
|
'react-refresh': reactRefresh,
|
||||||
react: react,
|
react: react,
|
||||||
import: importPlugin,
|
import: importPlugin,
|
||||||
|
sonarjs: sonarjs,
|
||||||
'@cspell': cspell,
|
'@cspell': cspell,
|
||||||
},
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect',
|
||||||
|
},
|
||||||
|
},
|
||||||
rules: {
|
rules: {
|
||||||
// Use recommended rules from plugins
|
// Use recommended rules from plugins
|
||||||
...tseslint.configs.recommended.rules,
|
|
||||||
...reactHooks.configs.recommended.rules,
|
...reactHooks.configs.recommended.rules,
|
||||||
...react.configs.recommended.rules,
|
...react.configs.recommended.rules,
|
||||||
|
|
||||||
|
// Essential TypeScript rules
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
ignoreRestSiblings: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'no-unused-vars': 'off', // Turn off base rule to use TypeScript version
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
|
|
||||||
|
// React-specific rules you wanted
|
||||||
'react-refresh/only-export-components': [
|
'react-refresh/only-export-components': [
|
||||||
'warn',
|
'warn',
|
||||||
{ allowConstantExport: true },
|
{ allowConstantExport: true },
|
||||||
],
|
],
|
||||||
// Override only the specific modern React patterns we want to enforce
|
|
||||||
'react/function-component-definition': [
|
'react/function-component-definition': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
|
|
@ -59,6 +75,8 @@ const config: Linter.Config[] = [
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'react/react-in-jsx-scope': 'off', // Not needed with new JSX transform
|
'react/react-in-jsx-scope': 'off', // Not needed with new JSX transform
|
||||||
|
|
||||||
|
// No default React imports - force specific imports
|
||||||
'no-restricted-imports': [
|
'no-restricted-imports': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
|
|
@ -72,33 +90,45 @@ const config: Linter.Config[] = [
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// Enforce named exports for React components
|
|
||||||
|
// Import rules - enforce named exports
|
||||||
'import/no-default-export': 'error',
|
'import/no-default-export': 'error',
|
||||||
'import/prefer-default-export': 'off',
|
'import/prefer-default-export': 'off',
|
||||||
|
|
||||||
// Enforce arrow function shorthand when possible
|
// Enforce arrow function shorthand when possible
|
||||||
'arrow-body-style': ['error', 'as-needed'],
|
'arrow-body-style': ['error', 'as-needed'],
|
||||||
'prefer-arrow-callback': ['error', { allowNamedFunctions: false }],
|
'prefer-arrow-callback': ['error', { allowNamedFunctions: false }],
|
||||||
// Disallow console.log usage
|
|
||||||
|
// Forbid console.log usage
|
||||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||||
// Warn about unnecessary explicit type annotations
|
|
||||||
|
// TypeScript rules
|
||||||
'@typescript-eslint/no-inferrable-types': 'warn',
|
'@typescript-eslint/no-inferrable-types': 'warn',
|
||||||
// Don't require explicit return types (prefer inference)
|
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
// Disable some overly strict security rules for Electron apps
|
|
||||||
'security/detect-non-literal-fs-filename': 'off',
|
// SonarJS rules - keep cognitive complexity reasonable
|
||||||
'security/detect-object-injection': 'off',
|
|
||||||
// Relax cognitive complexity for complex business logic
|
|
||||||
'sonarjs/cognitive-complexity': ['warn', 25],
|
'sonarjs/cognitive-complexity': ['warn', 25],
|
||||||
|
|
||||||
// Spell checking for code
|
// Spell checking for code
|
||||||
'@cspell/spellchecker': ['warn'],
|
'@cspell/spellchecker': ['warn'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// TypeScript definition files should have relaxed rules
|
||||||
|
files: ['**/*.d.ts'],
|
||||||
|
rules: {
|
||||||
|
// Allow unused variables in type definitions
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
|
// Allow any types in definitions
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// Allow default exports for config files
|
// Allow default exports for config files
|
||||||
files: [
|
files: [
|
||||||
'*.config.*',
|
'*.config.*',
|
||||||
'vite.config.*',
|
'vite.config.*',
|
||||||
'tailwind.config.*',
|
|
||||||
'eslint.config.*',
|
'eslint.config.*',
|
||||||
'postcss.config.*',
|
'postcss.config.*',
|
||||||
],
|
],
|
||||||
|
|
@ -106,36 +136,6 @@ const config: Linter.Config[] = [
|
||||||
'import/no-default-export': 'off',
|
'import/no-default-export': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
files: ['**/*.{js,jsx}'],
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
globals: {
|
|
||||||
...globals.browser,
|
|
||||||
...globals.node,
|
|
||||||
},
|
|
||||||
parserOptions: {
|
|
||||||
ecmaFeatures: {
|
|
||||||
jsx: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
'react-hooks': reactHooks,
|
|
||||||
'react-refresh': reactRefresh,
|
|
||||||
react: react,
|
|
||||||
import: importPlugin,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
...reactHooks.configs.recommended.rules,
|
|
||||||
...react.configs.recommended.rules,
|
|
||||||
'react-refresh/only-export-components': [
|
|
||||||
'warn',
|
|
||||||
{ allowConstantExport: true },
|
|
||||||
],
|
|
||||||
'react/react-in-jsx-scope': 'off', // Not needed with new JSX transform
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
||||||
1137
package-lock.json
generated
1137
package-lock.json
generated
File diff suppressed because it is too large
Load diff
46
package.json
46
package.json
|
|
@ -20,11 +20,10 @@
|
||||||
"format": "prettier --write . --ignore-path .gitignore",
|
"format": "prettier --write . --ignore-path .gitignore",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"lint:check": "eslint . --max-warnings 0",
|
"compile": "tsc --noEmit",
|
||||||
"type-check": "tsc --noEmit",
|
|
||||||
"spell-check": "cspell \"**/*.{ts,tsx,js,jsx,md,json}\" --no-progress",
|
"spell-check": "cspell \"**/*.{ts,tsx,js,jsx,md,json}\" --no-progress",
|
||||||
"spell-check:fix": "cspell \"**/*.{ts,tsx,js,jsx,md,json}\" --no-progress --show-suggestions",
|
"spell-check:fix": "cspell \"**/*.{ts,tsx,js,jsx,md,json}\" --no-progress --show-suggestions",
|
||||||
"check-all": "npm run lint && npm run type-check && npm run spell-check",
|
"check-all": "npm run lint && npm run compile && npm run spell-check",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
|
|
@ -44,12 +43,11 @@
|
||||||
"ai",
|
"ai",
|
||||||
"llm"
|
"llm"
|
||||||
],
|
],
|
||||||
"author": "",
|
"author": "Egor Philippov",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cspell/eslint-plugin": "^9.2.0",
|
"@cspell/eslint-plugin": "^9.2.0",
|
||||||
"@eslint/js": "^9.33.0",
|
"@eslint/js": "^9.33.0",
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
|
||||||
"@types/node": "^24.2.1",
|
"@types/node": "^24.2.1",
|
||||||
"@types/react": "^19.1.10",
|
"@types/react": "^19.1.10",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.1.7",
|
||||||
|
|
@ -66,14 +64,13 @@
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
"eslint-plugin-security": "^3.0.1",
|
|
||||||
"eslint-plugin-sonarjs": "^3.0.4",
|
"eslint-plugin-sonarjs": "^3.0.4",
|
||||||
"globals": "^16.3.0",
|
"globals": "^16.3.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
|
"jiti": "^2.5.1",
|
||||||
"lint-staged": "^16.1.5",
|
"lint-staged": "^16.1.5",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"rollup-plugin-visualizer": "^6.0.3",
|
"rollup-plugin-visualizer": "^6.0.3",
|
||||||
"tailwindcss": "^4.1.11",
|
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vite": "^7.1.2",
|
"vite": "^7.1.2",
|
||||||
"wait-on": "^8.0.4"
|
"wait-on": "^8.0.4"
|
||||||
|
|
@ -82,23 +79,41 @@
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@mantine/core": "^8.2.4",
|
"@mantine/core": "^8.2.4",
|
||||||
"@mantine/hooks": "^8.2.4",
|
"@mantine/hooks": "^8.2.4",
|
||||||
"@tabler/icons-react": "^3.34.1",
|
"@mantine/notifications": "^8.2.4",
|
||||||
|
"lucide-react": "^0.539.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1"
|
"react-dom": "^19.1.1"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.friendlykobold.app",
|
"appId": "com.friendlykobold.app",
|
||||||
"productName": "FriendlyKobold",
|
"productName": "FriendlyKobold",
|
||||||
|
"compression": "maximum",
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "release"
|
"output": "release"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist/**/*",
|
"out/**/*",
|
||||||
"dist-electron/**/*",
|
"!**/node_modules/**/*",
|
||||||
"node_modules/**/*"
|
"!out/renderer/node_modules/**/*",
|
||||||
|
"!**/*.map",
|
||||||
|
"!**/*.ts",
|
||||||
|
"!**/*.tsx",
|
||||||
|
"!**/test/**/*",
|
||||||
|
"!**/tests/**/*",
|
||||||
|
"!**/__tests__/**/*",
|
||||||
|
"!**/coverage/**/*",
|
||||||
|
"!**/.nyc_output/**/*",
|
||||||
|
"package.json"
|
||||||
|
],
|
||||||
|
"asarUnpack": [
|
||||||
|
"!**/node_modules/**/*"
|
||||||
],
|
],
|
||||||
"mac": {
|
"mac": {
|
||||||
"icon": "assets/icon.icns",
|
"category": "public.app-category.productivity",
|
||||||
|
"hardenedRuntime": true,
|
||||||
|
"gatekeeperAssess": false,
|
||||||
|
"entitlements": "build/entitlements.mac.plist",
|
||||||
|
"entitlementsInherit": "build/entitlements.mac.plist",
|
||||||
"target": [
|
"target": [
|
||||||
{
|
{
|
||||||
"target": "dmg",
|
"target": "dmg",
|
||||||
|
|
@ -110,7 +125,6 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"win": {
|
"win": {
|
||||||
"icon": "assets/icon.ico",
|
|
||||||
"target": [
|
"target": [
|
||||||
{
|
{
|
||||||
"target": "nsis",
|
"target": "nsis",
|
||||||
|
|
@ -121,7 +135,6 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"icon": "assets/icon.png",
|
|
||||||
"target": [
|
"target": [
|
||||||
{
|
{
|
||||||
"target": "AppImage",
|
"target": "AppImage",
|
||||||
|
|
@ -130,6 +143,9 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"nsis": {
|
||||||
|
"differentialPackage": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
const config = {
|
|
||||||
plugins: {
|
|
||||||
'@tailwindcss/postcss': {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = config;
|
|
||||||
271
src/App.tsx
271
src/App.tsx
|
|
@ -1,19 +1,82 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, ReactNode } from 'react';
|
||||||
import { AppShell, Group, ActionIcon, Tooltip, rem } from '@mantine/core';
|
import {
|
||||||
import { IconSettings } from '@tabler/icons-react';
|
AppShell,
|
||||||
|
Group,
|
||||||
|
ActionIcon,
|
||||||
|
Tooltip,
|
||||||
|
rem,
|
||||||
|
Transition,
|
||||||
|
Loader,
|
||||||
|
Center,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Button,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { Notifications } from '@mantine/notifications';
|
||||||
|
import { Settings, ArrowLeft } from 'lucide-react';
|
||||||
import { DownloadScreen } from '@/screens/DownloadScreen';
|
import { DownloadScreen } from '@/screens/DownloadScreen';
|
||||||
import { LaunchScreen } from '@/screens/LaunchScreen';
|
import { LaunchScreen } from '@/screens/LaunchScreen';
|
||||||
|
import { TerminalScreen } from '@/screens/TerminalScreen';
|
||||||
import { UpdateDialog } from '@/components/UpdateDialog';
|
import { UpdateDialog } from '@/components/UpdateDialog';
|
||||||
import { SettingsModal } from '@/components/SettingsModal';
|
import { SettingsModal } from '@/components/SettingsModal';
|
||||||
import type { UpdateInfo } from '@/types/electron';
|
import { useNotifications } from '@/hooks/useNotifications';
|
||||||
|
import type { UpdateInfo } from '@/types';
|
||||||
|
|
||||||
type Screen = 'download' | 'launch';
|
type Screen = 'download' | 'launch' | 'terminal';
|
||||||
|
|
||||||
|
interface ScreenTransitionProps {
|
||||||
|
isActive: boolean;
|
||||||
|
shouldAnimate: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScreenTransition = ({
|
||||||
|
isActive,
|
||||||
|
shouldAnimate,
|
||||||
|
children,
|
||||||
|
}: ScreenTransitionProps) => {
|
||||||
|
const getTransform = () => {
|
||||||
|
if (!shouldAnimate) return undefined;
|
||||||
|
const scale = isActive ? 1 : 0.98;
|
||||||
|
return `scale(${scale})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition
|
||||||
|
mounted={isActive}
|
||||||
|
transition="fade"
|
||||||
|
duration={shouldAnimate ? 350 : 0}
|
||||||
|
timingFunction="ease-out"
|
||||||
|
>
|
||||||
|
{(styles) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...styles,
|
||||||
|
position: isActive ? 'static' : 'absolute',
|
||||||
|
width: '100%',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
zIndex: isActive ? 1 : 0,
|
||||||
|
transform: `${styles.transform || ''} ${getTransform() || ''}`,
|
||||||
|
transition: shouldAnimate ? 'all 350ms ease-out' : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Transition>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const [currentScreen, setCurrentScreen] = useState<Screen>('download');
|
const [currentScreen, setCurrentScreen] = useState<Screen | null>(null);
|
||||||
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
|
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
|
||||||
const [showUpdateDialog, setShowUpdateDialog] = useState(false);
|
const [showUpdateDialog, setShowUpdateDialog] = useState(false);
|
||||||
const [settingsOpened, setSettingsOpened] = useState(false);
|
const [settingsOpened, setSettingsOpened] = useState(false);
|
||||||
|
const [hasInitialized, setHasInitialized] = useState(false);
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const notify = useNotifications();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkInstallation = async () => {
|
const checkInstallation = async () => {
|
||||||
|
|
@ -21,9 +84,13 @@ export const App = () => {
|
||||||
try {
|
try {
|
||||||
const installed = await window.electronAPI.kobold.isInstalled();
|
const installed = await window.electronAPI.kobold.isInstalled();
|
||||||
setCurrentScreen(installed ? 'launch' : 'download');
|
setCurrentScreen(installed ? 'launch' : 'download');
|
||||||
|
setHasInitialized(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking installation:', error);
|
console.error('Error checking installation:', error);
|
||||||
|
setHasInitialized(true);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
setHasInitialized(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -34,19 +101,62 @@ export const App = () => {
|
||||||
setUpdateInfo(info);
|
setUpdateInfo(info);
|
||||||
setShowUpdateDialog(true);
|
setShowUpdateDialog(true);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
const cleanupInstallDirListener =
|
||||||
if (window.electronAPI) {
|
window.electronAPI.kobold.onInstallDirChanged(() => {
|
||||||
|
checkInstallation();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
window.electronAPI.kobold.removeAllListeners('update-available');
|
window.electronAPI.kobold.removeAllListeners('update-available');
|
||||||
}
|
cleanupInstallDirListener();
|
||||||
};
|
};
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleInstallComplete = () => {
|
const handleDownloadComplete = () => {
|
||||||
|
notify.success(
|
||||||
|
'Download Complete',
|
||||||
|
'KoboldCpp has been successfully installed'
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
setCurrentScreen('launch');
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLaunch = () => {
|
||||||
|
setCurrentScreen('terminal');
|
||||||
|
notify.success('Launch Started', 'KoboldCpp is starting up...');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackToLaunch = () => {
|
||||||
setCurrentScreen('launch');
|
setCurrentScreen('launch');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEject = async () => {
|
||||||
|
// Show confirmation dialog
|
||||||
|
try {
|
||||||
|
const confirmed = await window.electronAPI.kobold.confirmEject();
|
||||||
|
if (!confirmed) {
|
||||||
|
return; // User cancelled
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error showing confirmation dialog:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.electronAPI?.kobold?.stopKoboldCpp) {
|
||||||
|
try {
|
||||||
|
await window.electronAPI.kobold.stopKoboldCpp();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error stopping KoboldCpp:', error);
|
||||||
|
notify.error('Stop Failed', 'Failed to stop KoboldCpp process');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBackToLaunch();
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpdateIgnore = () => {
|
const handleUpdateIgnore = () => {
|
||||||
setShowUpdateDialog(false);
|
setShowUpdateDialog(false);
|
||||||
};
|
};
|
||||||
|
|
@ -57,41 +167,108 @@ export const App = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell header={{ height: 60 }} padding="md">
|
<>
|
||||||
<AppShell.Header>
|
<Notifications
|
||||||
<Group h="100%" px="md" justify="flex-end">
|
position="bottom-right"
|
||||||
<Tooltip label="Settings" position="bottom">
|
zIndex={1000}
|
||||||
<ActionIcon
|
containerWidth={320}
|
||||||
variant="subtle"
|
|
||||||
size="lg"
|
|
||||||
onClick={() => setSettingsOpened(true)}
|
|
||||||
aria-label="Open settings"
|
|
||||||
>
|
|
||||||
<IconSettings style={{ width: rem(18), height: rem(18) }} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
|
||||||
</AppShell.Header>
|
|
||||||
|
|
||||||
<AppShell.Main>
|
|
||||||
{currentScreen === 'download' && (
|
|
||||||
<DownloadScreen onInstallComplete={handleInstallComplete} />
|
|
||||||
)}
|
|
||||||
{currentScreen === 'launch' && <LaunchScreen />}
|
|
||||||
|
|
||||||
{showUpdateDialog && updateInfo && (
|
|
||||||
<UpdateDialog
|
|
||||||
updateInfo={updateInfo}
|
|
||||||
onIgnore={handleUpdateIgnore}
|
|
||||||
onAccept={handleUpdateAccept}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AppShell.Main>
|
|
||||||
|
|
||||||
<SettingsModal
|
|
||||||
opened={settingsOpened}
|
|
||||||
onClose={() => setSettingsOpened(false)}
|
|
||||||
/>
|
/>
|
||||||
</AppShell>
|
<AppShell header={{ height: 60 }} padding="md">
|
||||||
|
<AppShell.Header
|
||||||
|
style={{
|
||||||
|
borderBottom: `1px solid var(--mantine-color-${colorScheme === 'dark' ? 'dark-4' : 'gray-3'})`,
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
background:
|
||||||
|
colorScheme === 'dark'
|
||||||
|
? 'rgba(26, 27, 30, 0.8)'
|
||||||
|
: 'rgba(255, 255, 255, 0.8)',
|
||||||
|
transition: 'all 200ms ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group h="100%" px="md" justify="space-between">
|
||||||
|
{currentScreen === 'terminal' && (
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
leftSection={<ArrowLeft size={16} />}
|
||||||
|
onClick={handleEject}
|
||||||
|
>
|
||||||
|
Eject
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Group ml="auto">
|
||||||
|
<Tooltip label="Settings" position="bottom">
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
size="xl"
|
||||||
|
onClick={() => setSettingsOpened(true)}
|
||||||
|
aria-label="Open settings"
|
||||||
|
style={{
|
||||||
|
transition: 'all 200ms ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Settings style={{ width: rem(20), height: rem(20) }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
<AppShell.Main
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
minHeight: 'calc(100vh - 60px)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentScreen === null ? (
|
||||||
|
<Center h="100%" style={{ minHeight: '400px' }}>
|
||||||
|
<Stack align="center" gap="lg">
|
||||||
|
<Loader size="xl" type="dots" />
|
||||||
|
<Text c="dimmed" size="lg">
|
||||||
|
Initializing...
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ScreenTransition
|
||||||
|
isActive={currentScreen === 'download'}
|
||||||
|
shouldAnimate={hasInitialized}
|
||||||
|
>
|
||||||
|
<DownloadScreen onDownloadComplete={handleDownloadComplete} />
|
||||||
|
</ScreenTransition>
|
||||||
|
|
||||||
|
<ScreenTransition
|
||||||
|
isActive={currentScreen === 'launch'}
|
||||||
|
shouldAnimate={hasInitialized}
|
||||||
|
>
|
||||||
|
<LaunchScreen onLaunch={handleLaunch} />
|
||||||
|
</ScreenTransition>
|
||||||
|
|
||||||
|
<ScreenTransition
|
||||||
|
isActive={currentScreen === 'terminal'}
|
||||||
|
shouldAnimate={hasInitialized}
|
||||||
|
>
|
||||||
|
<TerminalScreen />
|
||||||
|
</ScreenTransition>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showUpdateDialog && updateInfo && (
|
||||||
|
<UpdateDialog
|
||||||
|
updateInfo={updateInfo}
|
||||||
|
onIgnore={handleUpdateIgnore}
|
||||||
|
onAccept={handleUpdateAccept}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AppShell.Main>
|
||||||
|
<SettingsModal
|
||||||
|
opened={settingsOpened}
|
||||||
|
onClose={() => setSettingsOpened(false)}
|
||||||
|
/>
|
||||||
|
</AppShell>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
110
src/components/ConfigFileSelect.tsx
Normal file
110
src/components/ConfigFileSelect.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
import { Select, Text, Badge, Group } from '@mantine/core';
|
||||||
|
import { File } from 'lucide-react';
|
||||||
|
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
|
||||||
|
import type { ConfigFile } from '@/types';
|
||||||
|
|
||||||
|
interface ConfigFileSelectProps {
|
||||||
|
configFiles: ConfigFile[];
|
||||||
|
selectedFile: string | null;
|
||||||
|
loading: boolean;
|
||||||
|
onFileSelection: (fileName: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectItemProps extends ComponentPropsWithoutRef<'div'> {
|
||||||
|
label: string;
|
||||||
|
extension: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBadgeColor = (extension: string) => {
|
||||||
|
switch (extension.toLowerCase()) {
|
||||||
|
case '.kcpps':
|
||||||
|
return 'blue';
|
||||||
|
case '.kcppt':
|
||||||
|
return 'green';
|
||||||
|
default:
|
||||||
|
return 'gray';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
||||||
|
({ label, extension, ...others }, ref) => (
|
||||||
|
<div ref={ref} {...others}>
|
||||||
|
<Group justify="space-between" wrap="nowrap">
|
||||||
|
<Text size="sm" truncate>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Badge size="xs" variant="light" color={getBadgeColor(extension)}>
|
||||||
|
{extension}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
SelectItem.displayName = 'SelectItem';
|
||||||
|
|
||||||
|
export const ConfigFileSelect = ({
|
||||||
|
configFiles,
|
||||||
|
selectedFile,
|
||||||
|
loading,
|
||||||
|
onFileSelection,
|
||||||
|
}: ConfigFileSelectProps) => {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Text c="dimmed" ta="center">
|
||||||
|
Loading configuration files...
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configFiles.length === 0) {
|
||||||
|
return (
|
||||||
|
<Text c="dimmed" ta="center">
|
||||||
|
No configuration files found in the installation directory.
|
||||||
|
<br />
|
||||||
|
Please ensure your .kcpps or .kcppt files are in the correct location.
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectData = configFiles.map((file) => {
|
||||||
|
const extension = file.name.split('.').pop() || '';
|
||||||
|
const nameWithoutExtension = file.name.replace(`.${extension}`, '');
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: file.name,
|
||||||
|
label: nameWithoutExtension, // Clean label for selected value
|
||||||
|
extension: `.${extension}`, // Store extension separately
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
label="Configuration File"
|
||||||
|
placeholder="Select a configuration file"
|
||||||
|
value={selectedFile}
|
||||||
|
onChange={(value) => value && onFileSelection(value)}
|
||||||
|
data={selectData}
|
||||||
|
leftSection={<File size={16} />}
|
||||||
|
searchable
|
||||||
|
clearable={false}
|
||||||
|
w="100%"
|
||||||
|
filter={({ options, search }) =>
|
||||||
|
options.filter((option) => {
|
||||||
|
if ('label' in option) {
|
||||||
|
return option.label
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(search.toLowerCase().trim());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
renderOption={({ option }) => {
|
||||||
|
// Find the original data item to get the extension
|
||||||
|
const dataItem = selectData.find((item) => item.value === option.value);
|
||||||
|
const extension = dataItem?.extension || '';
|
||||||
|
return <SelectItem label={option.label} extension={extension} />;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,14 @@
|
||||||
import { Card, Stack, Group, Text, Badge, Button, Loader } from '@mantine/core';
|
import {
|
||||||
import { IconDownload } from '@tabler/icons-react';
|
Card,
|
||||||
|
Stack,
|
||||||
|
Group,
|
||||||
|
Text,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Loader,
|
||||||
|
Progress,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { Download } from 'lucide-react';
|
||||||
import { MouseEvent } from 'react';
|
import { MouseEvent } from 'react';
|
||||||
|
|
||||||
interface DownloadOptionCardProps {
|
interface DownloadOptionCardProps {
|
||||||
|
|
@ -9,6 +18,7 @@ interface DownloadOptionCardProps {
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
isRecommended: boolean;
|
isRecommended: boolean;
|
||||||
isDownloading: boolean;
|
isDownloading: boolean;
|
||||||
|
downloadProgress?: number;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
onDownload: (e: MouseEvent<HTMLButtonElement>) => void;
|
onDownload: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -20,6 +30,7 @@ export const DownloadOptionCard = ({
|
||||||
isSelected,
|
isSelected,
|
||||||
isRecommended,
|
isRecommended,
|
||||||
isDownloading,
|
isDownloading,
|
||||||
|
downloadProgress = 0,
|
||||||
onClick,
|
onClick,
|
||||||
onDownload,
|
onDownload,
|
||||||
}: DownloadOptionCardProps) => (
|
}: DownloadOptionCardProps) => (
|
||||||
|
|
@ -52,16 +63,12 @@ export const DownloadOptionCard = ({
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<Group justify="center" pt="sm">
|
<Stack gap="sm" pt="sm">
|
||||||
<Button
|
<Button
|
||||||
onClick={onDownload}
|
onClick={onDownload}
|
||||||
disabled={isDownloading}
|
disabled={isDownloading}
|
||||||
leftSection={
|
leftSection={
|
||||||
isDownloading ? (
|
isDownloading ? <Loader size="1rem" /> : <Download size="1rem" />
|
||||||
<Loader size="1rem" />
|
|
||||||
) : (
|
|
||||||
<IconDownload size="1rem" />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|
@ -69,7 +76,16 @@ export const DownloadOptionCard = ({
|
||||||
>
|
>
|
||||||
{isDownloading ? 'Downloading...' : 'Download'}
|
{isDownloading ? 'Downloading...' : 'Download'}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
|
||||||
|
{isDownloading && (
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Progress value={downloadProgress} color="blue" radius="xl" />
|
||||||
|
<Text size="xs" c="dimmed" ta="center">
|
||||||
|
{downloadProgress.toFixed(1)}% complete
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -11,25 +11,24 @@ import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Badge,
|
Badge,
|
||||||
|
Progress,
|
||||||
|
Loader,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconSettings,
|
Settings,
|
||||||
IconPalette,
|
Palette,
|
||||||
IconMoon,
|
Moon,
|
||||||
IconSun,
|
Sun,
|
||||||
IconDeviceDesktop,
|
Monitor,
|
||||||
IconAdjustments,
|
SlidersHorizontal,
|
||||||
IconFolder,
|
Folder,
|
||||||
IconFolderOpen,
|
FolderOpen,
|
||||||
IconVersions,
|
GitBranch,
|
||||||
IconDownload,
|
Download,
|
||||||
IconRefresh,
|
RotateCcw,
|
||||||
} from '@tabler/icons-react';
|
} from 'lucide-react';
|
||||||
import { useTheme } from '@/contexts/ThemeContext';
|
import { useTheme, type ThemeMode } from '@/contexts/ThemeContext';
|
||||||
import {
|
import { isAssetCompatibleWithPlatform } from '@/utils/platform';
|
||||||
getPlatformDisplayName,
|
|
||||||
isAssetCompatibleWithPlatform,
|
|
||||||
} from '@/utils/platform';
|
|
||||||
import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron';
|
import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron';
|
||||||
|
|
||||||
interface SettingsModalProps {
|
interface SettingsModalProps {
|
||||||
|
|
@ -40,7 +39,6 @@ interface SettingsModalProps {
|
||||||
export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
const { themeMode, setThemeMode } = useTheme();
|
const { themeMode, setThemeMode } = useTheme();
|
||||||
const [installDir, setInstallDir] = useState<string>('');
|
const [installDir, setInstallDir] = useState<string>('');
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [installedVersions, setInstalledVersions] = useState<
|
const [installedVersions, setInstalledVersions] = useState<
|
||||||
InstalledVersion[]
|
InstalledVersion[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
@ -52,12 +50,15 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
);
|
);
|
||||||
const [loadingRelease, setLoadingRelease] = useState(false);
|
const [loadingRelease, setLoadingRelease] = useState(false);
|
||||||
const [downloading, setDownloading] = useState<string | null>(null);
|
const [downloading, setDownloading] = useState<string | null>(null);
|
||||||
|
const [downloadProgress, setDownloadProgress] = useState<{
|
||||||
|
[key: string]: number;
|
||||||
|
}>({});
|
||||||
const [downloadingROCm, setDownloadingROCm] = useState(false);
|
const [downloadingROCm, setDownloadingROCm] = useState(false);
|
||||||
const [rocmDownload, setRocmDownload] = useState<{
|
const [rocmDownload, setRocmDownload] = useState<{
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
size: number;
|
size: number;
|
||||||
type: 'rocm';
|
version?: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [userPlatform, setUserPlatform] = useState<string>('');
|
const [userPlatform, setUserPlatform] = useState<string>('');
|
||||||
const [activeTab, setActiveTab] = useState('general');
|
const [activeTab, setActiveTab] = useState('general');
|
||||||
|
|
@ -85,8 +86,24 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
loadLatestRelease();
|
loadLatestRelease();
|
||||||
loadROCmDownload();
|
loadROCmDownload();
|
||||||
loadUserPlatform();
|
loadUserPlatform();
|
||||||
|
|
||||||
|
// Set up progress listener
|
||||||
|
const handleProgress = (progress: number) => {
|
||||||
|
if (downloading) {
|
||||||
|
setDownloadProgress((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[downloading]: progress,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.electronAPI.kobold.onDownloadProgress?.(handleProgress);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.electronAPI.kobold.removeAllListeners?.('download-progress');
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}, [opened]);
|
}, [opened, downloading]);
|
||||||
|
|
||||||
const loadCurrentInstallDir = async () => {
|
const loadCurrentInstallDir = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -141,6 +158,8 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
|
|
||||||
const handleDownload = async (assetName: string, downloadUrl: string) => {
|
const handleDownload = async (assetName: string, downloadUrl: string) => {
|
||||||
setDownloading(assetName);
|
setDownloading(assetName);
|
||||||
|
setDownloadProgress((prev) => ({ ...prev, [assetName]: 0 }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.electronAPI.kobold.downloadRelease({
|
const result = await window.electronAPI.kobold.downloadRelease({
|
||||||
name: assetName,
|
name: assetName,
|
||||||
|
|
@ -157,6 +176,11 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
console.error('Failed to download:', error);
|
console.error('Failed to download:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setDownloading(null);
|
setDownloading(null);
|
||||||
|
setDownloadProgress((prev) => {
|
||||||
|
const newProgress = { ...prev };
|
||||||
|
delete newProgress[assetName];
|
||||||
|
return newProgress;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -258,17 +282,15 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
const handleSelectInstallDir = async () => {
|
const handleSelectInstallDir = async () => {
|
||||||
if (!window.electronAPI) return;
|
if (!window.electronAPI) return;
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
try {
|
||||||
const selectedDir =
|
const selectedDir =
|
||||||
await window.electronAPI.kobold.selectInstallDirectory();
|
await window.electronAPI.kobold.selectInstallDirectory();
|
||||||
|
|
||||||
if (selectedDir) {
|
if (selectedDir) {
|
||||||
setInstallDir(selectedDir);
|
setInstallDir(selectedDir);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to select install directory:', error);
|
console.error('Failed to select install directory:', error);
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -278,7 +300,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title={
|
title={
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<IconSettings size={20} />
|
<Settings size={20} />
|
||||||
<Text fw={500}>Settings</Text>
|
<Text fw={500}>Settings</Text>
|
||||||
</Group>
|
</Group>
|
||||||
}
|
}
|
||||||
|
|
@ -319,7 +341,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
<Tabs.Tab
|
<Tabs.Tab
|
||||||
value="general"
|
value="general"
|
||||||
leftSection={
|
leftSection={
|
||||||
<IconAdjustments style={{ width: rem(16), height: rem(16) }} />
|
<SlidersHorizontal style={{ width: rem(16), height: rem(16) }} />
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
General
|
General
|
||||||
|
|
@ -327,7 +349,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
<Tabs.Tab
|
<Tabs.Tab
|
||||||
value="versions"
|
value="versions"
|
||||||
leftSection={
|
leftSection={
|
||||||
<IconVersions style={{ width: rem(16), height: rem(16) }} />
|
<GitBranch style={{ width: rem(16), height: rem(16) }} />
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Versions
|
Versions
|
||||||
|
|
@ -335,7 +357,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
<Tabs.Tab
|
<Tabs.Tab
|
||||||
value="appearance"
|
value="appearance"
|
||||||
leftSection={
|
leftSection={
|
||||||
<IconPalette style={{ width: rem(16), height: rem(16) }} />
|
<Palette style={{ width: rem(16), height: rem(16) }} />
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Appearance
|
Appearance
|
||||||
|
|
@ -349,7 +371,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
Installation Directory
|
Installation Directory
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" c="dimmed" mb="md">
|
<Text size="sm" c="dimmed" mb="md">
|
||||||
Choose where KoboldCpp files will be downloaded and stored
|
Choose where application files will be downloaded and stored
|
||||||
</Text>
|
</Text>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
|
@ -358,17 +380,14 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
placeholder="Default installation directory"
|
placeholder="Default installation directory"
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
leftSection={
|
leftSection={
|
||||||
<IconFolder style={{ width: rem(16), height: rem(16) }} />
|
<Folder style={{ width: rem(16), height: rem(16) }} />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleSelectInstallDir}
|
onClick={handleSelectInstallDir}
|
||||||
loading={loading}
|
|
||||||
leftSection={
|
leftSection={
|
||||||
<IconFolderOpen
|
<FolderOpen style={{ width: rem(16), height: rem(16) }} />
|
||||||
style={{ width: rem(16), height: rem(16) }}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Browse
|
Browse
|
||||||
|
|
@ -389,39 +408,44 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
onClick={loadLatestRelease}
|
onClick={loadLatestRelease}
|
||||||
loading={loadingRelease}
|
loading={loadingRelease}
|
||||||
leftSection={
|
leftSection={
|
||||||
<IconRefresh style={{ width: rem(14), height: rem(14) }} />
|
<RotateCcw style={{ width: rem(14), height: rem(14) }} />
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Check for Updates
|
Check for Updates
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="sm" c="dimmed" mb="md">
|
|
||||||
KoboldCpp versions available for{' '}
|
|
||||||
{getPlatformDisplayName(userPlatform)}. Click on downloaded
|
|
||||||
versions to set them as current.
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
{(() => {
|
{(() =>
|
||||||
const allVersions = getAllVersions();
|
getAllVersions().map((version, index) => (
|
||||||
return allVersions.map((version, index) => (
|
|
||||||
<Card
|
<Card
|
||||||
key={`${version.name}-${version.version}-${index}`}
|
key={`${version.name}-${version.version}-${index}`}
|
||||||
withBorder
|
withBorder
|
||||||
radius="sm"
|
radius="sm"
|
||||||
padding="sm"
|
padding="sm"
|
||||||
style={(() => {
|
style={{
|
||||||
let backgroundColor = undefined;
|
cursor:
|
||||||
if (version.isCurrent) {
|
version.isDownloaded && !version.isCurrent
|
||||||
backgroundColor = 'var(--mantine-color-blue-0)';
|
? 'pointer'
|
||||||
}
|
|
||||||
return {
|
|
||||||
backgroundColor,
|
|
||||||
borderColor: version.isCurrent
|
|
||||||
? 'var(--mantine-color-blue-6)'
|
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
}}
|
||||||
})()}
|
bd={
|
||||||
|
version.isCurrent
|
||||||
|
? '2px solid var(--mantine-color-blue-filled)'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
bg={
|
||||||
|
version.isCurrent
|
||||||
|
? 'var(--mantine-color-blue-light)'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onClick={
|
||||||
|
version.isDownloaded &&
|
||||||
|
!version.isCurrent &&
|
||||||
|
version.installedData
|
||||||
|
? () => handleVersionSelect(version.installedData!)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
|
|
@ -459,33 +483,38 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
loading={downloading === version.name}
|
loading={downloading === version.name}
|
||||||
disabled={downloading !== null}
|
disabled={downloading !== null}
|
||||||
leftSection={
|
leftSection={
|
||||||
<IconDownload
|
downloading === version.name ? (
|
||||||
style={{ width: rem(14), height: rem(14) }}
|
<Loader size="1rem" />
|
||||||
/>
|
) : (
|
||||||
|
<Download
|
||||||
|
style={{ width: rem(14), height: rem(14) }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Download
|
{downloading === version.name
|
||||||
|
? 'Downloading...'
|
||||||
|
: 'Download'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{version.isDownloaded &&
|
|
||||||
!version.isCurrent &&
|
|
||||||
version.installedData && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="xs"
|
|
||||||
color="blue"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleVersionSelect(version.installedData!);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Make Current
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
{downloading === version.name &&
|
||||||
|
downloadProgress[version.name] !== undefined && (
|
||||||
|
<Stack gap="xs" mt="sm">
|
||||||
|
<Progress
|
||||||
|
value={downloadProgress[version.name]}
|
||||||
|
color="blue"
|
||||||
|
radius="xl"
|
||||||
|
/>
|
||||||
|
<Text size="xs" c="dimmed" ta="center">
|
||||||
|
{downloadProgress[version.name].toFixed(1)}%
|
||||||
|
complete
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
));
|
)))()}
|
||||||
})()}
|
|
||||||
|
|
||||||
{userPlatform === 'linux' && rocmDownload && (
|
{userPlatform === 'linux' && rocmDownload && (
|
||||||
<Card withBorder radius="sm" padding="sm">
|
<Card withBorder radius="sm" padding="sm">
|
||||||
|
|
@ -497,7 +526,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed">
|
||||||
AMD GPU support with ROCm
|
Version {rocmDownload.version || 'latest'}
|
||||||
{rocmDownload.size && (
|
{rocmDownload.size && (
|
||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
|
|
@ -519,7 +548,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
loading={downloadingROCm}
|
loading={downloadingROCm}
|
||||||
disabled={downloading !== null || downloadingROCm}
|
disabled={downloading !== null || downloadingROCm}
|
||||||
leftSection={
|
leftSection={
|
||||||
<IconDownload
|
<Download
|
||||||
style={{ width: rem(14), height: rem(14) }}
|
style={{ width: rem(14), height: rem(14) }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
@ -556,14 +585,12 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
fullWidth
|
fullWidth
|
||||||
value={themeMode}
|
value={themeMode}
|
||||||
onChange={(value) =>
|
onChange={(value) => setThemeMode(value as ThemeMode)}
|
||||||
setThemeMode(value as 'light' | 'dark' | 'system')
|
|
||||||
}
|
|
||||||
data={[
|
data={[
|
||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<Group gap="xs" justify="center">
|
<Group gap="xs" justify="center">
|
||||||
<IconSun style={{ width: rem(16), height: rem(16) }} />
|
<Sun style={{ width: rem(16), height: rem(16) }} />
|
||||||
<span>Light</span>
|
<span>Light</span>
|
||||||
</Group>
|
</Group>
|
||||||
),
|
),
|
||||||
|
|
@ -572,7 +599,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<Group gap="xs" justify="center">
|
<Group gap="xs" justify="center">
|
||||||
<IconMoon style={{ width: rem(16), height: rem(16) }} />
|
<Moon style={{ width: rem(16), height: rem(16) }} />
|
||||||
<span>Dark</span>
|
<span>Dark</span>
|
||||||
</Group>
|
</Group>
|
||||||
),
|
),
|
||||||
|
|
@ -581,9 +608,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<Group gap="xs" justify="center">
|
<Group gap="xs" justify="center">
|
||||||
<IconDeviceDesktop
|
<Monitor style={{ width: rem(16), height: rem(16) }} />
|
||||||
style={{ width: rem(16), height: rem(16) }}
|
|
||||||
/>
|
|
||||||
<span>System</span>
|
<span>System</span>
|
||||||
</Group>
|
</Group>
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,5 @@
|
||||||
import { Modal, Text, Button, Group, Stack } from '@mantine/core';
|
import { Modal, Text, Button, Group, Stack } from '@mantine/core';
|
||||||
|
import type { GitHubRelease } from '@/types';
|
||||||
interface GitHubRelease {
|
|
||||||
tag_name: string;
|
|
||||||
name: string;
|
|
||||||
published_at: string;
|
|
||||||
body: string;
|
|
||||||
assets: GitHubAsset[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GitHubAsset {
|
|
||||||
name: string;
|
|
||||||
browser_download_url: string;
|
|
||||||
size: number;
|
|
||||||
created_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateDialogProps {
|
interface UpdateDialogProps {
|
||||||
updateInfo: {
|
updateInfo: {
|
||||||
|
|
|
||||||
36
src/constants/app.ts
Normal file
36
src/constants/app.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
export const APP_NAME = 'friendly-kobold';
|
||||||
|
|
||||||
|
export const CONFIG_FILE_NAME = 'config.json';
|
||||||
|
|
||||||
|
export const DIALOG_TITLES = {
|
||||||
|
SELECT_INSTALL_DIR: 'Select the Friendly Kobold Installation Directory',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const GITHUB_API = {
|
||||||
|
BASE_URL: 'https://api.github.com',
|
||||||
|
KOBOLDCPP_REPO: 'LostRuins/koboldcpp',
|
||||||
|
get LATEST_RELEASE_URL() {
|
||||||
|
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_REPO}/releases/latest`;
|
||||||
|
},
|
||||||
|
get ALL_RELEASES_URL() {
|
||||||
|
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_REPO}/releases`;
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const ASSET_SUFFIXES = {
|
||||||
|
ROCM: 'rocm',
|
||||||
|
NOCUDA: 'nocuda',
|
||||||
|
OLDPC: 'oldpc',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const KOBOLDAI_URLS = {
|
||||||
|
STANDARD_DOWNLOAD: 'https://koboldai.org/cpp',
|
||||||
|
ROCM_DOWNLOAD: 'https://koboldai.org/cpplinuxrocm',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const ROCM = {
|
||||||
|
BINARY_NAME: 'koboldcpp-linux-x64-rocm',
|
||||||
|
DOWNLOAD_URL: KOBOLDAI_URLS.ROCM_DOWNLOAD,
|
||||||
|
ERROR_MESSAGE: 'ROCm version is only available for Linux',
|
||||||
|
SIZE_BYTES: 1024 * 1024 * 1024, // 1GB
|
||||||
|
} as const;
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { MantineColorScheme } from '@mantine/core';
|
import { MantineColorScheme } from '@mantine/core';
|
||||||
|
|
||||||
type ThemeMode = 'light' | 'dark' | 'system';
|
export type ThemeMode = 'light' | 'dark' | 'system';
|
||||||
|
|
||||||
interface ThemeContextType {
|
interface ThemeContextType {
|
||||||
themeMode: ThemeMode;
|
themeMode: ThemeMode;
|
||||||
|
|
|
||||||
137
src/hooks/useLaunchConfig.ts
Normal file
137
src/hooks/useLaunchConfig.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import type { ConfigFile } from '@/types';
|
||||||
|
|
||||||
|
export const useLaunchConfig = () => {
|
||||||
|
const [serverOnly, setServerOnly] = useState<boolean>(false);
|
||||||
|
const [gpuLayers, setGpuLayers] = useState<number>(0);
|
||||||
|
const [autoGpuLayers, setAutoGpuLayers] = useState<boolean>(false);
|
||||||
|
const [contextSize, setContextSize] = useState<number>(2048);
|
||||||
|
const [modelPath, setModelPath] = useState<string>('');
|
||||||
|
const [additionalArguments, setAdditionalArguments] = useState<string>('');
|
||||||
|
|
||||||
|
const parseAndApplyConfigFile = useCallback(async (configPath: string) => {
|
||||||
|
const configData =
|
||||||
|
await window.electronAPI.kobold.parseConfigFile(configPath);
|
||||||
|
if (configData) {
|
||||||
|
if (typeof configData.gpulayers === 'number') {
|
||||||
|
setGpuLayers(configData.gpulayers);
|
||||||
|
} else {
|
||||||
|
setGpuLayers(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof configData.contextsize === 'number') {
|
||||||
|
setContextSize(configData.contextsize);
|
||||||
|
} else {
|
||||||
|
setContextSize(2048);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof configData.model_param === 'string') {
|
||||||
|
setModelPath(configData.model_param);
|
||||||
|
await window.electronAPI.config.setModelPath(configData.model_param);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setGpuLayers(0);
|
||||||
|
setContextSize(2048);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadSavedSettings = useCallback(async () => {
|
||||||
|
const [savedServerOnly, savedModelPath] = await Promise.all([
|
||||||
|
window.electronAPI.config.getServerOnly(),
|
||||||
|
window.electronAPI.config.getModelPath(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setServerOnly(savedServerOnly);
|
||||||
|
setModelPath(savedModelPath || '');
|
||||||
|
setGpuLayers(0);
|
||||||
|
setContextSize(2048);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadConfigFromFile = useCallback(
|
||||||
|
async (configFiles: ConfigFile[], savedConfig: string | null) => {
|
||||||
|
let currentSelectedFile = null;
|
||||||
|
|
||||||
|
if (savedConfig && configFiles.some((f) => f.name === savedConfig)) {
|
||||||
|
currentSelectedFile = savedConfig;
|
||||||
|
} else if (configFiles.length > 0) {
|
||||||
|
currentSelectedFile = configFiles[0].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSelectedFile) {
|
||||||
|
const selectedConfig = configFiles.find(
|
||||||
|
(f) => f.name === currentSelectedFile
|
||||||
|
);
|
||||||
|
if (selectedConfig) {
|
||||||
|
await parseAndApplyConfigFile(selectedConfig.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentSelectedFile;
|
||||||
|
},
|
||||||
|
[parseAndApplyConfigFile]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleServerOnlyChange = useCallback(async (checked: boolean) => {
|
||||||
|
setServerOnly(checked);
|
||||||
|
await window.electronAPI.config.setServerOnly(checked);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleGpuLayersChange = useCallback(async (value: number) => {
|
||||||
|
setGpuLayers(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const roundToValidContextSize = useCallback((value: number): number => {
|
||||||
|
if (value < 1024) {
|
||||||
|
return Math.round(value / 256) * 256;
|
||||||
|
}
|
||||||
|
return Math.round(value / 1024) * 1024;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleContextSizeChangeWithStep = useCallback(
|
||||||
|
async (value: number) => {
|
||||||
|
const roundedValue = roundToValidContextSize(value);
|
||||||
|
setContextSize(roundedValue);
|
||||||
|
},
|
||||||
|
[roundToValidContextSize]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleModelPathChange = useCallback(async (value: string) => {
|
||||||
|
setModelPath(value);
|
||||||
|
await window.electronAPI.config.setModelPath(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSelectModelFile = useCallback(async () => {
|
||||||
|
const filePath = await window.electronAPI.kobold.selectModelFile();
|
||||||
|
if (filePath) {
|
||||||
|
await handleModelPathChange(filePath);
|
||||||
|
}
|
||||||
|
}, [handleModelPathChange]);
|
||||||
|
|
||||||
|
const handleAdditionalArgumentsChange = useCallback((value: string) => {
|
||||||
|
setAdditionalArguments(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAutoGpuLayersChange = useCallback((checked: boolean) => {
|
||||||
|
setAutoGpuLayers(checked);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
serverOnly,
|
||||||
|
gpuLayers,
|
||||||
|
autoGpuLayers,
|
||||||
|
contextSize,
|
||||||
|
modelPath,
|
||||||
|
additionalArguments,
|
||||||
|
|
||||||
|
parseAndApplyConfigFile,
|
||||||
|
loadSavedSettings,
|
||||||
|
loadConfigFromFile,
|
||||||
|
handleServerOnlyChange,
|
||||||
|
handleGpuLayersChange,
|
||||||
|
handleAutoGpuLayersChange,
|
||||||
|
handleContextSizeChangeWithStep,
|
||||||
|
handleModelPathChange,
|
||||||
|
handleSelectModelFile,
|
||||||
|
handleAdditionalArgumentsChange,
|
||||||
|
};
|
||||||
|
};
|
||||||
91
src/hooks/useNotifications.ts
Normal file
91
src/hooks/useNotifications.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { createElement, type ReactNode } from 'react';
|
||||||
|
import { notifications } from '@mantine/notifications';
|
||||||
|
import { Check, X, Info, AlertTriangle } from 'lucide-react';
|
||||||
|
|
||||||
|
export const useNotifications = () => {
|
||||||
|
const success = (title: string, message?: string) => {
|
||||||
|
notifications.show({
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
color: 'green',
|
||||||
|
icon: createElement(Check, { size: 18 }),
|
||||||
|
position: 'top-right',
|
||||||
|
autoClose: 5000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const error = (title: string, message?: string) => {
|
||||||
|
notifications.show({
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
color: 'red',
|
||||||
|
icon: createElement(X, { size: 18 }),
|
||||||
|
position: 'top-right',
|
||||||
|
autoClose: 7000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const info = (title: string, message?: string) => {
|
||||||
|
notifications.show({
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
color: 'blue',
|
||||||
|
icon: createElement(Info, { size: 18 }),
|
||||||
|
position: 'top-right',
|
||||||
|
autoClose: 5000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const warning = (title: string, message?: string) => {
|
||||||
|
notifications.show({
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
color: 'yellow',
|
||||||
|
icon: createElement(AlertTriangle, { size: 18 }),
|
||||||
|
position: 'top-right',
|
||||||
|
autoClose: 6000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const custom = (options: {
|
||||||
|
title: string;
|
||||||
|
message?: string;
|
||||||
|
color?: string;
|
||||||
|
icon?: ReactNode;
|
||||||
|
autoClose?: number | false;
|
||||||
|
position?:
|
||||||
|
| 'top-left'
|
||||||
|
| 'top-right'
|
||||||
|
| 'top-center'
|
||||||
|
| 'bottom-left'
|
||||||
|
| 'bottom-right'
|
||||||
|
| 'bottom-center';
|
||||||
|
}) => {
|
||||||
|
const { title, message = '', ...rest } = options;
|
||||||
|
notifications.show({
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
position: 'top-right',
|
||||||
|
autoClose: 5000,
|
||||||
|
...rest,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const hide = (id: string) => {
|
||||||
|
notifications.hide(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clean = () => {
|
||||||
|
notifications.clean();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
info,
|
||||||
|
warning,
|
||||||
|
custom,
|
||||||
|
hide,
|
||||||
|
clean,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1 @@
|
||||||
@import '@mantine/core/styles.css';
|
@import '@mantine/core/styles.css';
|
||||||
|
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,13 @@ import { join } from 'path';
|
||||||
import { existsSync, mkdirSync } from 'fs';
|
import { existsSync, mkdirSync } from 'fs';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
|
|
||||||
import { WindowManager } from './managers/WindowManager';
|
import { WindowManager } from '@/main/managers/WindowManager';
|
||||||
import { ConfigManager } from './managers/ConfigManager';
|
import { ConfigManager } from '@/main/managers/ConfigManager';
|
||||||
import { KoboldCppManager } from './managers/KoboldCppManager';
|
import { KoboldCppManager } from '@/main/managers/KoboldCppManager';
|
||||||
import { GitHubService } from './services/GitHubService';
|
import { GitHubService } from '@/main/services/GitHubService';
|
||||||
import { GPUService } from './services/GPUService';
|
import { GPUService } from '@/main/services/GPUService';
|
||||||
import { IPCHandlers } from './utils/IPCHandlers';
|
import { IPCHandlers } from '@/main/utils/IPCHandlers';
|
||||||
|
import { APP_NAME, CONFIG_FILE_NAME } from '@/constants/app';
|
||||||
|
|
||||||
class FriendlyKoboldApp {
|
class FriendlyKoboldApp {
|
||||||
private windowManager: WindowManager;
|
private windowManager: WindowManager;
|
||||||
|
|
@ -25,7 +26,8 @@ class FriendlyKoboldApp {
|
||||||
this.gpuService = new GPUService();
|
this.gpuService = new GPUService();
|
||||||
this.koboldManager = new KoboldCppManager(
|
this.koboldManager = new KoboldCppManager(
|
||||||
this.configManager,
|
this.configManager,
|
||||||
this.githubService
|
this.githubService,
|
||||||
|
this.windowManager
|
||||||
);
|
);
|
||||||
this.ipcHandlers = new IPCHandlers(
|
this.ipcHandlers = new IPCHandlers(
|
||||||
this.koboldManager,
|
this.koboldManager,
|
||||||
|
|
@ -38,7 +40,7 @@ class FriendlyKoboldApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getConfigPath() {
|
private getConfigPath() {
|
||||||
return join(app.getPath('userData'), 'config.json');
|
return join(app.getPath('userData'), CONFIG_FILE_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDefaultInstallPath() {
|
private getDefaultInstallPath() {
|
||||||
|
|
@ -47,11 +49,11 @@ class FriendlyKoboldApp {
|
||||||
|
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
case 'win32':
|
case 'win32':
|
||||||
return join(home, 'FriendlyKobold');
|
return join(home, APP_NAME);
|
||||||
case 'darwin':
|
case 'darwin':
|
||||||
return join(home, 'Applications', 'FriendlyKobold');
|
return join(home, 'Applications', APP_NAME);
|
||||||
default:
|
default:
|
||||||
return join(home, '.local', 'share', 'friendly-kobold');
|
return join(home, '.local', 'share', APP_NAME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,11 +78,25 @@ class FriendlyKoboldApp {
|
||||||
this.ipcHandlers.setupHandlers();
|
this.ipcHandlers.setupHandlers();
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
app.quit();
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.on('before-quit', async (event) => {
|
||||||
|
// Prevent immediate quit to allow cleanup
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Clean up KoboldCpp process
|
||||||
|
await this.koboldManager.cleanup();
|
||||||
|
|
||||||
|
// Clean up window manager
|
||||||
|
this.windowManager.cleanup();
|
||||||
|
|
||||||
|
// Now actually quit
|
||||||
|
app.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
if (!this.windowManager.getMainWindow()) {
|
if (!this.windowManager.getMainWindow()) {
|
||||||
this.windowManager.createMainWindow();
|
this.windowManager.createMainWindow();
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ type ConfigValue = string | number | boolean | unknown[] | undefined;
|
||||||
|
|
||||||
interface AppConfig {
|
interface AppConfig {
|
||||||
installDir?: string;
|
installDir?: string;
|
||||||
currentVersion?: string;
|
currentKoboldBinary?: string;
|
||||||
selectedConfig?: string;
|
selectedConfig?: string;
|
||||||
|
serverOnly?: boolean;
|
||||||
|
modelPath?: string;
|
||||||
[key: string]: ConfigValue;
|
[key: string]: ConfigValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,12 +57,12 @@ export class ConfigManager {
|
||||||
this.saveConfig();
|
this.saveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentVersion(): string | undefined {
|
getCurrentKoboldBinary(): string | undefined {
|
||||||
return this.config.currentVersion;
|
return this.config.currentKoboldBinary as string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentVersion(version: string) {
|
setCurrentKoboldBinary(binaryPath: string) {
|
||||||
this.config.currentVersion = version;
|
this.config.currentKoboldBinary = binaryPath;
|
||||||
this.saveConfig();
|
this.saveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,4 +74,22 @@ export class ConfigManager {
|
||||||
this.config.selectedConfig = configName;
|
this.config.selectedConfig = configName;
|
||||||
this.saveConfig();
|
this.saveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getServerOnly(): boolean {
|
||||||
|
return this.config.serverOnly || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setServerOnly(serverOnly: boolean) {
|
||||||
|
this.config.serverOnly = serverOnly;
|
||||||
|
this.saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
getModelPath(): string | undefined {
|
||||||
|
return this.config.modelPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
setModelPath(path: string) {
|
||||||
|
this.config.modelPath = path;
|
||||||
|
this.saveConfig();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,18 @@
|
||||||
import { spawn, ChildProcess } from 'child_process';
|
import { spawn, ChildProcess } from 'child_process';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { existsSync, readdirSync, statSync, createWriteStream } from 'fs';
|
import {
|
||||||
|
existsSync,
|
||||||
|
readdirSync,
|
||||||
|
statSync,
|
||||||
|
createWriteStream,
|
||||||
|
chmodSync,
|
||||||
|
readFileSync,
|
||||||
|
} from 'fs';
|
||||||
import { dialog } from 'electron';
|
import { dialog } from 'electron';
|
||||||
import { GitHubService } from '../services/GitHubService';
|
import { GitHubService } from '@/main/services/GitHubService';
|
||||||
import { ConfigManager } from './ConfigManager';
|
import { ConfigManager } from '@/main/managers/ConfigManager';
|
||||||
|
import { WindowManager } from '@/main/managers/WindowManager';
|
||||||
|
import { APP_NAME, DIALOG_TITLES, ROCM } from '@/constants/app';
|
||||||
|
|
||||||
interface GitHubAsset {
|
interface GitHubAsset {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -39,7 +48,6 @@ interface ReleaseWithStatus {
|
||||||
export interface InstalledVersion {
|
export interface InstalledVersion {
|
||||||
version: string;
|
version: string;
|
||||||
path: string;
|
path: string;
|
||||||
type: 'github' | 'rocm';
|
|
||||||
downloadDate: string;
|
downloadDate: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
}
|
}
|
||||||
|
|
@ -49,13 +57,19 @@ export class KoboldCppManager {
|
||||||
private koboldProcess: ChildProcess | null = null;
|
private koboldProcess: ChildProcess | null = null;
|
||||||
private configManager: ConfigManager;
|
private configManager: ConfigManager;
|
||||||
private githubService: GitHubService;
|
private githubService: GitHubService;
|
||||||
|
private windowManager: WindowManager;
|
||||||
|
|
||||||
constructor(configManager: ConfigManager, githubService: GitHubService) {
|
constructor(
|
||||||
|
configManager: ConfigManager,
|
||||||
|
githubService: GitHubService,
|
||||||
|
windowManager: WindowManager
|
||||||
|
) {
|
||||||
this.configManager = configManager;
|
this.configManager = configManager;
|
||||||
this.githubService = githubService;
|
this.githubService = githubService;
|
||||||
|
this.windowManager = windowManager;
|
||||||
this.installDir =
|
this.installDir =
|
||||||
this.configManager.getInstallDir() ||
|
this.configManager.getInstallDir() ||
|
||||||
join(process.env.HOME || process.env.USERPROFILE || '.', 'KoboldCpp');
|
join(process.env.HOME || process.env.USERPROFILE || '.', APP_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadRelease(
|
async downloadRelease(
|
||||||
|
|
@ -105,14 +119,23 @@ export class KoboldCppManager {
|
||||||
reader.releaseLock();
|
reader.releaseLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
try {
|
||||||
|
chmodSync(filePath, 0o755);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to make binary executable:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentBinary = this.configManager.getCurrentKoboldBinary();
|
||||||
|
if (!currentBinary) {
|
||||||
|
this.configManager.setCurrentKoboldBinary(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInstalledVersions(): Promise<InstalledVersion[]> {
|
async getInstalledVersions(): Promise<InstalledVersion[]> {
|
||||||
const configData = this.configManager.get('installedVersions');
|
|
||||||
const configVersions: InstalledVersion[] = Array.isArray(configData)
|
|
||||||
? (configData as unknown as InstalledVersion[])
|
|
||||||
: [];
|
|
||||||
const scannedVersions: InstalledVersion[] = [];
|
const scannedVersions: InstalledVersion[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -126,48 +149,26 @@ export class KoboldCppManager {
|
||||||
statSync(filePath).isFile() &&
|
statSync(filePath).isFile() &&
|
||||||
(file.includes('koboldcpp') || file.includes('kobold'))
|
(file.includes('koboldcpp') || file.includes('kobold'))
|
||||||
) {
|
) {
|
||||||
const existingVersion = configVersions.find(
|
try {
|
||||||
(v: InstalledVersion) => v.path === filePath
|
const detectedVersion = await this.getVersionFromBinary(filePath);
|
||||||
);
|
const version = detectedVersion || 'unknown';
|
||||||
|
|
||||||
if (existingVersion) {
|
const newVersion: InstalledVersion = {
|
||||||
scannedVersions.push(existingVersion);
|
version,
|
||||||
} else {
|
path: filePath,
|
||||||
try {
|
downloadDate: new Date().toISOString(),
|
||||||
const detectedVersion =
|
filename: file,
|
||||||
await this.getVersionFromBinary(filePath);
|
};
|
||||||
const version = detectedVersion || 'unknown';
|
|
||||||
|
|
||||||
const newVersion: InstalledVersion = {
|
scannedVersions.push(newVersion);
|
||||||
version,
|
} catch (error) {
|
||||||
path: filePath,
|
console.warn(`Could not detect version for ${file}:`, error);
|
||||||
type: 'github',
|
|
||||||
downloadDate: new Date().toISOString(),
|
|
||||||
filename: file,
|
|
||||||
};
|
|
||||||
|
|
||||||
scannedVersions.push(newVersion);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Could not detect version for ${file}:`, error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Error scanning install directory:', error);
|
console.warn('Error scanning install directory:', error);
|
||||||
return configVersions.filter((version: InstalledVersion) =>
|
|
||||||
existsSync(version.path)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
scannedVersions.length !== configVersions.length ||
|
|
||||||
!scannedVersions.every((sv) =>
|
|
||||||
configVersions.some((cv: InstalledVersion) => cv.path === sv.path)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this.configManager.set('installedVersions', scannedVersions as unknown[]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return scannedVersions;
|
return scannedVersions;
|
||||||
|
|
@ -210,60 +211,87 @@ export class KoboldCppManager {
|
||||||
return configFiles.sort((a, b) => a.name.localeCompare(b.name));
|
return configFiles.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async parseConfigFile(filePath: string): Promise<{
|
||||||
|
gpulayers?: number;
|
||||||
|
contextsize?: number;
|
||||||
|
model_param?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
} | null> {
|
||||||
|
try {
|
||||||
|
if (!existsSync(filePath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = readFileSync(filePath, 'utf-8');
|
||||||
|
const config = JSON.parse(content);
|
||||||
|
|
||||||
|
return config;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error parsing config file:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectModelFile(): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const mainWindow = this.windowManager.getMainWindow();
|
||||||
|
if (!mainWindow) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
|
title: 'Select Model File',
|
||||||
|
filters: [
|
||||||
|
{ name: 'GGUF Files', extensions: ['gguf'] },
|
||||||
|
{ name: 'All Files', extensions: ['*'] },
|
||||||
|
],
|
||||||
|
properties: ['openFile'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.canceled || result.filePaths.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.filePaths[0];
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error selecting model file:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getCurrentVersion(): Promise<InstalledVersion | null> {
|
async getCurrentVersion(): Promise<InstalledVersion | null> {
|
||||||
const versions = await this.getInstalledVersions();
|
const versions = await this.getInstalledVersions();
|
||||||
const currentVersionString = this.configManager.getCurrentVersion();
|
const currentBinaryPath = this.configManager.getCurrentKoboldBinary();
|
||||||
|
|
||||||
if (currentVersionString) {
|
if (currentBinaryPath) {
|
||||||
const found =
|
const found = versions.find((v) => v.path === currentBinaryPath);
|
||||||
versions.find((v) => v.version === currentVersionString) || null;
|
if (found && existsSync(found.path)) {
|
||||||
return found;
|
return found;
|
||||||
|
}
|
||||||
|
// If the current binary no longer exists, clear it
|
||||||
|
this.configManager.setCurrentKoboldBinary('');
|
||||||
}
|
}
|
||||||
|
|
||||||
const fallback =
|
// If no current binary is set, return the most recent one
|
||||||
|
return (
|
||||||
versions.sort(
|
versions.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
new Date(b.downloadDate).getTime() -
|
new Date(b.downloadDate).getTime() -
|
||||||
new Date(a.downloadDate).getTime()
|
new Date(a.downloadDate).getTime()
|
||||||
)[0] || null;
|
)[0] || null
|
||||||
return fallback;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setCurrentVersion(version: string): Promise<boolean> {
|
async setCurrentVersion(version: string): Promise<boolean> {
|
||||||
const versions = await this.getInstalledVersions();
|
const versions = await this.getInstalledVersions();
|
||||||
const targetVersion = versions.find((v) => v.version === version);
|
const targetVersion = versions.find((v) => v.version === version);
|
||||||
|
|
||||||
if (!targetVersion || !existsSync(targetVersion.path)) {
|
if (targetVersion && existsSync(targetVersion.path)) {
|
||||||
if (targetVersion) {
|
this.configManager.setCurrentKoboldBinary(targetVersion.path);
|
||||||
const updatedVersions = versions.filter((v) => v.version !== version);
|
return true;
|
||||||
this.configManager.set(
|
|
||||||
'installedVersions',
|
|
||||||
updatedVersions as unknown[]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.configManager.setCurrentVersion(version);
|
return false;
|
||||||
|
|
||||||
const installedVersionsData = this.configManager.get('installedVersions');
|
|
||||||
const installedVersions: InstalledVersion[] = Array.isArray(
|
|
||||||
installedVersionsData
|
|
||||||
)
|
|
||||||
? (installedVersionsData as unknown as InstalledVersion[])
|
|
||||||
: [];
|
|
||||||
const versionToUpdate = installedVersions.find(
|
|
||||||
(v: InstalledVersion) => v.version === version
|
|
||||||
);
|
|
||||||
|
|
||||||
if (versionToUpdate) {
|
|
||||||
this.configManager.set(
|
|
||||||
'installedVersions',
|
|
||||||
installedVersions as unknown[]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getVersionFromBinary(binaryPath: string): Promise<string | null> {
|
async getVersionFromBinary(binaryPath: string): Promise<string | null> {
|
||||||
|
|
@ -338,48 +366,33 @@ export class KoboldCppManager {
|
||||||
return this.installDir;
|
return this.installDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getWindowManager() {
|
||||||
|
return this.windowManager;
|
||||||
|
}
|
||||||
|
|
||||||
async selectInstallDirectory(): Promise<string | null> {
|
async selectInstallDirectory(): Promise<string | null> {
|
||||||
const result = await dialog.showOpenDialog({
|
const result = await dialog.showOpenDialog({
|
||||||
properties: ['openDirectory', 'createDirectory'],
|
properties: ['openDirectory', 'createDirectory'],
|
||||||
title: 'Select KoboldCpp Installation Directory',
|
title: DIALOG_TITLES.SELECT_INSTALL_DIR,
|
||||||
defaultPath: this.installDir,
|
defaultPath: this.installDir,
|
||||||
|
buttonLabel: 'Select Directory',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.canceled && result.filePaths.length > 0) {
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
const newPath = join(result.filePaths[0], 'KoboldCpp');
|
this.installDir = result.filePaths[0];
|
||||||
this.installDir = newPath;
|
this.configManager.setInstallDir(result.filePaths[0]);
|
||||||
this.configManager.setInstallDir(newPath);
|
|
||||||
return newPath;
|
const mainWindow = this.windowManager.getMainWindow();
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.webContents.send('install-dir-changed', result.filePaths[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.filePaths[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addInstalledVersion(
|
|
||||||
version: string,
|
|
||||||
path: string,
|
|
||||||
type: 'github' | 'rocm' = 'github'
|
|
||||||
) {
|
|
||||||
const versions = await this.getInstalledVersions();
|
|
||||||
|
|
||||||
const filteredVersions = versions.filter((v) => v.version !== version);
|
|
||||||
const filename = path.split(/[/\\]/).pop() || 'unknown';
|
|
||||||
|
|
||||||
const newVersion: InstalledVersion = {
|
|
||||||
version,
|
|
||||||
path,
|
|
||||||
type,
|
|
||||||
downloadDate: new Date().toISOString(),
|
|
||||||
filename,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.configManager.set('installedVersions', [
|
|
||||||
...filteredVersions,
|
|
||||||
newVersion,
|
|
||||||
] as unknown[]);
|
|
||||||
this.configManager.setCurrentVersion(version);
|
|
||||||
}
|
|
||||||
|
|
||||||
async launchKobold(
|
async launchKobold(
|
||||||
versionPath: string,
|
versionPath: string,
|
||||||
args: string[] = [],
|
args: string[] = [],
|
||||||
|
|
@ -439,7 +452,6 @@ export class KoboldCppManager {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
size: number;
|
size: number;
|
||||||
type: 'rocm';
|
|
||||||
version?: string;
|
version?: string;
|
||||||
} | null> {
|
} | null> {
|
||||||
const platform = process.platform;
|
const platform = process.platform;
|
||||||
|
|
@ -451,15 +463,14 @@ export class KoboldCppManager {
|
||||||
const version = latestRelease?.tag_name?.replace(/^v/, '') || 'unknown';
|
const version = latestRelease?.tag_name?.replace(/^v/, '') || 'unknown';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'koboldcpp-linux-x64-rocm',
|
name: ROCM.BINARY_NAME,
|
||||||
url: 'https://koboldai.org/cpplinuxrocm',
|
url: ROCM.DOWNLOAD_URL,
|
||||||
size: 1024 * 1024 * 1024,
|
size: ROCM.SIZE_BYTES,
|
||||||
type: 'rocm',
|
|
||||||
version,
|
version,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadROCm(): Promise<{
|
async downloadROCm(onProgress?: (progress: number) => void): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
path?: string;
|
path?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
|
@ -469,11 +480,11 @@ export class KoboldCppManager {
|
||||||
if (platform !== 'linux') {
|
if (platform !== 'linux') {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'ROCm version is only available for Linux',
|
error: ROCM.ERROR_MESSAGE,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch('https://koboldai.org/cpplinuxrocm');
|
const response = await fetch(ROCM.DOWNLOAD_URL);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
|
@ -481,24 +492,52 @@ export class KoboldCppManager {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath = join(this.installDir, 'koboldcpp-linux-x64-rocm');
|
const totalBytes = ROCM.SIZE_BYTES;
|
||||||
|
let downloadedBytes = 0;
|
||||||
|
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
if (!reader) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to get response reader',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = join(this.installDir, ROCM.BINARY_NAME);
|
||||||
const writer = createWriteStream(filePath);
|
const writer = createWriteStream(filePath);
|
||||||
|
|
||||||
response.body?.pipeTo(
|
try {
|
||||||
new WritableStream({
|
while (true) {
|
||||||
write(chunk) {
|
const { done, value } = await reader.read();
|
||||||
writer.write(chunk);
|
|
||||||
},
|
|
||||||
close() {
|
|
||||||
writer.end();
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
if (done) break;
|
||||||
writer.on('finish', resolve);
|
|
||||||
writer.on('error', reject);
|
downloadedBytes += value.length;
|
||||||
});
|
writer.write(value);
|
||||||
|
|
||||||
|
if (onProgress && totalBytes > 0) {
|
||||||
|
onProgress((downloadedBytes / totalBytes) * 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.end();
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
writer.on('finish', resolve);
|
||||||
|
writer.on('error', reject);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
reader.releaseLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the binary executable on Unix-like systems (Linux/macOS)
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
try {
|
||||||
|
chmodSync(filePath, 0o755);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to make ROCm binary executable:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -589,65 +628,122 @@ export class KoboldCppManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async openInstallDialog(): Promise<{
|
|
||||||
success: boolean;
|
|
||||||
version?: string;
|
|
||||||
path?: string;
|
|
||||||
error?: string;
|
|
||||||
}> {
|
|
||||||
try {
|
|
||||||
const result = await dialog.showOpenDialog({
|
|
||||||
title: 'Select KoboldCpp executable',
|
|
||||||
filters: [
|
|
||||||
{ name: 'Executables', extensions: ['exe', 'app', 'AppImage'] },
|
|
||||||
{ name: 'All Files', extensions: ['*'] },
|
|
||||||
],
|
|
||||||
properties: ['openFile'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.canceled && result.filePaths.length > 0) {
|
|
||||||
const filePath = result.filePaths[0];
|
|
||||||
const detectedVersion = await this.getVersionFromBinary(filePath);
|
|
||||||
const version = detectedVersion || 'unknown';
|
|
||||||
|
|
||||||
await this.addInstalledVersion(version, filePath);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
version,
|
|
||||||
path: filePath,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: false, error: 'No file selected' };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: (error as Error).message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async launchKoboldCpp(
|
async launchKoboldCpp(
|
||||||
args: string[] = []
|
args: string[] = [],
|
||||||
|
configFilePath?: string
|
||||||
): Promise<{ success: boolean; pid?: number; error?: string }> {
|
): Promise<{ success: boolean; pid?: number; error?: string }> {
|
||||||
try {
|
try {
|
||||||
|
if (this.koboldProcess) {
|
||||||
|
this.stopKoboldCpp();
|
||||||
|
}
|
||||||
|
|
||||||
const currentVersion = await this.getCurrentVersion();
|
const currentVersion = await this.getCurrentVersion();
|
||||||
if (!currentVersion || !existsSync(currentVersion.path)) {
|
if (!currentVersion || !existsSync(currentVersion.path)) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'KoboldCpp not found or no version selected',
|
error: 'KoboldCpp not found',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const child = spawn(currentVersion.path, args, {
|
const finalArgs = [...args]; // Start with the provided arguments
|
||||||
detached: true,
|
|
||||||
stdio: 'ignore',
|
if (configFilePath && existsSync(configFilePath)) {
|
||||||
|
finalArgs.push('--config', configFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const child = spawn(currentVersion.path, finalArgs, {
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
detached: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.setCurrentVersion(currentVersion.version);
|
this.koboldProcess = child;
|
||||||
child.unref();
|
|
||||||
|
const mainWindow = this.windowManager.getMainWindow();
|
||||||
|
if (mainWindow) {
|
||||||
|
child.stdout?.on('data', (data) => {
|
||||||
|
const output = data.toString();
|
||||||
|
mainWindow.webContents.send('kobold-output', output);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr?.on('data', (data) => {
|
||||||
|
const output = data.toString();
|
||||||
|
mainWindow.webContents.send('kobold-output', output);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('exit', (code, signal) => {
|
||||||
|
const exitMessage = signal
|
||||||
|
? `\nProcess terminated with signal ${signal}\n`
|
||||||
|
: `\nProcess exited with code ${code}\n`;
|
||||||
|
mainWindow.webContents.send('kobold-output', exitMessage);
|
||||||
|
this.koboldProcess = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (error) => {
|
||||||
|
mainWindow.webContents.send(
|
||||||
|
'kobold-output',
|
||||||
|
`\nProcess error: ${error.message}\n`
|
||||||
|
);
|
||||||
|
this.koboldProcess = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true, pid: child.pid };
|
return { success: true, pid: child.pid };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: (error as Error).message };
|
return { success: false, error: (error as Error).message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stopKoboldCpp(): void {
|
||||||
|
if (this.koboldProcess) {
|
||||||
|
try {
|
||||||
|
// Try graceful termination first
|
||||||
|
this.koboldProcess.kill('SIGTERM');
|
||||||
|
|
||||||
|
// Force kill after 5 seconds if still running
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.koboldProcess && !this.koboldProcess.killed) {
|
||||||
|
this.koboldProcess.kill('SIGKILL');
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
this.koboldProcess = null;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error stopping KoboldCpp process:', error);
|
||||||
|
this.koboldProcess = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to handle app termination - ensures process cleanup
|
||||||
|
async cleanup(): Promise<void> {
|
||||||
|
if (this.koboldProcess) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (!this.koboldProcess) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up cleanup timeout
|
||||||
|
const cleanup = () => {
|
||||||
|
this.koboldProcess = null;
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for process exit
|
||||||
|
this.koboldProcess.once('exit', cleanup);
|
||||||
|
this.koboldProcess.once('error', cleanup);
|
||||||
|
|
||||||
|
// Try graceful shutdown
|
||||||
|
this.koboldProcess.kill('SIGTERM');
|
||||||
|
|
||||||
|
// Force kill after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.koboldProcess && !this.koboldProcess.killed) {
|
||||||
|
this.koboldProcess.kill('SIGKILL');
|
||||||
|
}
|
||||||
|
cleanup();
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { BrowserWindow, app, Menu, shell } from 'electron';
|
import { BrowserWindow, app, Menu, shell, Tray, nativeImage } from 'electron';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
export class WindowManager {
|
export class WindowManager {
|
||||||
private mainWindow: BrowserWindow | null = null;
|
private mainWindow: BrowserWindow | null = null;
|
||||||
|
private tray: Tray | null = null;
|
||||||
|
private isQuitting = false;
|
||||||
|
|
||||||
createMainWindow(): BrowserWindow {
|
createMainWindow(): BrowserWindow {
|
||||||
this.mainWindow = new BrowserWindow({
|
this.mainWindow = new BrowserWindow({
|
||||||
|
|
@ -28,6 +30,14 @@ export class WindowManager {
|
||||||
this.mainWindow = null;
|
this.mainWindow = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.mainWindow.on('close', (event) => {
|
||||||
|
if (this.tray && !this.isQuitting) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.mainWindow?.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.createSystemTray();
|
||||||
this.setupContextMenu();
|
this.setupContextMenu();
|
||||||
return this.mainWindow;
|
return this.mainWindow;
|
||||||
}
|
}
|
||||||
|
|
@ -36,10 +46,60 @@ export class WindowManager {
|
||||||
return this.mainWindow;
|
return this.mainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createSystemTray() {
|
||||||
|
// Create system tray icon
|
||||||
|
const iconPath = join(process.cwd(), 'assets', 'icon.png');
|
||||||
|
this.tray = new Tray(nativeImage.createFromPath(iconPath));
|
||||||
|
|
||||||
|
this.tray.setToolTip('Friendly Kobold');
|
||||||
|
|
||||||
|
// Create context menu for tray
|
||||||
|
const trayMenu = Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: 'Show',
|
||||||
|
click: () => {
|
||||||
|
this.mainWindow?.show();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Hide',
|
||||||
|
click: () => {
|
||||||
|
this.mainWindow?.hide();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Quit',
|
||||||
|
click: () => {
|
||||||
|
this.isQuitting = true;
|
||||||
|
app.quit();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.tray.setContextMenu(trayMenu);
|
||||||
|
|
||||||
|
// Double-click to show/hide window
|
||||||
|
this.tray.on('double-click', () => {
|
||||||
|
if (this.mainWindow?.isVisible()) {
|
||||||
|
this.mainWindow.hide();
|
||||||
|
} else {
|
||||||
|
this.mainWindow?.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public cleanup() {
|
||||||
|
this.tray?.destroy();
|
||||||
|
this.tray = null;
|
||||||
|
}
|
||||||
|
|
||||||
private setupContextMenu() {
|
private setupContextMenu() {
|
||||||
if (!this.mainWindow) return;
|
if (!this.mainWindow) return;
|
||||||
|
|
||||||
this.mainWindow.webContents.on('context-menu', (_event, params) => {
|
this.mainWindow.webContents.on('context-menu', (_event, params) => {
|
||||||
|
const hasLinkURL = !!params.linkURL;
|
||||||
|
|
||||||
const menu = Menu.buildFromTemplate([
|
const menu = Menu.buildFromTemplate([
|
||||||
{
|
{
|
||||||
label: 'Inspect Element',
|
label: 'Inspect Element',
|
||||||
|
|
@ -53,10 +113,10 @@ export class WindowManager {
|
||||||
{ label: 'Paste', role: 'paste' },
|
{ label: 'Paste', role: 'paste' },
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ label: 'Select All', role: 'selectAll' },
|
{ label: 'Select All', role: 'selectAll' },
|
||||||
{ type: 'separator' },
|
...(hasLinkURL ? [{ type: 'separator' as const }] : []),
|
||||||
{
|
{
|
||||||
label: 'Open Link in Browser',
|
label: 'Open Link in Browser',
|
||||||
visible: !!params.linkURL,
|
visible: hasLinkURL,
|
||||||
click: () => {
|
click: () => {
|
||||||
if (params.linkURL) {
|
if (params.linkURL) {
|
||||||
shell.openExternal(params.linkURL);
|
shell.openExternal(params.linkURL);
|
||||||
|
|
@ -78,6 +138,7 @@ export class WindowManager {
|
||||||
label: 'Quit',
|
label: 'Quit',
|
||||||
accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q',
|
accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q',
|
||||||
click: () => {
|
click: () => {
|
||||||
|
this.isQuitting = true;
|
||||||
app.quit();
|
app.quit();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { GitHubRelease } from '../../types/electron';
|
import type { GitHubRelease } from '@/types/electron';
|
||||||
|
import { GITHUB_API } from '@/constants/app';
|
||||||
|
|
||||||
export class GitHubService {
|
export class GitHubService {
|
||||||
private lastApiCall = 0;
|
private lastApiCall = 0;
|
||||||
|
|
@ -13,16 +14,14 @@ export class GitHubService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(GITHUB_API.LATEST_RELEASE_URL);
|
||||||
'https://api.github.com/repos/LostRuins/koboldcpp/releases/latest'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'GitHub API rate limit reached, using cached data or fallback'
|
'GitHub API rate limit reached, using cached data if available'
|
||||||
);
|
);
|
||||||
return this.cachedRelease || this.getFallbackRelease();
|
return this.cachedRelease;
|
||||||
}
|
}
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
@ -32,7 +31,7 @@ export class GitHubService {
|
||||||
return this.cachedRelease;
|
return this.cachedRelease;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching latest release:', error);
|
console.error('Error fetching latest release:', error);
|
||||||
return this.cachedRelease || this.getFallbackRelease();
|
return this.cachedRelease;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,16 +46,14 @@ export class GitHubService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(GITHUB_API.ALL_RELEASES_URL);
|
||||||
'https://api.github.com/repos/LostRuins/koboldcpp/releases'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
console.warn('GitHub API rate limit reached, using cached data');
|
console.warn(
|
||||||
return this.cachedReleases.length > 0
|
'GitHub API rate limit reached, using cached data if available'
|
||||||
? this.cachedReleases
|
);
|
||||||
: [this.getFallbackRelease()];
|
return this.cachedReleases;
|
||||||
}
|
}
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
@ -66,32 +63,7 @@ export class GitHubService {
|
||||||
return this.cachedReleases;
|
return this.cachedReleases;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching releases:', error);
|
console.error('Error fetching releases:', error);
|
||||||
return this.cachedReleases.length > 0
|
return this.cachedReleases;
|
||||||
? this.cachedReleases
|
|
||||||
: [this.getFallbackRelease()];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getFallbackRelease(): GitHubRelease {
|
|
||||||
return {
|
|
||||||
tag_name: 'v1.70.1',
|
|
||||||
name: 'KoboldCpp v1.70.1 (Fallback)',
|
|
||||||
published_at: new Date().toISOString(),
|
|
||||||
body: 'Fallback release data - GitHub API unavailable',
|
|
||||||
assets: [
|
|
||||||
{
|
|
||||||
name: 'koboldcpp-linux-x64',
|
|
||||||
browser_download_url: 'https://koboldai.org/cpp',
|
|
||||||
size: 50000000,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'koboldcpp-linux-x64-rocm',
|
|
||||||
browser_download_url: 'https://koboldai.org/cpplinuxrocm',
|
|
||||||
size: 80000000,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { ipcMain } from 'electron';
|
import { ipcMain, dialog } from 'electron';
|
||||||
import { shell, app } from 'electron';
|
import { shell, app } from 'electron';
|
||||||
import { KoboldCppManager } from '../managers/KoboldCppManager';
|
import { KoboldCppManager } from '@/main/managers/KoboldCppManager';
|
||||||
import { ConfigManager } from '../managers/ConfigManager';
|
import { ConfigManager } from '@/main/managers/ConfigManager';
|
||||||
import { GitHubService } from '../services/GitHubService';
|
import { GitHubService } from '@/main/services/GitHubService';
|
||||||
import { GPUService } from '../services/GPUService';
|
import { GPUService } from '@/main/services/GPUService';
|
||||||
|
|
||||||
export class IPCHandlers {
|
export class IPCHandlers {
|
||||||
private koboldManager: KoboldCppManager;
|
private koboldManager: KoboldCppManager;
|
||||||
|
|
@ -32,11 +32,43 @@ export class IPCHandlers {
|
||||||
this.githubService.getAllReleases()
|
this.githubService.getAllReleases()
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle('kobold:checkForUpdates', async () => {
|
||||||
'kobold:downloadRelease',
|
const latest = await this.githubService.getLatestRelease();
|
||||||
async (_event, asset, onProgress) =>
|
return latest;
|
||||||
this.koboldManager.downloadRelease(asset, onProgress)
|
});
|
||||||
);
|
|
||||||
|
ipcMain.handle('kobold:openInstallDialog', async () => {
|
||||||
|
const result = await dialog.showOpenDialog({
|
||||||
|
properties: ['openDirectory'],
|
||||||
|
title: 'Select Installation Directory',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
|
return result.filePaths[0];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('kobold:downloadRelease', async (_event, asset) => {
|
||||||
|
try {
|
||||||
|
const mainWindow = this.koboldManager
|
||||||
|
.getWindowManager()
|
||||||
|
.getMainWindow();
|
||||||
|
|
||||||
|
const filePath = await this.koboldManager.downloadRelease(
|
||||||
|
asset,
|
||||||
|
(progress: number) => {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.webContents.send('download-progress', progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return { success: true, path: filePath };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('kobold:getInstalledVersions', () =>
|
ipcMain.handle('kobold:getInstalledVersions', () =>
|
||||||
this.koboldManager.getInstalledVersions()
|
this.koboldManager.getInstalledVersions()
|
||||||
|
|
@ -74,12 +106,6 @@ export class IPCHandlers {
|
||||||
this.koboldManager.selectInstallDirectory()
|
this.koboldManager.selectInstallDirectory()
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle(
|
|
||||||
'kobold:addInstalledVersion',
|
|
||||||
(_event, version, path, type) =>
|
|
||||||
this.koboldManager.addInstalledVersion(version, path, type)
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle('kobold:detectGPU', () => this.gpuService.detectGPU());
|
ipcMain.handle('kobold:detectGPU', () => this.gpuService.detectGPU());
|
||||||
|
|
||||||
ipcMain.handle('kobold:getPlatform', () => ({
|
ipcMain.handle('kobold:getPlatform', () => ({
|
||||||
|
|
@ -93,20 +119,20 @@ export class IPCHandlers {
|
||||||
|
|
||||||
ipcMain.handle('kobold:downloadROCm', async () => {
|
ipcMain.handle('kobold:downloadROCm', async () => {
|
||||||
try {
|
try {
|
||||||
return await this.koboldManager.downloadROCm();
|
const mainWindow = this.koboldManager
|
||||||
|
.getWindowManager()
|
||||||
|
.getMainWindow();
|
||||||
|
|
||||||
|
return await this.koboldManager.downloadROCm((progress: number) => {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.webContents.send('download-progress', progress);
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: (error as Error).message };
|
return { success: false, error: (error as Error).message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('kobold:launchKobold', (_event, versionPath, args) =>
|
|
||||||
this.koboldManager.launchKobold(versionPath, args)
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle('kobold:stopKobold', () => this.koboldManager.stopKobold());
|
|
||||||
|
|
||||||
ipcMain.handle('kobold:isRunning', () => this.koboldManager.isRunning());
|
|
||||||
|
|
||||||
ipcMain.handle('kobold:getInstalledVersion', () =>
|
ipcMain.handle('kobold:getInstalledVersion', () =>
|
||||||
this.koboldManager.getInstalledVersion()
|
this.koboldManager.getInstalledVersion()
|
||||||
);
|
);
|
||||||
|
|
@ -115,20 +141,58 @@ export class IPCHandlers {
|
||||||
this.koboldManager.getVersionFromBinary(binaryPath)
|
this.koboldManager.getVersionFromBinary(binaryPath)
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:checkForUpdates', () =>
|
|
||||||
this.koboldManager.checkForUpdates()
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle('kobold:getLatestReleaseWithStatus', () =>
|
ipcMain.handle('kobold:getLatestReleaseWithStatus', () =>
|
||||||
this.koboldManager.getLatestReleaseWithDownloadStatus()
|
this.koboldManager.getLatestReleaseWithDownloadStatus()
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:launchKoboldCpp', (_event, args) =>
|
ipcMain.handle('kobold:launchKoboldCpp', (_event, args, configFilePath) =>
|
||||||
this.koboldManager.launchKoboldCpp(args)
|
this.koboldManager.launchKoboldCpp(args, configFilePath)
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:openInstallDialog', () =>
|
ipcMain.handle('kobold:stopKoboldCpp', () =>
|
||||||
this.koboldManager.openInstallDialog()
|
this.koboldManager.stopKoboldCpp()
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle('kobold:confirmEject', async () => {
|
||||||
|
const mainWindow = this.koboldManager.getWindowManager().getMainWindow();
|
||||||
|
if (!mainWindow) return false;
|
||||||
|
|
||||||
|
const result = await dialog.showMessageBox(mainWindow, {
|
||||||
|
type: 'warning',
|
||||||
|
title: 'Confirm Eject',
|
||||||
|
message: 'Are you sure you want to stop KoboldCpp?',
|
||||||
|
detail:
|
||||||
|
'This will terminate the running process and return to the launch screen.',
|
||||||
|
buttons: ['Cancel', 'Stop KoboldCpp'],
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.response === 1; // Returns true if user clicked "Stop KoboldCpp"
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('kobold:parseConfigFile', (_event, filePath) =>
|
||||||
|
this.koboldManager.parseConfigFile(filePath)
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle('kobold:selectModelFile', () =>
|
||||||
|
this.koboldManager.selectModelFile()
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle('config:getServerOnly', () =>
|
||||||
|
this.configManager.getServerOnly()
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle('config:setServerOnly', (_event, serverOnly) =>
|
||||||
|
this.configManager.setServerOnly(serverOnly)
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle('config:getModelPath', () =>
|
||||||
|
this.configManager.getModelPath()
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle('config:setModelPath', (_event, path) =>
|
||||||
|
this.configManager.setModelPath(path)
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('config:get', (_event, key) => this.configManager.get(key));
|
ipcMain.handle('config:get', (_event, key) => this.configManager.get(key));
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
import { contextBridge, ipcRenderer, type IpcRendererEvent } from 'electron';
|
import { contextBridge, ipcRenderer, type IpcRendererEvent } from 'electron';
|
||||||
import type { KoboldAPI, AppAPI, UpdateInfo } from '../types/electron';
|
import type {
|
||||||
|
KoboldAPI,
|
||||||
|
AppAPI,
|
||||||
|
ConfigAPI,
|
||||||
|
UpdateInfo,
|
||||||
|
} from '@/types/electron';
|
||||||
|
|
||||||
const koboldAPI: KoboldAPI = {
|
const koboldAPI: KoboldAPI = {
|
||||||
isInstalled: () => ipcRenderer.invoke('kobold:isInstalled'),
|
isInstalled: () => ipcRenderer.invoke('kobold:isInstalled'),
|
||||||
|
|
@ -10,7 +15,6 @@ const koboldAPI: KoboldAPI = {
|
||||||
ipcRenderer.invoke('kobold:setCurrentVersion', version),
|
ipcRenderer.invoke('kobold:setCurrentVersion', version),
|
||||||
getVersionFromBinary: (binaryPath: string) =>
|
getVersionFromBinary: (binaryPath: string) =>
|
||||||
ipcRenderer.invoke('kobold:getVersionFromBinary', binaryPath),
|
ipcRenderer.invoke('kobold:getVersionFromBinary', binaryPath),
|
||||||
checkForUpdates: () => ipcRenderer.invoke('kobold:checkForUpdates'),
|
|
||||||
getLatestReleaseWithStatus: () =>
|
getLatestReleaseWithStatus: () =>
|
||||||
ipcRenderer.invoke('kobold:getLatestReleaseWithStatus'),
|
ipcRenderer.invoke('kobold:getLatestReleaseWithStatus'),
|
||||||
getROCmDownload: () => ipcRenderer.invoke('kobold:getROCmDownload'),
|
getROCmDownload: () => ipcRenderer.invoke('kobold:getROCmDownload'),
|
||||||
|
|
@ -24,12 +28,19 @@ const koboldAPI: KoboldAPI = {
|
||||||
ipcRenderer.invoke('kobold:selectInstallDirectory'),
|
ipcRenderer.invoke('kobold:selectInstallDirectory'),
|
||||||
downloadRelease: (asset) =>
|
downloadRelease: (asset) =>
|
||||||
ipcRenderer.invoke('kobold:downloadRelease', asset),
|
ipcRenderer.invoke('kobold:downloadRelease', asset),
|
||||||
launchKoboldCpp: (args) => ipcRenderer.invoke('kobold:launchKoboldCpp', args),
|
launchKoboldCpp: (args?: string[], configFilePath?: string) =>
|
||||||
|
ipcRenderer.invoke('kobold:launchKoboldCpp', args, configFilePath),
|
||||||
openInstallDialog: () => ipcRenderer.invoke('kobold:openInstallDialog'),
|
openInstallDialog: () => ipcRenderer.invoke('kobold:openInstallDialog'),
|
||||||
|
checkForUpdates: () => ipcRenderer.invoke('kobold:checkForUpdates'),
|
||||||
getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'),
|
getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'),
|
||||||
getSelectedConfig: () => ipcRenderer.invoke('kobold:getSelectedConfig'),
|
getSelectedConfig: () => ipcRenderer.invoke('kobold:getSelectedConfig'),
|
||||||
setSelectedConfig: (configName: string) =>
|
setSelectedConfig: (configName: string) =>
|
||||||
ipcRenderer.invoke('kobold:setSelectedConfig', configName),
|
ipcRenderer.invoke('kobold:setSelectedConfig', configName),
|
||||||
|
parseConfigFile: (filePath: string) =>
|
||||||
|
ipcRenderer.invoke('kobold:parseConfigFile', filePath),
|
||||||
|
selectModelFile: () => ipcRenderer.invoke('kobold:selectModelFile'),
|
||||||
|
stopKoboldCpp: () => ipcRenderer.invoke('kobold:stopKoboldCpp'),
|
||||||
|
confirmEject: () => ipcRenderer.invoke('kobold:confirmEject'),
|
||||||
onDownloadProgress: (callback) => {
|
onDownloadProgress: (callback) => {
|
||||||
ipcRenderer.on(
|
ipcRenderer.on(
|
||||||
'download-progress',
|
'download-progress',
|
||||||
|
|
@ -42,6 +53,22 @@ const koboldAPI: KoboldAPI = {
|
||||||
(_: IpcRendererEvent, updateInfo: UpdateInfo) => callback(updateInfo)
|
(_: IpcRendererEvent, updateInfo: UpdateInfo) => callback(updateInfo)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
onInstallDirChanged: (callback: (newPath: string) => void) => {
|
||||||
|
const handler = (_: IpcRendererEvent, newPath: string) => callback(newPath);
|
||||||
|
ipcRenderer.on('install-dir-changed', handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.removeListener('install-dir-changed', handler);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onKoboldOutput: (callback: (data: string) => void) => {
|
||||||
|
const handler = (_: IpcRendererEvent, data: string) => callback(data);
|
||||||
|
ipcRenderer.on('kobold-output', handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.removeListener('kobold-output', handler);
|
||||||
|
};
|
||||||
|
},
|
||||||
removeAllListeners: (channel) => {
|
removeAllListeners: (channel) => {
|
||||||
ipcRenderer.removeAllListeners(channel);
|
ipcRenderer.removeAllListeners(channel);
|
||||||
},
|
},
|
||||||
|
|
@ -52,7 +79,20 @@ const appAPI: AppAPI = {
|
||||||
openExternal: (url) => ipcRenderer.invoke('app:openExternal', url),
|
openExternal: (url) => ipcRenderer.invoke('app:openExternal', url),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const configAPI: ConfigAPI = {
|
||||||
|
getServerOnly: () => ipcRenderer.invoke('config:getServerOnly'),
|
||||||
|
setServerOnly: (serverOnly: boolean) =>
|
||||||
|
ipcRenderer.invoke('config:setServerOnly', serverOnly),
|
||||||
|
getModelPath: () => ipcRenderer.invoke('config:getModelPath'),
|
||||||
|
setModelPath: (path: string) =>
|
||||||
|
ipcRenderer.invoke('config:setModelPath', path),
|
||||||
|
get: (key: string) => ipcRenderer.invoke('config:get', key),
|
||||||
|
set: (key: string, value: unknown) =>
|
||||||
|
ipcRenderer.invoke('config:set', key, value),
|
||||||
|
};
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
kobold: koboldAPI,
|
kobold: koboldAPI,
|
||||||
app: appAPI,
|
app: appAPI,
|
||||||
|
config: configAPI,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,7 @@
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import {
|
import { Card, Text, Title, Loader, Stack, Container } from '@mantine/core';
|
||||||
Card,
|
import { useNotifications } from '@/hooks/useNotifications';
|
||||||
Text,
|
import { DownloadOptionCard } from '@/components/DownloadOptionCard';
|
||||||
Title,
|
|
||||||
Loader,
|
|
||||||
Alert,
|
|
||||||
Stack,
|
|
||||||
Container,
|
|
||||||
Progress,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { IconAlertCircle } from '@tabler/icons-react';
|
|
||||||
import { DownloadOptionCard } from '../components/DownloadOptionCard';
|
|
||||||
import {
|
import {
|
||||||
getPlatformDisplayName,
|
getPlatformDisplayName,
|
||||||
filterAssetsByPlatform,
|
filterAssetsByPlatform,
|
||||||
|
|
@ -21,27 +12,15 @@ import {
|
||||||
isAssetRecommended,
|
isAssetRecommended,
|
||||||
sortAssetsByRecommendation,
|
sortAssetsByRecommendation,
|
||||||
} from '@/utils/assets';
|
} from '@/utils/assets';
|
||||||
|
import { ROCM } from '@/constants/app';
|
||||||
|
import type { GitHubAsset, GitHubRelease } from '@/types';
|
||||||
|
|
||||||
interface DownloadScreenProps {
|
interface DownloadScreenProps {
|
||||||
onInstallComplete: () => void;
|
onDownloadComplete: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GitHubAsset {
|
export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
name: string;
|
const notify = useNotifications();
|
||||||
browser_download_url: string;
|
|
||||||
size: number;
|
|
||||||
created_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GitHubRelease {
|
|
||||||
tag_name: string;
|
|
||||||
name: string;
|
|
||||||
published_at: string;
|
|
||||||
body: string;
|
|
||||||
assets: GitHubAsset[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DownloadScreen = ({ onInstallComplete }: DownloadScreenProps) => {
|
|
||||||
const [latestRelease, setLatestRelease] = useState<GitHubRelease | null>(
|
const [latestRelease, setLatestRelease] = useState<GitHubRelease | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
@ -53,15 +32,15 @@ export const DownloadScreen = ({ onInstallComplete }: DownloadScreenProps) => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [downloading, setDownloading] = useState(false);
|
const [downloading, setDownloading] = useState(false);
|
||||||
const [downloadProgress, setDownloadProgress] = useState(0);
|
const [downloadProgress, setDownloadProgress] = useState(0);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [downloadingType, setDownloadingType] = useState<
|
||||||
|
'asset' | 'rocm' | null
|
||||||
|
>(null);
|
||||||
const [rocmDownload, setRocmDownload] = useState<{
|
const [rocmDownload, setRocmDownload] = useState<{
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
size: number;
|
size: number;
|
||||||
type: 'rocm';
|
|
||||||
version?: string;
|
version?: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [downloadingROCm, setDownloadingROCm] = useState(false);
|
|
||||||
|
|
||||||
const loadLatestReleaseAndPlatform = useCallback(async () => {
|
const loadLatestReleaseAndPlatform = useCallback(async () => {
|
||||||
if (!window.electronAPI) return;
|
if (!window.electronAPI) return;
|
||||||
|
|
@ -90,18 +69,25 @@ export const DownloadScreen = ({ onInstallComplete }: DownloadScreenProps) => {
|
||||||
setHasAMDGPU(false);
|
setHasAMDGPU(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filtered = filterAssetsByPlatform(
|
if (releaseData) {
|
||||||
releaseData.assets,
|
const filtered = filterAssetsByPlatform(
|
||||||
platformInfo.platform
|
releaseData.assets,
|
||||||
);
|
platformInfo.platform
|
||||||
setFilteredAssets(filtered);
|
);
|
||||||
|
setFilteredAssets(filtered);
|
||||||
|
} else {
|
||||||
|
notify.error(
|
||||||
|
'Error',
|
||||||
|
'GitHub API is currently unavailable. Please try again later.'
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError('Failed to load release information');
|
notify.error('Error', 'Failed to load release information');
|
||||||
console.error('Error loading release:', err);
|
console.error('Error loading release:', err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [notify]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadLatestReleaseAndPlatform();
|
loadLatestReleaseAndPlatform();
|
||||||
|
|
@ -119,83 +105,73 @@ export const DownloadScreen = ({ onInstallComplete }: DownloadScreenProps) => {
|
||||||
};
|
};
|
||||||
}, [loadLatestReleaseAndPlatform]);
|
}, [loadLatestReleaseAndPlatform]);
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async (type: 'asset' | 'rocm' = 'asset') => {
|
||||||
if (!selectedAsset || !window.electronAPI) return;
|
if (!window.electronAPI) return;
|
||||||
|
if (type === 'asset' && !selectedAsset) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setDownloading(true);
|
setDownloading(true);
|
||||||
setDownloadProgress(0);
|
setDownloadProgress(0);
|
||||||
setError(null);
|
setDownloadingType(type);
|
||||||
|
|
||||||
const result =
|
const result =
|
||||||
await window.electronAPI.kobold.downloadRelease(selectedAsset);
|
type === 'rocm'
|
||||||
|
? await window.electronAPI.kobold.downloadROCm()
|
||||||
|
: await window.electronAPI.kobold.downloadRelease(selectedAsset!);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
onInstallComplete();
|
onDownloadComplete();
|
||||||
} else {
|
} else {
|
||||||
setError(result.error || 'Download failed');
|
notify.error(
|
||||||
|
'Download Failed',
|
||||||
|
result.error || `${type === 'rocm' ? 'ROCm' : ''} Download failed`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError('Download failed');
|
notify.error(
|
||||||
console.error('Download error:', err);
|
'Download Failed',
|
||||||
|
`${type === 'rocm' ? 'ROCm' : ''} Download failed`
|
||||||
|
);
|
||||||
|
console.error(`${type === 'rocm' ? 'ROCm' : ''} Download error:`, err);
|
||||||
} finally {
|
} finally {
|
||||||
setDownloading(false);
|
setDownloading(false);
|
||||||
setDownloadProgress(0);
|
setDownloadProgress(0);
|
||||||
|
setDownloadingType(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownloadROCm = async () => {
|
const renderROCmCard = () => {
|
||||||
if (!window.electronAPI) return;
|
if (!rocmDownload) return null;
|
||||||
|
|
||||||
try {
|
return (
|
||||||
setDownloadingROCm(true);
|
<DownloadOptionCard
|
||||||
setDownloadProgress(0);
|
name={ROCM.BINARY_NAME}
|
||||||
setError(null);
|
description={getAssetDescription(ROCM.BINARY_NAME)}
|
||||||
|
size={formatFileSize(ROCM.SIZE_BYTES)}
|
||||||
const result = await window.electronAPI.kobold.downloadROCm();
|
isSelected={selectedROCm}
|
||||||
|
isRecommended={isAssetRecommended(ROCM.BINARY_NAME, hasAMDGPU)}
|
||||||
if (result.success) {
|
isDownloading={downloading && downloadingType === 'rocm'}
|
||||||
onInstallComplete();
|
downloadProgress={downloadingType === 'rocm' ? downloadProgress : 0}
|
||||||
} else {
|
onClick={() => {
|
||||||
setError(result.error || 'ROCm download failed');
|
if (!selectedROCm) {
|
||||||
}
|
setSelectedROCm(true);
|
||||||
} catch (err) {
|
setSelectedAsset(null);
|
||||||
setError('ROCm download failed');
|
}
|
||||||
console.error('ROCm download error:', err);
|
}}
|
||||||
} finally {
|
onDownload={(e) => {
|
||||||
setDownloadingROCm(false);
|
e.stopPropagation();
|
||||||
setDownloadProgress(0);
|
handleDownload('rocm');
|
||||||
}
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="md" py="xl">
|
<Container size="sm" py="xl">
|
||||||
<Stack gap="xl">
|
<Stack gap="xl">
|
||||||
{error && (
|
|
||||||
<Alert
|
|
||||||
icon={<IconAlertCircle size="1rem" />}
|
|
||||||
color="red"
|
|
||||||
variant="light"
|
|
||||||
>
|
|
||||||
{error}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{downloading && selectedAsset && (
|
|
||||||
<Card withBorder radius="md" shadow="sm">
|
|
||||||
<Stack gap="md">
|
|
||||||
<Text fw={500}>Downloading {selectedAsset.name}...</Text>
|
|
||||||
<Progress value={downloadProgress} color="blue" radius="xl" />
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{downloadProgress.toFixed(1)}% complete
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Card withBorder radius="md" shadow="sm">
|
<Card withBorder radius="md" shadow="sm">
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Title order={3}>Available Downloads for Your Platform</Title>
|
<Title order={3}>Available Binaries for Your Platform</Title>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Stack align="center" gap="md" py="xl">
|
<Stack align="center" gap="md" py="xl">
|
||||||
|
|
@ -234,31 +210,7 @@ export const DownloadScreen = ({ onInstallComplete }: DownloadScreenProps) => {
|
||||||
|
|
||||||
{filteredAssets.length > 0 || rocmDownload ? (
|
{filteredAssets.length > 0 || rocmDownload ? (
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
{rocmDownload && hasAMDGPU && (
|
{rocmDownload && hasAMDGPU && renderROCmCard()}
|
||||||
<DownloadOptionCard
|
|
||||||
name="koboldcpp-linux-x64-rocm"
|
|
||||||
description={getAssetDescription(
|
|
||||||
'koboldcpp-linux-x64-rocm'
|
|
||||||
)}
|
|
||||||
size="~1GB"
|
|
||||||
isSelected={selectedROCm}
|
|
||||||
isRecommended={isAssetRecommended(
|
|
||||||
'koboldcpp-linux-x64-rocm',
|
|
||||||
hasAMDGPU
|
|
||||||
)}
|
|
||||||
isDownloading={downloadingROCm}
|
|
||||||
onClick={() => {
|
|
||||||
if (!selectedROCm) {
|
|
||||||
setSelectedROCm(true);
|
|
||||||
setSelectedAsset(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDownload={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleDownloadROCm();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{sortAssetsByRecommendation(
|
{sortAssetsByRecommendation(
|
||||||
filteredAssets,
|
filteredAssets,
|
||||||
|
|
@ -274,7 +226,18 @@ export const DownloadScreen = ({ onInstallComplete }: DownloadScreenProps) => {
|
||||||
asset.name,
|
asset.name,
|
||||||
hasAMDGPU
|
hasAMDGPU
|
||||||
)}
|
)}
|
||||||
isDownloading={downloading}
|
isDownloading={
|
||||||
|
downloading &&
|
||||||
|
downloadingType === 'asset' &&
|
||||||
|
selectedAsset?.name === asset.name
|
||||||
|
}
|
||||||
|
downloadProgress={
|
||||||
|
downloading &&
|
||||||
|
downloadingType === 'asset' &&
|
||||||
|
selectedAsset?.name === asset.name
|
||||||
|
? downloadProgress
|
||||||
|
: 0
|
||||||
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (selectedAsset?.name !== asset.name) {
|
if (selectedAsset?.name !== asset.name) {
|
||||||
setSelectedAsset(asset);
|
setSelectedAsset(asset);
|
||||||
|
|
@ -288,48 +251,18 @@ export const DownloadScreen = ({ onInstallComplete }: DownloadScreenProps) => {
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{rocmDownload && !hasAMDGPU && (
|
{rocmDownload && !hasAMDGPU && renderROCmCard()}
|
||||||
<DownloadOptionCard
|
|
||||||
name="koboldcpp-linux-x64-rocm"
|
|
||||||
description={getAssetDescription(
|
|
||||||
'koboldcpp-linux-x64-rocm'
|
|
||||||
)}
|
|
||||||
size="~1GB"
|
|
||||||
isSelected={selectedROCm}
|
|
||||||
isRecommended={isAssetRecommended(
|
|
||||||
'koboldcpp-linux-x64-rocm',
|
|
||||||
hasAMDGPU
|
|
||||||
)}
|
|
||||||
isDownloading={downloadingROCm}
|
|
||||||
onClick={() => {
|
|
||||||
if (!selectedROCm) {
|
|
||||||
setSelectedROCm(true);
|
|
||||||
setSelectedAsset(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDownload={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleDownloadROCm();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<Alert
|
<Card withBorder p="md" bg="yellow.0" c="yellow.9">
|
||||||
icon={<IconAlertCircle size="1rem" />}
|
|
||||||
color="yellow"
|
|
||||||
variant="light"
|
|
||||||
>
|
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Text fw={500}>No downloads available</Text>
|
<Text fw={500}>No downloads available</Text>
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
No downloads available for your platform (
|
No downloads available for your platform (
|
||||||
{getPlatformDisplayName(userPlatform)}). This might
|
{getPlatformDisplayName(userPlatform)}).
|
||||||
be a new release that doesn't have builds ready
|
|
||||||
yet.
|
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Alert>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -7,31 +7,57 @@ import {
|
||||||
Stack,
|
Stack,
|
||||||
Group,
|
Group,
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Select,
|
Switch,
|
||||||
|
Slider,
|
||||||
|
TextInput,
|
||||||
|
NumberInput,
|
||||||
|
Tooltip,
|
||||||
|
Checkbox,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconFile, IconRefresh } from '@tabler/icons-react';
|
import { RotateCcw, File, Info } from 'lucide-react';
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { ConfigFileSelect } from '@/components/ConfigFileSelect';
|
||||||
|
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
||||||
|
import type { ConfigFile } from '@/types';
|
||||||
|
|
||||||
interface ConfigFile {
|
interface LaunchScreenProps {
|
||||||
name: string;
|
onLaunch: () => void;
|
||||||
path: string;
|
|
||||||
size: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LaunchScreen = () => {
|
export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
const [configFiles, setConfigFiles] = useState<ConfigFile[]>([]);
|
const [configFiles, setConfigFiles] = useState<ConfigFile[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||||||
const [, setInstallDir] = useState<string>('');
|
const [, setInstallDir] = useState<string>('');
|
||||||
|
const {
|
||||||
|
serverOnly,
|
||||||
|
gpuLayers,
|
||||||
|
autoGpuLayers,
|
||||||
|
contextSize,
|
||||||
|
modelPath,
|
||||||
|
additionalArguments,
|
||||||
|
parseAndApplyConfigFile,
|
||||||
|
loadSavedSettings,
|
||||||
|
loadConfigFromFile,
|
||||||
|
handleServerOnlyChange,
|
||||||
|
handleGpuLayersChange,
|
||||||
|
handleAutoGpuLayersChange,
|
||||||
|
handleContextSizeChangeWithStep,
|
||||||
|
handleModelPathChange,
|
||||||
|
handleSelectModelFile,
|
||||||
|
handleAdditionalArgumentsChange,
|
||||||
|
} = useLaunchConfig();
|
||||||
|
|
||||||
const loadConfigFiles = useCallback(async () => {
|
const loadConfigFiles = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const [files, currentDir, savedConfig] = await Promise.all([
|
const [files, currentDir, savedConfig] = await Promise.all([
|
||||||
window.electronAPI.kobold.getConfigFiles(),
|
window.electronAPI.kobold.getConfigFiles(),
|
||||||
window.electronAPI.kobold.getCurrentInstallDir(),
|
window.electronAPI.kobold.getCurrentInstallDir(),
|
||||||
window.electronAPI.kobold.getSelectedConfig(),
|
window.electronAPI.kobold.getSelectedConfig(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setConfigFiles(files);
|
setConfigFiles(files);
|
||||||
setInstallDir(currentDir);
|
setInstallDir(currentDir);
|
||||||
|
|
||||||
|
|
@ -40,73 +66,78 @@ export const LaunchScreen = () => {
|
||||||
} else if (files.length > 0 && !selectedFile) {
|
} else if (files.length > 0 && !selectedFile) {
|
||||||
setSelectedFile(files[0].name);
|
setSelectedFile(files[0].name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await loadSavedSettings();
|
||||||
|
|
||||||
|
const currentSelectedFile = await loadConfigFromFile(files, savedConfig);
|
||||||
|
if (currentSelectedFile && !selectedFile) {
|
||||||
|
setSelectedFile(currentSelectedFile);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [selectedFile]);
|
}, [selectedFile, loadSavedSettings, loadConfigFromFile]);
|
||||||
|
|
||||||
const handleFileSelection = async (fileName: string) => {
|
const handleFileSelection = async (fileName: string) => {
|
||||||
setSelectedFile(fileName);
|
setSelectedFile(fileName);
|
||||||
await window.electronAPI.kobold.setSelectedConfig(fileName);
|
await window.electronAPI.kobold.setSelectedConfig(fileName);
|
||||||
|
|
||||||
|
const selectedConfig = configFiles.find((f) => f.name === fileName);
|
||||||
|
if (selectedConfig) {
|
||||||
|
await parseAndApplyConfigFile(selectedConfig.path);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadConfigFiles();
|
void loadConfigFiles();
|
||||||
|
|
||||||
|
const handleInstallDirChange = () => {
|
||||||
|
void loadConfigFiles();
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanup = window.electronAPI.kobold.onInstallDirChanged(
|
||||||
|
handleInstallDirChange
|
||||||
|
);
|
||||||
|
|
||||||
|
return cleanup;
|
||||||
}, [loadConfigFiles]);
|
}, [loadConfigFiles]);
|
||||||
|
|
||||||
const renderContent = () => {
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<Text c="dimmed" ta="center">
|
|
||||||
Loading configuration files...
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configFiles.length === 0) {
|
|
||||||
return (
|
|
||||||
<Text c="dimmed" ta="center">
|
|
||||||
No configuration files found in the installation directory.
|
|
||||||
<br />
|
|
||||||
Please ensure your .kcpps or .kcppt files are in the correct location.
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectData = configFiles.map((file) => ({
|
|
||||||
value: file.name,
|
|
||||||
label: file.name,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
label="Configuration File"
|
|
||||||
placeholder="Select a configuration file"
|
|
||||||
value={selectedFile}
|
|
||||||
onChange={(value) => value && handleFileSelection(value)}
|
|
||||||
data={selectData}
|
|
||||||
leftSection={<IconFile size={16} />}
|
|
||||||
searchable
|
|
||||||
clearable={false}
|
|
||||||
w="100%"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLaunch = async () => {
|
const handleLaunch = async () => {
|
||||||
if (!selectedFile) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const selectedConfig = configFiles.find((f) => f.name === selectedFile);
|
const selectedConfig = selectedFile
|
||||||
if (selectedConfig) {
|
? configFiles.find((f) => f.name === selectedFile)
|
||||||
const result = await window.electronAPI.kobold.launchKoboldCpp([
|
: null;
|
||||||
selectedConfig.path,
|
|
||||||
]);
|
const args: string[] = [];
|
||||||
if (result.success) {
|
|
||||||
// Launch successful
|
if (modelPath) {
|
||||||
} else {
|
args.push('--model', modelPath);
|
||||||
console.error('Launch failed:', result.error);
|
}
|
||||||
}
|
|
||||||
|
if (autoGpuLayers) {
|
||||||
|
args.push('--gpulayers', '-1');
|
||||||
|
} else if (gpuLayers > 0) {
|
||||||
|
args.push('--gpulayers', gpuLayers.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contextSize) {
|
||||||
|
args.push('--contextsize', contextSize.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (additionalArguments.trim()) {
|
||||||
|
const additionalArgs = additionalArguments.trim().split(/\s+/);
|
||||||
|
args.push(...additionalArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await window.electronAPI.kobold.launchKoboldCpp(
|
||||||
|
args,
|
||||||
|
selectedConfig?.path
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
onLaunch();
|
||||||
|
} else {
|
||||||
|
console.error('Launch failed:', result.error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error launching KoboldCpp:', error);
|
console.error('Error launching KoboldCpp:', error);
|
||||||
|
|
@ -114,10 +145,10 @@ export const LaunchScreen = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="md" py="xl">
|
<Container size="sm" py="xl">
|
||||||
<Card withBorder radius="md" shadow="sm">
|
<Card withBorder radius="md" shadow="sm">
|
||||||
<Stack gap="lg" align="center">
|
<Stack gap="lg">
|
||||||
<Title order={2}>Launch Configuration</Title>
|
<Title order={3}>Launch Configuration</Title>
|
||||||
|
|
||||||
<Card withBorder radius="md" w="100%">
|
<Card withBorder radius="md" w="100%">
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb="md">
|
||||||
|
|
@ -128,11 +159,198 @@ export const LaunchScreen = () => {
|
||||||
loading={loading}
|
loading={loading}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<IconRefresh size={16} />
|
<RotateCcw size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{renderContent()}
|
<ConfigFileSelect
|
||||||
|
configFiles={configFiles}
|
||||||
|
selectedFile={selectedFile}
|
||||||
|
loading={loading}
|
||||||
|
onFileSelection={handleFileSelection}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card withBorder radius="md" w="100%">
|
||||||
|
<Text fw={500} mb="md">
|
||||||
|
Launch Settings
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Stack gap="l">
|
||||||
|
<div>
|
||||||
|
<Text size="sm" fw={500} mb="xs">
|
||||||
|
Model File
|
||||||
|
</Text>
|
||||||
|
<Group gap="xs">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Select a .gguf model file"
|
||||||
|
value={modelPath}
|
||||||
|
onChange={(event) =>
|
||||||
|
handleModelPathChange(event.currentTarget.value)
|
||||||
|
}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleSelectModelFile}
|
||||||
|
variant="light"
|
||||||
|
leftSection={<File size={16} />}
|
||||||
|
>
|
||||||
|
Browse
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Group justify="space-between" align="center" mb="xs">
|
||||||
|
<Group gap="xs" align="center">
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
GPU Layers
|
||||||
|
</Text>
|
||||||
|
<Tooltip
|
||||||
|
label="The number of layer's to offload to your GPU's VRAM. Ideally the entire LLM should fit inside the VRAM for optimal performance."
|
||||||
|
multiline
|
||||||
|
w={300}
|
||||||
|
withArrow
|
||||||
|
>
|
||||||
|
<ActionIcon variant="subtle" size="xs" color="gray">
|
||||||
|
<Info size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
<Group gap="lg" align="center">
|
||||||
|
<Group gap="xs" align="center">
|
||||||
|
<Checkbox
|
||||||
|
label="Auto"
|
||||||
|
checked={autoGpuLayers}
|
||||||
|
onChange={(event) =>
|
||||||
|
handleAutoGpuLayersChange(event.currentTarget.checked)
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
label="Automatically try to allocate the GPU layers based on available VRAM."
|
||||||
|
multiline
|
||||||
|
w={300}
|
||||||
|
withArrow
|
||||||
|
>
|
||||||
|
<ActionIcon variant="subtle" size="xs" color="gray">
|
||||||
|
<Info size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
<NumberInput
|
||||||
|
value={gpuLayers}
|
||||||
|
onChange={(value) =>
|
||||||
|
handleGpuLayersChange(Number(value) || 0)
|
||||||
|
}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
size="sm"
|
||||||
|
w={80}
|
||||||
|
disabled={autoGpuLayers}
|
||||||
|
hideControls
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
<Slider
|
||||||
|
value={gpuLayers}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
onChange={handleGpuLayersChange}
|
||||||
|
disabled={autoGpuLayers}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Group justify="space-between" align="center" mb="xs">
|
||||||
|
<Group gap="xs" align="center">
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
Context Size
|
||||||
|
</Text>
|
||||||
|
<Tooltip
|
||||||
|
label="Controls the memory allocated for maximum context size. The larger the context, the larger the required memory."
|
||||||
|
multiline
|
||||||
|
w={300}
|
||||||
|
withArrow
|
||||||
|
>
|
||||||
|
<ActionIcon variant="subtle" size="xs" color="gray">
|
||||||
|
<Info size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
<NumberInput
|
||||||
|
value={contextSize}
|
||||||
|
onChange={(value) =>
|
||||||
|
handleContextSizeChangeWithStep(Number(value) || 256)
|
||||||
|
}
|
||||||
|
min={256}
|
||||||
|
max={131072}
|
||||||
|
step={256}
|
||||||
|
size="sm"
|
||||||
|
w={100}
|
||||||
|
hideControls
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Slider
|
||||||
|
value={contextSize}
|
||||||
|
min={256}
|
||||||
|
max={131072}
|
||||||
|
step={1}
|
||||||
|
onChange={handleContextSizeChangeWithStep}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Group gap="xs" align="center" mb="xs">
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
Additional arguments
|
||||||
|
</Text>
|
||||||
|
<Tooltip
|
||||||
|
label="Additional command line arguments to pass to the KoboldCPP binary. Leave this empty if you don't know what they are."
|
||||||
|
multiline
|
||||||
|
w={300}
|
||||||
|
withArrow
|
||||||
|
>
|
||||||
|
<ActionIcon variant="subtle" size="xs" color="gray">
|
||||||
|
<Info size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Additional command line arguments"
|
||||||
|
value={additionalArguments}
|
||||||
|
onChange={(event) =>
|
||||||
|
handleAdditionalArgumentsChange(event.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Group gap="xs" align="center" mb="xs">
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
Server-only mode
|
||||||
|
</Text>
|
||||||
|
<Tooltip
|
||||||
|
label="In server-only mode, the KoboldAI Lite web UI won't be displayed. Use this if you'll be using your own frontend to interact with the LLM."
|
||||||
|
multiline
|
||||||
|
w={300}
|
||||||
|
withArrow
|
||||||
|
>
|
||||||
|
<ActionIcon variant="subtle" size="xs" color="gray">
|
||||||
|
<Info size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
<Switch
|
||||||
|
checked={serverOnly}
|
||||||
|
onChange={(event) =>
|
||||||
|
handleServerOnlyChange(event.currentTarget.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Group gap="md" justify="center">
|
<Group gap="md" justify="center">
|
||||||
|
|
|
||||||
121
src/screens/TerminalScreen.tsx
Normal file
121
src/screens/TerminalScreen.tsx
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
|
Group,
|
||||||
|
ScrollArea,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
|
|
||||||
|
export const TerminalScreen = () => {
|
||||||
|
const [terminalContent, setTerminalContent] = useState<string>('');
|
||||||
|
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||||
|
const viewportRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewportRef.current) {
|
||||||
|
viewportRef.current.scrollTop = viewportRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [terminalContent]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (window.electronAPI?.kobold?.onKoboldOutput) {
|
||||||
|
const cleanup = window.electronAPI.kobold.onKoboldOutput(
|
||||||
|
(data: string) => {
|
||||||
|
setTerminalContent((prev) => {
|
||||||
|
// Handle carriage returns for progress bars
|
||||||
|
const lines = prev.split('\n');
|
||||||
|
const newData = data.toString();
|
||||||
|
|
||||||
|
// If the new data contains carriage returns, handle line overwriting
|
||||||
|
if (newData.includes('\r')) {
|
||||||
|
const parts = newData.split('\r');
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
if (i === 0) {
|
||||||
|
// First part gets appended to the last line
|
||||||
|
if (lines.length > 0) {
|
||||||
|
lines[lines.length - 1] += parts[i];
|
||||||
|
} else {
|
||||||
|
lines.push(parts[i]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Subsequent parts overwrite the last line
|
||||||
|
if (lines.length > 0) {
|
||||||
|
lines[lines.length - 1] = parts[i];
|
||||||
|
} else {
|
||||||
|
lines.push(parts[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
} else {
|
||||||
|
// No carriage returns, just append
|
||||||
|
return prev + newData;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return cleanup;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="lg" style={{ height: '100%', paddingTop: '2rem' }}>
|
||||||
|
<Stack gap="lg" style={{ height: '100%' }}>
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<Title order={3}>KoboldCpp Terminal</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
withBorder
|
||||||
|
radius="md"
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
backgroundColor: 'var(--mantine-color-dark-8)',
|
||||||
|
minHeight: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollArea
|
||||||
|
ref={scrollAreaRef}
|
||||||
|
viewportRef={viewportRef}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", monospace',
|
||||||
|
fontSize: '14px',
|
||||||
|
lineHeight: 1.4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box p="md">
|
||||||
|
{terminalContent.length === 0 ? (
|
||||||
|
<Text c="dimmed" style={{ fontFamily: 'inherit' }}>
|
||||||
|
Starting KoboldCpp...
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text
|
||||||
|
component="pre"
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
fontSize: 'inherit',
|
||||||
|
lineHeight: 'inherit',
|
||||||
|
color: 'var(--mantine-color-gray-0)',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{terminalContent}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</ScrollArea>
|
||||||
|
</Card>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
24
src/types/electron.d.ts
vendored
24
src/types/electron.d.ts
vendored
|
|
@ -73,7 +73,8 @@ export interface KoboldAPI {
|
||||||
checkForUpdates: () => Promise<UpdateInfo | null>;
|
checkForUpdates: () => Promise<UpdateInfo | null>;
|
||||||
getLatestReleaseWithStatus: () => Promise<ReleaseWithStatus | null>;
|
getLatestReleaseWithStatus: () => Promise<ReleaseWithStatus | null>;
|
||||||
launchKoboldCpp: (
|
launchKoboldCpp: (
|
||||||
args?: string[]
|
args?: string[],
|
||||||
|
configFilePath?: string
|
||||||
) => Promise<{ success: boolean; pid?: number; error?: string }>;
|
) => Promise<{ success: boolean; pid?: number; error?: string }>;
|
||||||
openInstallDialog: () => Promise<{ success: boolean; path?: string }>;
|
openInstallDialog: () => Promise<{ success: boolean; path?: string }>;
|
||||||
getConfigFiles: () => Promise<
|
getConfigFiles: () => Promise<
|
||||||
|
|
@ -81,8 +82,19 @@ export interface KoboldAPI {
|
||||||
>;
|
>;
|
||||||
getSelectedConfig: () => Promise<string | null>;
|
getSelectedConfig: () => Promise<string | null>;
|
||||||
setSelectedConfig: (configName: string) => Promise<boolean>;
|
setSelectedConfig: (configName: string) => Promise<boolean>;
|
||||||
|
parseConfigFile: (filePath: string) => Promise<{
|
||||||
|
gpulayers?: number;
|
||||||
|
contextsize?: number;
|
||||||
|
model_param?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
} | null>;
|
||||||
|
selectModelFile: () => Promise<string | null>;
|
||||||
|
stopKoboldCpp: () => void;
|
||||||
|
confirmEject: () => Promise<boolean>;
|
||||||
onDownloadProgress: (callback: (progress: number) => void) => void;
|
onDownloadProgress: (callback: (progress: number) => void) => void;
|
||||||
onUpdateAvailable: (callback: (updateInfo: UpdateInfo) => void) => void;
|
onUpdateAvailable: (callback: (updateInfo: UpdateInfo) => void) => void;
|
||||||
|
onInstallDirChanged: (callback: (newPath: string) => void) => () => void;
|
||||||
|
onKoboldOutput: (callback: (data: string) => void) => () => void;
|
||||||
removeAllListeners: (channel: string) => void;
|
removeAllListeners: (channel: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,11 +103,21 @@ export interface AppAPI {
|
||||||
openExternal: (url: string) => Promise<void>;
|
openExternal: (url: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConfigAPI {
|
||||||
|
getServerOnly: () => Promise<boolean>;
|
||||||
|
setServerOnly: (serverOnly: boolean) => Promise<void>;
|
||||||
|
getModelPath: () => Promise<string | null>;
|
||||||
|
setModelPath: (path: string) => Promise<void>;
|
||||||
|
get: (key: string) => Promise<unknown>;
|
||||||
|
set: (key: string, value: unknown) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
electronAPI: {
|
electronAPI: {
|
||||||
kobold: KoboldAPI;
|
kobold: KoboldAPI;
|
||||||
app: AppAPI;
|
app: AppAPI;
|
||||||
|
config: ConfigAPI;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
50
src/types/index.ts
Normal file
50
src/types/index.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
// Common interfaces used across multiple components
|
||||||
|
export interface ConfigFile {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GitHubAsset {
|
||||||
|
name: string;
|
||||||
|
browser_download_url: string;
|
||||||
|
size: number;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GitHubRelease {
|
||||||
|
tag_name: string;
|
||||||
|
name: string;
|
||||||
|
published_at: string;
|
||||||
|
body: string;
|
||||||
|
assets: GitHubAsset[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateInfo {
|
||||||
|
currentVersion: string;
|
||||||
|
latestVersion: string;
|
||||||
|
releaseInfo: GitHubRelease;
|
||||||
|
hasUpdate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReleaseWithStatus {
|
||||||
|
release: GitHubRelease;
|
||||||
|
availableAssets: Array<{
|
||||||
|
asset: GitHubAsset;
|
||||||
|
isDownloaded: boolean;
|
||||||
|
installedVersion?: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstalledVersion {
|
||||||
|
version: string;
|
||||||
|
path: string;
|
||||||
|
downloadDate: string;
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ROCmDownload {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
|
import { ASSET_SUFFIXES } from '@/constants/app';
|
||||||
|
|
||||||
export const getAssetDescription = (assetName: string): string => {
|
export const getAssetDescription = (assetName: string): string => {
|
||||||
const name = assetName.toLowerCase();
|
const name = assetName.toLowerCase();
|
||||||
|
|
||||||
if (name.includes('rocm')) {
|
if (name.includes(ASSET_SUFFIXES.ROCM)) {
|
||||||
return 'Optimized for AMD GPUs with ROCm support.';
|
return 'Optimized for AMD GPUs with ROCm support.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name.endsWith('oldpc')) {
|
if (name.endsWith(ASSET_SUFFIXES.OLDPC)) {
|
||||||
return 'Meant for old PCs that cannot normally run the standard build.';
|
return 'Meant for old PCs that cannot normally run the standard build.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name.endsWith('nocuda')) {
|
if (name.endsWith(ASSET_SUFFIXES.NOCUDA)) {
|
||||||
return 'Standard build with NVIDIA CUDA removed for minimal file size.';
|
return 'Standard build with NVIDIA CUDA support removed for minimal file size.';
|
||||||
}
|
}
|
||||||
|
|
||||||
return "Standard build that's ideal for most cases.";
|
return "Standard build that's ideal for most cases.";
|
||||||
|
|
@ -22,15 +24,15 @@ export const isAssetRecommended = (
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const name = assetName.toLowerCase();
|
const name = assetName.toLowerCase();
|
||||||
|
|
||||||
if (hasAMDGPU && name.includes('rocm')) {
|
if (hasAMDGPU && name.includes(ASSET_SUFFIXES.ROCM)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!hasAMDGPU &&
|
!hasAMDGPU &&
|
||||||
!name.includes('rocm') &&
|
!name.includes(ASSET_SUFFIXES.ROCM) &&
|
||||||
!name.endsWith('oldpc') &&
|
!name.endsWith(ASSET_SUFFIXES.OLDPC) &&
|
||||||
!name.endsWith('nocuda')
|
!name.endsWith(ASSET_SUFFIXES.NOCUDA)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import type { Config } from 'tailwindcss';
|
|
||||||
|
|
||||||
const config: Config = {
|
|
||||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
|
||||||
theme: {
|
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
important: '#root',
|
|
||||||
} satisfies Config;
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
|
|
@ -9,7 +9,11 @@
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/main/**/*", "src/preload/**/*"],
|
"include": ["src/main/**/*", "src/preload/**/*"],
|
||||||
"exclude": ["node_modules", "dist", "dist-electron"]
|
"exclude": ["node_modules", "dist", "dist-electron"]
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue