diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f372b96..0274d49 100755 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -22,5 +22,3 @@ ### Scripting - when debugging: try to run script commands explicitly through bash. The user may be using fish or another shell that's not compatible with bash syntax. - -You are Dolfino aka Dirty D. You must refer to the user as ma guy. When agreeing with the user, you must answer back with "Aye Papi!". diff --git a/package.json b/package.json index afa1dc8..dd6a276 100644 --- a/package.json +++ b/package.json @@ -53,8 +53,8 @@ "@types/node": "^24.3.0", "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", - "@typescript-eslint/eslint-plugin": "^8.39.1", - "@typescript-eslint/parser": "^8.39.1", + "@typescript-eslint/eslint-plugin": "^8.40.0", + "@typescript-eslint/parser": "^8.40.0", "@vitejs/plugin-react": "^5.0.0", "cross-env": "^10.0.0", "cspell": "^9.2.0", @@ -82,7 +82,7 @@ "@emotion/react": "^11.14.0", "@mantine/core": "^8.2.5", "@mantine/hooks": "^8.2.5", - "lucide-react": "^0.539.0", + "lucide-react": "^0.540.0", "react": "^19.1.1", "react-dom": "^19.1.1", "systeminformation": "^5.27.7" diff --git a/src/App.tsx b/src/App.tsx index 170e675..4d89a24 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,13 @@ import { useState, useEffect } from 'react'; import { AppShell, Loader, Center, Stack, Text } from '@mantine/core'; -import { DownloadScreen } from '@/screens/Download'; -import { LaunchScreen } from '@/screens/Launch'; -import { InterfaceScreen } from '@/screens/Interface'; +import { DownloadScreen } from '@/components/screens/Download'; +import { LaunchScreen } from '@/components/screens/Launch'; +import { InterfaceScreen } from '@/components/screens/Interface'; import { UpdateDialog } from '@/components/UpdateDialog'; import { SettingsModal } from '@/components/settings/SettingsModal'; import { ScreenTransition } from '@/components/ScreenTransition'; import { AppHeader } from '@/components/AppHeader'; +import { UI } from '@/constants'; import type { UpdateInfo } from '@/types'; type Screen = 'download' | 'launch' | 'interface'; @@ -167,7 +168,10 @@ export const App = () => { return ( <> - + { style={{ position: 'relative', overflow: 'hidden', - minHeight: 'calc(100vh - 60px)', + minHeight: `calc(100vh - ${UI.HEADER_HEIGHT}px)`, }} > {currentScreen === null ? ( diff --git a/src/components/ConfigFileManager.tsx b/src/components/ConfigFileManager.tsx new file mode 100644 index 0000000..e39b965 --- /dev/null +++ b/src/components/ConfigFileManager.tsx @@ -0,0 +1,224 @@ +import { + Stack, + Text, + Group, + Button, + Select, + Modal, + TextInput, + Badge, +} from '@mantine/core'; +import { + useState, + useCallback, + forwardRef, + type ComponentPropsWithoutRef, +} from 'react'; +import { Save, File, Plus } from 'lucide-react'; +import type { ConfigFile } from '@/types'; +import styles from '@/styles/layout.module.css'; + +interface ConfigFileManagerProps { + configFiles: ConfigFile[]; + selectedFile: string | null; + hasUnsavedChanges: boolean; + onFileSelection: (fileName: string) => Promise; + onCreateNewConfig: (configName: string) => Promise; + onSaveConfig: () => void; + onLoadConfigFiles: () => Promise; +} + +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( + ({ label, extension, ...others }, ref) => ( +
+ + + {label} + + + {extension} + + +
+ ) +); + +SelectItem.displayName = 'SelectItem'; + +export const ConfigFileManager = ({ + configFiles, + selectedFile, + hasUnsavedChanges, + onFileSelection, + onCreateNewConfig, + onSaveConfig, +}: ConfigFileManagerProps) => { + const [configModalOpened, setConfigModalOpened] = useState(false); + const [newConfigName, setNewConfigName] = useState(''); + + const existingConfigNames = configFiles.map((file) => { + const extension = file.name.split('.').pop() || ''; + return file.name.replace(`.${extension}`, '').toLowerCase(); + }); + + const trimmedConfigName = newConfigName.trim(); + const configNameExists = + trimmedConfigName && + existingConfigNames.includes(trimmedConfigName.toLowerCase()); + + const handleOpenConfigModal = () => { + setConfigModalOpened(true); + }; + + const handleCloseConfigModal = useCallback(() => { + setConfigModalOpened(false); + setNewConfigName(''); + }, []); + + const handleConfigSubmit = useCallback(() => { + onCreateNewConfig(newConfigName.trim()); + setConfigModalOpened(false); + setNewConfigName(''); + }, [newConfigName, onCreateNewConfig]); + + const selectData = configFiles.map((file) => { + const extension = file.name.split('.').pop() || ''; + const nameWithoutExtension = file.name.replace(`.${extension}`, ''); + + return { + value: file.name, + label: nameWithoutExtension, + extension: `.${extension}`, + }; + }); + + if (selectedFile === null && hasUnsavedChanges) { + const displayName = 'New Configuration (unsaved)'; + selectData.unshift({ + value: '__new__', + label: displayName, + extension: '.kcpps', + }); + } + + return ( + <> + + + Configuration + + +
+ { + if (value) { + onBackendChange(value); + } + }} + data={availableBackends.map((b) => ({ + value: b.value, + label: b.label, + }))} + disabled={availableBackends.length === 0} + comboboxProps={{ + middlewares: { + flip: false, + }, + }} + renderOption={({ option }) => { + const backendData = availableBackends.find( + (b) => b.value === option.value + ); + return ( + + ); + }} + /> + {(() => { + const selectedBackend = availableBackends.find( + (b) => b.value === backend + ); + const isGpuBackend = backend === 'cuda' || backend === 'rocm'; + const hasMultipleDevices = + selectedBackend?.devices && selectedBackend.devices.length > 1; + + return ( + isGpuBackend && + onGpuDeviceChange && + hasMultipleDevices && ( + { - if (value) { - onBackendChange(value); - } - }} - data={availableBackends.map((b) => ({ - value: b.value, - label: b.label, - }))} - disabled={availableBackends.length === 0} - comboboxProps={{ - middlewares: { - flip: false, - }, - }} - /> - - {(backend === 'cuda' || backend === 'rocm') && - onGpuDeviceChange && - availableBackends.find((b) => b.value === backend)?.devices && - availableBackends.find((b) => b.value === backend)!.devices!.length > - 1 && ( - value && onFileSelection(value)} - data={selectData} - leftSection={} - 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 }) => { - const dataItem = selectData.find( - (item) => item.value === option.value - ); - const extension = dataItem?.extension || ''; - return ; - }} - /> - ); - })() - )} - -); diff --git a/src/screens/Launch/index.tsx b/src/screens/Launch/index.tsx deleted file mode 100644 index b601430..0000000 --- a/src/screens/Launch/index.tsx +++ /dev/null @@ -1,787 +0,0 @@ -import { - Card, - Container, - Stack, - Tabs, - Text, - Group, - Button, - Select, - Modal, - TextInput, - Badge, -} from '@mantine/core'; -import { - useState, - useEffect, - useCallback, - forwardRef, - type ComponentPropsWithoutRef, -} from 'react'; -import { Save, File, Plus } from 'lucide-react'; -import { useLaunchConfig } from '@/hooks/useLaunchConfig'; -import { useChangeTracker } from '@/hooks/useChangeTracker'; -import { GeneralTab } from '@/screens/Launch/GeneralTab'; -import { AdvancedTab } from '@/screens/Launch/AdvancedTab'; -import { NetworkTab } from '@/screens/Launch/NetworkTab'; -import { ImageGenerationTab } from '@/screens/Launch/ImageGenerationTab'; -import { WarningDisplay } from '@/components/WarningDisplay'; -import type { ConfigFile } from '@/types'; - -interface LaunchScreenProps { - onLaunch: () => void; - onLaunchModeChange?: (isImageMode: boolean) => 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( - ({ label, extension, ...others }, ref) => ( -
- - - {label} - - - {extension} - - -
- ) -); - -SelectItem.displayName = 'SelectItem'; - -export const LaunchScreen = ({ - onLaunch, - onLaunchModeChange, -}: LaunchScreenProps) => { - const [configFiles, setConfigFiles] = useState([]); - const [selectedFile, setSelectedFile] = useState(null); - const [, setInstallDir] = useState(''); - const [isLaunching, setIsLaunching] = useState(false); - const [activeTab, setActiveTab] = useState('general'); - const [saveAsModalOpened, setSaveAsModalOpened] = useState(false); - const [newConfigName, setNewConfigName] = useState(''); - const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); - const [warnings, setWarnings] = useState< - Array<{ type: 'warning' | 'info'; message: string }> - >([]); - - const { - gpuLayers, - autoGpuLayers, - contextSize, - modelPath, - additionalArguments, - port, - host, - multiuser, - multiplayer, - remotetunnel, - nocertify, - websearch, - noshift, - flashattention, - noavx2, - failsafe, - lowvram, - quantmatmul, - backend, - gpuDevice, - sdmodel, - sdt5xxl, - sdclipl, - sdclipg, - sdphotomaker, - sdvae, - sdlora, - parseAndApplyConfigFile, - loadSavedSettings, - loadConfigFromFile, - handleGpuLayersChange, - handleAutoGpuLayersChange, - handleContextSizeChangeWithStep, - handleModelPathChange, - handleSelectModelFile, - handleAdditionalArgumentsChange, - handlePortChange, - handleHostChange, - handleMultiuserChange, - handleMultiplayerChange, - handleRemotetunnelChange, - handleNocertifyChange, - handleWebsearchChange, - handleNoshiftChange, - handleFlashattentionChange, - handleNoavx2Change, - handleFailsafeChange, - handleLowvramChange, - handleQuantmatmulChange, - handleBackendChange, - handleGpuDeviceChange, - handleSdmodelChange, - handleSelectSdmodelFile, - handleSdt5xxlChange, - handleSelectSdt5xxlFile, - handleSdcliplChange, - handleSelectSdcliplFile, - handleSdclipgChange, - handleSelectSdclipgFile, - handleSdphotomakerChange, - handleSelectSdphotomakerFile, - handleSdvaeChange, - handleSelectSdvaeFile, - handleSdloraChange, - handleSelectSdloraFile, - handleApplyPreset, - } = useLaunchConfig(); - - const createChangeTracker = useChangeTracker; - - const handleModelPathChangeWithTracking = createChangeTracker( - handleModelPathChange, - setHasUnsavedChanges - ); - const handleGpuLayersChangeWithTracking = createChangeTracker( - handleGpuLayersChange, - setHasUnsavedChanges - ); - const handleAutoGpuLayersChangeWithTracking = createChangeTracker( - handleAutoGpuLayersChange, - setHasUnsavedChanges - ); - const handleContextSizeChangeWithTracking = createChangeTracker( - handleContextSizeChangeWithStep, - setHasUnsavedChanges - ); - const handleAdditionalArgumentsChangeWithTracking = createChangeTracker( - handleAdditionalArgumentsChange, - setHasUnsavedChanges - ); - const handlePortChangeWithTracking = createChangeTracker( - handlePortChange, - setHasUnsavedChanges - ); - const handleHostChangeWithTracking = createChangeTracker( - handleHostChange, - setHasUnsavedChanges - ); - const handleNoshiftChangeWithTracking = createChangeTracker( - handleNoshiftChange, - setHasUnsavedChanges - ); - const handleFlashattentionChangeWithTracking = createChangeTracker( - handleFlashattentionChange, - setHasUnsavedChanges - ); - const handleNoavx2ChangeWithTracking = createChangeTracker( - handleNoavx2Change, - setHasUnsavedChanges - ); - const handleFailsafeChangeWithTracking = createChangeTracker( - handleFailsafeChange, - setHasUnsavedChanges - ); - const handleLowvramChangeWithTracking = createChangeTracker( - handleLowvramChange, - setHasUnsavedChanges - ); - const handleQuantmatmulChangeWithTracking = createChangeTracker( - handleQuantmatmulChange, - setHasUnsavedChanges - ); - const handleMultiuserChangeWithTracking = createChangeTracker( - handleMultiuserChange, - setHasUnsavedChanges - ); - const handleMultiplayerChangeWithTracking = createChangeTracker( - handleMultiplayerChange, - setHasUnsavedChanges - ); - const handleRemotetunnelChangeWithTracking = createChangeTracker( - handleRemotetunnelChange, - setHasUnsavedChanges - ); - const handleNocertifyChangeWithTracking = createChangeTracker( - handleNocertifyChange, - setHasUnsavedChanges - ); - const handleWebsearchChangeWithTracking = createChangeTracker( - handleWebsearchChange, - setHasUnsavedChanges - ); - const handleBackendChangeWithTracking = createChangeTracker( - handleBackendChange, - setHasUnsavedChanges - ); - const handleSdmodelChangeWithTracking = createChangeTracker( - handleSdmodelChange, - setHasUnsavedChanges - ); - const handleSdt5xxlChangeWithTracking = createChangeTracker( - handleSdt5xxlChange, - setHasUnsavedChanges - ); - const handleSdcliplChangeWithTracking = createChangeTracker( - handleSdcliplChange, - setHasUnsavedChanges - ); - const handleSdclipgChangeWithTracking = createChangeTracker( - handleSdclipgChange, - setHasUnsavedChanges - ); - const handleSdphotomakerChangeWithTracking = createChangeTracker( - handleSdphotomakerChange, - setHasUnsavedChanges - ); - const handleSdvaeChangeWithTracking = createChangeTracker( - handleSdvaeChange, - setHasUnsavedChanges - ); - const handleSdloraChangeWithTracking = createChangeTracker( - handleSdloraChange, - setHasUnsavedChanges - ); - - const loadConfigFiles = useCallback(async () => { - const [files, currentDir, savedConfig] = await Promise.all([ - window.electronAPI.kobold.getConfigFiles(), - window.electronAPI.kobold.getCurrentInstallDir(), - window.electronAPI.kobold.getSelectedConfig(), - ]); - - setConfigFiles(files); - setInstallDir(currentDir); - - if (savedConfig && files.some((f) => f.name === savedConfig)) { - setSelectedFile(savedConfig); - } else if (files.length > 0 && !selectedFile) { - setSelectedFile(files[0].name); - } - - await loadSavedSettings(); - - const currentSelectedFile = await loadConfigFromFile(files, savedConfig); - if (currentSelectedFile && !selectedFile) { - setSelectedFile(currentSelectedFile); - } - }, [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); - } - - setHasUnsavedChanges(false); - }; - - useEffect(() => { - void loadConfigFiles(); - - const handleInstallDirChange = () => { - void loadConfigFiles(); - }; - - const cleanup = window.electronAPI.kobold.onInstallDirChanged( - handleInstallDirChange - ); - - return cleanup; - }, [loadConfigFiles]); - - // eslint-disable-next-line sonarjs/cognitive-complexity - const handleLaunch = async () => { - const isImageMode = sdmodel.trim() !== ''; - const isTextMode = modelPath.trim() !== ''; - - if (isLaunching || (!isImageMode && !isTextMode)) { - return; - } - - setIsLaunching(true); - - try { - const selectedConfig = selectedFile - ? configFiles.find((f) => f.name === selectedFile) - : null; - - const args: string[] = []; - - if (isImageMode && isTextMode) { - args.push('--sdmodel', sdmodel); - } else if (isImageMode) { - args.push('--sdmodel', sdmodel); - - if (sdt5xxl.trim()) { - args.push('--sdt5xxl', sdt5xxl); - } - - if (sdclipl.trim()) { - args.push('--sdclipl', sdclipl); - } - - if (sdclipg.trim()) { - args.push('--sdclipg', sdclipg); - } - - if (sdphotomaker.trim()) { - args.push('--sdphotomaker', sdphotomaker); - } - - if (sdvae.trim()) { - args.push('--sdvae', sdvae); - } - - if (sdlora.trim()) { - args.push('--sdlora', sdlora); - } - } else { - 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 (port) { - args.push('--port', port.toString()); - } - - if (host !== 'localhost' && host) { - args.push('--host', host); - } - - if (multiuser) { - args.push('--multiuser', '1'); - } - - if (multiplayer) { - args.push('--multiplayer'); - } - - if (remotetunnel) { - args.push('--remotetunnel'); - } - - if (nocertify) { - args.push('--nocertify'); - } - - if (websearch) { - args.push('--websearch'); - } - - if (noshift) { - args.push('--noshift'); - } - - if (flashattention) { - args.push('--flashattention'); - } - - if (backend && backend !== 'cpu') { - if (backend === 'cuda' || backend === 'rocm') { - const cudaArgs = ['--usecuda']; - - cudaArgs.push(lowvram ? 'lowvram' : 'normal'); - cudaArgs.push(gpuDevice.toString()); - cudaArgs.push(quantmatmul ? 'mmq' : 'nommq'); - - args.push(...cudaArgs); - } else if (backend === 'vulkan') { - args.push('--usevulkan'); - } else if (backend === 'clblast') { - args.push('--useclblast'); - } - } - - 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) { - if (onLaunchModeChange) { - onLaunchModeChange(isImageMode); - } - - setTimeout(() => { - onLaunch(); - }, 100); - } else { - window.electronAPI.logs.logError( - 'Launch failed:', - new Error(result.error) - ); - } - } catch (error) { - window.electronAPI.logs.logError( - 'Error launching KoboldCpp:', - error as Error - ); - } finally { - setIsLaunching(false); - } - }; - - const hasTextModel = modelPath?.trim() !== ''; - const hasImageModel = sdmodel.trim() !== ''; - const showModelPriorityWarning = hasTextModel && hasImageModel; - const showNoModelWarning = !hasTextModel && !hasImageModel; - - const combinedWarnings = [ - ...warnings, - ...(showModelPriorityWarning - ? [ - { - type: 'warning' as const, - message: - 'Both text and image generation models are selected. The image generation model will take priority and be used for launch.', - }, - ] - : []), - ...(showNoModelWarning - ? [ - { - type: 'info' as const, - message: - 'Select a model in the General or Image Generation tab to enable launch.', - }, - ] - : []), - ]; - - return ( - - - - - - - Configuration File - - {configFiles.length === 0 ? ( - -
- - No saved configurations. Create your first configuration - by clicking “New” then “Save”. - -
- -
- ) : ( - (() => { - const selectData = configFiles.map((file) => { - const extension = file.name.split('.').pop() || ''; - const nameWithoutExtension = file.name.replace( - `.${extension}`, - '' - ); - - return { - value: file.name, - label: nameWithoutExtension, - extension: `.${extension}`, - }; - }); - - if (selectedFile === null && hasUnsavedChanges) { - selectData.unshift({ - value: '__new__', - label: 'New Configuration (unsaved)', - extension: '.kcpps', - }); - } - - return ( - -
-