bug fixes and small improvements, new zoom level setting
|
|
@ -72,6 +72,10 @@ The AUR package automatically handles installation, desktop integration, and sys
|
|||
|
||||
<img src="screenshots/gen-img.png" alt="Image Generation" width="600">
|
||||
|
||||
### Seemless SillyTavern integration
|
||||
|
||||
<img src="screenshots/sillytavern.png" alt="SillyTavern integration" width="600">
|
||||
|
||||
</div>
|
||||
<!-- markdownlint-enable MD033 -->
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "gerbil",
|
||||
"productName": "Gerbil",
|
||||
"version": "0.9.4",
|
||||
"version": "0.9.5",
|
||||
"description": "Run Large Language Models locally",
|
||||
"main": "out/main/index.js",
|
||||
"homepage": "./",
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 71 KiB |
BIN
screenshots/sillytavern.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 290 KiB After Width: | Height: | Size: 253 KiB |
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 188 KiB |
36
src/App.tsx
|
|
@ -22,7 +22,6 @@ export const App = () => {
|
|||
const [hasInitialized, setHasInitialized] = useState(false);
|
||||
const [activeInterfaceTab, setActiveInterfaceTab] =
|
||||
useState<InterfaceTab>('terminal');
|
||||
const [isImageGenerationMode, setIsImageGenerationMode] = useState(false);
|
||||
const [frontendPreference, setFrontendPreference] =
|
||||
useState<FrontendPreference>('koboldcpp');
|
||||
|
||||
|
|
@ -159,6 +158,7 @@ export const App = () => {
|
|||
onTabChange={setActiveInterfaceTab}
|
||||
onEject={handleEject}
|
||||
onOpenSettings={() => setModalOpen('settings', true)}
|
||||
frontendPreference={frontendPreference}
|
||||
/>
|
||||
|
||||
<AppShell.Main>
|
||||
|
|
@ -191,10 +191,7 @@ export const App = () => {
|
|||
isActive={currentScreen === 'launch'}
|
||||
shouldAnimate={hasInitialized}
|
||||
>
|
||||
<LaunchScreen
|
||||
onLaunch={handleLaunch}
|
||||
onLaunchModeChange={setIsImageGenerationMode}
|
||||
/>
|
||||
<LaunchScreen onLaunch={handleLaunch} />
|
||||
</ScreenTransition>
|
||||
|
||||
<ScreenTransition
|
||||
|
|
@ -204,28 +201,25 @@ export const App = () => {
|
|||
<InterfaceScreen
|
||||
activeTab={activeInterfaceTab}
|
||||
onTabChange={setActiveInterfaceTab}
|
||||
isImageGenerationMode={isImageGenerationMode}
|
||||
frontendPreference={frontendPreference}
|
||||
/>
|
||||
</ScreenTransition>
|
||||
</>
|
||||
)}
|
||||
|
||||
{showUpdateModal && binaryUpdateInfo && (
|
||||
<UpdateAvailableModal
|
||||
opened={showUpdateModal}
|
||||
onClose={dismissUpdate}
|
||||
currentVersion={binaryUpdateInfo.currentVersion}
|
||||
availableUpdate={binaryUpdateInfo.availableUpdate}
|
||||
onUpdate={handleBinaryUpdate}
|
||||
isDownloading={
|
||||
downloading === binaryUpdateInfo.availableUpdate.name
|
||||
}
|
||||
downloadProgress={
|
||||
downloadProgress[binaryUpdateInfo.availableUpdate.name] || 0
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<UpdateAvailableModal
|
||||
opened={showUpdateModal && !!binaryUpdateInfo}
|
||||
onClose={dismissUpdate}
|
||||
currentVersion={binaryUpdateInfo?.currentVersion}
|
||||
availableUpdate={binaryUpdateInfo?.availableUpdate}
|
||||
onUpdate={handleBinaryUpdate}
|
||||
isDownloading={downloading === binaryUpdateInfo?.availableUpdate.name}
|
||||
downloadProgress={
|
||||
binaryUpdateInfo
|
||||
? downloadProgress[binaryUpdateInfo.availableUpdate.name] || 0
|
||||
: 0
|
||||
}
|
||||
/>
|
||||
</AppShell.Main>
|
||||
<SettingsModal
|
||||
opened={modals.settings}
|
||||
|
|
|
|||
|
|
@ -549,10 +549,12 @@ export const CommandLineArgumentsModal = ({
|
|||
>
|
||||
<Group gap="xs" wrap="wrap" justify="space-between">
|
||||
<Group gap="xs" wrap="wrap" style={{ flex: 1 }}>
|
||||
<Code style={{ fontSize: '14px', fontWeight: 600 }}>{arg.flag}</Code>
|
||||
<Code style={{ fontSize: '0.875rem', fontWeight: 600 }}>
|
||||
{arg.flag}
|
||||
</Code>
|
||||
{arg.aliases &&
|
||||
arg.aliases.map((alias) => (
|
||||
<Code key={alias} style={{ fontSize: '12px' }} c="dimmed">
|
||||
<Code key={alias} style={{ fontSize: '0.75em' }} c="dimmed">
|
||||
{alias}
|
||||
</Code>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -22,11 +22,6 @@ export const ScreenTransition = ({
|
|||
<div
|
||||
style={{
|
||||
...styles,
|
||||
position: isActive ? 'static' : 'absolute',
|
||||
width: '100%',
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: isActive ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,10 @@ import { useState } from 'react';
|
|||
import { soundAssets, playSound, initializeAudio } from '@/utils/sounds';
|
||||
import { useAppUpdateChecker } from '@/hooks/useAppUpdateChecker';
|
||||
import { useModalStore } from '@/stores/modal';
|
||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||
import iconUrl from '/icon.png';
|
||||
import { PRODUCT_NAME, TITLEBAR_HEIGHT } from '@/constants';
|
||||
import type { InterfaceTab, Screen } from '@/types';
|
||||
import { FRONTENDS, PRODUCT_NAME, TITLEBAR_HEIGHT } from '@/constants';
|
||||
import type { FrontendPreference, InterfaceTab, Screen } from '@/types';
|
||||
|
||||
interface TitleBarProps {
|
||||
currentScreen: Screen;
|
||||
|
|
@ -29,6 +30,7 @@ interface TitleBarProps {
|
|||
onTabChange?: (tab: InterfaceTab) => void;
|
||||
onEject?: () => void;
|
||||
onOpenSettings?: () => void;
|
||||
frontendPreference?: FrontendPreference;
|
||||
}
|
||||
|
||||
export const TitleBar = ({
|
||||
|
|
@ -37,16 +39,19 @@ export const TitleBar = ({
|
|||
onTabChange,
|
||||
onEject,
|
||||
onOpenSettings,
|
||||
frontendPreference,
|
||||
}: TitleBarProps) => {
|
||||
const computedColorScheme = useComputedColorScheme('light', {
|
||||
getInitialValueInEffect: false,
|
||||
});
|
||||
const { hasUpdate, openReleasePage } = useAppUpdateChecker();
|
||||
const { isAnyModalOpen } = useModalStore();
|
||||
const { isImageGenerationMode } = useLaunchConfigStore();
|
||||
const [logoClickCount, setLogoClickCount] = useState(0);
|
||||
const [isElephantMode, setIsElephantMode] = useState(false);
|
||||
const [isMouseSqueaking, setIsMouseSqueaking] = useState(false);
|
||||
const [isMaximized, setIsMaximized] = useState(false);
|
||||
const [isSelectOpen, setIsSelectOpen] = useState(false);
|
||||
|
||||
const handleMinimize = () => {
|
||||
window.electronAPI.app.minimizeWindow();
|
||||
|
|
@ -101,7 +106,8 @@ export const TitleBar = ({
|
|||
? 'var(--mantine-color-dark-7)'
|
||||
: 'var(--mantine-color-gray-0)',
|
||||
borderBottom: '1px solid var(--mantine-color-default-border)',
|
||||
WebkitAppRegion: isAnyModalOpen() ? 'no-drag' : 'drag',
|
||||
WebkitAppRegion:
|
||||
isAnyModalOpen() || isSelectOpen ? 'no-drag' : 'drag',
|
||||
userSelect: 'none',
|
||||
position: 'relative',
|
||||
}}
|
||||
|
|
@ -154,10 +160,17 @@ export const TitleBar = ({
|
|||
onTabChange?.(value as InterfaceTab);
|
||||
}
|
||||
}}
|
||||
onDropdownOpen={() => setIsSelectOpen(true)}
|
||||
onDropdownClose={() => setIsSelectOpen(false)}
|
||||
data={[
|
||||
{
|
||||
value: 'chat',
|
||||
label: 'Chat',
|
||||
label:
|
||||
frontendPreference === 'sillytavern'
|
||||
? FRONTENDS.SILLYTAVERN
|
||||
: isImageGenerationMode
|
||||
? FRONTENDS.STABLE_UI
|
||||
: FRONTENDS.KOBOLDAI_LITE,
|
||||
},
|
||||
{ value: 'terminal', label: 'Terminal' },
|
||||
{ value: 'eject', label: 'Eject' },
|
||||
|
|
@ -167,7 +180,7 @@ export const TitleBar = ({
|
|||
size="sm"
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
minWidth: '120px',
|
||||
minWidth: '7.5rem',
|
||||
}}
|
||||
styles={{
|
||||
input: {
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import { safeExecute } from '@/utils/logger';
|
|||
interface UpdateAvailableModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
currentVersion: InstalledVersion;
|
||||
availableUpdate: DownloadItem;
|
||||
currentVersion?: InstalledVersion;
|
||||
availableUpdate?: DownloadItem;
|
||||
onUpdate: (download: DownloadItem) => Promise<void>;
|
||||
isDownloading?: boolean;
|
||||
downloadProgress?: number;
|
||||
|
|
@ -38,11 +38,13 @@ export const UpdateAvailableModal = ({
|
|||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
const handleUpdate = async () => {
|
||||
setIsUpdating(true);
|
||||
await safeExecute(async () => {
|
||||
await onUpdate(availableUpdate);
|
||||
onClose();
|
||||
}, 'Failed to update:');
|
||||
if (availableUpdate) {
|
||||
setIsUpdating(true);
|
||||
await safeExecute(async () => {
|
||||
await onUpdate(availableUpdate);
|
||||
onClose();
|
||||
}, 'Failed to update:');
|
||||
}
|
||||
setIsUpdating(false);
|
||||
};
|
||||
|
||||
|
|
@ -65,7 +67,7 @@ export const UpdateAvailableModal = ({
|
|||
Current Version
|
||||
</Text>
|
||||
<Text fw={500} size="sm">
|
||||
{currentVersion.version}
|
||||
{currentVersion?.version}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
|
|
@ -78,14 +80,16 @@ export const UpdateAvailableModal = ({
|
|||
Available Version
|
||||
</Text>
|
||||
<Text fw={500} size="sm" c="orange">
|
||||
{availableUpdate.version}
|
||||
{availableUpdate?.version}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
<Text size="xs" c="dimmed">
|
||||
Binary: {getDisplayNameFromPath(currentVersion)}
|
||||
</Text>
|
||||
{currentVersion && (
|
||||
<Text size="xs" c="dimmed">
|
||||
Binary: {getDisplayNameFromPath(currentVersion)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Group gap="xs" align="center" mt="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
|
|
@ -93,17 +97,17 @@ export const UpdateAvailableModal = ({
|
|||
</Text>
|
||||
<Anchor
|
||||
size="xs"
|
||||
href={`https://github.com/${GITHUB_API.KOBOLDCPP_REPO}/releases/tag/v${availableUpdate.version}`}
|
||||
href={`https://github.com/${GITHUB_API.KOBOLDCPP_REPO}/releases/tag/v${availableUpdate?.version}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() =>
|
||||
window.electronAPI.app.openExternal(
|
||||
`https://github.com/${GITHUB_API.KOBOLDCPP_REPO}/releases/tag/v${availableUpdate.version}`
|
||||
`https://github.com/${GITHUB_API.KOBOLDCPP_REPO}/releases/tag/v${availableUpdate?.version}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<Group gap={4} align="center">
|
||||
<span>v{availableUpdate.version}</span>
|
||||
<span>v{availableUpdate?.version}</span>
|
||||
<ExternalLink size={12} />
|
||||
</Group>
|
||||
</Anchor>
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
}, [downloading]);
|
||||
|
||||
return (
|
||||
<Container size="sm">
|
||||
<Container size="sm" mt="md">
|
||||
<Stack gap="xl">
|
||||
<Card withBorder radius="md" shadow="sm">
|
||||
<Stack gap="lg">
|
||||
|
|
|
|||
|
|
@ -1,22 +1,20 @@
|
|||
import { useRef } from 'react';
|
||||
import { Box, Text, Stack } from '@mantine/core';
|
||||
import { SILLYTAVERN, FRONTENDS } from '@/constants';
|
||||
import type { ServerTabMode, FrontendPreference } from '@/types';
|
||||
import { SILLYTAVERN, FRONTENDS, TITLEBAR_HEIGHT } from '@/constants';
|
||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||
import type { FrontendPreference } from '@/types';
|
||||
|
||||
interface ServerTabProps {
|
||||
serverUrl?: string;
|
||||
isServerReady?: boolean;
|
||||
mode: ServerTabMode;
|
||||
frontendPreference?: FrontendPreference;
|
||||
}
|
||||
|
||||
export const ServerTab = ({
|
||||
serverUrl,
|
||||
isServerReady,
|
||||
mode,
|
||||
frontendPreference = 'koboldcpp',
|
||||
}: ServerTabProps) => {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const { isImageGenerationMode } = useLaunchConfigStore();
|
||||
|
||||
if (!isServerReady || !serverUrl) {
|
||||
return (
|
||||
|
|
@ -34,8 +32,8 @@ export const ServerTab = ({
|
|||
Waiting for the KoboldCpp server to start...
|
||||
</Text>
|
||||
<Text c="dimmed" size="sm">
|
||||
The {mode === 'chat' ? 'chat' : 'image generation'} interface will
|
||||
load automatically when ready
|
||||
The {isImageGenerationMode ? 'image generation' : 'chat'} interface
|
||||
will load automatically when ready
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
|
@ -49,23 +47,21 @@ export const ServerTab = ({
|
|||
iframeUrl = SILLYTAVERN.PROXY_URL;
|
||||
title = FRONTENDS.SILLYTAVERN;
|
||||
} else {
|
||||
iframeUrl = mode === 'image-generation' ? `${serverUrl}/sdui` : serverUrl;
|
||||
title =
|
||||
mode === 'image-generation'
|
||||
? FRONTENDS.STABLE_UI
|
||||
: FRONTENDS.KOBOLDAI_LITE;
|
||||
iframeUrl = isImageGenerationMode ? `${serverUrl}/sdui` : serverUrl;
|
||||
title = isImageGenerationMode
|
||||
? FRONTENDS.STABLE_UI
|
||||
: FRONTENDS.KOBOLDAI_LITE;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 'calc(100vh - 2rem)',
|
||||
height: `calc(100vh - ${TITLEBAR_HEIGHT})`,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={iframeUrl}
|
||||
title={title}
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from '@mantine/core';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import styles from '@/styles/layout.module.css';
|
||||
import { SERVER_READY_SIGNALS } from '@/constants';
|
||||
import { SERVER_READY_SIGNALS, TITLEBAR_HEIGHT } from '@/constants';
|
||||
import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal';
|
||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||
import type { FrontendPreference } from '@/types';
|
||||
|
|
@ -104,7 +104,7 @@ export const TerminalTab = ({
|
|||
return (
|
||||
<Box
|
||||
style={{
|
||||
height: 'calc(100vh - 2rem)',
|
||||
height: `calc(100vh - ${TITLEBAR_HEIGHT})`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: isDark
|
||||
|
|
@ -133,7 +133,7 @@ export const TerminalTab = ({
|
|||
margin: 0,
|
||||
fontFamily:
|
||||
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
fontSize: '14px',
|
||||
fontSize: '0.875rem',
|
||||
lineHeight: 1.4,
|
||||
color: isDark
|
||||
? 'var(--mantine-color-gray-0)'
|
||||
|
|
@ -158,10 +158,10 @@ export const TerminalTab = ({
|
|||
onClick={scrollToBottom}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '20px',
|
||||
right: '20px',
|
||||
bottom: '1.25rem',
|
||||
right: '1.25rem',
|
||||
zIndex: 10,
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
|
||||
boxShadow: '0 0.125rem 0.5rem rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
aria-label="Scroll to bottom"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -6,14 +6,12 @@ import type { InterfaceTab, FrontendPreference } from '@/types';
|
|||
interface InterfaceScreenProps {
|
||||
activeTab?: InterfaceTab | null;
|
||||
onTabChange?: (tab: InterfaceTab) => void;
|
||||
isImageGenerationMode?: boolean;
|
||||
frontendPreference?: FrontendPreference;
|
||||
}
|
||||
|
||||
export const InterfaceScreen = ({
|
||||
activeTab,
|
||||
onTabChange,
|
||||
isImageGenerationMode = false,
|
||||
frontendPreference = 'koboldcpp',
|
||||
}: InterfaceScreenProps) => {
|
||||
const [serverUrl, setServerUrl] = useState<string>('');
|
||||
|
|
@ -48,7 +46,6 @@ export const InterfaceScreen = ({
|
|||
<ServerTab
|
||||
serverUrl={serverUrl}
|
||||
isServerReady={isServerReady}
|
||||
mode={isImageGenerationMode ? 'image-generation' : 'chat'}
|
||||
frontendPreference={frontendPreference}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
|||
|
||||
export const GeneralTab = () => {
|
||||
const {
|
||||
modelPath,
|
||||
model,
|
||||
contextSize,
|
||||
handleModelPathChange,
|
||||
handleModelChange,
|
||||
handleSelectModelFile,
|
||||
handleContextSizeChangeWithStep,
|
||||
} = useLaunchConfig();
|
||||
|
|
@ -19,10 +19,10 @@ export const GeneralTab = () => {
|
|||
|
||||
<ModelFileField
|
||||
label="Text Model File"
|
||||
value={modelPath}
|
||||
value={model}
|
||||
placeholder="Select a .gguf model file or enter a direct URL to file"
|
||||
tooltip="Select a GGUF text generation model file for chat and completion tasks."
|
||||
onChange={handleModelPathChange}
|
||||
onChange={handleModelChange}
|
||||
onSelectFile={handleSelectModelFile}
|
||||
showSearchHF
|
||||
searchUrl="https://huggingface.co/models?pipeline_tag=text-generation&library=gguf&sort=trending"
|
||||
|
|
|
|||
|
|
@ -15,13 +15,9 @@ import type { ConfigFile } from '@/types';
|
|||
|
||||
interface LaunchScreenProps {
|
||||
onLaunch: () => void;
|
||||
onLaunchModeChange?: (isImageMode: boolean) => void;
|
||||
}
|
||||
|
||||
export const LaunchScreen = ({
|
||||
onLaunch,
|
||||
onLaunchModeChange,
|
||||
}: LaunchScreenProps) => {
|
||||
export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||
const [configFiles, setConfigFiles] = useState<ConfigFile[]>([]);
|
||||
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||||
const [, setInstallDir] = useState<string>('');
|
||||
|
|
@ -33,7 +29,7 @@ export const LaunchScreen = ({
|
|||
gpuLayers,
|
||||
autoGpuLayers,
|
||||
contextSize,
|
||||
modelPath,
|
||||
model,
|
||||
additionalArguments,
|
||||
port,
|
||||
host,
|
||||
|
|
@ -65,19 +61,18 @@ export const LaunchScreen = ({
|
|||
moeexperts,
|
||||
parseAndApplyConfigFile,
|
||||
loadConfigFromFile,
|
||||
handleModelPathChange,
|
||||
handleModelChange,
|
||||
handleBackendChange,
|
||||
} = useLaunchConfig();
|
||||
|
||||
const { isLaunching, handleLaunch } = useLaunchLogic({
|
||||
modelPath,
|
||||
model,
|
||||
sdmodel,
|
||||
onLaunch,
|
||||
onLaunchModeChange,
|
||||
});
|
||||
|
||||
const { warnings: combinedWarnings } = useWarnings({
|
||||
modelPath,
|
||||
model,
|
||||
sdmodel,
|
||||
backend,
|
||||
noavx2,
|
||||
|
|
@ -97,26 +92,26 @@ export const LaunchScreen = ({
|
|||
}, [backend, handleBackendChange]);
|
||||
|
||||
const setInitialDefaults = useCallback(
|
||||
async (currentModelPath: string, currentSdModel: string) => {
|
||||
async (currentModel: string, currentSdModel: string) => {
|
||||
await tryExecute(async () => {
|
||||
if (
|
||||
!defaultsSetRef.current &&
|
||||
!currentModelPath.trim() &&
|
||||
!currentModel.trim() &&
|
||||
!currentSdModel.trim()
|
||||
) {
|
||||
handleModelPathChange(DEFAULT_MODEL_URL);
|
||||
handleModelChange(DEFAULT_MODEL_URL);
|
||||
defaultsSetRef.current = true;
|
||||
}
|
||||
}, 'Failed to set initial defaults:');
|
||||
},
|
||||
[handleModelPathChange]
|
||||
[handleModelChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (configLoaded && !defaultsSetRef.current) {
|
||||
void setHappyDefaults();
|
||||
if (!modelPath.trim() && !sdmodel.trim()) {
|
||||
void setInitialDefaults(modelPath, sdmodel);
|
||||
if (!model.trim() && !sdmodel.trim()) {
|
||||
void setInitialDefaults(model, sdmodel);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
|
@ -160,7 +155,7 @@ export const LaunchScreen = ({
|
|||
autoGpuLayers: autoGpuLayers,
|
||||
gpulayers: gpuLayers,
|
||||
contextsize: contextSize,
|
||||
model: modelPath,
|
||||
model,
|
||||
additionalArguments,
|
||||
port,
|
||||
host,
|
||||
|
|
@ -290,7 +285,7 @@ export const LaunchScreen = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<Container size="sm">
|
||||
<Container size="sm" mt="md">
|
||||
<Stack gap="md">
|
||||
<Card
|
||||
withBorder
|
||||
|
|
@ -314,7 +309,7 @@ export const LaunchScreen = ({
|
|||
onChange={setActiveTab}
|
||||
styles={{
|
||||
root: {
|
||||
maxHeight: '22rem',
|
||||
maxHeight: '59vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
|
|
@ -354,18 +349,18 @@ export const LaunchScreen = ({
|
|||
<WarningDisplay warnings={combinedWarnings}>
|
||||
<Button
|
||||
radius="md"
|
||||
disabled={(!modelPath && !sdmodel) || isLaunching}
|
||||
disabled={(!model && !sdmodel) || isLaunching}
|
||||
onClick={handleLaunchClick}
|
||||
size="lg"
|
||||
variant="filled"
|
||||
color="blue"
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: '16px',
|
||||
padding: '12px 28px',
|
||||
minWidth: '120px',
|
||||
fontSize: '1em',
|
||||
padding: '0.75rem 1.75rem',
|
||||
minWidth: '7.5rem',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px',
|
||||
letterSpacing: '0.03125rem',
|
||||
}}
|
||||
>
|
||||
Launch
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ interface WelcomeScreenProps {
|
|||
}
|
||||
|
||||
export const WelcomeScreen = ({ onGetStarted }: WelcomeScreenProps) => (
|
||||
<Container size="md">
|
||||
<Container size="md" mt="md">
|
||||
<Stack gap="xl">
|
||||
<Card withBorder radius="md" shadow="sm" p="xl">
|
||||
<Stack gap="lg" align="center">
|
||||
|
|
@ -66,21 +66,13 @@ export const WelcomeScreen = ({ onGetStarted }: WelcomeScreenProps) => (
|
|||
- Everything runs on your computer, no data sent to servers
|
||||
</Text>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<Text>
|
||||
<Text component="span" fw={500}>
|
||||
No subscription fees
|
||||
</Text>{' '}
|
||||
- Use powerful AI models for free once downloaded
|
||||
</Text>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<Text>
|
||||
<Text component="span" fw={500}>
|
||||
Hardware acceleration
|
||||
</Text>{' '}
|
||||
- Supports CUDA, ROCm, Vulkan, and CLBlast backends for faster
|
||||
inference
|
||||
- Supports CUDA, ROCm, Vulkan, CLBlast and CPU backends for
|
||||
faster inference
|
||||
</Text>
|
||||
</List.Item>
|
||||
</List>
|
||||
|
|
|
|||
|
|
@ -122,7 +122,12 @@ export const AboutTab = () => {
|
|||
<Stack gap="xs">
|
||||
{versionItems.map((item, index) => (
|
||||
<Group key={index} gap="md" align="center" wrap="nowrap">
|
||||
<Text size="sm" fw={500} c="dimmed" style={{ minWidth: '120px' }}>
|
||||
<Text
|
||||
size="sm"
|
||||
fw={500}
|
||||
c="dimmed"
|
||||
style={{ minWidth: '7.5rem' }}
|
||||
>
|
||||
{item.label}:
|
||||
</Text>
|
||||
<Text
|
||||
|
|
|
|||
|
|
@ -6,8 +6,18 @@ import {
|
|||
rem,
|
||||
useMantineColorScheme,
|
||||
useComputedColorScheme,
|
||||
Slider,
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
import { Sun, Moon, Monitor } from 'lucide-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { safeExecute } from '@/utils/logger';
|
||||
import {
|
||||
zoomLevelToPercentage,
|
||||
percentageToZoomLevel,
|
||||
isValidZoomPercentage,
|
||||
} from '@/utils/zoom';
|
||||
import { ZOOM } from '@/constants';
|
||||
|
||||
export const AppearanceTab = () => {
|
||||
const { colorScheme, setColorScheme } = useMantineColorScheme();
|
||||
|
|
@ -16,6 +26,51 @@ export const AppearanceTab = () => {
|
|||
});
|
||||
const isDark = computedColorScheme === 'dark';
|
||||
|
||||
const [zoomLevel, setZoomLevel] = useState<number>(ZOOM.DEFAULT_LEVEL);
|
||||
const [zoomPercentage, setZoomPercentage] = useState<string>(
|
||||
ZOOM.DEFAULT_PERCENTAGE.toString()
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const loadZoomLevel = async () => {
|
||||
const currentZoom = await safeExecute(
|
||||
() => window.electronAPI.app.getZoomLevel(),
|
||||
'Failed to load zoom level:'
|
||||
);
|
||||
if (typeof currentZoom === 'number') {
|
||||
setZoomLevel(currentZoom);
|
||||
setZoomPercentage(zoomLevelToPercentage(currentZoom).toString());
|
||||
}
|
||||
};
|
||||
|
||||
void loadZoomLevel();
|
||||
}, []);
|
||||
|
||||
const handleZoomChange = async (newZoomLevel: number) => {
|
||||
setZoomLevel(newZoomLevel);
|
||||
const percentage = zoomLevelToPercentage(newZoomLevel);
|
||||
setZoomPercentage(percentage.toString());
|
||||
|
||||
await safeExecute(
|
||||
() => window.electronAPI.app.setZoomLevel(newZoomLevel),
|
||||
'Failed to set zoom level:'
|
||||
);
|
||||
};
|
||||
|
||||
const handleZoomPercentageChange = async (value: string) => {
|
||||
setZoomPercentage(value);
|
||||
const numValue = Number(value);
|
||||
if (!isNaN(numValue) && isValidZoomPercentage(numValue)) {
|
||||
const newZoomLevel = percentageToZoomLevel(numValue);
|
||||
setZoomLevel(newZoomLevel);
|
||||
|
||||
await safeExecute(
|
||||
() => window.electronAPI.app.setZoomLevel(newZoomLevel),
|
||||
'Failed to set zoom level:'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="lg" h="100%">
|
||||
<div>
|
||||
|
|
@ -72,6 +127,71 @@ export const AppearanceTab = () => {
|
|||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text fw={500} mb="sm">
|
||||
Zoom Level
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" mb="md">
|
||||
Adjust the zoom level of the application interface
|
||||
</Text>
|
||||
<Group gap="md" align="flex-end">
|
||||
<div style={{ flex: 1 }}>
|
||||
<Group justify="space-between" align="center" mb="xs">
|
||||
<TextInput
|
||||
value={zoomPercentage}
|
||||
onChange={(event) =>
|
||||
handleZoomPercentageChange(event.currentTarget.value)
|
||||
}
|
||||
onBlur={(event) => {
|
||||
const value = event.currentTarget.value;
|
||||
const numValue = Number(value);
|
||||
if (isNaN(numValue) || !isValidZoomPercentage(numValue)) {
|
||||
setZoomPercentage(
|
||||
zoomLevelToPercentage(zoomLevel).toString()
|
||||
);
|
||||
}
|
||||
}}
|
||||
rightSection={
|
||||
<Text size="sm" c="dimmed">
|
||||
%
|
||||
</Text>
|
||||
}
|
||||
size="sm"
|
||||
w={80}
|
||||
styles={{
|
||||
input: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Group>
|
||||
<Slider
|
||||
value={zoomLevel}
|
||||
onChange={handleZoomChange}
|
||||
min={ZOOM.MIN_LEVEL}
|
||||
max={ZOOM.MAX_LEVEL}
|
||||
step={0.25}
|
||||
marks={[
|
||||
{
|
||||
value: ZOOM.MIN_LEVEL,
|
||||
label: `${ZOOM.MIN_PERCENTAGE}%`,
|
||||
},
|
||||
{
|
||||
value: ZOOM.DEFAULT_LEVEL,
|
||||
label: `${ZOOM.DEFAULT_PERCENTAGE}%`,
|
||||
},
|
||||
{
|
||||
value: ZOOM.MAX_LEVEL,
|
||||
label: `${ZOOM.MAX_PERCENTAGE}%`,
|
||||
},
|
||||
]}
|
||||
label={(value) => `${zoomLevelToPercentage(value)}%`}
|
||||
style={{ marginBottom: '0.5rem' }}
|
||||
/>
|
||||
</div>
|
||||
</Group>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -68,14 +68,14 @@ export const SettingsModal = ({
|
|||
lockScroll={false}
|
||||
styles={{
|
||||
body: {
|
||||
height: '440px',
|
||||
height: '27.5rem',
|
||||
padding: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
},
|
||||
content: {
|
||||
height: '500px',
|
||||
height: '31.25rem',
|
||||
paddingBottom: 0,
|
||||
},
|
||||
}}
|
||||
|
|
@ -96,8 +96,8 @@ export const SettingsModal = ({
|
|||
panel: {
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
paddingLeft: '24px',
|
||||
paddingRight: '24px',
|
||||
paddingLeft: '1.5rem',
|
||||
paddingRight: '1.5rem',
|
||||
},
|
||||
tabLabel: {
|
||||
textAlign: 'left',
|
||||
|
|
|
|||
|
|
@ -46,3 +46,12 @@ export const FRONTENDS = {
|
|||
STABLE_UI: 'Stable UI',
|
||||
SILLYTAVERN: 'SillyTavern',
|
||||
} as const;
|
||||
|
||||
export const ZOOM = {
|
||||
MIN_LEVEL: -5,
|
||||
MAX_LEVEL: 5,
|
||||
MIN_PERCENTAGE: 25,
|
||||
MAX_PERCENTAGE: 500,
|
||||
DEFAULT_LEVEL: 0,
|
||||
DEFAULT_PERCENTAGE: 100,
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export const useLaunchConfig = () => {
|
|||
gpuLayers: state.gpuLayers,
|
||||
autoGpuLayers: state.autoGpuLayers,
|
||||
contextSize: state.contextSize,
|
||||
modelPath: state.modelPath,
|
||||
model: state.model,
|
||||
additionalArguments: state.additionalArguments,
|
||||
port: state.port,
|
||||
host: state.host,
|
||||
|
|
@ -45,7 +45,7 @@ export const useLaunchConfig = () => {
|
|||
handleGpuLayersChange: state.setGpuLayers,
|
||||
handleAutoGpuLayersChange: state.setAutoGpuLayers,
|
||||
handleContextSizeChangeWithStep: state.contextSizeChangeWithStep,
|
||||
handleModelPathChange: state.setModelPath,
|
||||
handleModelChange: state.setModel,
|
||||
handleAdditionalArgumentsChange: state.setAdditionalArguments,
|
||||
handlePortChange: state.setPort,
|
||||
handleHostChange: state.setHost,
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@ import { error } from '@/utils/logger';
|
|||
import type { SdConvDirectMode } from '@/types';
|
||||
|
||||
interface UseLaunchLogicProps {
|
||||
modelPath: string;
|
||||
model: string;
|
||||
sdmodel: string;
|
||||
onLaunch: () => void;
|
||||
onLaunchModeChange?: (isImageMode: boolean) => void;
|
||||
}
|
||||
|
||||
interface LaunchArgs {
|
||||
|
|
@ -45,7 +44,7 @@ interface LaunchArgs {
|
|||
|
||||
const buildModelArgs = (
|
||||
isImageMode: boolean,
|
||||
modelPath: string,
|
||||
model: string,
|
||||
sdmodel: string,
|
||||
launchArgs: LaunchArgs
|
||||
): string[] => {
|
||||
|
|
@ -77,7 +76,7 @@ const buildModelArgs = (
|
|||
args.push('--sdconvdirect', launchArgs.sdconvdirect);
|
||||
}
|
||||
} else {
|
||||
args.push('--model', modelPath);
|
||||
args.push('--model', model);
|
||||
}
|
||||
|
||||
return args;
|
||||
|
|
@ -239,17 +238,16 @@ function parseCLBlastDevice(deviceString: string): {
|
|||
}
|
||||
|
||||
export const useLaunchLogic = ({
|
||||
modelPath,
|
||||
model,
|
||||
sdmodel,
|
||||
onLaunch,
|
||||
onLaunchModeChange,
|
||||
}: UseLaunchLogicProps) => {
|
||||
const [isLaunching, setIsLaunching] = useState(false);
|
||||
|
||||
const handleLaunch = useCallback(
|
||||
async (launchArgs: LaunchArgs) => {
|
||||
const isImageMode = sdmodel.trim() !== '';
|
||||
const isTextMode = modelPath.trim() !== '';
|
||||
const isTextMode = model.trim() !== '';
|
||||
|
||||
if (isLaunching || (!isImageMode && !isTextMode)) {
|
||||
return;
|
||||
|
|
@ -261,7 +259,7 @@ export const useLaunchLogic = ({
|
|||
|
||||
try {
|
||||
const args: string[] = [
|
||||
...buildModelArgs(isImageMode, modelPath, sdmodel, launchArgs),
|
||||
...buildModelArgs(isImageMode, model, sdmodel, launchArgs),
|
||||
...buildConfigArgs(isImageMode, launchArgs),
|
||||
...buildBackendArgs(launchArgs),
|
||||
];
|
||||
|
|
@ -276,9 +274,7 @@ export const useLaunchLogic = ({
|
|||
const result = await window.electronAPI.kobold.launchKoboldCpp(args);
|
||||
|
||||
if (result.success) {
|
||||
if (onLaunchModeChange) {
|
||||
onLaunchModeChange(isImageMode);
|
||||
}
|
||||
onLaunch();
|
||||
} else {
|
||||
const errorMessage = result.error || 'Unknown launch error';
|
||||
window.electronAPI.logs.logError(
|
||||
|
|
@ -292,7 +288,7 @@ export const useLaunchLogic = ({
|
|||
setIsLaunching(false);
|
||||
}
|
||||
},
|
||||
[modelPath, sdmodel, isLaunching, onLaunch, onLaunchModeChange]
|
||||
[model, sdmodel, isLaunching, onLaunch]
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useMemo, useEffect, useState, useCallback } from 'react';
|
||||
import { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { safeExecute } from '@/utils/logger';
|
||||
import type { BackendOption } from '@/types';
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ export interface Warning {
|
|||
}
|
||||
|
||||
interface UseWarningsProps {
|
||||
modelPath: string;
|
||||
model: string;
|
||||
sdmodel: string;
|
||||
backend?: string;
|
||||
noavx2?: boolean;
|
||||
|
|
@ -17,11 +17,11 @@ interface UseWarningsProps {
|
|||
}
|
||||
|
||||
const checkModelWarnings = (
|
||||
modelPath: string,
|
||||
model: string,
|
||||
sdmodel: string,
|
||||
configLoaded: boolean
|
||||
): Warning[] => {
|
||||
const hasTextModel = modelPath?.trim() !== '';
|
||||
const hasTextModel = model?.trim() !== '';
|
||||
const hasImageModel = sdmodel.trim() !== '';
|
||||
const showModelPriorityWarning = hasTextModel && hasImageModel;
|
||||
const showNoModelWarning = !hasTextModel && !hasImageModel && configLoaded;
|
||||
|
|
@ -230,7 +230,7 @@ const checkBackendWarnings = async (params?: {
|
|||
};
|
||||
|
||||
export const useWarnings = ({
|
||||
modelPath,
|
||||
model,
|
||||
sdmodel,
|
||||
backend,
|
||||
noavx2 = false,
|
||||
|
|
@ -240,8 +240,8 @@ export const useWarnings = ({
|
|||
const [backendWarnings, setBackendWarnings] = useState<Warning[]>([]);
|
||||
|
||||
const modelWarnings = useMemo(
|
||||
() => checkModelWarnings(modelPath, sdmodel, configLoaded),
|
||||
[modelPath, sdmodel, configLoaded]
|
||||
() => checkModelWarnings(model, sdmodel, configLoaded),
|
||||
[model, sdmodel, configLoaded]
|
||||
);
|
||||
|
||||
const updateBackendWarnings = useCallback(async () => {
|
||||
|
|
|
|||
|
|
@ -139,8 +139,8 @@ export class IPCHandlers {
|
|||
this.koboldManager.parseConfigFile(filePath)
|
||||
);
|
||||
|
||||
ipcMain.handle('kobold:selectModelFile', () =>
|
||||
this.koboldManager.selectModelFile()
|
||||
ipcMain.handle('kobold:selectModelFile', (_, title) =>
|
||||
this.koboldManager.selectModelFile(title)
|
||||
);
|
||||
|
||||
ipcMain.handle('config:get', (_, key) => this.configManager.get(key));
|
||||
|
|
@ -165,9 +165,14 @@ export class IPCHandlers {
|
|||
ipcMain.handle('app:openExternal', async (_, url) => {
|
||||
try {
|
||||
await shell.openExternal(url);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
this.logManager.logError(
|
||||
'Failed to open external URL:',
|
||||
error as Error
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to open external URL: ${(error as Error).message}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -175,9 +180,11 @@ export class IPCHandlers {
|
|||
try {
|
||||
const logsDir = this.logManager.getLogsDirectory();
|
||||
await shell.openPath(logsDir);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
this.logManager.logError('Failed to open logs folder:', error as Error);
|
||||
throw new Error(
|
||||
`Failed to open logs folder: ${(error as Error).message}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -200,6 +207,32 @@ export class IPCHandlers {
|
|||
mainWindow?.close();
|
||||
});
|
||||
|
||||
ipcMain.handle('app:getZoomLevel', async () => {
|
||||
const mainWindow = this.windowManager.getMainWindow();
|
||||
if (mainWindow) {
|
||||
return mainWindow.webContents.getZoomLevel();
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
ipcMain.handle('app:setZoomLevel', async (_, level: number) => {
|
||||
const mainWindow = this.windowManager.getMainWindow();
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.setZoomLevel(level);
|
||||
await this.configManager.set('zoomLevel', level);
|
||||
}
|
||||
});
|
||||
|
||||
const mainWindow = this.windowManager.getMainWindow();
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.once('did-finish-load', async () => {
|
||||
const savedZoomLevel = await this.configManager.get('zoomLevel');
|
||||
if (typeof savedZoomLevel === 'number') {
|
||||
mainWindow.webContents.setZoomLevel(savedZoomLevel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ipcMain.handle('logs:logError', (_, message: string, error?: Error) => {
|
||||
this.logManager.logError(message, error);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -398,7 +398,7 @@ export class KoboldCppManager {
|
|||
}
|
||||
}
|
||||
|
||||
async selectModelFile(): Promise<string | null> {
|
||||
async selectModelFile(title = 'Select Model File'): Promise<string | null> {
|
||||
try {
|
||||
const mainWindow = this.windowManager.getMainWindow();
|
||||
if (!mainWindow) {
|
||||
|
|
@ -406,9 +406,12 @@ export class KoboldCppManager {
|
|||
}
|
||||
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
title: 'Select Model File',
|
||||
title,
|
||||
filters: [
|
||||
{ name: 'GGUF Files', extensions: ['gguf'] },
|
||||
{
|
||||
name: 'Model Files',
|
||||
extensions: ['gguf', 'safetensors'],
|
||||
},
|
||||
{ name: 'All Files', extensions: ['*'] },
|
||||
],
|
||||
properties: ['openFile'],
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export class WindowManager {
|
|||
const iconImage = nativeImage.createFromPath(iconPath);
|
||||
|
||||
const { workAreaSize } = screen.getPrimaryDisplay();
|
||||
const windowHeight = Math.floor(workAreaSize.height * 0.85);
|
||||
const windowHeight = Math.floor(workAreaSize.height * 0.86);
|
||||
|
||||
this.mainWindow = new BrowserWindow({
|
||||
width: 1000,
|
||||
|
|
|
|||
|
|
@ -5,13 +5,12 @@ import type {
|
|||
ConfigAPI,
|
||||
LogsAPI,
|
||||
SillyTavernAPI,
|
||||
KoboldConfig,
|
||||
} from '@/types/electron';
|
||||
|
||||
const koboldAPI: KoboldAPI = {
|
||||
getInstalledVersions: () => ipcRenderer.invoke('kobold:getInstalledVersions'),
|
||||
getCurrentVersion: () => ipcRenderer.invoke('kobold:getCurrentVersion'),
|
||||
setCurrentVersion: (version: string) =>
|
||||
setCurrentVersion: (version) =>
|
||||
ipcRenderer.invoke('kobold:setCurrentVersion', version),
|
||||
getPlatform: () => ipcRenderer.invoke('kobold:getPlatform'),
|
||||
detectGPU: () => ipcRenderer.invoke('kobold:detectGPU'),
|
||||
|
|
@ -21,24 +20,24 @@ const koboldAPI: KoboldAPI = {
|
|||
detectGPUMemory: () => ipcRenderer.invoke('kobold:detectGPUMemory'),
|
||||
detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'),
|
||||
detectBackendSupport: () => ipcRenderer.invoke('kobold:detectBackendSupport'),
|
||||
getAvailableBackends: (includeDisabled?: boolean) =>
|
||||
getAvailableBackends: (includeDisabled) =>
|
||||
ipcRenderer.invoke('kobold:getAvailableBackends', includeDisabled),
|
||||
getCurrentInstallDir: () => ipcRenderer.invoke('kobold:getCurrentInstallDir'),
|
||||
selectInstallDirectory: () =>
|
||||
ipcRenderer.invoke('kobold:selectInstallDirectory'),
|
||||
downloadRelease: (asset) =>
|
||||
ipcRenderer.invoke('kobold:downloadRelease', asset),
|
||||
launchKoboldCpp: (args?: string[]) =>
|
||||
ipcRenderer.invoke('kobold:launchKoboldCpp', args),
|
||||
launchKoboldCpp: (args) => ipcRenderer.invoke('kobold:launchKoboldCpp', args),
|
||||
getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'),
|
||||
saveConfigFile: (configName: string, configData: KoboldConfig) =>
|
||||
saveConfigFile: (configName, configData) =>
|
||||
ipcRenderer.invoke('kobold:saveConfigFile', configName, configData),
|
||||
getSelectedConfig: () => ipcRenderer.invoke('kobold:getSelectedConfig'),
|
||||
setSelectedConfig: (configName: string) =>
|
||||
setSelectedConfig: (configName) =>
|
||||
ipcRenderer.invoke('kobold:setSelectedConfig', configName),
|
||||
parseConfigFile: (filePath: string) =>
|
||||
parseConfigFile: (filePath) =>
|
||||
ipcRenderer.invoke('kobold:parseConfigFile', filePath),
|
||||
selectModelFile: () => ipcRenderer.invoke('kobold:selectModelFile'),
|
||||
selectModelFile: (title) =>
|
||||
ipcRenderer.invoke('kobold:selectModelFile', title),
|
||||
stopKoboldCpp: () => ipcRenderer.invoke('kobold:stopKoboldCpp'),
|
||||
onDownloadProgress: (callback) => {
|
||||
ipcRenderer.on(
|
||||
|
|
@ -46,7 +45,7 @@ const koboldAPI: KoboldAPI = {
|
|||
(_: IpcRendererEvent, progress: number) => callback(progress)
|
||||
);
|
||||
},
|
||||
onInstallDirChanged: (callback: (newPath: string) => void) => {
|
||||
onInstallDirChanged: (callback) => {
|
||||
const handler = (_: IpcRendererEvent, newPath: string) => callback(newPath);
|
||||
ipcRenderer.on('install-dir-changed', handler);
|
||||
|
||||
|
|
@ -54,7 +53,7 @@ const koboldAPI: KoboldAPI = {
|
|||
ipcRenderer.removeListener('install-dir-changed', handler);
|
||||
};
|
||||
},
|
||||
onVersionsUpdated: (callback: () => void) => {
|
||||
onVersionsUpdated: (callback) => {
|
||||
const handler = () => callback();
|
||||
ipcRenderer.on('versions-updated', handler);
|
||||
|
||||
|
|
@ -62,7 +61,7 @@ const koboldAPI: KoboldAPI = {
|
|||
ipcRenderer.removeListener('versions-updated', handler);
|
||||
};
|
||||
},
|
||||
onKoboldOutput: (callback: (data: string) => void) => {
|
||||
onKoboldOutput: (callback) => {
|
||||
const handler = (_: IpcRendererEvent, data: string) => callback(data);
|
||||
ipcRenderer.on('kobold-output', handler);
|
||||
|
||||
|
|
@ -83,16 +82,17 @@ const appAPI: AppAPI = {
|
|||
minimizeWindow: () => ipcRenderer.invoke('app:minimizeWindow'),
|
||||
maximizeWindow: () => ipcRenderer.invoke('app:maximizeWindow'),
|
||||
closeWindow: () => ipcRenderer.invoke('app:closeWindow'),
|
||||
getZoomLevel: () => ipcRenderer.invoke('app:getZoomLevel'),
|
||||
setZoomLevel: (level) => ipcRenderer.invoke('app:setZoomLevel', level),
|
||||
};
|
||||
|
||||
const configAPI: ConfigAPI = {
|
||||
get: (key: string) => ipcRenderer.invoke('config:get', key),
|
||||
set: (key: string, value: unknown) =>
|
||||
ipcRenderer.invoke('config:set', key, value),
|
||||
get: (key) => ipcRenderer.invoke('config:get', key),
|
||||
set: (key, value) => ipcRenderer.invoke('config:set', key, value),
|
||||
};
|
||||
|
||||
const logsAPI: LogsAPI = {
|
||||
logError: (message: string, error?: Error) =>
|
||||
logError: (message, error) =>
|
||||
ipcRenderer.invoke('logs:logError', message, error),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ interface LaunchConfigState {
|
|||
gpuLayers: number;
|
||||
autoGpuLayers: boolean;
|
||||
contextSize: number;
|
||||
modelPath: string;
|
||||
model: string;
|
||||
additionalArguments: string;
|
||||
port?: number;
|
||||
host: string;
|
||||
|
|
@ -37,11 +37,12 @@ interface LaunchConfigState {
|
|||
sdconvdirect: SdConvDirectMode;
|
||||
moecpu: number;
|
||||
moeexperts: number;
|
||||
isImageGenerationMode: boolean;
|
||||
|
||||
setGpuLayers: (layers: number) => void;
|
||||
setAutoGpuLayers: (auto: boolean) => void;
|
||||
setContextSize: (size: number) => void;
|
||||
setModelPath: (path: string) => void;
|
||||
setModel: (path: string) => void;
|
||||
setAdditionalArguments: (args: string) => void;
|
||||
setPort: (port?: number) => void;
|
||||
setHost: (host: string) => void;
|
||||
|
|
@ -93,7 +94,7 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
|
|||
gpuLayers: 0,
|
||||
autoGpuLayers: true,
|
||||
contextSize: DEFAULT_CONTEXT_SIZE,
|
||||
modelPath: '',
|
||||
model: '',
|
||||
additionalArguments: '',
|
||||
port: undefined,
|
||||
host: '',
|
||||
|
|
@ -124,10 +125,12 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
|
|||
moecpu: 0,
|
||||
moeexperts: -1,
|
||||
|
||||
isImageGenerationMode: false,
|
||||
|
||||
setGpuLayers: (layers) => set({ gpuLayers: layers }),
|
||||
setAutoGpuLayers: (auto) => set({ autoGpuLayers: auto }),
|
||||
setContextSize: (size) => set({ contextSize: size }),
|
||||
setModelPath: (path) => set({ modelPath: path }),
|
||||
setModel: (path) => set({ model: path }),
|
||||
setAdditionalArguments: (args) => set({ additionalArguments: args }),
|
||||
setPort: (port) => set({ port }),
|
||||
setHost: (host) => set({ host }),
|
||||
|
|
@ -152,7 +155,11 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
|
|||
setGpuDeviceSelection: (selection) => set({ gpuDeviceSelection: selection }),
|
||||
setTensorSplit: (split) => set({ tensorSplit: split }),
|
||||
setGpuPlatform: (platform) => set({ gpuPlatform: platform }),
|
||||
setSdmodel: (model) => set({ sdmodel: model }),
|
||||
setSdmodel: (model) =>
|
||||
set({
|
||||
sdmodel: model,
|
||||
isImageGenerationMode: Boolean(model?.trim()),
|
||||
}),
|
||||
setSdt5xxl: (model) => set({ sdt5xxl: model }),
|
||||
setSdclipl: (model) => set({ sdclipl: model }),
|
||||
setSdclipg: (model) => set({ sdclipg: model }),
|
||||
|
|
@ -190,7 +197,7 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
|
|||
}
|
||||
|
||||
if (typeof configData.model === 'string') {
|
||||
updates.modelPath = configData.model;
|
||||
updates.model = configData.model;
|
||||
}
|
||||
|
||||
if (typeof configData.additionalArguments === 'string') {
|
||||
|
|
@ -316,6 +323,7 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
|
|||
|
||||
if (typeof configData.sdmodel === 'string') {
|
||||
updates.sdmodel = configData.sdmodel;
|
||||
updates.isImageGenerationMode = Boolean(configData.sdmodel?.trim());
|
||||
}
|
||||
|
||||
if (typeof configData.sdt5xxl === 'string') {
|
||||
|
|
@ -387,56 +395,75 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
|
|||
},
|
||||
|
||||
selectModelFile: async () => {
|
||||
const result = await window.electronAPI.kobold.selectModelFile();
|
||||
const result = await window.electronAPI.kobold.selectModelFile(
|
||||
'Select a Text Model File'
|
||||
);
|
||||
if (result) {
|
||||
set({ modelPath: result });
|
||||
set({ model: result });
|
||||
}
|
||||
},
|
||||
|
||||
selectSdmodelFile: async () => {
|
||||
const result = await window.electronAPI.kobold.selectModelFile();
|
||||
const result = await window.electronAPI.kobold.selectModelFile(
|
||||
'Select a Image Gen. Model File'
|
||||
);
|
||||
if (result) {
|
||||
set({ sdmodel: result });
|
||||
set({
|
||||
sdmodel: result,
|
||||
isImageGenerationMode: Boolean(result?.trim()),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
selectSdt5xxlFile: async () => {
|
||||
const result = await window.electronAPI.kobold.selectModelFile();
|
||||
const result = await window.electronAPI.kobold.selectModelFile(
|
||||
'Select a SDT5XXL Model File'
|
||||
);
|
||||
if (result) {
|
||||
set({ sdt5xxl: result });
|
||||
}
|
||||
},
|
||||
|
||||
selectSdcliplFile: async () => {
|
||||
const result = await window.electronAPI.kobold.selectModelFile();
|
||||
const result = await window.electronAPI.kobold.selectModelFile(
|
||||
'Select a SDCLIP-L Model File'
|
||||
);
|
||||
if (result) {
|
||||
set({ sdclipl: result });
|
||||
}
|
||||
},
|
||||
|
||||
selectSdclipgFile: async () => {
|
||||
const result = await window.electronAPI.kobold.selectModelFile();
|
||||
const result = await window.electronAPI.kobold.selectModelFile(
|
||||
'Select a SDCLIP-G Model File'
|
||||
);
|
||||
if (result) {
|
||||
set({ sdclipg: result });
|
||||
}
|
||||
},
|
||||
|
||||
selectSdphotomakerFile: async () => {
|
||||
const result = await window.electronAPI.kobold.selectModelFile();
|
||||
const result = await window.electronAPI.kobold.selectModelFile(
|
||||
'Select a SDPhotoMaker Model File'
|
||||
);
|
||||
if (result) {
|
||||
set({ sdphotomaker: result });
|
||||
}
|
||||
},
|
||||
|
||||
selectSdvaeFile: async () => {
|
||||
const result = await window.electronAPI.kobold.selectModelFile();
|
||||
const result = await window.electronAPI.kobold.selectModelFile(
|
||||
'Select a SDVAE Model File'
|
||||
);
|
||||
if (result) {
|
||||
set({ sdvae: result });
|
||||
}
|
||||
},
|
||||
|
||||
selectSdloraFile: async () => {
|
||||
const result = await window.electronAPI.kobold.selectModelFile();
|
||||
const result = await window.electronAPI.kobold.selectModelFile(
|
||||
'Select a SDLORA Model File'
|
||||
);
|
||||
if (result) {
|
||||
set({ sdlora: result });
|
||||
}
|
||||
|
|
@ -450,6 +477,7 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
|
|||
applyImageModelPreset: (preset: ImageModelPreset) => {
|
||||
set({
|
||||
sdmodel: preset.sdmodel,
|
||||
isImageGenerationMode: Boolean(preset.sdmodel?.trim()),
|
||||
sdt5xxl: preset.sdt5xxl,
|
||||
sdclipl: preset.sdclipl,
|
||||
sdclipg: preset.sdclipg || '',
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
position: relative !important;
|
||||
top: 2.5rem !important;
|
||||
margin-top: 0 !important;
|
||||
padding-top: 1rem !important;
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
min-height: calc(100vh - 2.5rem) !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@
|
|||
.terminalScrollArea {
|
||||
flex: 1;
|
||||
font-family: Monaco, Menlo, 'Ubuntu Mono', monospace;
|
||||
font-size: 14px;
|
||||
font-size: 0.875em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
|
|
|||
5
src/types/electron.d.ts
vendored
|
|
@ -57,7 +57,6 @@ export interface DownloadItem {
|
|||
export interface KoboldConfig {
|
||||
gpulayers?: number;
|
||||
contextsize?: number;
|
||||
model_param?: string;
|
||||
port?: number;
|
||||
host?: string;
|
||||
multiuser?: number;
|
||||
|
|
@ -122,7 +121,7 @@ export interface KoboldAPI {
|
|||
getSelectedConfig: () => Promise<string | null>;
|
||||
setSelectedConfig: (configName: string) => Promise<boolean>;
|
||||
parseConfigFile: (filePath: string) => Promise<KoboldConfig | null>;
|
||||
selectModelFile: () => Promise<string | null>;
|
||||
selectModelFile: (title?: string) => Promise<string | null>;
|
||||
stopKoboldCpp: () => void;
|
||||
onDownloadProgress: (callback: (progress: number) => void) => void;
|
||||
onInstallDirChanged: (callback: (newPath: string) => void) => () => void;
|
||||
|
|
@ -150,6 +149,8 @@ export interface AppAPI {
|
|||
minimizeWindow: () => void;
|
||||
maximizeWindow: () => void;
|
||||
closeWindow: () => void;
|
||||
getZoomLevel: () => Promise<number>;
|
||||
setZoomLevel: (level: number) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface ConfigAPI {
|
||||
|
|
|
|||
2
src/types/index.d.ts
vendored
|
|
@ -10,8 +10,6 @@ export type SdConvDirectMode = 'off' | 'vaeonly' | 'full';
|
|||
|
||||
export type FrontendPreference = 'koboldcpp' | 'sillytavern';
|
||||
|
||||
export type ServerTabMode = 'chat' | 'image-generation';
|
||||
|
||||
export type Screen = 'welcome' | 'download' | 'launch' | 'interface';
|
||||
|
||||
export interface GitHubAsset {
|
||||
|
|
|
|||
10
src/utils/zoom.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { ZOOM } from '@/constants';
|
||||
|
||||
export const zoomLevelToPercentage = (zoomLevel: number): number =>
|
||||
Math.round(Math.pow(1.2, zoomLevel) * 100);
|
||||
|
||||
export const percentageToZoomLevel = (percentage: number): number =>
|
||||
Math.log(percentage / 100) / Math.log(1.2);
|
||||
|
||||
export const isValidZoomPercentage = (percentage: number): boolean =>
|
||||
percentage >= ZOOM.MIN_PERCENTAGE && percentage <= ZOOM.MAX_PERCENTAGE;
|
||||