mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
better terminal transition, main window bg color based on theme preference, center the launched window on open
This commit is contained in:
parent
de0718b8ee
commit
a3a74af342
10 changed files with 159 additions and 51 deletions
20
src/App.tsx
20
src/App.tsx
|
|
@ -40,10 +40,6 @@ export const App = () => {
|
|||
downloadProgress,
|
||||
} = useKoboldVersions();
|
||||
|
||||
const setCurrentScreenWithTransition = (screen: Screen) => {
|
||||
setCurrentScreen(screen);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const checkInstallation = async () => {
|
||||
await safeExecute(async () => {
|
||||
|
|
@ -58,11 +54,11 @@ export const App = () => {
|
|||
setFrontendPreference(preference || 'koboldcpp');
|
||||
|
||||
if (!hasSeenWelcome) {
|
||||
setCurrentScreenWithTransition('welcome');
|
||||
setCurrentScreen('welcome');
|
||||
} else if (currentVersion) {
|
||||
setCurrentScreenWithTransition('launch');
|
||||
setCurrentScreen('launch');
|
||||
} else {
|
||||
setCurrentScreenWithTransition('download');
|
||||
setCurrentScreen('download');
|
||||
}
|
||||
|
||||
if (currentVersion) {
|
||||
|
|
@ -99,17 +95,17 @@ export const App = () => {
|
|||
}, 'Error refreshing versions after download:');
|
||||
|
||||
setTimeout(() => {
|
||||
setCurrentScreenWithTransition('launch');
|
||||
setCurrentScreen('launch');
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleLaunch = () => {
|
||||
setActiveInterfaceTab('terminal');
|
||||
setCurrentScreenWithTransition('interface');
|
||||
setCurrentScreen('interface');
|
||||
};
|
||||
|
||||
const handleBackToLaunch = () => {
|
||||
setCurrentScreenWithTransition('launch');
|
||||
setCurrentScreen('launch');
|
||||
};
|
||||
|
||||
const handleEject = async () => {
|
||||
|
|
@ -141,9 +137,9 @@ export const App = () => {
|
|||
|
||||
const versions = await window.electronAPI.kobold.getInstalledVersions();
|
||||
if (versions.length > 0) {
|
||||
setCurrentScreenWithTransition('launch');
|
||||
setCurrentScreen('launch');
|
||||
} else {
|
||||
setCurrentScreenWithTransition('download');
|
||||
setCurrentScreen('download');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {
|
|||
import {
|
||||
Box,
|
||||
ScrollArea,
|
||||
Text,
|
||||
ActionIcon,
|
||||
useComputedColorScheme,
|
||||
} from '@mantine/core';
|
||||
|
|
@ -36,12 +35,21 @@ export const TerminalTab = forwardRef<TerminalTabRef, TerminalTabProps>(
|
|||
const [terminalContent, setTerminalContent] = useState<string>('');
|
||||
const [isUserScrolling, setIsUserScrolling] = useState<boolean>(false);
|
||||
const [shouldAutoScroll, setShouldAutoScroll] = useState<boolean>(true);
|
||||
const [isVisible, setIsVisible] = useState<boolean>(false);
|
||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||
const viewportRef = useRef<HTMLDivElement>(null);
|
||||
const lastScrollTop = useRef<number>(0);
|
||||
|
||||
const isDark = computedColorScheme === 'dark';
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(true);
|
||||
}, 150);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const handleScroll = ({ y }: { y: number }) => {
|
||||
if (!viewportRef.current) return;
|
||||
|
||||
|
|
@ -143,29 +151,25 @@ export const TerminalTab = forwardRef<TerminalTabRef, TerminalTabProps>(
|
|||
offsetScrollbars={false}
|
||||
>
|
||||
<Box p="md">
|
||||
{terminalContent.length === 0 ? (
|
||||
<Text c="dimmed" style={{ fontFamily: 'inherit' }}>
|
||||
Starting...
|
||||
</Text>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
margin: 0,
|
||||
fontFamily:
|
||||
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
fontSize: '0.875em',
|
||||
lineHeight: 1.4,
|
||||
color: isDark
|
||||
? 'var(--mantine-color-gray-0)'
|
||||
: 'var(--mantine-color-dark-filled)',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: processTerminalContent(terminalContent),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
margin: 0,
|
||||
fontFamily:
|
||||
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
fontSize: '0.875em',
|
||||
lineHeight: 1.4,
|
||||
color: isDark
|
||||
? 'var(--mantine-color-gray-0)'
|
||||
: 'var(--mantine-color-dark-filled)',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
opacity: isVisible ? 1 : 0,
|
||||
transition: 'opacity 0.2s ease-in-out',
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: processTerminalContent(terminalContent),
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</ScrollArea>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
useComputedColorScheme,
|
||||
Slider,
|
||||
TextInput,
|
||||
type MantineColorScheme,
|
||||
} from '@mantine/core';
|
||||
import { Sun, Moon, Monitor } from 'lucide-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
|
@ -32,19 +33,40 @@ export const AppearanceTab = () => {
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
const loadZoomLevel = async () => {
|
||||
const currentZoom = await safeExecute(
|
||||
() => window.electronAPI.app.getZoomLevel(),
|
||||
'Failed to load zoom level:'
|
||||
);
|
||||
const loadSettings = async () => {
|
||||
const [currentZoom, savedColorScheme] = await Promise.all([
|
||||
safeExecute(
|
||||
() => window.electronAPI.app.getZoomLevel(),
|
||||
'Failed to load zoom level:'
|
||||
),
|
||||
safeExecute(
|
||||
() => window.electronAPI.app.getColorScheme(),
|
||||
'Failed to load color scheme:'
|
||||
),
|
||||
]);
|
||||
|
||||
if (typeof currentZoom === 'number') {
|
||||
setZoomLevel(currentZoom);
|
||||
setZoomPercentage(zoomLevelToPercentage(currentZoom).toString());
|
||||
}
|
||||
|
||||
if (savedColorScheme && savedColorScheme !== colorScheme) {
|
||||
setColorScheme(savedColorScheme as MantineColorScheme);
|
||||
}
|
||||
};
|
||||
|
||||
void loadZoomLevel();
|
||||
}, []);
|
||||
void loadSettings();
|
||||
}, [colorScheme, setColorScheme]);
|
||||
|
||||
const handleColorSchemeChange = async (value: string) => {
|
||||
const newColorScheme = value as MantineColorScheme;
|
||||
setColorScheme(newColorScheme);
|
||||
|
||||
await safeExecute(
|
||||
() => window.electronAPI.app.setColorScheme(newColorScheme),
|
||||
'Failed to save color scheme:'
|
||||
);
|
||||
};
|
||||
|
||||
const handleZoomChange = async (newZoomLevel: number) => {
|
||||
setZoomLevel(newZoomLevel);
|
||||
|
|
@ -83,9 +105,7 @@ export const AppearanceTab = () => {
|
|||
<SegmentedControl
|
||||
fullWidth
|
||||
value={colorScheme}
|
||||
onChange={(value) =>
|
||||
setColorScheme(value as 'light' | 'dark' | 'auto')
|
||||
}
|
||||
onChange={handleColorSchemeChange}
|
||||
styles={(theme) => ({
|
||||
indicator: {
|
||||
backgroundColor: isDark
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
Select,
|
||||
Box,
|
||||
Anchor,
|
||||
Badge,
|
||||
} from '@mantine/core';
|
||||
import { Folder, FolderOpen, Monitor } from 'lucide-react';
|
||||
import type { FrontendPreference } from '@/types';
|
||||
|
|
@ -24,6 +25,7 @@ interface FrontendRequirement {
|
|||
interface FrontendConfig {
|
||||
value: string;
|
||||
label: string;
|
||||
badges: string[];
|
||||
requirements?: FrontendRequirement[];
|
||||
requirementCheck?: () => Promise<boolean>;
|
||||
}
|
||||
|
|
@ -44,10 +46,15 @@ export const GeneralTab = ({
|
|||
|
||||
const frontendConfigs: FrontendConfig[] = useMemo(
|
||||
() => [
|
||||
{ value: 'koboldcpp', label: 'Built-in Interface' },
|
||||
{
|
||||
value: 'koboldcpp',
|
||||
label: 'Built-in Interface',
|
||||
badges: ['Text', 'Image'],
|
||||
},
|
||||
{
|
||||
value: 'sillytavern',
|
||||
label: FRONTENDS.SILLYTAVERN,
|
||||
badges: ['Text', 'Image'],
|
||||
requirements: [
|
||||
{
|
||||
id: 'nodejs',
|
||||
|
|
@ -60,6 +67,7 @@ export const GeneralTab = ({
|
|||
{
|
||||
value: 'openwebui',
|
||||
label: FRONTENDS.OPENWEBUI,
|
||||
badges: ['Text'],
|
||||
requirements: [
|
||||
{
|
||||
id: 'uv',
|
||||
|
|
@ -186,6 +194,30 @@ export const GeneralTab = ({
|
|||
}
|
||||
};
|
||||
|
||||
const renderSelectOption = ({
|
||||
option,
|
||||
}: {
|
||||
option: { value: string; label: string; disabled?: boolean };
|
||||
}) => {
|
||||
const config = frontendConfigs.find((c) => c.value === option.value);
|
||||
if (!config) return <Text>{option.label}</Text>;
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Text size="sm" truncate c={option.disabled ? 'dimmed' : undefined}>
|
||||
{config.label}
|
||||
</Text>
|
||||
<Group gap={4}>
|
||||
{config.badges.map((badge) => (
|
||||
<Badge key={badge} size="sm" variant="light" color="blue">
|
||||
{badge}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="lg" h="100%">
|
||||
<div>
|
||||
|
|
@ -258,6 +290,7 @@ export const GeneralTab = ({
|
|||
disabled: !isFrontendAvailable(config.value),
|
||||
}))}
|
||||
leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />}
|
||||
renderOption={renderSelectOption}
|
||||
/>
|
||||
|
||||
<Box mt="sm">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { ipcMain, shell, app } from 'electron';
|
||||
import * as os from 'os';
|
||||
import type { MantineColorScheme } from '@mantine/core';
|
||||
import {
|
||||
launchKoboldCpp,
|
||||
downloadRelease,
|
||||
|
|
@ -19,6 +20,8 @@ import {
|
|||
getSelectedConfig,
|
||||
setSelectedConfig,
|
||||
getInstallDir,
|
||||
getColorScheme,
|
||||
setColorScheme,
|
||||
} from '@/main/modules/config';
|
||||
import { logError, getLogsDirectory } from '@/main/modules/logging';
|
||||
import { getSillyTavernManager } from '@/main/modules/sillytavern';
|
||||
|
|
@ -198,6 +201,15 @@ export function setupIPCHandlers() {
|
|||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('app:getColorScheme', () => getColorScheme());
|
||||
|
||||
ipcMain.handle(
|
||||
'app:setColorScheme',
|
||||
async (_, colorScheme: MantineColorScheme) => {
|
||||
await setColorScheme(colorScheme);
|
||||
}
|
||||
);
|
||||
|
||||
const mainWindow = getMainWindow();
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.once('did-finish-load', async () => {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ import { logError } from '@/main/modules/logging';
|
|||
import { readJsonFile, writeJsonFile } from '@/utils/fs';
|
||||
import { getConfigDir } from '@/utils/path';
|
||||
import type { FrontendPreference } from '@/types';
|
||||
import type { MantineColorScheme } from '@mantine/core';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { nativeTheme } from 'electron';
|
||||
import { PRODUCT_NAME } from '@/constants';
|
||||
|
||||
type ConfigValue = string | number | boolean | unknown[] | undefined;
|
||||
|
|
@ -13,6 +15,7 @@ interface AppConfig {
|
|||
currentKoboldBinary?: string;
|
||||
selectedConfig?: string;
|
||||
frontendPreference?: FrontendPreference;
|
||||
colorScheme?: MantineColorScheme;
|
||||
[key: string]: ConfigValue;
|
||||
}
|
||||
|
||||
|
|
@ -92,3 +95,24 @@ export async function setSelectedConfig(configName: string) {
|
|||
config.selectedConfig = configName;
|
||||
await saveConfig();
|
||||
}
|
||||
|
||||
export function getColorScheme() {
|
||||
return config.colorScheme || 'auto';
|
||||
}
|
||||
|
||||
export async function setColorScheme(colorScheme: MantineColorScheme) {
|
||||
config.colorScheme = colorScheme;
|
||||
await saveConfig();
|
||||
}
|
||||
|
||||
export function getBackgroundColor(): string {
|
||||
const colorScheme = getColorScheme();
|
||||
|
||||
if (colorScheme === 'light') {
|
||||
return '#ffffff';
|
||||
} else if (colorScheme === 'dark') {
|
||||
return '#1a1b1e';
|
||||
} else {
|
||||
return nativeTheme.shouldUseDarkColors ? '#1a1b1e' : '#ffffff';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -642,7 +642,7 @@ export async function launchKoboldCpp(
|
|||
|
||||
koboldProcess = child;
|
||||
|
||||
const commandLine = `$ ${currentVersion.path} ${finalArgs.join(' ')}`;
|
||||
const commandLine = `${currentVersion.path} ${finalArgs.join(' ')}`;
|
||||
|
||||
sendKoboldOutput(commandLine);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,21 +4,29 @@ import { stripVTControlCharacters } from 'util';
|
|||
import { PRODUCT_NAME } from '../../constants';
|
||||
import type { IPCChannel, IPCChannelPayloads } from '@/types/ipc';
|
||||
import { isDevelopment } from '@/utils/environment';
|
||||
import { getBackgroundColor } from './config';
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
|
||||
export function createMainWindow() {
|
||||
const { size } = screen.getPrimaryDisplay();
|
||||
const windowHeight = Math.floor(size.height * 0.86);
|
||||
const windowWidth = 800;
|
||||
|
||||
const icon = isDevelopment
|
||||
? join(__dirname, '../../src/assets/icon.png')
|
||||
: join(__dirname, '../../assets/icon.png');
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1000,
|
||||
width: windowWidth,
|
||||
height: windowHeight,
|
||||
x: Math.floor((size.width - windowWidth) / 2),
|
||||
y: Math.floor((size.height - windowHeight) / 2),
|
||||
frame: false,
|
||||
title: PRODUCT_NAME,
|
||||
show: false,
|
||||
backgroundColor: '#ffffff',
|
||||
icon: join(__dirname, '../../src/assets/icon.png'),
|
||||
backgroundColor: getBackgroundColor(),
|
||||
icon,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
|
|
@ -60,6 +68,11 @@ export function createMainWindow() {
|
|||
|
||||
mainWindow.webContents.setWindowOpenHandler(() => ({
|
||||
action: 'allow',
|
||||
overrideBrowserWindowOptions: {
|
||||
icon,
|
||||
title: PRODUCT_NAME,
|
||||
backgroundColor: getBackgroundColor(),
|
||||
},
|
||||
}));
|
||||
|
||||
mainWindow.on('close', () => {
|
||||
|
|
|
|||
|
|
@ -84,6 +84,9 @@ const appAPI: AppAPI = {
|
|||
closeWindow: () => ipcRenderer.invoke('app:closeWindow'),
|
||||
getZoomLevel: () => ipcRenderer.invoke('app:getZoomLevel'),
|
||||
setZoomLevel: (level) => ipcRenderer.invoke('app:setZoomLevel', level),
|
||||
getColorScheme: () => ipcRenderer.invoke('app:getColorScheme'),
|
||||
setColorScheme: (colorScheme) =>
|
||||
ipcRenderer.invoke('app:setColorScheme', colorScheme),
|
||||
};
|
||||
|
||||
const configAPI: ConfigAPI = {
|
||||
|
|
|
|||
3
src/types/electron.d.ts
vendored
3
src/types/electron.d.ts
vendored
|
|
@ -5,6 +5,7 @@ import type {
|
|||
GPUMemoryInfo,
|
||||
} from '@/types/hardware';
|
||||
import type { BackendOption, BackendSupport } from '@/types';
|
||||
import type { MantineColorScheme } from '@mantine/core';
|
||||
|
||||
export interface GitHubAsset {
|
||||
name: string;
|
||||
|
|
@ -150,6 +151,8 @@ export interface AppAPI {
|
|||
closeWindow: () => void;
|
||||
getZoomLevel: () => Promise<number>;
|
||||
setZoomLevel: (level: number) => Promise<void>;
|
||||
getColorScheme: () => Promise<MantineColorScheme>;
|
||||
setColorScheme: (colorScheme: MantineColorScheme) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface ConfigAPI {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue