Skip to content

webui : add a preset feature to the settings #14523

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified tools/server/public/index.html.gz
100644 → 100755
Binary file not shown.
187 changes: 182 additions & 5 deletions tools/server/webui/src/components/SettingDialog.tsx
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ import StorageUtils from '../utils/storage';
import { classNames, isBoolean, isNumeric, isString } from '../utils/misc';
import {
BeakerIcon,
BookmarkIcon,
ChatBubbleOvalLeftEllipsisIcon,
Cog6ToothIcon,
FunnelIcon,
HandRaisedIcon,
SquaresPlusIcon,
TrashIcon,
} from '@heroicons/react/24/outline';
import { OpenInNewTab } from '../utils/common';
import { useModals } from './ModalProvider';
import { SettingsPreset } from '../utils/types';

type SettKey = keyof typeof CONFIG_DEFAULT;

Expand Down Expand Up @@ -74,7 +77,155 @@ interface SettingSection {

const ICON_CLASSNAME = 'w-4 h-4 mr-1 inline';

const SETTING_SECTIONS: SettingSection[] = [
// Presets Component
function PresetsManager({
currentConfig,
onLoadPreset,
}: {
currentConfig: typeof CONFIG_DEFAULT;
onLoadPreset: (config: typeof CONFIG_DEFAULT) => void;
}) {
const [presets, setPresets] = useState<SettingsPreset[]>(() =>
StorageUtils.getPresets()
);
const [presetName, setPresetName] = useState('');
const [selectedPresetId, setSelectedPresetId] = useState<string | null>(null);
const { showConfirm, showAlert } = useModals();

const handleSavePreset = async () => {
if (!presetName.trim()) {
await showAlert('Please enter a preset name');
return;
}

// Check if preset name already exists
const existingPreset = presets.find((p) => p.name === presetName.trim());
if (existingPreset) {
if (
await showConfirm(
`Preset "${presetName}" already exists. Do you want to overwrite it?`
)
) {
StorageUtils.updatePreset(existingPreset.id, currentConfig);
setPresets(StorageUtils.getPresets());
setPresetName('');
await showAlert('Preset updated successfully');
}
} else {
const newPreset = StorageUtils.savePreset(
presetName.trim(),
currentConfig
);
setPresets([...presets, newPreset]);
setPresetName('');
await showAlert('Preset saved successfully');
}
};

const handleLoadPreset = async (preset: SettingsPreset) => {
if (
await showConfirm(
`Load preset "${preset.name}"? Current settings will be replaced.`
)
) {
onLoadPreset(preset.config as typeof CONFIG_DEFAULT);
setSelectedPresetId(preset.id);
}
};

const handleDeletePreset = async (preset: SettingsPreset) => {
if (await showConfirm(`Delete preset "${preset.name}"?`)) {
StorageUtils.deletePreset(preset.id);
setPresets(presets.filter((p) => p.id !== preset.id));
if (selectedPresetId === preset.id) {
setSelectedPresetId(null);
}
}
};

return (
<div className="space-y-4">
{/* Save current settings as preset */}
<div className="form-control">
<label className="label">
<span className="label-text">Save current settings as preset</span>
</label>
<div className="join">
<input
type="text"
placeholder="Enter preset name"
className="input input-bordered join-item flex-1"
value={presetName}
onChange={(e) => setPresetName(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleSavePreset();
}
}}
/>
<button
className="btn btn-primary join-item"
onClick={handleSavePreset}
>
Save Preset
</button>
</div>
</div>

{/* List of saved presets */}
<div className="form-control">
<label className="label">
<span className="label-text">Saved presets</span>
</label>
{presets.length === 0 ? (
<div className="alert">
<span>No presets saved yet</span>
</div>
) : (
<div className="space-y-2 max-h-64 overflow-y-auto">
{presets.map((preset) => (
<div
key={preset.id}
className={classNames({
'card bg-base-200 p-3': true,
'ring-2 ring-primary': selectedPresetId === preset.id,
})}
>
<div className="flex items-center justify-between">
<div>
<h4 className="font-semibold">{preset.name}</h4>
<p className="text-sm opacity-70">
Created: {new Date(preset.createdAt).toLocaleString()}
</p>
</div>
<div className="flex gap-2">
<button
className="btn btn-sm btn-primary"
onClick={() => handleLoadPreset(preset)}
>
Load
</button>
<button
className="btn btn-sm btn-error"
onClick={() => handleDeletePreset(preset)}
>
<TrashIcon className="w-4 h-4" />
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}

const SETTING_SECTIONS = (
localConfig: typeof CONFIG_DEFAULT,
setLocalConfig: (config: typeof CONFIG_DEFAULT) => void
): SettingSection[] => [
{
title: (
<>
Expand Down Expand Up @@ -267,6 +418,26 @@ const SETTING_SECTIONS: SettingSection[] = [
},
],
},
{
title: (
<>
<BookmarkIcon className={ICON_CLASSNAME} />
Presets
</>
),
fields: [
{
type: SettingInputType.CUSTOM,
key: 'custom', // dummy key for presets
component: () => (
<PresetsManager
currentConfig={localConfig}
onLoadPreset={setLocalConfig}
/>
),
},
],
},
];

export default function SettingDialog({
Expand All @@ -285,6 +456,12 @@ export default function SettingDialog({
);
const { showConfirm, showAlert } = useModals();

// Generate sections with access to local state
const SETTING_SECTIONS_GENERATED = SETTING_SECTIONS(
localConfig,
setLocalConfig
);

const resetConfig = async () => {
if (await showConfirm('Are you sure you want to reset all settings?')) {
setLocalConfig(CONFIG_DEFAULT);
Expand Down Expand Up @@ -351,7 +528,7 @@ export default function SettingDialog({
aria-description="Settings sections"
tabIndex={0}
>
{SETTING_SECTIONS.map((section, idx) => (
{SETTING_SECTIONS_GENERATED.map((section, idx) => (
<button
key={idx}
className={classNames({
Expand All @@ -374,10 +551,10 @@ export default function SettingDialog({
>
<details className="dropdown">
<summary className="btn bt-sm w-full m-1">
{SETTING_SECTIONS[sectionIdx].title}
{SETTING_SECTIONS_GENERATED[sectionIdx].title}
</summary>
<ul className="menu dropdown-content bg-base-100 rounded-box z-[1] w-52 p-2 shadow">
{SETTING_SECTIONS.map((section, idx) => (
{SETTING_SECTIONS_GENERATED.map((section, idx) => (
<div
key={idx}
className={classNames({
Expand All @@ -396,7 +573,7 @@ export default function SettingDialog({

{/* Right panel, showing setting fields */}
<div className="grow overflow-y-auto px-4">
{SETTING_SECTIONS[sectionIdx].fields.map((field, idx) => {
{SETTING_SECTIONS_GENERATED[sectionIdx].fields.map((field, idx) => {
const key = `${sectionIdx}-${idx}`;
if (field.type === SettingInputType.SHORT_INPUT) {
return (
Expand Down
40 changes: 39 additions & 1 deletion tools/server/webui/src/utils/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// format: { [convId]: { id: string, lastModified: number, messages: [...] } }

import { CONFIG_DEFAULT } from '../Config';
import { Conversation, Message, TimingReport } from './types';
import { Conversation, Message, TimingReport, SettingsPreset } from './types';
import Dexie, { Table } from 'dexie';

const event = new EventTarget();
Expand Down Expand Up @@ -213,6 +213,44 @@ const StorageUtils = {
localStorage.setItem('theme', theme);
}
},

// manage presets
getPresets(): SettingsPreset[] {
const presetsJson = localStorage.getItem('presets');
if (!presetsJson) return [];
try {
return JSON.parse(presetsJson);
} catch (e) {
console.error('Failed to parse presets', e);
return [];
}
},
savePreset(name: string, config: typeof CONFIG_DEFAULT): SettingsPreset {
const presets = StorageUtils.getPresets();
const now = Date.now();
const preset: SettingsPreset = {
id: `preset-${now}`,
name,
createdAt: now,
config: { ...config }, // copy the config
};
presets.push(preset);
localStorage.setItem('presets', JSON.stringify(presets));
return preset;
},
updatePreset(id: string, config: typeof CONFIG_DEFAULT): void {
const presets = StorageUtils.getPresets();
const index = presets.findIndex((p) => p.id === id);
if (index !== -1) {
presets[index].config = { ...config };
localStorage.setItem('presets', JSON.stringify(presets));
}
},
deletePreset(id: string): void {
const presets = StorageUtils.getPresets();
const filtered = presets.filter((p) => p.id !== id);
localStorage.setItem('presets', JSON.stringify(filtered));
},
};

export default StorageUtils;
Expand Down
7 changes: 7 additions & 0 deletions tools/server/webui/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,10 @@ export interface LlamaCppServerProps {
};
// TODO: support params
}

export interface SettingsPreset {
id: string; // format: `preset-{timestamp}`
name: string;
createdAt: number; // timestamp from Date.now()
config: Record<string, string | number | boolean>; // partial CONFIG_DEFAULT
}