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
|
||||
Version 3, 29 June 2007
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
|
|
@ -7,17 +7,15 @@
|
|||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
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
|
||||
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
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
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.
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
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
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
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:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
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
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
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.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
|
@ -72,7 +60,7 @@ modification follow.
|
|||
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
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
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
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
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
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.
|
||||
|
||||
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
|
||||
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>
|
||||
|
||||
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
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
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/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
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".
|
||||
If your software can interact with users remotely through a computer
|
||||
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
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
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.
|
||||
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/>.
|
||||
|
||||
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
|
||||
|
||||
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",
|
||||
"language": "en",
|
||||
"words": [
|
||||
// Project specific terms
|
||||
"kobold",
|
||||
"koboldcpp",
|
||||
"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",
|
||||
"addEventListener",
|
||||
"admin",
|
||||
"allowRunningInsecureContent",
|
||||
"amdgpu",
|
||||
"wmic",
|
||||
"oldpc",
|
||||
"nocuda",
|
||||
"cooldown",
|
||||
"togglefullscreen",
|
||||
"libvk",
|
||||
"swiftshader",
|
||||
"icns",
|
||||
"nsis",
|
||||
"api",
|
||||
"apis",
|
||||
"appendChild",
|
||||
"arg",
|
||||
"args",
|
||||
"asar",
|
||||
"async",
|
||||
"auth",
|
||||
"await",
|
||||
"babel",
|
||||
"basename",
|
||||
"bgcolor",
|
||||
"browserWindow",
|
||||
"bundler",
|
||||
"bundling",
|
||||
"can",
|
||||
"classList",
|
||||
"className",
|
||||
"cloneNode",
|
||||
"codeinterface",
|
||||
"tabler",
|
||||
"deps",
|
||||
"devs",
|
||||
"repo",
|
||||
"repos",
|
||||
"componentDidCatch",
|
||||
"componentDidMount",
|
||||
"componentDidUpdate",
|
||||
"componentWillUnmount",
|
||||
"config",
|
||||
"configs",
|
||||
"contextBridge",
|
||||
"contextIsolation",
|
||||
"contextsize",
|
||||
"cooldown",
|
||||
"couldn",
|
||||
"createContext",
|
||||
"createElement",
|
||||
"createRef",
|
||||
"css",
|
||||
"cuda",
|
||||
"dataset",
|
||||
"deps",
|
||||
"destructuring",
|
||||
"devs",
|
||||
"dirname",
|
||||
"doesn",
|
||||
"don",
|
||||
"Egor",
|
||||
"env",
|
||||
"envs",
|
||||
"eot",
|
||||
"eslint",
|
||||
"filename",
|
||||
"filenames",
|
||||
"filepath",
|
||||
"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",
|
||||
"flexdir",
|
||||
"flexwrap",
|
||||
"gridcol",
|
||||
"gridrow",
|
||||
"bgcolor",
|
||||
"textcolor",
|
||||
"fontsize",
|
||||
"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",
|
||||
"wordspacing",
|
||||
"textalign",
|
||||
"textdecoration",
|
||||
"texttransform",
|
||||
"whitespace",
|
||||
"wordbreak",
|
||||
"wordwrap",
|
||||
"libvk",
|
||||
"lineheight",
|
||||
"linted",
|
||||
"localhost",
|
||||
"mainWindow",
|
||||
"minified",
|
||||
"minify",
|
||||
"moz",
|
||||
"namespace",
|
||||
"namespaces",
|
||||
"newpackagename",
|
||||
"nocuda",
|
||||
"nodeIntegration",
|
||||
"nsis",
|
||||
"nvidia",
|
||||
"oldpc",
|
||||
"openblas",
|
||||
"otf",
|
||||
"overflow",
|
||||
"overflowx",
|
||||
"overflowy",
|
||||
"scrollbar",
|
||||
"webkit",
|
||||
"moz",
|
||||
// Build tools
|
||||
"sourcemap",
|
||||
"sourcemaps",
|
||||
"minify",
|
||||
"minified",
|
||||
"bundler",
|
||||
"bundling",
|
||||
"treeshake",
|
||||
"treeshaking",
|
||||
"param",
|
||||
"params",
|
||||
"parcel",
|
||||
"pathname",
|
||||
"pathnames",
|
||||
"Philippov",
|
||||
"png",
|
||||
"polyfill",
|
||||
"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",
|
||||
"transpiled",
|
||||
"transpiling",
|
||||
"babel",
|
||||
"rollup",
|
||||
"webpack",
|
||||
"parcel",
|
||||
// Common abbreviations
|
||||
"utils",
|
||||
"util",
|
||||
"impl",
|
||||
"impls",
|
||||
"spec",
|
||||
"specs",
|
||||
"param",
|
||||
"params",
|
||||
"arg",
|
||||
"args",
|
||||
"env",
|
||||
"envs",
|
||||
"var",
|
||||
"vars",
|
||||
"tmp",
|
||||
"temp",
|
||||
"auth",
|
||||
"admin",
|
||||
"api",
|
||||
"apis",
|
||||
"url",
|
||||
"urls",
|
||||
"treemap",
|
||||
"treeshake",
|
||||
"treeshaking",
|
||||
"tsconfig",
|
||||
"tsx",
|
||||
"ttf",
|
||||
"typeof",
|
||||
"typescript",
|
||||
"uri",
|
||||
"uris",
|
||||
"http",
|
||||
"https",
|
||||
"localhost",
|
||||
"hostname",
|
||||
"subdomain",
|
||||
"namespace",
|
||||
"namespaces",
|
||||
// File extensions
|
||||
"json",
|
||||
"yaml",
|
||||
"toml",
|
||||
"xml",
|
||||
"html",
|
||||
"css",
|
||||
"scss",
|
||||
"sass",
|
||||
"less",
|
||||
"svg",
|
||||
"png",
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"gif",
|
||||
"url",
|
||||
"urls",
|
||||
"useCallback",
|
||||
"useContext",
|
||||
"useEffect",
|
||||
"useMemo",
|
||||
"useReducer",
|
||||
"useRef",
|
||||
"useState",
|
||||
"util",
|
||||
"utils",
|
||||
"var",
|
||||
"vars",
|
||||
"vite",
|
||||
"vitejs",
|
||||
"vitest",
|
||||
"webContents",
|
||||
"webkit",
|
||||
"webp",
|
||||
"ico",
|
||||
"webpack",
|
||||
"webSecurity",
|
||||
"whitespace",
|
||||
"wmic",
|
||||
"woff",
|
||||
"woff2",
|
||||
"ttf",
|
||||
"otf",
|
||||
"eot",
|
||||
// Common contractions and informal words
|
||||
"doesn",
|
||||
"don",
|
||||
"won",
|
||||
"can",
|
||||
"couldn",
|
||||
"shouldn",
|
||||
"wordbreak",
|
||||
"wordspacing",
|
||||
"wordwrap",
|
||||
"wouldn",
|
||||
// Documentation examples
|
||||
"newpackagename"
|
||||
"xml",
|
||||
"yaml"
|
||||
],
|
||||
"ignorePaths": [
|
||||
"node_modules/**",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@ import { visualizer } from 'rollup-plugin-visualizer';
|
|||
export default defineConfig({
|
||||
main: {
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
define: {
|
||||
'process.env.VITE_DEV_SERVER_URL': JSON.stringify(
|
||||
process.env.VITE_DEV_SERVER_URL
|
||||
|
|
@ -14,6 +19,11 @@ export default defineConfig({
|
|||
},
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
},
|
||||
renderer: {
|
||||
root: '.',
|
||||
|
|
|
|||
108
eslint.config.ts
108
eslint.config.ts
|
|
@ -1,37 +1,35 @@
|
|||
import js from '@eslint/js';
|
||||
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 reactRefresh from 'eslint-plugin-react-refresh';
|
||||
import react from 'eslint-plugin-react';
|
||||
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 security from 'eslint-plugin-security';
|
||||
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'],
|
||||
},
|
||||
js.configs.recommended,
|
||||
sonarjs.configs.recommended,
|
||||
security.configs.recommended,
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
files: ['**/*.{js,mjs,cjs,ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': tseslint,
|
||||
|
|
@ -39,18 +37,36 @@ const config: Linter.Config[] = [
|
|||
'react-refresh': reactRefresh,
|
||||
react: react,
|
||||
import: importPlugin,
|
||||
sonarjs: sonarjs,
|
||||
'@cspell': cspell,
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// Use recommended rules from plugins
|
||||
...tseslint.configs.recommended.rules,
|
||||
...reactHooks.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': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
// Override only the specific modern React patterns we want to enforce
|
||||
'react/function-component-definition': [
|
||||
'error',
|
||||
{
|
||||
|
|
@ -59,6 +75,8 @@ const config: Linter.Config[] = [
|
|||
},
|
||||
],
|
||||
'react/react-in-jsx-scope': 'off', // Not needed with new JSX transform
|
||||
|
||||
// No default React imports - force specific imports
|
||||
'no-restricted-imports': [
|
||||
'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/prefer-default-export': 'off',
|
||||
|
||||
// Enforce arrow function shorthand when possible
|
||||
'arrow-body-style': ['error', 'as-needed'],
|
||||
'prefer-arrow-callback': ['error', { allowNamedFunctions: false }],
|
||||
// Disallow console.log usage
|
||||
|
||||
// Forbid console.log usage
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
// Warn about unnecessary explicit type annotations
|
||||
|
||||
// TypeScript rules
|
||||
'@typescript-eslint/no-inferrable-types': 'warn',
|
||||
// Don't require explicit return types (prefer inference)
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
// Disable some overly strict security rules for Electron apps
|
||||
'security/detect-non-literal-fs-filename': 'off',
|
||||
'security/detect-object-injection': 'off',
|
||||
// Relax cognitive complexity for complex business logic
|
||||
|
||||
// SonarJS rules - keep cognitive complexity reasonable
|
||||
'sonarjs/cognitive-complexity': ['warn', 25],
|
||||
|
||||
// Spell checking for code
|
||||
'@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
|
||||
files: [
|
||||
'*.config.*',
|
||||
'vite.config.*',
|
||||
'tailwind.config.*',
|
||||
'eslint.config.*',
|
||||
'postcss.config.*',
|
||||
],
|
||||
|
|
@ -106,36 +136,6 @@ const config: Linter.Config[] = [
|
|||
'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;
|
||||
|
|
|
|||
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",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"lint:check": "eslint . --max-warnings 0",
|
||||
"type-check": "tsc --noEmit",
|
||||
"compile": "tsc --noEmit",
|
||||
"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",
|
||||
"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"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
|
@ -44,12 +43,11 @@
|
|||
"ai",
|
||||
"llm"
|
||||
],
|
||||
"author": "",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"author": "Egor Philippov",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@cspell/eslint-plugin": "^9.2.0",
|
||||
"@eslint/js": "^9.33.0",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@types/node": "^24.2.1",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
|
|
@ -66,14 +64,13 @@
|
|||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"eslint-plugin-security": "^3.0.1",
|
||||
"eslint-plugin-sonarjs": "^3.0.4",
|
||||
"globals": "^16.3.0",
|
||||
"husky": "^9.1.7",
|
||||
"jiti": "^2.5.1",
|
||||
"lint-staged": "^16.1.5",
|
||||
"prettier": "^3.6.2",
|
||||
"rollup-plugin-visualizer": "^6.0.3",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.1.2",
|
||||
"wait-on": "^8.0.4"
|
||||
|
|
@ -82,23 +79,41 @@
|
|||
"@emotion/react": "^11.14.0",
|
||||
"@mantine/core": "^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-dom": "^19.1.1"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.friendlykobold.app",
|
||||
"productName": "FriendlyKobold",
|
||||
"compression": "maximum",
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"dist-electron/**/*",
|
||||
"node_modules/**/*"
|
||||
"out/**/*",
|
||||
"!**/node_modules/**/*",
|
||||
"!out/renderer/node_modules/**/*",
|
||||
"!**/*.map",
|
||||
"!**/*.ts",
|
||||
"!**/*.tsx",
|
||||
"!**/test/**/*",
|
||||
"!**/tests/**/*",
|
||||
"!**/__tests__/**/*",
|
||||
"!**/coverage/**/*",
|
||||
"!**/.nyc_output/**/*",
|
||||
"package.json"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"!**/node_modules/**/*"
|
||||
],
|
||||
"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": "dmg",
|
||||
|
|
@ -110,7 +125,6 @@
|
|||
]
|
||||
},
|
||||
"win": {
|
||||
"icon": "assets/icon.ico",
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
|
|
@ -121,7 +135,6 @@
|
|||
]
|
||||
},
|
||||
"linux": {
|
||||
"icon": "assets/icon.png",
|
||||
"target": [
|
||||
{
|
||||
"target": "AppImage",
|
||||
|
|
@ -130,6 +143,9 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"nsis": {
|
||||
"differentialPackage": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
const config = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
215
src/App.tsx
215
src/App.tsx
|
|
@ -1,19 +1,82 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { AppShell, Group, ActionIcon, Tooltip, rem } from '@mantine/core';
|
||||
import { IconSettings } from '@tabler/icons-react';
|
||||
import { useState, useEffect, ReactNode } from 'react';
|
||||
import {
|
||||
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 { LaunchScreen } from '@/screens/LaunchScreen';
|
||||
import { TerminalScreen } from '@/screens/TerminalScreen';
|
||||
import { UpdateDialog } from '@/components/UpdateDialog';
|
||||
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 = () => {
|
||||
const [currentScreen, setCurrentScreen] = useState<Screen>('download');
|
||||
const [currentScreen, setCurrentScreen] = useState<Screen | null>(null);
|
||||
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
|
||||
const [showUpdateDialog, setShowUpdateDialog] = useState(false);
|
||||
const [settingsOpened, setSettingsOpened] = useState(false);
|
||||
const [hasInitialized, setHasInitialized] = useState(false);
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const notify = useNotifications();
|
||||
|
||||
useEffect(() => {
|
||||
const checkInstallation = async () => {
|
||||
|
|
@ -21,9 +84,13 @@ export const App = () => {
|
|||
try {
|
||||
const installed = await window.electronAPI.kobold.isInstalled();
|
||||
setCurrentScreen(installed ? 'launch' : 'download');
|
||||
setHasInitialized(true);
|
||||
} catch (error) {
|
||||
console.error('Error checking installation:', error);
|
||||
setHasInitialized(true);
|
||||
}
|
||||
} else {
|
||||
setHasInitialized(true);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -34,17 +101,60 @@ export const App = () => {
|
|||
setUpdateInfo(info);
|
||||
setShowUpdateDialog(true);
|
||||
});
|
||||
}
|
||||
|
||||
const cleanupInstallDirListener =
|
||||
window.electronAPI.kobold.onInstallDirChanged(() => {
|
||||
checkInstallation();
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (window.electronAPI) {
|
||||
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');
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
|
|
@ -57,27 +167,94 @@ export const App = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Notifications
|
||||
position="bottom-right"
|
||||
zIndex={1000}
|
||||
containerWidth={320}
|
||||
/>
|
||||
<AppShell header={{ height: 60 }} padding="md">
|
||||
<AppShell.Header>
|
||||
<Group h="100%" px="md" justify="flex-end">
|
||||
<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"
|
||||
size="lg"
|
||||
color="gray"
|
||||
size="xl"
|
||||
onClick={() => setSettingsOpened(true)}
|
||||
aria-label="Open settings"
|
||||
style={{
|
||||
transition: 'all 200ms ease',
|
||||
}}
|
||||
>
|
||||
<IconSettings style={{ width: rem(18), height: rem(18) }} />
|
||||
<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>
|
||||
|
||||
<AppShell.Main>
|
||||
{currentScreen === 'download' && (
|
||||
<DownloadScreen onInstallComplete={handleInstallComplete} />
|
||||
<ScreenTransition
|
||||
isActive={currentScreen === 'launch'}
|
||||
shouldAnimate={hasInitialized}
|
||||
>
|
||||
<LaunchScreen onLaunch={handleLaunch} />
|
||||
</ScreenTransition>
|
||||
|
||||
<ScreenTransition
|
||||
isActive={currentScreen === 'terminal'}
|
||||
shouldAnimate={hasInitialized}
|
||||
>
|
||||
<TerminalScreen />
|
||||
</ScreenTransition>
|
||||
</>
|
||||
)}
|
||||
{currentScreen === 'launch' && <LaunchScreen />}
|
||||
|
||||
{showUpdateDialog && updateInfo && (
|
||||
<UpdateDialog
|
||||
|
|
@ -87,11 +264,11 @@ export const App = () => {
|
|||
/>
|
||||
)}
|
||||
</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 { IconDownload } from '@tabler/icons-react';
|
||||
import {
|
||||
Card,
|
||||
Stack,
|
||||
Group,
|
||||
Text,
|
||||
Badge,
|
||||
Button,
|
||||
Loader,
|
||||
Progress,
|
||||
} from '@mantine/core';
|
||||
import { Download } from 'lucide-react';
|
||||
import { MouseEvent } from 'react';
|
||||
|
||||
interface DownloadOptionCardProps {
|
||||
|
|
@ -9,6 +18,7 @@ interface DownloadOptionCardProps {
|
|||
isSelected: boolean;
|
||||
isRecommended: boolean;
|
||||
isDownloading: boolean;
|
||||
downloadProgress?: number;
|
||||
onClick: () => void;
|
||||
onDownload: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
|
|
@ -20,6 +30,7 @@ export const DownloadOptionCard = ({
|
|||
isSelected,
|
||||
isRecommended,
|
||||
isDownloading,
|
||||
downloadProgress = 0,
|
||||
onClick,
|
||||
onDownload,
|
||||
}: DownloadOptionCardProps) => (
|
||||
|
|
@ -52,16 +63,12 @@ export const DownloadOptionCard = ({
|
|||
</Text>
|
||||
|
||||
{isSelected && (
|
||||
<Group justify="center" pt="sm">
|
||||
<Stack gap="sm" pt="sm">
|
||||
<Button
|
||||
onClick={onDownload}
|
||||
disabled={isDownloading}
|
||||
leftSection={
|
||||
isDownloading ? (
|
||||
<Loader size="1rem" />
|
||||
) : (
|
||||
<IconDownload size="1rem" />
|
||||
)
|
||||
isDownloading ? <Loader size="1rem" /> : <Download size="1rem" />
|
||||
}
|
||||
size="sm"
|
||||
radius="md"
|
||||
|
|
@ -69,7 +76,16 @@ export const DownloadOptionCard = ({
|
|||
>
|
||||
{isDownloading ? 'Downloading...' : 'Download'}
|
||||
</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>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -11,25 +11,24 @@ import {
|
|||
Button,
|
||||
Card,
|
||||
Badge,
|
||||
Progress,
|
||||
Loader,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconSettings,
|
||||
IconPalette,
|
||||
IconMoon,
|
||||
IconSun,
|
||||
IconDeviceDesktop,
|
||||
IconAdjustments,
|
||||
IconFolder,
|
||||
IconFolderOpen,
|
||||
IconVersions,
|
||||
IconDownload,
|
||||
IconRefresh,
|
||||
} from '@tabler/icons-react';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import {
|
||||
getPlatformDisplayName,
|
||||
isAssetCompatibleWithPlatform,
|
||||
} from '@/utils/platform';
|
||||
Settings,
|
||||
Palette,
|
||||
Moon,
|
||||
Sun,
|
||||
Monitor,
|
||||
SlidersHorizontal,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
GitBranch,
|
||||
Download,
|
||||
RotateCcw,
|
||||
} from 'lucide-react';
|
||||
import { useTheme, type ThemeMode } from '@/contexts/ThemeContext';
|
||||
import { isAssetCompatibleWithPlatform } from '@/utils/platform';
|
||||
import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron';
|
||||
|
||||
interface SettingsModalProps {
|
||||
|
|
@ -40,7 +39,6 @@ interface SettingsModalProps {
|
|||
export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||
const { themeMode, setThemeMode } = useTheme();
|
||||
const [installDir, setInstallDir] = useState<string>('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [installedVersions, setInstalledVersions] = useState<
|
||||
InstalledVersion[]
|
||||
>([]);
|
||||
|
|
@ -52,12 +50,15 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
|||
);
|
||||
const [loadingRelease, setLoadingRelease] = useState(false);
|
||||
const [downloading, setDownloading] = useState<string | null>(null);
|
||||
const [downloadProgress, setDownloadProgress] = useState<{
|
||||
[key: string]: number;
|
||||
}>({});
|
||||
const [downloadingROCm, setDownloadingROCm] = useState(false);
|
||||
const [rocmDownload, setRocmDownload] = useState<{
|
||||
name: string;
|
||||
url: string;
|
||||
size: number;
|
||||
type: 'rocm';
|
||||
version?: string;
|
||||
} | null>(null);
|
||||
const [userPlatform, setUserPlatform] = useState<string>('');
|
||||
const [activeTab, setActiveTab] = useState('general');
|
||||
|
|
@ -85,8 +86,24 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
|||
loadLatestRelease();
|
||||
loadROCmDownload();
|
||||
loadUserPlatform();
|
||||
|
||||
// Set up progress listener
|
||||
const handleProgress = (progress: number) => {
|
||||
if (downloading) {
|
||||
setDownloadProgress((prev) => ({
|
||||
...prev,
|
||||
[downloading]: progress,
|
||||
}));
|
||||
}
|
||||
}, [opened]);
|
||||
};
|
||||
|
||||
window.electronAPI.kobold.onDownloadProgress?.(handleProgress);
|
||||
|
||||
return () => {
|
||||
window.electronAPI.kobold.removeAllListeners?.('download-progress');
|
||||
};
|
||||
}
|
||||
}, [opened, downloading]);
|
||||
|
||||
const loadCurrentInstallDir = async () => {
|
||||
try {
|
||||
|
|
@ -141,6 +158,8 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
|||
|
||||
const handleDownload = async (assetName: string, downloadUrl: string) => {
|
||||
setDownloading(assetName);
|
||||
setDownloadProgress((prev) => ({ ...prev, [assetName]: 0 }));
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.kobold.downloadRelease({
|
||||
name: assetName,
|
||||
|
|
@ -157,6 +176,11 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
|||
console.error('Failed to download:', error);
|
||||
} finally {
|
||||
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 () => {
|
||||
if (!window.electronAPI) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const selectedDir =
|
||||
await window.electronAPI.kobold.selectInstallDirectory();
|
||||
|
||||
if (selectedDir) {
|
||||
setInstallDir(selectedDir);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to select install directory:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -278,7 +300,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
|||
onClose={onClose}
|
||||
title={
|
||||
<Group gap="xs">
|
||||
<IconSettings size={20} />
|
||||
<Settings size={20} />
|
||||
<Text fw={500}>Settings</Text>
|
||||
</Group>
|
||||
}
|
||||
|
|
@ -319,7 +341,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
|||
<Tabs.Tab
|
||||
value="general"
|
||||
leftSection={
|
||||
<IconAdjustments style={{ width: rem(16), height: rem(16) }} />
|
||||
<SlidersHorizontal style={{ width: rem(16), height: rem(16) }} />
|
||||
}
|
||||
>
|
||||
General
|
||||
|
|
@ -327,7 +349,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
|||
<Tabs.Tab
|
||||
value="versions"
|
||||
leftSection={
|
||||
<IconVersions style={{ width: rem(16), height: rem(16) }} />
|
||||
<GitBranch style={{ width: rem(16), height: rem(16) }} />
|
||||
}
|
||||
>
|
||||
Versions
|
||||
|
|
@ -335,7 +357,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
|||
<Tabs.Tab
|
||||
value="appearance"
|
||||
leftSection={
|
||||
<IconPalette style={{ width: rem(16), height: rem(16) }} />
|
||||
<Palette style={{ width: rem(16), height: rem(16) }} />
|
||||
}
|
||||
>
|
||||
Appearance
|
||||
|
|
@ -349,7 +371,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
|||
Installation Directory
|
||||
</Text>
|
||||
<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>
|
||||
<Group gap="xs">
|
||||
<TextInput
|
||||
|
|
@ -358,17 +380,14 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
|||
placeholder="Default installation directory"
|
||||
style={{ flex: 1 }}
|
||||
leftSection={
|
||||
<IconFolder style={{ width: rem(16), height: rem(16) }} />
|
||||
<Folder style={{ width: rem(16), height: rem(16) }} />
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleSelectInstallDir}
|
||||
loading={loading}
|
||||
leftSection={
|
||||
<IconFolderOpen
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
/>
|
||||
<FolderOpen style={{ width: rem(16), height: rem(16) }} />
|
||||
}
|
||||
>
|
||||
Browse
|
||||
|
|
@ -389,39 +408,44 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
|||
onClick={loadLatestRelease}
|
||||
loading={loadingRelease}
|
||||
leftSection={
|
||||
<IconRefresh style={{ width: rem(14), height: rem(14) }} />
|
||||
<RotateCcw style={{ width: rem(14), height: rem(14) }} />
|
||||
}
|
||||
>
|
||||
Check for Updates
|
||||
</Button>
|
||||
</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">
|
||||
{(() => {
|
||||
const allVersions = getAllVersions();
|
||||
return allVersions.map((version, index) => (
|
||||
{(() =>
|
||||
getAllVersions().map((version, index) => (
|
||||
<Card
|
||||
key={`${version.name}-${version.version}-${index}`}
|
||||
withBorder
|
||||
radius="sm"
|
||||
padding="sm"
|
||||
style={(() => {
|
||||
let backgroundColor = undefined;
|
||||
if (version.isCurrent) {
|
||||
backgroundColor = 'var(--mantine-color-blue-0)';
|
||||
}
|
||||
return {
|
||||
backgroundColor,
|
||||
borderColor: version.isCurrent
|
||||
? 'var(--mantine-color-blue-6)'
|
||||
style={{
|
||||
cursor:
|
||||
version.isDownloaded && !version.isCurrent
|
||||
? 'pointer'
|
||||
: 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">
|
||||
<div style={{ flex: 1 }}>
|
||||
|
|
@ -459,33 +483,38 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
|||
loading={downloading === version.name}
|
||||
disabled={downloading !== null}
|
||||
leftSection={
|
||||
<IconDownload
|
||||
downloading === version.name ? (
|
||||
<Loader size="1rem" />
|
||||
) : (
|
||||
<Download
|
||||
style={{ width: rem(14), height: rem(14) }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
)}
|
||||
{version.isDownloaded &&
|
||||
!version.isCurrent &&
|
||||
version.installedData && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
color="blue"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleVersionSelect(version.installedData!);
|
||||
}}
|
||||
>
|
||||
Make Current
|
||||
{downloading === version.name
|
||||
? 'Downloading...'
|
||||
: 'Download'}
|
||||
</Button>
|
||||
)}
|
||||
</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>
|
||||
));
|
||||
})()}
|
||||
)))()}
|
||||
|
||||
{userPlatform === 'linux' && rocmDownload && (
|
||||
<Card withBorder radius="sm" padding="sm">
|
||||
|
|
@ -497,7 +526,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
|||
</Text>
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed">
|
||||
AMD GPU support with ROCm
|
||||
Version {rocmDownload.version || 'latest'}
|
||||
{rocmDownload.size && (
|
||||
<>
|
||||
{' '}
|
||||
|
|
@ -519,7 +548,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
|||
loading={downloadingROCm}
|
||||
disabled={downloading !== null || downloadingROCm}
|
||||
leftSection={
|
||||
<IconDownload
|
||||
<Download
|
||||
style={{ width: rem(14), height: rem(14) }}
|
||||
/>
|
||||
}
|
||||
|
|
@ -556,14 +585,12 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
|||
<SegmentedControl
|
||||
fullWidth
|
||||
value={themeMode}
|
||||
onChange={(value) =>
|
||||
setThemeMode(value as 'light' | 'dark' | 'system')
|
||||
}
|
||||
onChange={(value) => setThemeMode(value as ThemeMode)}
|
||||
data={[
|
||||
{
|
||||
label: (
|
||||
<Group gap="xs" justify="center">
|
||||
<IconSun style={{ width: rem(16), height: rem(16) }} />
|
||||
<Sun style={{ width: rem(16), height: rem(16) }} />
|
||||
<span>Light</span>
|
||||
</Group>
|
||||
),
|
||||
|
|
@ -572,7 +599,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
|||
{
|
||||
label: (
|
||||
<Group gap="xs" justify="center">
|
||||
<IconMoon style={{ width: rem(16), height: rem(16) }} />
|
||||
<Moon style={{ width: rem(16), height: rem(16) }} />
|
||||
<span>Dark</span>
|
||||
</Group>
|
||||
),
|
||||
|
|
@ -581,9 +608,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
|||
{
|
||||
label: (
|
||||
<Group gap="xs" justify="center">
|
||||
<IconDeviceDesktop
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
/>
|
||||
<Monitor style={{ width: rem(16), height: rem(16) }} />
|
||||
<span>System</span>
|
||||
</Group>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,19 +1,5 @@
|
|||
import { Modal, Text, Button, Group, Stack } from '@mantine/core';
|
||||
|
||||
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;
|
||||
}
|
||||
import type { GitHubRelease } from '@/types';
|
||||
|
||||
interface UpdateDialogProps {
|
||||
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';
|
||||
import { MantineColorScheme } from '@mantine/core';
|
||||
|
||||
type ThemeMode = 'light' | 'dark' | 'system';
|
||||
export type ThemeMode = 'light' | 'dark' | 'system';
|
||||
|
||||
interface ThemeContextType {
|
||||
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';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@ import { join } from 'path';
|
|||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
|
||||
import { WindowManager } from './managers/WindowManager';
|
||||
import { ConfigManager } from './managers/ConfigManager';
|
||||
import { KoboldCppManager } from './managers/KoboldCppManager';
|
||||
import { GitHubService } from './services/GitHubService';
|
||||
import { GPUService } from './services/GPUService';
|
||||
import { IPCHandlers } from './utils/IPCHandlers';
|
||||
import { WindowManager } from '@/main/managers/WindowManager';
|
||||
import { ConfigManager } from '@/main/managers/ConfigManager';
|
||||
import { KoboldCppManager } from '@/main/managers/KoboldCppManager';
|
||||
import { GitHubService } from '@/main/services/GitHubService';
|
||||
import { GPUService } from '@/main/services/GPUService';
|
||||
import { IPCHandlers } from '@/main/utils/IPCHandlers';
|
||||
import { APP_NAME, CONFIG_FILE_NAME } from '@/constants/app';
|
||||
|
||||
class FriendlyKoboldApp {
|
||||
private windowManager: WindowManager;
|
||||
|
|
@ -25,7 +26,8 @@ class FriendlyKoboldApp {
|
|||
this.gpuService = new GPUService();
|
||||
this.koboldManager = new KoboldCppManager(
|
||||
this.configManager,
|
||||
this.githubService
|
||||
this.githubService,
|
||||
this.windowManager
|
||||
);
|
||||
this.ipcHandlers = new IPCHandlers(
|
||||
this.koboldManager,
|
||||
|
|
@ -38,7 +40,7 @@ class FriendlyKoboldApp {
|
|||
}
|
||||
|
||||
private getConfigPath() {
|
||||
return join(app.getPath('userData'), 'config.json');
|
||||
return join(app.getPath('userData'), CONFIG_FILE_NAME);
|
||||
}
|
||||
|
||||
private getDefaultInstallPath() {
|
||||
|
|
@ -47,11 +49,11 @@ class FriendlyKoboldApp {
|
|||
|
||||
switch (platform) {
|
||||
case 'win32':
|
||||
return join(home, 'FriendlyKobold');
|
||||
return join(home, APP_NAME);
|
||||
case 'darwin':
|
||||
return join(home, 'Applications', 'FriendlyKobold');
|
||||
return join(home, 'Applications', APP_NAME);
|
||||
default:
|
||||
return join(home, '.local', 'share', 'friendly-kobold');
|
||||
return join(home, '.local', 'share', APP_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,11 +78,25 @@ class FriendlyKoboldApp {
|
|||
this.ipcHandlers.setupHandlers();
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
if (process.platform === 'darwin') {
|
||||
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', () => {
|
||||
if (!this.windowManager.getMainWindow()) {
|
||||
this.windowManager.createMainWindow();
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ type ConfigValue = string | number | boolean | unknown[] | undefined;
|
|||
|
||||
interface AppConfig {
|
||||
installDir?: string;
|
||||
currentVersion?: string;
|
||||
currentKoboldBinary?: string;
|
||||
selectedConfig?: string;
|
||||
serverOnly?: boolean;
|
||||
modelPath?: string;
|
||||
[key: string]: ConfigValue;
|
||||
}
|
||||
|
||||
|
|
@ -55,12 +57,12 @@ export class ConfigManager {
|
|||
this.saveConfig();
|
||||
}
|
||||
|
||||
getCurrentVersion(): string | undefined {
|
||||
return this.config.currentVersion;
|
||||
getCurrentKoboldBinary(): string | undefined {
|
||||
return this.config.currentKoboldBinary as string | undefined;
|
||||
}
|
||||
|
||||
setCurrentVersion(version: string) {
|
||||
this.config.currentVersion = version;
|
||||
setCurrentKoboldBinary(binaryPath: string) {
|
||||
this.config.currentKoboldBinary = binaryPath;
|
||||
this.saveConfig();
|
||||
}
|
||||
|
||||
|
|
@ -72,4 +74,22 @@ export class ConfigManager {
|
|||
this.config.selectedConfig = configName;
|
||||
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 { join } from 'path';
|
||||
import { existsSync, readdirSync, statSync, createWriteStream } from 'fs';
|
||||
import {
|
||||
existsSync,
|
||||
readdirSync,
|
||||
statSync,
|
||||
createWriteStream,
|
||||
chmodSync,
|
||||
readFileSync,
|
||||
} from 'fs';
|
||||
import { dialog } from 'electron';
|
||||
import { GitHubService } from '../services/GitHubService';
|
||||
import { ConfigManager } from './ConfigManager';
|
||||
import { GitHubService } from '@/main/services/GitHubService';
|
||||
import { ConfigManager } from '@/main/managers/ConfigManager';
|
||||
import { WindowManager } from '@/main/managers/WindowManager';
|
||||
import { APP_NAME, DIALOG_TITLES, ROCM } from '@/constants/app';
|
||||
|
||||
interface GitHubAsset {
|
||||
name: string;
|
||||
|
|
@ -39,7 +48,6 @@ interface ReleaseWithStatus {
|
|||
export interface InstalledVersion {
|
||||
version: string;
|
||||
path: string;
|
||||
type: 'github' | 'rocm';
|
||||
downloadDate: string;
|
||||
filename: string;
|
||||
}
|
||||
|
|
@ -49,13 +57,19 @@ export class KoboldCppManager {
|
|||
private koboldProcess: ChildProcess | null = null;
|
||||
private configManager: ConfigManager;
|
||||
private githubService: GitHubService;
|
||||
private windowManager: WindowManager;
|
||||
|
||||
constructor(configManager: ConfigManager, githubService: GitHubService) {
|
||||
constructor(
|
||||
configManager: ConfigManager,
|
||||
githubService: GitHubService,
|
||||
windowManager: WindowManager
|
||||
) {
|
||||
this.configManager = configManager;
|
||||
this.githubService = githubService;
|
||||
this.windowManager = windowManager;
|
||||
this.installDir =
|
||||
this.configManager.getInstallDir() ||
|
||||
join(process.env.HOME || process.env.USERPROFILE || '.', 'KoboldCpp');
|
||||
join(process.env.HOME || process.env.USERPROFILE || '.', APP_NAME);
|
||||
}
|
||||
|
||||
async downloadRelease(
|
||||
|
|
@ -105,14 +119,23 @@ export class KoboldCppManager {
|
|||
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;
|
||||
}
|
||||
|
||||
async getInstalledVersions(): Promise<InstalledVersion[]> {
|
||||
const configData = this.configManager.get('installedVersions');
|
||||
const configVersions: InstalledVersion[] = Array.isArray(configData)
|
||||
? (configData as unknown as InstalledVersion[])
|
||||
: [];
|
||||
const scannedVersions: InstalledVersion[] = [];
|
||||
|
||||
try {
|
||||
|
|
@ -126,22 +149,13 @@ export class KoboldCppManager {
|
|||
statSync(filePath).isFile() &&
|
||||
(file.includes('koboldcpp') || file.includes('kobold'))
|
||||
) {
|
||||
const existingVersion = configVersions.find(
|
||||
(v: InstalledVersion) => v.path === filePath
|
||||
);
|
||||
|
||||
if (existingVersion) {
|
||||
scannedVersions.push(existingVersion);
|
||||
} else {
|
||||
try {
|
||||
const detectedVersion =
|
||||
await this.getVersionFromBinary(filePath);
|
||||
const detectedVersion = await this.getVersionFromBinary(filePath);
|
||||
const version = detectedVersion || 'unknown';
|
||||
|
||||
const newVersion: InstalledVersion = {
|
||||
version,
|
||||
path: filePath,
|
||||
type: 'github',
|
||||
downloadDate: new Date().toISOString(),
|
||||
filename: file,
|
||||
};
|
||||
|
|
@ -153,21 +167,8 @@ export class KoboldCppManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (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;
|
||||
|
|
@ -210,62 +211,89 @@ export class KoboldCppManager {
|
|||
return configFiles.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
async getCurrentVersion(): Promise<InstalledVersion | null> {
|
||||
const versions = await this.getInstalledVersions();
|
||||
const currentVersionString = this.configManager.getCurrentVersion();
|
||||
|
||||
if (currentVersionString) {
|
||||
const found =
|
||||
versions.find((v) => v.version === currentVersionString) || null;
|
||||
return found;
|
||||
async parseConfigFile(filePath: string): Promise<{
|
||||
gpulayers?: number;
|
||||
contextsize?: number;
|
||||
model_param?: string;
|
||||
[key: string]: unknown;
|
||||
} | null> {
|
||||
try {
|
||||
if (!existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fallback =
|
||||
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> {
|
||||
const versions = await this.getInstalledVersions();
|
||||
const currentBinaryPath = this.configManager.getCurrentKoboldBinary();
|
||||
|
||||
if (currentBinaryPath) {
|
||||
const found = versions.find((v) => v.path === currentBinaryPath);
|
||||
if (found && existsSync(found.path)) {
|
||||
return found;
|
||||
}
|
||||
// If the current binary no longer exists, clear it
|
||||
this.configManager.setCurrentKoboldBinary('');
|
||||
}
|
||||
|
||||
// If no current binary is set, return the most recent one
|
||||
return (
|
||||
versions.sort(
|
||||
(a, b) =>
|
||||
new Date(b.downloadDate).getTime() -
|
||||
new Date(a.downloadDate).getTime()
|
||||
)[0] || null;
|
||||
return fallback;
|
||||
)[0] || null
|
||||
);
|
||||
}
|
||||
|
||||
async setCurrentVersion(version: string): Promise<boolean> {
|
||||
const versions = await this.getInstalledVersions();
|
||||
const targetVersion = versions.find((v) => v.version === version);
|
||||
|
||||
if (!targetVersion || !existsSync(targetVersion.path)) {
|
||||
if (targetVersion) {
|
||||
const updatedVersions = versions.filter((v) => v.version !== version);
|
||||
this.configManager.set(
|
||||
'installedVersions',
|
||||
updatedVersions as unknown[]
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
this.configManager.setCurrentVersion(version);
|
||||
|
||||
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[]
|
||||
);
|
||||
}
|
||||
|
||||
if (targetVersion && existsSync(targetVersion.path)) {
|
||||
this.configManager.setCurrentKoboldBinary(targetVersion.path);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async getVersionFromBinary(binaryPath: string): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
|
|
@ -338,48 +366,33 @@ export class KoboldCppManager {
|
|||
return this.installDir;
|
||||
}
|
||||
|
||||
getWindowManager() {
|
||||
return this.windowManager;
|
||||
}
|
||||
|
||||
async selectInstallDirectory(): Promise<string | null> {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
title: 'Select KoboldCpp Installation Directory',
|
||||
title: DIALOG_TITLES.SELECT_INSTALL_DIR,
|
||||
defaultPath: this.installDir,
|
||||
buttonLabel: 'Select Directory',
|
||||
});
|
||||
|
||||
if (!result.canceled && result.filePaths.length > 0) {
|
||||
const newPath = join(result.filePaths[0], 'KoboldCpp');
|
||||
this.installDir = newPath;
|
||||
this.configManager.setInstallDir(newPath);
|
||||
return newPath;
|
||||
this.installDir = result.filePaths[0];
|
||||
this.configManager.setInstallDir(result.filePaths[0]);
|
||||
|
||||
const mainWindow = this.windowManager.getMainWindow();
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('install-dir-changed', result.filePaths[0]);
|
||||
}
|
||||
|
||||
return result.filePaths[0];
|
||||
}
|
||||
|
||||
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(
|
||||
versionPath: string,
|
||||
args: string[] = [],
|
||||
|
|
@ -439,7 +452,6 @@ export class KoboldCppManager {
|
|||
name: string;
|
||||
url: string;
|
||||
size: number;
|
||||
type: 'rocm';
|
||||
version?: string;
|
||||
} | null> {
|
||||
const platform = process.platform;
|
||||
|
|
@ -451,15 +463,14 @@ export class KoboldCppManager {
|
|||
const version = latestRelease?.tag_name?.replace(/^v/, '') || 'unknown';
|
||||
|
||||
return {
|
||||
name: 'koboldcpp-linux-x64-rocm',
|
||||
url: 'https://koboldai.org/cpplinuxrocm',
|
||||
size: 1024 * 1024 * 1024,
|
||||
type: 'rocm',
|
||||
name: ROCM.BINARY_NAME,
|
||||
url: ROCM.DOWNLOAD_URL,
|
||||
size: ROCM.SIZE_BYTES,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
async downloadROCm(): Promise<{
|
||||
async downloadROCm(onProgress?: (progress: number) => void): Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
error?: string;
|
||||
|
|
@ -469,11 +480,11 @@ export class KoboldCppManager {
|
|||
if (platform !== 'linux') {
|
||||
return {
|
||||
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) {
|
||||
return {
|
||||
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);
|
||||
|
||||
response.body?.pipeTo(
|
||||
new WritableStream({
|
||||
write(chunk) {
|
||||
writer.write(chunk);
|
||||
},
|
||||
close() {
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) break;
|
||||
|
||||
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 {
|
||||
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(
|
||||
args: string[] = []
|
||||
args: string[] = [],
|
||||
configFilePath?: string
|
||||
): Promise<{ success: boolean; pid?: number; error?: string }> {
|
||||
try {
|
||||
if (this.koboldProcess) {
|
||||
this.stopKoboldCpp();
|
||||
}
|
||||
|
||||
const currentVersion = await this.getCurrentVersion();
|
||||
if (!currentVersion || !existsSync(currentVersion.path)) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'KoboldCpp not found or no version selected',
|
||||
error: 'KoboldCpp not found',
|
||||
};
|
||||
}
|
||||
|
||||
const child = spawn(currentVersion.path, args, {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
const finalArgs = [...args]; // Start with the provided arguments
|
||||
|
||||
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);
|
||||
child.unref();
|
||||
this.koboldProcess = child;
|
||||
|
||||
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 };
|
||||
} catch (error) {
|
||||
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';
|
||||
|
||||
export class WindowManager {
|
||||
private mainWindow: BrowserWindow | null = null;
|
||||
private tray: Tray | null = null;
|
||||
private isQuitting = false;
|
||||
|
||||
createMainWindow(): BrowserWindow {
|
||||
this.mainWindow = new BrowserWindow({
|
||||
|
|
@ -28,6 +30,14 @@ export class WindowManager {
|
|||
this.mainWindow = null;
|
||||
});
|
||||
|
||||
this.mainWindow.on('close', (event) => {
|
||||
if (this.tray && !this.isQuitting) {
|
||||
event.preventDefault();
|
||||
this.mainWindow?.hide();
|
||||
}
|
||||
});
|
||||
|
||||
this.createSystemTray();
|
||||
this.setupContextMenu();
|
||||
return this.mainWindow;
|
||||
}
|
||||
|
|
@ -36,10 +46,60 @@ export class WindowManager {
|
|||
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() {
|
||||
if (!this.mainWindow) return;
|
||||
|
||||
this.mainWindow.webContents.on('context-menu', (_event, params) => {
|
||||
const hasLinkURL = !!params.linkURL;
|
||||
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Inspect Element',
|
||||
|
|
@ -53,10 +113,10 @@ export class WindowManager {
|
|||
{ label: 'Paste', role: 'paste' },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Select All', role: 'selectAll' },
|
||||
{ type: 'separator' },
|
||||
...(hasLinkURL ? [{ type: 'separator' as const }] : []),
|
||||
{
|
||||
label: 'Open Link in Browser',
|
||||
visible: !!params.linkURL,
|
||||
visible: hasLinkURL,
|
||||
click: () => {
|
||||
if (params.linkURL) {
|
||||
shell.openExternal(params.linkURL);
|
||||
|
|
@ -78,6 +138,7 @@ export class WindowManager {
|
|||
label: 'Quit',
|
||||
accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q',
|
||||
click: () => {
|
||||
this.isQuitting = true;
|
||||
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 {
|
||||
private lastApiCall = 0;
|
||||
|
|
@ -13,16 +14,14 @@ export class GitHubService {
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
'https://api.github.com/repos/LostRuins/koboldcpp/releases/latest'
|
||||
);
|
||||
const response = await fetch(GITHUB_API.LATEST_RELEASE_URL);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
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}`);
|
||||
}
|
||||
|
|
@ -32,7 +31,7 @@ export class GitHubService {
|
|||
return this.cachedRelease;
|
||||
} catch (error) {
|
||||
console.error('Error fetching latest release:', error);
|
||||
return this.cachedRelease || this.getFallbackRelease();
|
||||
return this.cachedRelease;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -47,16 +46,14 @@ export class GitHubService {
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
'https://api.github.com/repos/LostRuins/koboldcpp/releases'
|
||||
);
|
||||
const response = await fetch(GITHUB_API.ALL_RELEASES_URL);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
console.warn('GitHub API rate limit reached, using cached data');
|
||||
return this.cachedReleases.length > 0
|
||||
? this.cachedReleases
|
||||
: [this.getFallbackRelease()];
|
||||
console.warn(
|
||||
'GitHub API rate limit reached, using cached data if available'
|
||||
);
|
||||
return this.cachedReleases;
|
||||
}
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
|
@ -66,32 +63,7 @@ export class GitHubService {
|
|||
return this.cachedReleases;
|
||||
} catch (error) {
|
||||
console.error('Error fetching releases:', error);
|
||||
return this.cachedReleases.length > 0
|
||||
? this.cachedReleases
|
||||
: [this.getFallbackRelease()];
|
||||
return this.cachedReleases;
|
||||
}
|
||||
}
|
||||
|
||||
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 { KoboldCppManager } from '../managers/KoboldCppManager';
|
||||
import { ConfigManager } from '../managers/ConfigManager';
|
||||
import { GitHubService } from '../services/GitHubService';
|
||||
import { GPUService } from '../services/GPUService';
|
||||
import { KoboldCppManager } from '@/main/managers/KoboldCppManager';
|
||||
import { ConfigManager } from '@/main/managers/ConfigManager';
|
||||
import { GitHubService } from '@/main/services/GitHubService';
|
||||
import { GPUService } from '@/main/services/GPUService';
|
||||
|
||||
export class IPCHandlers {
|
||||
private koboldManager: KoboldCppManager;
|
||||
|
|
@ -32,12 +32,44 @@ export class IPCHandlers {
|
|||
this.githubService.getAllReleases()
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
'kobold:downloadRelease',
|
||||
async (_event, asset, onProgress) =>
|
||||
this.koboldManager.downloadRelease(asset, onProgress)
|
||||
ipcMain.handle('kobold:checkForUpdates', async () => {
|
||||
const latest = await this.githubService.getLatestRelease();
|
||||
return latest;
|
||||
});
|
||||
|
||||
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', () =>
|
||||
this.koboldManager.getInstalledVersions()
|
||||
);
|
||||
|
|
@ -74,12 +106,6 @@ export class IPCHandlers {
|
|||
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:getPlatform', () => ({
|
||||
|
|
@ -93,20 +119,20 @@ export class IPCHandlers {
|
|||
|
||||
ipcMain.handle('kobold:downloadROCm', async () => {
|
||||
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) {
|
||||
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', () =>
|
||||
this.koboldManager.getInstalledVersion()
|
||||
);
|
||||
|
|
@ -115,20 +141,58 @@ export class IPCHandlers {
|
|||
this.koboldManager.getVersionFromBinary(binaryPath)
|
||||
);
|
||||
|
||||
ipcMain.handle('kobold:checkForUpdates', () =>
|
||||
this.koboldManager.checkForUpdates()
|
||||
);
|
||||
|
||||
ipcMain.handle('kobold:getLatestReleaseWithStatus', () =>
|
||||
this.koboldManager.getLatestReleaseWithDownloadStatus()
|
||||
);
|
||||
|
||||
ipcMain.handle('kobold:launchKoboldCpp', (_event, args) =>
|
||||
this.koboldManager.launchKoboldCpp(args)
|
||||
ipcMain.handle('kobold:launchKoboldCpp', (_event, args, configFilePath) =>
|
||||
this.koboldManager.launchKoboldCpp(args, configFilePath)
|
||||
);
|
||||
|
||||
ipcMain.handle('kobold:openInstallDialog', () =>
|
||||
this.koboldManager.openInstallDialog()
|
||||
ipcMain.handle('kobold:stopKoboldCpp', () =>
|
||||
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));
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
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 = {
|
||||
isInstalled: () => ipcRenderer.invoke('kobold:isInstalled'),
|
||||
|
|
@ -10,7 +15,6 @@ const koboldAPI: KoboldAPI = {
|
|||
ipcRenderer.invoke('kobold:setCurrentVersion', version),
|
||||
getVersionFromBinary: (binaryPath: string) =>
|
||||
ipcRenderer.invoke('kobold:getVersionFromBinary', binaryPath),
|
||||
checkForUpdates: () => ipcRenderer.invoke('kobold:checkForUpdates'),
|
||||
getLatestReleaseWithStatus: () =>
|
||||
ipcRenderer.invoke('kobold:getLatestReleaseWithStatus'),
|
||||
getROCmDownload: () => ipcRenderer.invoke('kobold:getROCmDownload'),
|
||||
|
|
@ -24,12 +28,19 @@ const koboldAPI: KoboldAPI = {
|
|||
ipcRenderer.invoke('kobold:selectInstallDirectory'),
|
||||
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'),
|
||||
checkForUpdates: () => ipcRenderer.invoke('kobold:checkForUpdates'),
|
||||
getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'),
|
||||
getSelectedConfig: () => ipcRenderer.invoke('kobold:getSelectedConfig'),
|
||||
setSelectedConfig: (configName: string) =>
|
||||
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) => {
|
||||
ipcRenderer.on(
|
||||
'download-progress',
|
||||
|
|
@ -42,6 +53,22 @@ const koboldAPI: KoboldAPI = {
|
|||
(_: 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) => {
|
||||
ipcRenderer.removeAllListeners(channel);
|
||||
},
|
||||
|
|
@ -52,7 +79,20 @@ const appAPI: AppAPI = {
|
|||
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', {
|
||||
kobold: koboldAPI,
|
||||
app: appAPI,
|
||||
config: configAPI,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,16 +1,7 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Text,
|
||||
Title,
|
||||
Loader,
|
||||
Alert,
|
||||
Stack,
|
||||
Container,
|
||||
Progress,
|
||||
} from '@mantine/core';
|
||||
import { IconAlertCircle } from '@tabler/icons-react';
|
||||
import { DownloadOptionCard } from '../components/DownloadOptionCard';
|
||||
import { Card, Text, Title, Loader, Stack, Container } from '@mantine/core';
|
||||
import { useNotifications } from '@/hooks/useNotifications';
|
||||
import { DownloadOptionCard } from '@/components/DownloadOptionCard';
|
||||
import {
|
||||
getPlatformDisplayName,
|
||||
filterAssetsByPlatform,
|
||||
|
|
@ -21,27 +12,15 @@ import {
|
|||
isAssetRecommended,
|
||||
sortAssetsByRecommendation,
|
||||
} from '@/utils/assets';
|
||||
import { ROCM } from '@/constants/app';
|
||||
import type { GitHubAsset, GitHubRelease } from '@/types';
|
||||
|
||||
interface DownloadScreenProps {
|
||||
onInstallComplete: () => void;
|
||||
onDownloadComplete: () => void;
|
||||
}
|
||||
|
||||
interface GitHubAsset {
|
||||
name: string;
|
||||
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) => {
|
||||
export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||
const notify = useNotifications();
|
||||
const [latestRelease, setLatestRelease] = useState<GitHubRelease | null>(
|
||||
null
|
||||
);
|
||||
|
|
@ -53,15 +32,15 @@ export const DownloadScreen = ({ onInstallComplete }: DownloadScreenProps) => {
|
|||
const [loading, setLoading] = useState(true);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [downloadProgress, setDownloadProgress] = useState(0);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [downloadingType, setDownloadingType] = useState<
|
||||
'asset' | 'rocm' | null
|
||||
>(null);
|
||||
const [rocmDownload, setRocmDownload] = useState<{
|
||||
name: string;
|
||||
url: string;
|
||||
size: number;
|
||||
type: 'rocm';
|
||||
version?: string;
|
||||
} | null>(null);
|
||||
const [downloadingROCm, setDownloadingROCm] = useState(false);
|
||||
|
||||
const loadLatestReleaseAndPlatform = useCallback(async () => {
|
||||
if (!window.electronAPI) return;
|
||||
|
|
@ -90,18 +69,25 @@ export const DownloadScreen = ({ onInstallComplete }: DownloadScreenProps) => {
|
|||
setHasAMDGPU(false);
|
||||
}
|
||||
|
||||
if (releaseData) {
|
||||
const filtered = filterAssetsByPlatform(
|
||||
releaseData.assets,
|
||||
platformInfo.platform
|
||||
);
|
||||
setFilteredAssets(filtered);
|
||||
} else {
|
||||
notify.error(
|
||||
'Error',
|
||||
'GitHub API is currently unavailable. Please try again later.'
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to load release information');
|
||||
notify.error('Error', 'Failed to load release information');
|
||||
console.error('Error loading release:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [notify]);
|
||||
|
||||
useEffect(() => {
|
||||
loadLatestReleaseAndPlatform();
|
||||
|
|
@ -119,83 +105,73 @@ export const DownloadScreen = ({ onInstallComplete }: DownloadScreenProps) => {
|
|||
};
|
||||
}, [loadLatestReleaseAndPlatform]);
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!selectedAsset || !window.electronAPI) return;
|
||||
const handleDownload = async (type: 'asset' | 'rocm' = 'asset') => {
|
||||
if (!window.electronAPI) return;
|
||||
if (type === 'asset' && !selectedAsset) return;
|
||||
|
||||
try {
|
||||
setDownloading(true);
|
||||
setDownloadProgress(0);
|
||||
setError(null);
|
||||
setDownloadingType(type);
|
||||
|
||||
const result =
|
||||
await window.electronAPI.kobold.downloadRelease(selectedAsset);
|
||||
type === 'rocm'
|
||||
? await window.electronAPI.kobold.downloadROCm()
|
||||
: await window.electronAPI.kobold.downloadRelease(selectedAsset!);
|
||||
|
||||
if (result.success) {
|
||||
onInstallComplete();
|
||||
onDownloadComplete();
|
||||
} else {
|
||||
setError(result.error || 'Download failed');
|
||||
notify.error(
|
||||
'Download Failed',
|
||||
result.error || `${type === 'rocm' ? 'ROCm' : ''} Download failed`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Download failed');
|
||||
console.error('Download error:', err);
|
||||
notify.error(
|
||||
'Download Failed',
|
||||
`${type === 'rocm' ? 'ROCm' : ''} Download failed`
|
||||
);
|
||||
console.error(`${type === 'rocm' ? 'ROCm' : ''} Download error:`, err);
|
||||
} finally {
|
||||
setDownloading(false);
|
||||
setDownloadProgress(0);
|
||||
setDownloadingType(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadROCm = async () => {
|
||||
if (!window.electronAPI) return;
|
||||
const renderROCmCard = () => {
|
||||
if (!rocmDownload) return null;
|
||||
|
||||
try {
|
||||
setDownloadingROCm(true);
|
||||
setDownloadProgress(0);
|
||||
setError(null);
|
||||
|
||||
const result = await window.electronAPI.kobold.downloadROCm();
|
||||
|
||||
if (result.success) {
|
||||
onInstallComplete();
|
||||
} else {
|
||||
setError(result.error || 'ROCm download failed');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('ROCm download failed');
|
||||
console.error('ROCm download error:', err);
|
||||
} finally {
|
||||
setDownloadingROCm(false);
|
||||
setDownloadProgress(0);
|
||||
return (
|
||||
<DownloadOptionCard
|
||||
name={ROCM.BINARY_NAME}
|
||||
description={getAssetDescription(ROCM.BINARY_NAME)}
|
||||
size={formatFileSize(ROCM.SIZE_BYTES)}
|
||||
isSelected={selectedROCm}
|
||||
isRecommended={isAssetRecommended(ROCM.BINARY_NAME, hasAMDGPU)}
|
||||
isDownloading={downloading && downloadingType === 'rocm'}
|
||||
downloadProgress={downloadingType === 'rocm' ? downloadProgress : 0}
|
||||
onClick={() => {
|
||||
if (!selectedROCm) {
|
||||
setSelectedROCm(true);
|
||||
setSelectedAsset(null);
|
||||
}
|
||||
}}
|
||||
onDownload={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDownload('rocm');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Container size="md" py="xl">
|
||||
<Container size="sm" py="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">
|
||||
<Stack gap="lg">
|
||||
<Title order={3}>Available Downloads for Your Platform</Title>
|
||||
<Title order={3}>Available Binaries for Your Platform</Title>
|
||||
|
||||
{loading ? (
|
||||
<Stack align="center" gap="md" py="xl">
|
||||
|
|
@ -234,31 +210,7 @@ export const DownloadScreen = ({ onInstallComplete }: DownloadScreenProps) => {
|
|||
|
||||
{filteredAssets.length > 0 || rocmDownload ? (
|
||||
<Stack gap="sm">
|
||||
{rocmDownload && hasAMDGPU && (
|
||||
<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();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{rocmDownload && hasAMDGPU && renderROCmCard()}
|
||||
|
||||
{sortAssetsByRecommendation(
|
||||
filteredAssets,
|
||||
|
|
@ -274,7 +226,18 @@ export const DownloadScreen = ({ onInstallComplete }: DownloadScreenProps) => {
|
|||
asset.name,
|
||||
hasAMDGPU
|
||||
)}
|
||||
isDownloading={downloading}
|
||||
isDownloading={
|
||||
downloading &&
|
||||
downloadingType === 'asset' &&
|
||||
selectedAsset?.name === asset.name
|
||||
}
|
||||
downloadProgress={
|
||||
downloading &&
|
||||
downloadingType === 'asset' &&
|
||||
selectedAsset?.name === asset.name
|
||||
? downloadProgress
|
||||
: 0
|
||||
}
|
||||
onClick={() => {
|
||||
if (selectedAsset?.name !== asset.name) {
|
||||
setSelectedAsset(asset);
|
||||
|
|
@ -288,48 +251,18 @@ export const DownloadScreen = ({ onInstallComplete }: DownloadScreenProps) => {
|
|||
/>
|
||||
))}
|
||||
|
||||
{rocmDownload && !hasAMDGPU && (
|
||||
<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();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{rocmDownload && !hasAMDGPU && renderROCmCard()}
|
||||
</Stack>
|
||||
) : (
|
||||
<Alert
|
||||
icon={<IconAlertCircle size="1rem" />}
|
||||
color="yellow"
|
||||
variant="light"
|
||||
>
|
||||
<Card withBorder p="md" bg="yellow.0" c="yellow.9">
|
||||
<Stack gap="xs">
|
||||
<Text fw={500}>No downloads available</Text>
|
||||
<Text size="sm">
|
||||
No downloads available for your platform (
|
||||
{getPlatformDisplayName(userPlatform)}). This might
|
||||
be a new release that doesn't have builds ready
|
||||
yet.
|
||||
{getPlatformDisplayName(userPlatform)}).
|
||||
</Text>
|
||||
</Stack>
|
||||
</Alert>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -7,31 +7,57 @@ import {
|
|||
Stack,
|
||||
Group,
|
||||
ActionIcon,
|
||||
Select,
|
||||
Switch,
|
||||
Slider,
|
||||
TextInput,
|
||||
NumberInput,
|
||||
Tooltip,
|
||||
Checkbox,
|
||||
} from '@mantine/core';
|
||||
import { IconFile, IconRefresh } from '@tabler/icons-react';
|
||||
import { RotateCcw, File, Info } from 'lucide-react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { ConfigFileSelect } from '@/components/ConfigFileSelect';
|
||||
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
||||
import type { ConfigFile } from '@/types';
|
||||
|
||||
interface ConfigFile {
|
||||
name: string;
|
||||
path: string;
|
||||
size: number;
|
||||
interface LaunchScreenProps {
|
||||
onLaunch: () => void;
|
||||
}
|
||||
|
||||
export const LaunchScreen = () => {
|
||||
export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||
const [configFiles, setConfigFiles] = useState<ConfigFile[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||||
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 () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const [files, currentDir, savedConfig] = await Promise.all([
|
||||
window.electronAPI.kobold.getConfigFiles(),
|
||||
window.electronAPI.kobold.getCurrentInstallDir(),
|
||||
window.electronAPI.kobold.getSelectedConfig(),
|
||||
]);
|
||||
|
||||
setConfigFiles(files);
|
||||
setInstallDir(currentDir);
|
||||
|
||||
|
|
@ -40,84 +66,89 @@ export const LaunchScreen = () => {
|
|||
} else if (files.length > 0 && !selectedFile) {
|
||||
setSelectedFile(files[0].name);
|
||||
}
|
||||
|
||||
await loadSavedSettings();
|
||||
|
||||
const currentSelectedFile = await loadConfigFromFile(files, savedConfig);
|
||||
if (currentSelectedFile && !selectedFile) {
|
||||
setSelectedFile(currentSelectedFile);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [selectedFile]);
|
||||
}, [selectedFile, loadSavedSettings, loadConfigFromFile]);
|
||||
|
||||
const handleFileSelection = async (fileName: string) => {
|
||||
setSelectedFile(fileName);
|
||||
await window.electronAPI.kobold.setSelectedConfig(fileName);
|
||||
|
||||
const selectedConfig = configFiles.find((f) => f.name === fileName);
|
||||
if (selectedConfig) {
|
||||
await parseAndApplyConfigFile(selectedConfig.path);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void 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 handleInstallDirChange = () => {
|
||||
void loadConfigFiles();
|
||||
};
|
||||
|
||||
const handleLaunch = async () => {
|
||||
if (!selectedFile) return;
|
||||
const cleanup = window.electronAPI.kobold.onInstallDirChanged(
|
||||
handleInstallDirChange
|
||||
);
|
||||
|
||||
return cleanup;
|
||||
}, [loadConfigFiles]);
|
||||
|
||||
const handleLaunch = async () => {
|
||||
try {
|
||||
const selectedConfig = configFiles.find((f) => f.name === selectedFile);
|
||||
if (selectedConfig) {
|
||||
const result = await window.electronAPI.kobold.launchKoboldCpp([
|
||||
selectedConfig.path,
|
||||
]);
|
||||
const selectedConfig = selectedFile
|
||||
? configFiles.find((f) => f.name === selectedFile)
|
||||
: null;
|
||||
|
||||
const args: string[] = [];
|
||||
|
||||
if (modelPath) {
|
||||
args.push('--model', modelPath);
|
||||
}
|
||||
|
||||
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) {
|
||||
// Launch successful
|
||||
onLaunch();
|
||||
} else {
|
||||
console.error('Launch failed:', result.error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error launching KoboldCpp:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container size="md" py="xl">
|
||||
<Container size="sm" py="xl">
|
||||
<Card withBorder radius="md" shadow="sm">
|
||||
<Stack gap="lg" align="center">
|
||||
<Title order={2}>Launch Configuration</Title>
|
||||
<Stack gap="lg">
|
||||
<Title order={3}>Launch Configuration</Title>
|
||||
|
||||
<Card withBorder radius="md" w="100%">
|
||||
<Group justify="space-between" mb="md">
|
||||
|
|
@ -128,11 +159,198 @@ export const LaunchScreen = () => {
|
|||
loading={loading}
|
||||
size="sm"
|
||||
>
|
||||
<IconRefresh size={16} />
|
||||
<RotateCcw size={16} />
|
||||
</ActionIcon>
|
||||
</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>
|
||||
|
||||
<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>;
|
||||
getLatestReleaseWithStatus: () => Promise<ReleaseWithStatus | null>;
|
||||
launchKoboldCpp: (
|
||||
args?: string[]
|
||||
args?: string[],
|
||||
configFilePath?: string
|
||||
) => Promise<{ success: boolean; pid?: number; error?: string }>;
|
||||
openInstallDialog: () => Promise<{ success: boolean; path?: string }>;
|
||||
getConfigFiles: () => Promise<
|
||||
|
|
@ -81,8 +82,19 @@ export interface KoboldAPI {
|
|||
>;
|
||||
getSelectedConfig: () => Promise<string | null>;
|
||||
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;
|
||||
onUpdateAvailable: (callback: (updateInfo: UpdateInfo) => void) => void;
|
||||
onInstallDirChanged: (callback: (newPath: string) => void) => () => void;
|
||||
onKoboldOutput: (callback: (data: string) => void) => () => void;
|
||||
removeAllListeners: (channel: string) => void;
|
||||
}
|
||||
|
||||
|
|
@ -91,11 +103,21 @@ export interface AppAPI {
|
|||
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 {
|
||||
interface Window {
|
||||
electronAPI: {
|
||||
kobold: KoboldAPI;
|
||||
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 => {
|
||||
const name = assetName.toLowerCase();
|
||||
|
||||
if (name.includes('rocm')) {
|
||||
if (name.includes(ASSET_SUFFIXES.ROCM)) {
|
||||
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.';
|
||||
}
|
||||
|
||||
if (name.endsWith('nocuda')) {
|
||||
return 'Standard build with NVIDIA CUDA removed for minimal file size.';
|
||||
if (name.endsWith(ASSET_SUFFIXES.NOCUDA)) {
|
||||
return 'Standard build with NVIDIA CUDA support removed for minimal file size.';
|
||||
}
|
||||
|
||||
return "Standard build that's ideal for most cases.";
|
||||
|
|
@ -22,15 +24,15 @@ export const isAssetRecommended = (
|
|||
): boolean => {
|
||||
const name = assetName.toLowerCase();
|
||||
|
||||
if (hasAMDGPU && name.includes('rocm')) {
|
||||
if (hasAMDGPU && name.includes(ASSET_SUFFIXES.ROCM)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!hasAMDGPU &&
|
||||
!name.includes('rocm') &&
|
||||
!name.endsWith('oldpc') &&
|
||||
!name.endsWith('nocuda')
|
||||
!name.includes(ASSET_SUFFIXES.ROCM) &&
|
||||
!name.endsWith(ASSET_SUFFIXES.OLDPC) &&
|
||||
!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,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"skipLibCheck": true
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/main/**/*", "src/preload/**/*"],
|
||||
"exclude": ["node_modules", "dist", "dist-electron"]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue