mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
UI updates based on impeccable passes
This commit is contained in:
parent
bbea8a537b
commit
8c1082627f
36 changed files with 320 additions and 325 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -3,6 +3,8 @@ dist/
|
|||
dist-electron/
|
||||
out/
|
||||
release/
|
||||
.agents/
|
||||
.impeccable.md
|
||||
*.log
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "gerbil",
|
||||
"version": "1.22.1",
|
||||
"version": "1.23.0",
|
||||
"description": "Run Large Language Models locally",
|
||||
"keywords": [
|
||||
"ai",
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
"@codemirror/search": "^6.6.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.41.0",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@fontsource/geist": "^5.2.8",
|
||||
"@huggingface/gguf": "^0.4.2",
|
||||
"@mantine/core": "^9.0.1",
|
||||
"@mantine/hooks": "^9.0.1",
|
||||
|
|
|
|||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
|
|
@ -42,7 +42,7 @@ importers:
|
|||
'@codemirror/view':
|
||||
specifier: ^6.41.0
|
||||
version: 6.41.0
|
||||
'@fontsource/inter':
|
||||
'@fontsource/geist':
|
||||
specifier: ^5.2.8
|
||||
version: 5.2.8
|
||||
'@huggingface/gguf':
|
||||
|
|
@ -489,8 +489,8 @@ packages:
|
|||
'@floating-ui/utils@0.2.11':
|
||||
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
|
||||
|
||||
'@fontsource/inter@5.2.8':
|
||||
resolution: {integrity: sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==}
|
||||
'@fontsource/geist@5.2.8':
|
||||
resolution: {integrity: sha512-pI54klK6vz8fVpAVyV+iODGLEzw73cs1XJqV9PIXgW8MYMmBcwK6lKKaWqJrNHwEx8ppz9cuhussb6arXQ/PVQ==}
|
||||
|
||||
'@huggingface/gguf@0.4.2':
|
||||
resolution: {integrity: sha512-pog57zGj/u8uZ6cFi2vH9MkWvq+ZtFDa5iUeNxggDSle+04htJQFbl31TcauGUhnxTBJokFexWK9WL2BakLR0Q==}
|
||||
|
|
@ -3530,7 +3530,7 @@ snapshots:
|
|||
|
||||
'@floating-ui/utils@0.2.11': {}
|
||||
|
||||
'@fontsource/inter@5.2.8': {}
|
||||
'@fontsource/geist@5.2.8': {}
|
||||
|
||||
'@huggingface/gguf@0.4.2':
|
||||
dependencies:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,17 @@
|
|||
import { ActionIcon, Button, Tooltip } from '@mantine/core';
|
||||
import { Activity } from 'lucide-react';
|
||||
|
||||
const BADGE_STYLE = {
|
||||
borderRadius: '0.75rem',
|
||||
fontSize: '0.7em',
|
||||
fontWeight: 500,
|
||||
height: 'auto',
|
||||
margin: '0.125rem 0',
|
||||
minWidth: '5rem',
|
||||
padding: '0.25rem 0.5rem',
|
||||
textAlign: 'center',
|
||||
} as const;
|
||||
|
||||
interface PerformanceBadgeProps {
|
||||
label?: string;
|
||||
value?: string;
|
||||
|
|
@ -25,7 +36,12 @@ export const PerformanceBadge = ({
|
|||
if (iconOnly) {
|
||||
return (
|
||||
<Tooltip label={tooltipLabel} position="top">
|
||||
<ActionIcon size="sm" variant="subtle" onClick={() => void handlePerformanceClick()}>
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
aria-label="View performance details"
|
||||
onClick={() => void handlePerformanceClick()}
|
||||
>
|
||||
<Activity size="1.125rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
|
@ -37,16 +53,7 @@ export const PerformanceBadge = ({
|
|||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
style={{
|
||||
borderRadius: '0.75rem',
|
||||
fontSize: '0.7em',
|
||||
fontWeight: 500,
|
||||
height: 'auto',
|
||||
margin: '0.125rem 0',
|
||||
minWidth: '5rem',
|
||||
padding: '0.25rem 0.5rem',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
style={BADGE_STYLE}
|
||||
onClick={() => void handlePerformanceClick()}
|
||||
>
|
||||
{label}: {value}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export const AppRouter = ({
|
|||
const isInterfaceScreen = currentScreen === 'interface';
|
||||
|
||||
return (
|
||||
<>
|
||||
<main style={{ display: 'contents' }}>
|
||||
<ScreenTransition isActive={currentScreen === 'welcome'} shouldAnimate={hasInitialized}>
|
||||
<WelcomeScreen onGetStarted={onWelcomeComplete} />
|
||||
</ScreenTransition>
|
||||
|
|
@ -43,6 +43,6 @@ export const AppRouter = ({
|
|||
<ScreenTransition isActive={isInterfaceScreen} shouldAnimate={hasInitialized}>
|
||||
<InterfaceScreen activeTab={activeInterfaceTab} isServerReady={isServerReady} />
|
||||
</ScreenTransition>
|
||||
</>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,12 +13,14 @@ export const ScreenTransition = ({ isActive, shouldAnimate, children }: ScreenTr
|
|||
transition="fade"
|
||||
duration={shouldAnimate ? 100 : 0}
|
||||
timingFunction="ease-out"
|
||||
keepMounted={false}
|
||||
>
|
||||
{(styles) => (
|
||||
<div
|
||||
style={{
|
||||
...styles,
|
||||
}}
|
||||
inert={!isActive ? true : undefined}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,12 +15,8 @@ export const StatusBar = () => {
|
|||
const [memoryMetrics, setMemoryMetrics] = useState<MemoryMetrics | null>(null);
|
||||
const [gpuMetrics, setGpuMetrics] = useState<GpuMetrics | null>(null);
|
||||
const [tunnelBaseUrl, setTunnelBaseUrl] = useState<string | null>(null);
|
||||
const {
|
||||
resolvedColorScheme: colorScheme,
|
||||
systemMonitoringEnabled,
|
||||
frontendPreference,
|
||||
imageGenerationFrontendPreference,
|
||||
} = usePreferencesStore();
|
||||
const { systemMonitoringEnabled, frontendPreference, imageGenerationFrontendPreference } =
|
||||
usePreferencesStore();
|
||||
const { isVisible, setVisible } = useNotepadStore();
|
||||
const { isImageGenerationMode } = useLaunchConfigStore();
|
||||
|
||||
|
|
@ -100,13 +96,16 @@ export const StatusBar = () => {
|
|||
gap="xs"
|
||||
justify="space-between"
|
||||
h="100%"
|
||||
bg={colorScheme === 'dark' ? 'dark.6' : 'gray.1'}
|
||||
style={{
|
||||
backgroundColor: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6))',
|
||||
}}
|
||||
>
|
||||
<Group gap="xs">
|
||||
<Tooltip label="Notepad" disabled={isVisible}>
|
||||
<ActionIcon
|
||||
variant={isVisible ? 'filled' : 'subtle'}
|
||||
size="sm"
|
||||
aria-label="Toggle notepad"
|
||||
onClick={() => setVisible(!isVisible)}
|
||||
>
|
||||
<NotepadText size="1.25rem" />
|
||||
|
|
@ -120,6 +119,7 @@ export const StatusBar = () => {
|
|||
variant="subtle"
|
||||
size="sm"
|
||||
color={copied ? 'teal' : undefined}
|
||||
aria-label={copied ? 'Copied!' : 'Copy tunnel URL'}
|
||||
onClick={copy}
|
||||
>
|
||||
{copied ? <Check size="1.25rem" /> : <Globe size="1.25rem" />}
|
||||
|
|
|
|||
|
|
@ -33,11 +33,7 @@ const renderOption = ({ option }: { option: SelectOption }) => (
|
|||
);
|
||||
|
||||
export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: TitleBarProps) => {
|
||||
const {
|
||||
resolvedColorScheme: colorScheme,
|
||||
frontendPreference,
|
||||
imageGenerationFrontendPreference,
|
||||
} = usePreferencesStore();
|
||||
const { frontendPreference, imageGenerationFrontendPreference } = usePreferencesStore();
|
||||
const { handleLogoClick, getLogoStyles } = useLogoClickSounds();
|
||||
const [isMaximized, setIsMaximized] = useState(false);
|
||||
const [isSelectOpen, setIsSelectOpen] = useState(false);
|
||||
|
|
@ -74,8 +70,7 @@ export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: Ti
|
|||
style={{
|
||||
WebkitAppRegion: isSelectOpen ? 'no-drag' : 'drag',
|
||||
alignItems: 'center',
|
||||
backgroundColor:
|
||||
colorScheme === 'dark' ? 'var(--mantine-color-dark-6)' : 'var(--mantine-color-gray-1)',
|
||||
backgroundColor: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6))',
|
||||
borderBottom: '1px solid var(--mantine-color-default-border)',
|
||||
display: 'flex',
|
||||
height: TITLEBAR_HEIGHT,
|
||||
|
|
@ -154,9 +149,7 @@ export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: Ti
|
|||
<Box
|
||||
style={{
|
||||
backgroundColor:
|
||||
colorScheme === 'dark'
|
||||
? 'var(--mantine-color-dark-3)'
|
||||
: 'var(--mantine-color-gray-4)',
|
||||
'light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-3))',
|
||||
height: '1.25rem',
|
||||
margin: '0 0.25rem',
|
||||
width: '0.1rem',
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export const UpdateAvailableModal = ({
|
|||
closeOnEscape={!isDownloading && !isUpdating}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Card withBorder radius="md" p="md" bd="2px solid orange">
|
||||
<Card withBorder radius="md" p="md" bd="2px solid var(--mantine-color-orange-5)">
|
||||
<Stack gap="xs">
|
||||
<Group gap="md" align="center">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -72,7 +72,6 @@ export const UpdateButton = () => {
|
|||
color={color}
|
||||
size={TITLEBAR_HEIGHT}
|
||||
aria-label={label}
|
||||
tabIndex={-1}
|
||||
onClick={onClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { Download, Trash2 } from 'lucide-react';
|
|||
import type { MouseEvent } from 'react';
|
||||
|
||||
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import type { BackendInfo } from '@/types';
|
||||
import { isWindowsROCmBuild, pretifyBinName } from '@/utils/assets';
|
||||
|
||||
|
|
@ -30,7 +29,6 @@ export const DownloadCard = ({
|
|||
onRedownload,
|
||||
onDelete,
|
||||
}: DownloadCardProps) => {
|
||||
const { resolvedColorScheme: colorScheme } = usePreferencesStore();
|
||||
const { downloading, downloadProgress } = useKoboldBackendsStore();
|
||||
|
||||
const isLoading = downloading === backend.name;
|
||||
|
|
@ -156,8 +154,10 @@ export const DownloadCard = ({
|
|||
radius="sm"
|
||||
padding="sm"
|
||||
{...(backend.isCurrent && {
|
||||
bd: `2px solid var(--mantine-color-${colorScheme === 'dark' ? 'blue-4' : 'blue-6'})`,
|
||||
bg: colorScheme === 'dark' ? 'dark.6' : 'gray.0',
|
||||
style: {
|
||||
border: 'light-dark(var(--mantine-color-brand-6), var(--mantine-color-brand-4))',
|
||||
backgroundColor: 'light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6))',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<Group justify="space-between" align="center">
|
||||
|
|
@ -167,7 +167,7 @@ export const DownloadCard = ({
|
|||
{pretifyBinName(backend.name)}
|
||||
</Text>
|
||||
{backend.isCurrent && (
|
||||
<Badge variant="light" color="blue" size="sm">
|
||||
<Badge variant="light" color="brand" size="sm">
|
||||
Current
|
||||
</Badge>
|
||||
)}
|
||||
|
|
@ -217,8 +217,8 @@ export const DownloadCard = ({
|
|||
|
||||
{isLoading && currentProgress !== undefined && (
|
||||
<Stack gap="xs" mt="sm">
|
||||
<Progress value={currentProgress} color="blue" radius="xl" />
|
||||
<Text size="xs" c="dimmed" ta="center">
|
||||
<Progress value={currentProgress} color="brand" radius="xl" />
|
||||
<Text size="xs" c="dimmed" ta="center" aria-live="polite">
|
||||
{currentProgress === 100
|
||||
? '100.0% complete'
|
||||
: `${currentProgress.toFixed(1).padStart(5, ' ')}% complete`}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ interface InfoTooltipProps {
|
|||
|
||||
export const InfoTooltip = ({ label, multiline = true, width = 300 }: InfoTooltipProps) => (
|
||||
<Tooltip label={label} multiline={multiline} w={width}>
|
||||
<ActionIcon variant="subtle" size="xs" color="gray">
|
||||
<ActionIcon variant="subtle" size="sm" color="gray" aria-label="More information">
|
||||
<Info size={14} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import type { MouseEvent } from 'react';
|
|||
|
||||
import { NOTEPAD_MIN_HEIGHT, NOTEPAD_MIN_WIDTH } from '@/constants/notepad';
|
||||
import { useNotepadStore } from '@/stores/notepad';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
|
||||
import { CloseConfirmModal } from './CloseConfirmModal.tsx';
|
||||
import { NotepadEditor } from './Editor.tsx';
|
||||
|
|
@ -23,7 +22,6 @@ export const NotepadContainer = () => {
|
|||
isVisible,
|
||||
setVisible,
|
||||
} = useNotepadStore();
|
||||
const { resolvedColorScheme } = usePreferencesStore();
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [resizeDirection, setResizeDirection] = useState<string | null>(null);
|
||||
|
|
@ -125,10 +123,7 @@ export const NotepadContainer = () => {
|
|||
shadow="lg"
|
||||
withBorder
|
||||
style={{
|
||||
backgroundColor:
|
||||
resolvedColorScheme === 'dark'
|
||||
? 'var(--mantine-color-dark-6)'
|
||||
: 'var(--mantine-color-white)',
|
||||
backgroundColor: 'light-dark(var(--mantine-color-white), var(--mantine-color-dark-6))',
|
||||
bottom: 24,
|
||||
cursor: 'default',
|
||||
height: position.height,
|
||||
|
|
@ -199,11 +194,8 @@ export const NotepadContainer = () => {
|
|||
<Box
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
borderBottom: `1px solid ${
|
||||
resolvedColorScheme === 'dark'
|
||||
? 'var(--mantine-color-dark-4)'
|
||||
: 'var(--mantine-color-gray-3)'
|
||||
}`,
|
||||
borderBottom:
|
||||
'1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4))',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
minHeight: 28,
|
||||
|
|
@ -220,13 +212,31 @@ export const NotepadContainer = () => {
|
|||
paddingRight: 8,
|
||||
}}
|
||||
>
|
||||
<ActionIcon variant="subtle" size="xs" onClick={() => setVisible(false)}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
aria-label="Minimize notepad"
|
||||
onClick={() => setVisible(false)}
|
||||
>
|
||||
<Minus size="1rem" />
|
||||
</ActionIcon>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box style={{ flex: 1, position: 'relative' }}>
|
||||
<Box
|
||||
role="tabpanel"
|
||||
id={
|
||||
activeTab
|
||||
? `notepad-panel-${activeTab.title.replace(/[^a-z0-9]/gi, '-').toLowerCase()}`
|
||||
: undefined
|
||||
}
|
||||
aria-labelledby={
|
||||
activeTab
|
||||
? `notepad-tab-${activeTab.title.replace(/[^a-z0-9]/gi, '-').toLowerCase()}`
|
||||
: undefined
|
||||
}
|
||||
style={{ flex: 1, position: 'relative' }}
|
||||
>
|
||||
{activeTab && <NotepadEditor key={activeTab.title} tab={activeTab} />}
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -20,24 +20,22 @@ export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
|
|||
const { saveTabContent, showLineNumbers, setShowLineNumbers } = useNotepadStore();
|
||||
const { resolvedColorScheme } = usePreferencesStore();
|
||||
const [content, setContent] = useState(() => tab.content);
|
||||
const [saveTimeout, setSaveTimeout] = useState<ReturnType<typeof setTimeout> | null>(null);
|
||||
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const editorRef = useRef<ReactCodeMirrorRef>(null);
|
||||
|
||||
const handleContentChange = useCallback(
|
||||
(newContent: string) => {
|
||||
setContent(newContent);
|
||||
|
||||
if (saveTimeout) {
|
||||
clearTimeout(saveTimeout);
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current);
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
saveTimeoutRef.current = setTimeout(() => {
|
||||
void saveTabContent(tab.title, newContent);
|
||||
}, 500);
|
||||
|
||||
setSaveTimeout(timeout);
|
||||
},
|
||||
[tab.title, saveTabContent, saveTimeout],
|
||||
[tab.title, saveTabContent],
|
||||
);
|
||||
|
||||
const handleEditorContextMenu = (e: MouseEvent) => {
|
||||
|
|
@ -56,11 +54,11 @@ export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
|
|||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (saveTimeout) {
|
||||
clearTimeout(saveTimeout);
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current);
|
||||
}
|
||||
},
|
||||
[saveTimeout],
|
||||
[],
|
||||
);
|
||||
|
||||
const extensions = [
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ import { X } from 'lucide-react';
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import type { DragEvent, KeyboardEvent, MouseEvent } from 'react';
|
||||
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
|
||||
interface TabProps {
|
||||
title: string;
|
||||
index: number;
|
||||
|
|
@ -34,7 +32,6 @@ export const Tab = ({
|
|||
showLineNumbers,
|
||||
setShowLineNumbers,
|
||||
}: TabProps) => {
|
||||
const { resolvedColorScheme } = usePreferencesStore();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editingTitle, setEditingTitle] = useState(title);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
|
@ -97,28 +94,32 @@ export const Tab = ({
|
|||
|
||||
return (
|
||||
<Box
|
||||
id={`notepad-tab-${title.replace(/[^a-z0-9]/gi, '-').toLowerCase()}`}
|
||||
role="tab"
|
||||
aria-selected={isActive}
|
||||
aria-controls={`notepad-panel-${title.replace(/[^a-z0-9]/gi, '-').toLowerCase()}`}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
draggable={!isEditing}
|
||||
onClick={handleTabClick}
|
||||
onMouseDown={handleMouseDown}
|
||||
onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (!isEditing && (e.key === 'Enter' || e.key === ' ')) {
|
||||
e.preventDefault();
|
||||
handleTabClick();
|
||||
}
|
||||
}}
|
||||
onDragStart={(e) => onDragStart(e, index)}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={(e) => onDrop(e, index)}
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
backgroundColor: isActive
|
||||
? resolvedColorScheme === 'dark'
|
||||
? 'var(--mantine-color-dark-4)'
|
||||
: 'var(--mantine-color-gray-1)'
|
||||
? 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-4))'
|
||||
: isDragOver
|
||||
? resolvedColorScheme === 'dark'
|
||||
? 'var(--mantine-color-dark-5)'
|
||||
: 'var(--mantine-color-gray-2)'
|
||||
? 'light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5))'
|
||||
: 'transparent',
|
||||
borderRight: `1px solid ${
|
||||
resolvedColorScheme === 'dark'
|
||||
? 'var(--mantine-color-dark-4)'
|
||||
: 'var(--mantine-color-gray-3)'
|
||||
}`,
|
||||
borderRight:
|
||||
'1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4))',
|
||||
cursor: isEditing ? 'default' : 'pointer',
|
||||
display: 'flex',
|
||||
gap: '0.25rem',
|
||||
|
|
@ -173,7 +174,7 @@ export const Tab = ({
|
|||
</Text>
|
||||
)}
|
||||
|
||||
<ActionIcon variant="subtle" size="xs" onClick={onClose}>
|
||||
<ActionIcon variant="subtle" size="sm" aria-label="Close tab" onClick={onClose}>
|
||||
<X size="0.625rem" />
|
||||
</ActionIcon>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { ActionIcon, Box } from '@mantine/core';
|
||||
import { Plus } from 'lucide-react';
|
||||
import type { DragEvent, MouseEvent } from 'react';
|
||||
import type { DragEvent, KeyboardEvent, MouseEvent } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Tab } from '@/components/Notepad/Tab';
|
||||
import { useNotepadStore } from '@/stores/notepad';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
|
||||
interface NotepadTabsProps {
|
||||
onCreateNewTab: () => Promise<void>;
|
||||
|
|
@ -27,7 +26,6 @@ export const NotepadTabs = ({ onCreateNewTab, onCloseTab }: NotepadTabsProps) =>
|
|||
showLineNumbers,
|
||||
setShowLineNumbers,
|
||||
} = useNotepadStore();
|
||||
const { resolvedColorScheme } = usePreferencesStore();
|
||||
const [draggedTabIndex, setDraggedTabIndex] = useState<number | null>(null);
|
||||
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
||||
|
||||
|
|
@ -53,6 +51,19 @@ export const NotepadTabs = ({ onCreateNewTab, onCloseTab }: NotepadTabsProps) =>
|
|||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||
const currentIndex = tabs.findIndex((t) => t.title === activeTabId);
|
||||
if (e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
const next = (currentIndex + 1) % tabs.length;
|
||||
setActiveTab(tabs[next].title);
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
const prev = (currentIndex - 1 + tabs.length) % tabs.length;
|
||||
setActiveTab(tabs[prev].title);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragStart = (e: DragEvent, index: number) => {
|
||||
setDraggedTabIndex(index);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
|
|
@ -79,13 +90,13 @@ export const NotepadTabs = ({ onCreateNewTab, onCloseTab }: NotepadTabsProps) =>
|
|||
|
||||
return (
|
||||
<Box
|
||||
role="tablist"
|
||||
aria-label="Notepad tabs"
|
||||
onContextMenu={handleTabBarContextMenu}
|
||||
onKeyDown={handleKeyDown}
|
||||
style={{
|
||||
borderBottom: `1px solid ${
|
||||
resolvedColorScheme === 'dark'
|
||||
? 'var(--mantine-color-dark-4)'
|
||||
: 'var(--mantine-color-gray-3)'
|
||||
}`,
|
||||
borderBottom:
|
||||
'1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4))',
|
||||
display: 'flex',
|
||||
minHeight: '2rem',
|
||||
overflow: 'hidden',
|
||||
|
|
@ -116,7 +127,8 @@ export const NotepadTabs = ({ onCreateNewTab, onCloseTab }: NotepadTabsProps) =>
|
|||
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
size="sm"
|
||||
aria-label="New tab"
|
||||
onClick={() => void onCreateNewTab()}
|
||||
style={{
|
||||
alignSelf: 'center',
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ export const Switch = (props: SwitchProps) => (
|
|||
<MantineSwitch
|
||||
{...props}
|
||||
styles={{
|
||||
thumb: { transition: 'none' },
|
||||
track: { transition: 'none' },
|
||||
thumb: { transition: 'left 100ms ease, background-color 100ms ease' },
|
||||
track: { transition: 'background-color 100ms ease' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ export const WarningDisplay = ({ warnings, children }: WarningDisplayProps) => {
|
|||
warningMessages[0].message
|
||||
) : (
|
||||
<List size="sm" spacing={4}>
|
||||
{warningMessages.map((warning, index) => (
|
||||
<List.Item key={index}>{warning.message}</List.Item>
|
||||
{warningMessages.map((warning) => (
|
||||
<List.Item key={warning.message}>{warning.message}</List.Item>
|
||||
))}
|
||||
</List>
|
||||
)
|
||||
|
|
@ -48,8 +48,8 @@ export const WarningDisplay = ({ warnings, children }: WarningDisplayProps) => {
|
|||
infoMessages[0].message
|
||||
) : (
|
||||
<List size="sm" spacing={4}>
|
||||
{infoMessages.map((info, index) => (
|
||||
<List.Item key={index}>{info.message}</List.Item>
|
||||
{infoMessages.map((info) => (
|
||||
<List.Item key={info.message}>{info.message}</List.Item>
|
||||
))}
|
||||
</List>
|
||||
)
|
||||
|
|
@ -57,7 +57,7 @@ export const WarningDisplay = ({ warnings, children }: WarningDisplayProps) => {
|
|||
multiline
|
||||
maw={320}
|
||||
>
|
||||
<Info size={18} color="var(--mantine-color-blue-6)" strokeWidth={2} />
|
||||
<Info size={18} color="var(--mantine-color-brand-5)" strokeWidth={2} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Card, Container, Loader, Stack, Text, Title } from '@mantine/core';
|
||||
import { Container, Loader, Stack, Text, Title } from '@mantine/core';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { DownloadCard } from '@/components/DownloadCard';
|
||||
|
|
@ -59,66 +59,62 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
|
||||
return (
|
||||
<Container size="sm" mt="md">
|
||||
<Card withBorder radius="md" shadow="sm">
|
||||
<Stack gap="lg">
|
||||
<Title order={3}>Select a Backend</Title>
|
||||
<Stack gap="lg">
|
||||
<Title order={3}>Select a Backend</Title>
|
||||
|
||||
{loading ? (
|
||||
<Stack align="center" gap="md" py="xl">
|
||||
<Loader color="blue" />
|
||||
<Text c="dimmed">Preparing download options...</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
<>
|
||||
{availableDownloads.length > 0 ? (
|
||||
<Stack gap="sm">
|
||||
{availableDownloads.map((download) => {
|
||||
const isDownloading =
|
||||
Boolean(downloading) && downloadingAsset === download.name;
|
||||
{loading ? (
|
||||
<Stack align="center" gap="md" py="xl">
|
||||
<Loader color="brand" />
|
||||
<Text c="dimmed">Preparing download options...</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
<>
|
||||
{availableDownloads.length > 0 ? (
|
||||
<Stack gap="sm">
|
||||
{availableDownloads.map((download) => {
|
||||
const isDownloading = Boolean(downloading) && downloadingAsset === download.name;
|
||||
|
||||
return (
|
||||
<div key={download.name} ref={isDownloading ? downloadingItemRef : null}>
|
||||
<DownloadCard
|
||||
backend={{
|
||||
downloadUrl: download.url,
|
||||
hasUpdate: false,
|
||||
isCurrent: false,
|
||||
isInstalled: false,
|
||||
name: download.name,
|
||||
size: download.size,
|
||||
version: download.version ?? '',
|
||||
}}
|
||||
size={formatDownloadSize(download.size, download.url)}
|
||||
description={getAssetDescription(download.name)}
|
||||
disabled={
|
||||
importing ||
|
||||
(Boolean(downloading) && downloadingAsset !== download.name)
|
||||
}
|
||||
onDownload={(e) => {
|
||||
e.stopPropagation();
|
||||
void handleDownload(download);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed" ta="center">
|
||||
Unable to fetch downloads for your platform ({getPlatformDisplayName(platform)}).
|
||||
Check your internet connection and try again.
|
||||
</Text>
|
||||
)}
|
||||
return (
|
||||
<div key={download.name} ref={isDownloading ? downloadingItemRef : null}>
|
||||
<DownloadCard
|
||||
backend={{
|
||||
downloadUrl: download.url,
|
||||
hasUpdate: false,
|
||||
isCurrent: false,
|
||||
isInstalled: false,
|
||||
name: download.name,
|
||||
size: download.size,
|
||||
version: download.version ?? '',
|
||||
}}
|
||||
size={formatDownloadSize(download.size, download.url)}
|
||||
description={getAssetDescription(download.name)}
|
||||
disabled={
|
||||
importing || (Boolean(downloading) && downloadingAsset !== download.name)
|
||||
}
|
||||
onDownload={(e) => {
|
||||
e.stopPropagation();
|
||||
void handleDownload(download);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed" ta="center">
|
||||
Unable to fetch downloads for your platform ({getPlatformDisplayName(platform)}).
|
||||
Check your internet connection and try again.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<ImportBackendLink
|
||||
disabled={Boolean(downloading)}
|
||||
onSuccess={onDownloadComplete}
|
||||
onImportingChange={setImporting}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
<ImportBackendLink
|
||||
disabled={Boolean(downloading)}
|
||||
onSuccess={onDownloadComplete}
|
||||
onImportingChange={setImporting}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { ChevronDown } from 'lucide-react';
|
|||
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
|
||||
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal';
|
||||
|
||||
export interface TerminalTabRef {
|
||||
|
|
@ -11,7 +10,6 @@ export interface TerminalTabRef {
|
|||
}
|
||||
|
||||
export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
|
||||
const { resolvedColorScheme: colorScheme } = usePreferencesStore();
|
||||
const [terminalContent, setTerminalContent] = useState('');
|
||||
const [isUserScrolling, setIsUserScrolling] = useState(false);
|
||||
const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
|
||||
|
|
@ -87,9 +85,7 @@ export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
|
|||
<Box
|
||||
style={{
|
||||
backgroundColor:
|
||||
colorScheme === 'dark'
|
||||
? 'var(--mantine-color-dark-filled)'
|
||||
: 'var(--mantine-color-gray-0)',
|
||||
'light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-filled))',
|
||||
borderRadius: 'inherit',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
|
@ -101,22 +97,14 @@ export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
|
|||
ref={scrollAreaRef}
|
||||
viewportRef={viewportRef}
|
||||
onScrollPositionChange={handleScroll}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontFamily: 'Monaco, Menlo, Ubuntu Mono, monospace',
|
||||
fontSize: '0.875em',
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
style={{ flex: 1 }}
|
||||
scrollbarSize={8}
|
||||
offsetScrollbars={false}
|
||||
>
|
||||
<Box p="md">
|
||||
<div
|
||||
style={{
|
||||
color:
|
||||
colorScheme === 'dark'
|
||||
? 'var(--mantine-color-gray-0)'
|
||||
: 'var(--mantine-color-dark-filled)',
|
||||
color: 'light-dark(var(--mantine-color-dark-filled), var(--mantine-color-gray-0))',
|
||||
fontFamily:
|
||||
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
fontSize: '0.875em',
|
||||
|
|
@ -137,13 +125,13 @@ export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
|
|||
{isUserScrolling && !shouldAutoScroll && (
|
||||
<ActionIcon
|
||||
variant="filled"
|
||||
color="blue"
|
||||
color="brand"
|
||||
size="lg"
|
||||
radius="xl"
|
||||
onClick={scrollToBottom}
|
||||
style={{
|
||||
bottom: '1.25rem',
|
||||
boxShadow: '0 0.125rem 0.5rem rgba(0, 0, 0, 0.3)',
|
||||
boxShadow: 'var(--gerbil-shadow-sm)',
|
||||
position: 'absolute',
|
||||
right: '1.25rem',
|
||||
zIndex: 10,
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ export const AdvancedTab = () => {
|
|||
</Group>
|
||||
<Stack gap="xs">
|
||||
{preLaunchCommands.map((command, index) => (
|
||||
<Group key={index} gap="xs">
|
||||
<Group key={`cmd-${index}-${command}`} gap="xs">
|
||||
<TextInput
|
||||
placeholder="Enter a shell command"
|
||||
value={command}
|
||||
|
|
@ -174,6 +174,7 @@ export const AdvancedTab = () => {
|
|||
variant="subtle"
|
||||
color="red"
|
||||
disabled={preLaunchCommands.length === 1}
|
||||
aria-label={`Remove command ${index + 1}`}
|
||||
onClick={() => {
|
||||
const newCommands = preLaunchCommands.filter((_, i) => i !== index);
|
||||
setPreLaunchCommands(newCommands.length === 0 ? [''] : newCommands);
|
||||
|
|
|
|||
|
|
@ -885,7 +885,11 @@ export const CommandLineArgumentsModal = ({
|
|||
gap="xs"
|
||||
p="sm"
|
||||
style={{
|
||||
borderLeft: '3px solid var(--mantine-color-blue-4)',
|
||||
background:
|
||||
'light-dark(var(--mantine-color-brand-0), var(--mantine-color-dark-6))',
|
||||
borderRadius: 'var(--mantine-radius-sm)',
|
||||
border:
|
||||
'1px solid light-dark(var(--mantine-color-brand-2), var(--mantine-color-dark-4))',
|
||||
}}
|
||||
>
|
||||
<Group gap="xs" wrap="wrap" justify="space-between">
|
||||
|
|
@ -897,7 +901,7 @@ export const CommandLineArgumentsModal = ({
|
|||
</Code>
|
||||
))}
|
||||
{arg.type && (
|
||||
<Badge size="xs" variant="light" color="blue">
|
||||
<Badge size="xs" variant="light" color="brand">
|
||||
{arg.type}
|
||||
</Badge>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const AccelerationSelectItem = ({
|
|||
discreteDevices.length > 0 && (
|
||||
<Group gap={4}>
|
||||
{discreteDevices.slice(0, 2).map((device, index) => (
|
||||
<Badge key={index} size="md" variant="light" color="blue">
|
||||
<Badge key={index} size="md" variant="light" color="brand">
|
||||
{renderDeviceName(device)}
|
||||
</Badge>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -76,9 +76,10 @@ export const ModelCard = ({ modelId }: ModelCardProps) => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
img: ({ node: _, ...props }) => (
|
||||
img: ({ node: _, alt, ...props }) => (
|
||||
<img
|
||||
{...props}
|
||||
alt={alt ?? ''}
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
height: 'auto',
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ export const ModelsTable = ({
|
|||
</Table.Td>
|
||||
<Table.Td>
|
||||
{model.paramSize && (
|
||||
<Badge size="sm" variant="light" color="blue">
|
||||
<Badge size="sm" variant="light" color="brand">
|
||||
{model.paramSize}
|
||||
</Badge>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ export const HuggingFaceSearchModal = ({
|
|||
<Stack gap="md" style={{ height: '100%' }}>
|
||||
{selectedModel ? (
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<ActionIcon variant="subtle" onClick={handleBack}>
|
||||
<ActionIcon variant="subtle" aria-label="Back to search results" onClick={handleBack}>
|
||||
<ArrowLeft size={18} />
|
||||
</ActionIcon>
|
||||
<Stack gap={0} style={{ flex: 1, minWidth: 0 }}>
|
||||
|
|
@ -148,7 +148,11 @@ export const HuggingFaceSearchModal = ({
|
|||
</Text>
|
||||
</Stack>
|
||||
<Tooltip label="Open on Hugging Face">
|
||||
<ActionIcon variant="subtle" onClick={handleOpenExternal}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
aria-label="Open on Hugging Face"
|
||||
onClick={handleOpenExternal}
|
||||
>
|
||||
<ExternalLink size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -140,7 +140,12 @@ export const ModelFileField = ({
|
|||
</Button>
|
||||
{searchParams && (
|
||||
<Tooltip label="Search Hugging Face">
|
||||
<ActionIcon onClick={() => setSearchModalOpened(true)} variant="outline" size="lg">
|
||||
<ActionIcon
|
||||
onClick={() => setSearchModalOpened(true)}
|
||||
variant="outline"
|
||||
size="lg"
|
||||
aria-label="Search Hugging Face"
|
||||
>
|
||||
<Search size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
|
@ -156,9 +161,10 @@ export const ModelFileField = ({
|
|||
<ActionIcon
|
||||
onClick={() => void handleAnalyzeModel()}
|
||||
variant="light"
|
||||
color="blue"
|
||||
color="brand"
|
||||
size="lg"
|
||||
disabled={validationState === 'neutral' || validationState === 'invalid'}
|
||||
aria-label="Analyze model"
|
||||
>
|
||||
<Info size={16} />
|
||||
</ActionIcon>
|
||||
|
|
|
|||
|
|
@ -425,7 +425,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
onClick={handleLaunchClick}
|
||||
size="lg"
|
||||
variant="filled"
|
||||
color="blue"
|
||||
color="brand"
|
||||
style={{
|
||||
fontSize: '1em',
|
||||
fontWeight: 600,
|
||||
|
|
|
|||
|
|
@ -1,17 +1,5 @@
|
|||
import iconUrl from '/icon.png';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Container,
|
||||
Group,
|
||||
Image,
|
||||
List,
|
||||
Stack,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { Check } from 'lucide-react';
|
||||
import { Button, Container, Group, Image, List, Stack, Text, Title } from '@mantine/core';
|
||||
|
||||
import { PRODUCT_NAME } from '@/constants';
|
||||
|
||||
|
|
@ -21,77 +9,51 @@ interface WelcomeScreenProps {
|
|||
|
||||
export const WelcomeScreen = ({ onGetStarted }: WelcomeScreenProps) => (
|
||||
<Container size="md" mt="md">
|
||||
<Stack gap="xl">
|
||||
<Card withBorder radius="md" shadow="sm" p="xl">
|
||||
<Stack gap="lg" align="center">
|
||||
<Stack gap="md" align="center">
|
||||
<Group gap="md" mr="xl" align="center">
|
||||
<Image src={iconUrl} alt={PRODUCT_NAME} w={36} h={36} />
|
||||
<Title order={1} ta="center">
|
||||
{PRODUCT_NAME}
|
||||
</Title>
|
||||
</Group>
|
||||
<Text size="lg" c="dimmed" ta="center" maw={600}>
|
||||
Run Large Language Models locally
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack gap="lg" maw={560}>
|
||||
<Stack gap="sm">
|
||||
<Group gap="md" align="center">
|
||||
<Image src={iconUrl} alt={PRODUCT_NAME} w={36} h={36} />
|
||||
<Title order={1}>{PRODUCT_NAME}</Title>
|
||||
</Group>
|
||||
<Text size="lg" c="dimmed">
|
||||
Run Large Language Models locally on your hardware.
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack gap="lg" w="100%" maw={600}>
|
||||
<List
|
||||
spacing="sm"
|
||||
size="sm"
|
||||
center
|
||||
icon={
|
||||
<ThemeIcon color="green" size={20} radius="xl">
|
||||
<Check size={12} />
|
||||
</ThemeIcon>
|
||||
}
|
||||
>
|
||||
<List.Item>
|
||||
<Text>
|
||||
<Text component="span" fw={500}>
|
||||
Chat with AI models
|
||||
</Text>{' '}
|
||||
- Have conversations, ask questions, get help with writing
|
||||
</Text>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<Text>
|
||||
<Text component="span" fw={500}>
|
||||
Generate images
|
||||
</Text>{' '}
|
||||
- Easily create artwork and illustrations with LLMs
|
||||
</Text>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<Text>
|
||||
<Text component="span" fw={500}>
|
||||
Complete privacy
|
||||
</Text>{' '}
|
||||
- Everything runs on your computer, no data sent to servers
|
||||
</Text>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<Text>
|
||||
<Text component="span" fw={500}>
|
||||
Hardware acceleration
|
||||
</Text>{' '}
|
||||
- Supports CUDA, ROCm and Vulkan backends
|
||||
</Text>
|
||||
</List.Item>
|
||||
</List>
|
||||
<List spacing="xs" size="sm">
|
||||
<List.Item>
|
||||
<Text fw={500} component="span">
|
||||
Chat
|
||||
</Text>{' '}
|
||||
— conversations, questions, writing help
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<Text fw={500} component="span">
|
||||
Image generation
|
||||
</Text>{' '}
|
||||
— artwork and illustrations via LLMs
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<Text fw={500} component="span">
|
||||
Private
|
||||
</Text>{' '}
|
||||
— everything runs locally, no data leaves the machine
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<Text fw={500} component="span">
|
||||
Accelerated
|
||||
</Text>{' '}
|
||||
— CUDA, ROCm, and Vulkan backends supported
|
||||
</List.Item>
|
||||
</List>
|
||||
|
||||
<Text size="xs" c="dimmed" ta="center" mt="sm">
|
||||
Hardware acceleration requires appropriate drivers to be manually installed (CUDA for
|
||||
NVIDIA GPUs, ROCm for AMD GPUs, etc.)
|
||||
</Text>
|
||||
</Stack>
|
||||
<Text size="xs" c="dimmed">
|
||||
Hardware acceleration requires drivers installed separately (CUDA for NVIDIA, ROCm for AMD).
|
||||
</Text>
|
||||
|
||||
<Button size="lg" mt="lg" onClick={onGetStarted}>
|
||||
Get Started
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card>
|
||||
<Button size="md" onClick={onGetStarted} style={{ alignSelf: 'flex-start' }}>
|
||||
Get Started
|
||||
</Button>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export const AboutTab = () => {
|
|||
<Text size="xl" fw={600}>
|
||||
{PRODUCT_NAME}
|
||||
</Text>
|
||||
<Badge variant="light" color="blue" size="lg" style={{ textTransform: 'none' }}>
|
||||
<Badge variant="light" color="brand" size="lg" style={{ textTransform: 'none' }}>
|
||||
v{versionInfo.appVersion}
|
||||
</Badge>
|
||||
</Group>
|
||||
|
|
@ -84,17 +84,17 @@ export const AboutTab = () => {
|
|||
</Group>
|
||||
</Card>
|
||||
|
||||
<Card withBorder radius="md" p="md">
|
||||
<Text size="lg" fw={500} mb="md">
|
||||
<Stack gap="xs">
|
||||
<Text size="sm" fw={500} c="dimmed">
|
||||
About {PRODUCT_NAME}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" mb="md">
|
||||
<Text size="sm" c="dimmed">
|
||||
{PRODUCT_NAME} is a user-friendly desktop application that makes it easy to run large
|
||||
language models locally on your machine. Whether you're looking to chat with AI
|
||||
models, generate images, or explore different interfaces like SillyTavern and Open WebUI,{' '}
|
||||
{PRODUCT_NAME} provides a streamlined experience for local AI.
|
||||
</Text>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Group, rem, SegmentedControl, Slider, Stack, Text, TextInput } from '@m
|
|||
import type { MantineColorScheme } from '@mantine/core';
|
||||
import { Monitor, Moon, Sun } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
import { FrontendInterfaceSelector } from '@/components/settings/FrontendInterfaceSelector';
|
||||
import { ZOOM } from '@/constants';
|
||||
|
|
@ -13,13 +14,7 @@ interface AppearanceTabProps {
|
|||
}
|
||||
|
||||
export const AppearanceTab = ({ isOnInterfaceScreen = false }: AppearanceTabProps) => {
|
||||
const {
|
||||
rawColorScheme,
|
||||
resolvedColorScheme,
|
||||
setColorScheme: setStoreColorScheme,
|
||||
} = usePreferencesStore();
|
||||
const isDark = resolvedColorScheme === 'dark';
|
||||
|
||||
const { rawColorScheme, setColorScheme: setStoreColorScheme } = usePreferencesStore();
|
||||
const [zoomLevel, setZoomLevel] = useState(ZOOM.DEFAULT_LEVEL as number);
|
||||
const [zoomPercentage, setZoomPercentage] = useState(ZOOM.DEFAULT_PERCENTAGE.toString());
|
||||
|
||||
|
|
@ -74,15 +69,16 @@ export const AppearanceTab = ({ isOnInterfaceScreen = false }: AppearanceTabProp
|
|||
fullWidth
|
||||
value={rawColorScheme}
|
||||
onChange={handleColorSchemeChange}
|
||||
styles={(theme) => ({
|
||||
indicator: {
|
||||
backgroundColor: isDark ? theme.colors.dark[5] : theme.colors.gray[2],
|
||||
border: `1px solid ${isDark ? theme.colors.dark[4] : theme.colors.gray[4]}`,
|
||||
},
|
||||
root: {
|
||||
border: `0.5px solid ${isDark ? theme.colors.dark[4] : theme.colors.gray[4]}`,
|
||||
},
|
||||
})}
|
||||
style={
|
||||
{
|
||||
'--sc-indicator-color':
|
||||
'light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5))',
|
||||
'--sc-indicator-border':
|
||||
'light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))',
|
||||
border:
|
||||
'0.5px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))',
|
||||
} as CSSProperties
|
||||
}
|
||||
data={[
|
||||
{
|
||||
label: (
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ export const BackendsTab = () => {
|
|||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
size="sm"
|
||||
c="blue"
|
||||
c="brand"
|
||||
>
|
||||
<Group gap={4} align="center">
|
||||
<span>Release notes</span>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
SlidersHorizontal,
|
||||
Wrench,
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Modal } from '@/components/Modal';
|
||||
import { AboutTab } from '@/components/settings/AboutTab';
|
||||
|
|
@ -38,23 +38,6 @@ export const SettingsModal = ({
|
|||
|
||||
const effectiveActiveTab = !showBackendsTab && activeTab === 'backends' ? 'general' : activeTab;
|
||||
|
||||
useEffect(() => {
|
||||
if (opened) {
|
||||
const originalOverflow = document.body.style.overflow;
|
||||
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
||||
|
||||
document.body.style.overflow = 'hidden';
|
||||
document.body.style.paddingRight = `${scrollbarWidth}px`;
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = originalOverflow;
|
||||
document.body.style.paddingRight = '';
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [opened]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,32 @@
|
|||
@import '@mantine/core/styles.css';
|
||||
@import '@fontsource/inter/latin-400.css';
|
||||
@import '@fontsource/inter/latin-500.css';
|
||||
@import '@fontsource/inter/latin-600.css';
|
||||
@import '@fontsource/geist/latin-400.css';
|
||||
@import '@fontsource/geist/latin-500.css';
|
||||
@import '@fontsource/geist/latin-600.css';
|
||||
|
||||
:root {
|
||||
--gerbil-window-border: light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.08));
|
||||
--gerbil-scrollbar-thumb: rgba(134, 142, 150, 0.5);
|
||||
--gerbil-scrollbar-thumb-hover: rgba(134, 142, 150, 0.8);
|
||||
--gerbil-scrollbar-thumb-active: rgba(73, 80, 87, 0.9);
|
||||
--gerbil-shadow-sm: 0 0.125rem 0.5rem rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme='dark'] {
|
||||
--gerbil-scrollbar-thumb: rgba(173, 181, 189, 0.5);
|
||||
--gerbil-scrollbar-thumb-hover: rgba(173, 181, 189, 0.8);
|
||||
--gerbil-scrollbar-thumb-active: rgba(206, 212, 218, 0.9);
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
border-left: 1px solid light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.08));
|
||||
border-right: 1px solid light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.08));
|
||||
border-left: 1px solid var(--gerbil-window-border);
|
||||
border-right: 1px solid var(--gerbil-window-border);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme='light'] .mantine-Select-option:hover {
|
||||
background-color: #e9ecef;
|
||||
background-color: var(--mantine-color-gray-2);
|
||||
}
|
||||
|
||||
/* Custom scrollbars */
|
||||
|
|
@ -27,30 +41,30 @@
|
|||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(134, 142, 150, 0.5);
|
||||
background: var(--gerbil-scrollbar-thumb);
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.2s ease;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(134, 142, 150, 0.8);
|
||||
background: var(--gerbil-scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:active {
|
||||
background: rgba(73, 80, 87, 0.9);
|
||||
background: var(--gerbil-scrollbar-thumb-active);
|
||||
}
|
||||
|
||||
/* Dark mode scrollbar support */
|
||||
[data-mantine-color-scheme='dark'] ::-webkit-scrollbar-thumb {
|
||||
background: rgba(173, 181, 189, 0.5);
|
||||
background: var(--gerbil-scrollbar-thumb);
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme='dark'] ::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(173, 181, 189, 0.8);
|
||||
background: var(--gerbil-scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme='dark'] ::-webkit-scrollbar-thumb:active {
|
||||
background: rgba(206, 212, 218, 0.9);
|
||||
background: var(--gerbil-scrollbar-thumb-active);
|
||||
}
|
||||
|
||||
/* TitleBar gerbil icon animations */
|
||||
|
|
|
|||
24
src/theme.ts
24
src/theme.ts
|
|
@ -3,7 +3,21 @@ import type { CSSVariablesResolver } from '@mantine/core';
|
|||
|
||||
export const theme = createTheme({
|
||||
black: '#101113',
|
||||
primaryColor: 'brand',
|
||||
colors: {
|
||||
// Steel-blue: technical, enthusiast-grade — vibrant but cooler than Mantine default
|
||||
brand: [
|
||||
'#eef5ff', // 0 — faint wash
|
||||
'#d8eaff', // 1
|
||||
'#b0d0ff', // 2
|
||||
'#7aaff7', // 3
|
||||
'#4890f0', // 4 — dark mode accent
|
||||
'#2b72e0', // 5
|
||||
'#1e5ec8', // 6 — light primary
|
||||
'#184eb0', // 7
|
||||
'#113d88', // 8 — dark primary
|
||||
'#0c2d64', // 9 — deep
|
||||
],
|
||||
gray: [
|
||||
'#f8f9fa',
|
||||
'#f1f3f4',
|
||||
|
|
@ -52,9 +66,9 @@ export const theme = createTheme({
|
|||
},
|
||||
},
|
||||
},
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
fontFamily: 'Geist, sans-serif',
|
||||
headings: {
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
fontFamily: 'Geist, sans-serif',
|
||||
},
|
||||
white: '#fafafa',
|
||||
});
|
||||
|
|
@ -65,14 +79,16 @@ export const cssVariablesResolver: CSSVariablesResolver = (t) => {
|
|||
variables: { ...v8.variables },
|
||||
dark: {
|
||||
...v8.dark,
|
||||
'--mantine-color-body': '#0f0f0f',
|
||||
'--mantine-color-default-border': '#2a2a2a',
|
||||
'--mantine-color-body': 'oklch(12% 0.008 240)',
|
||||
'--mantine-color-default-border': 'oklch(22% 0.008 240)',
|
||||
'--gerbil-link-color': 'var(--mantine-color-brand-4)',
|
||||
},
|
||||
light: {
|
||||
...v8.light,
|
||||
'--mantine-color-body': '#fafafa',
|
||||
'--mantine-color-white': '#fafafa',
|
||||
'--mantine-color-default-border': '#dee2e6',
|
||||
'--gerbil-link-color': 'var(--mantine-color-brand-5)',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const linkifyText = (text: string) =>
|
|||
const cleanUrl = url.replace(/[.,;:!?]+$/, '');
|
||||
const trailingPunctuation = url.slice(cleanUrl.length);
|
||||
|
||||
return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer" style="color: #339af0; text-decoration: underline; cursor: pointer;">${cleanUrl}</a>${trailingPunctuation}`;
|
||||
return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer" style="color: var(--gerbil-link-color, #4f8cc5); text-decoration: underline; cursor: pointer;">${cleanUrl}</a>${trailingPunctuation}`;
|
||||
});
|
||||
|
||||
const escapeHtmlExceptLinks = (text: string) => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue