diff --git a/src/components/SettingDialog.tsx b/src/components/SettingDialog.tsx index 53da22a..a56037a 100644 --- a/src/components/SettingDialog.tsx +++ b/src/components/SettingDialog.tsx @@ -3,10 +3,13 @@ import { ArrowPathIcon, ArrowUpTrayIcon, BeakerIcon, + BookmarkIcon, ChatBubbleLeftEllipsisIcon, ChatBubbleLeftRightIcon, ChatBubbleOvalLeftEllipsisIcon, CircleStackIcon, + CloudArrowDownIcon, + CloudArrowUpIcon, Cog6ToothIcon, CogIcon, CpuChipIcon, @@ -15,20 +18,22 @@ import { HandRaisedIcon, RocketLaunchIcon, SquaresPlusIcon, + TrashIcon, TvIcon, } from '@heroicons/react/24/outline'; -import React, { ReactElement, useEffect, useMemo, useState } from 'react'; +import React, { FC, ReactElement, useEffect, useMemo, useState } from 'react'; import { baseUrl, CONFIG_DEFAULT, INFERENCE_PROVIDERS, isDev } from '../config'; import { useAppContext } from '../context/app.context'; import { useInferenceContext } from '../context/inference.context'; import * as lang from '../lang/en.json'; -import { OpenInNewTab } from '../utils/common'; +import { dateFormatter, OpenInNewTab } from '../utils/common'; import { InferenceApiModel } from '../utils/inferenceApi'; import { classNames, isBoolean, isNumeric, isString } from '../utils/misc'; import StorageUtils from '../utils/storage'; import { Configuration, ConfigurationKey, + ConfigurationPreset, InferenceProvidersKey, ProviderOption, } from '../utils/types'; @@ -60,7 +65,12 @@ interface SettingFieldInput { interface SettingFieldCustom { type: SettingInputType.CUSTOM; - key: ConfigurationKey | 'custom' | 'import-export' | 'fetch-models'; + key: + | ConfigurationKey + | 'custom' + | 'import-export' + | 'preset-manager' + | 'fetch-models'; component: | string | React.FC<{ @@ -234,6 +244,23 @@ const getSettingTabsConfiguration = ( ], }, + /* Presets */ + { + title: ( + <> + + Presets + + ), + fields: [ + { + type: SettingInputType.CUSTOM, + key: 'preset-manager', + component: UnusedCustomField, + }, + ], + }, + /* Import/Export */ { title: ( @@ -599,6 +626,14 @@ export default function SettingDialog({ return ( ); + case 'preset-manager': + return ( + + ); case 'fetch-models': return ( + + {/* List of saved presets */} + + {lang.settings.presetManager.savedPresets.title} + + + {presets.length === 0 && ( +
+
+
+ )} + + {presets.length > 0 && ( +
+ {presets + .sort((a, b) => b.createdAt - a.createdAt) + .map((preset) => ( +
+
+
+

{preset.name}

+

+ Created: {dateFormatter.format(preset.createdAt)} +

+
+ +
+ + +
+
+
+ ))} +
+ )} + + ); +}; + const ImportExportComponent: React.FC<{ onClose: () => void }> = ({ onClose, }) => { diff --git a/src/context/app.context.tsx b/src/context/app.context.tsx index 4310480..72ea009 100644 --- a/src/context/app.context.tsx +++ b/src/context/app.context.tsx @@ -1,11 +1,15 @@ import React, { createContext, useContext, useEffect, useState } from 'react'; +import toast from 'react-hot-toast'; import { CONFIG_DEFAULT, isDev } from '../config'; import StorageUtils from '../utils/storage'; -import { Configuration } from '../utils/types'; +import { Configuration, ConfigurationPreset } from '../utils/types'; interface AppContextValue { config: Configuration; saveConfig: (config: Configuration) => void; + presets: ConfigurationPreset[]; + savePreset: (name: string, config: Configuration) => Promise; + removePreset: (name: string) => Promise; showSettings: boolean; setShowSettings: (show: boolean) => void; } @@ -13,6 +17,9 @@ interface AppContextValue { const AppContext = createContext({ config: {} as Configuration, saveConfig: () => {}, + presets: [], + savePreset: () => new Promise(() => {}), + removePreset: () => new Promise(() => {}), showSettings: false, setShowSettings: () => {}, }); @@ -23,11 +30,18 @@ export const AppContextProvider = ({ children: React.ReactElement; }) => { const [config, setConfig] = useState(CONFIG_DEFAULT); + const [presets, setPresets] = useState([]); const [showSettings, setShowSettings] = useState(false); useEffect(() => { - if (isDev) console.debug('Load config'); - setConfig(StorageUtils.getConfig()); + const init = async () => { + if (isDev) console.debug('Load config'); + setConfig(StorageUtils.getConfig()); + + if (isDev) console.debug('Load presets'); + setPresets(await StorageUtils.getPresets()); + }; + init(); }, []); const saveConfig = (config: Configuration) => { @@ -36,11 +50,28 @@ export const AppContextProvider = ({ setConfig(config); }; + const savePreset = async (name: string, config: Configuration) => { + if (isDev) console.debug('Save preset', { name, config }); + StorageUtils.savePreset(name, config); + setPresets(await StorageUtils.getPresets()); + toast.success('Preset is saved successfully'); + }; + + const removePreset = async (name: string) => { + if (isDev) console.debug('Remove preset', name); + StorageUtils.removePreset(name); + setPresets(await StorageUtils.getPresets()); + toast.success('Preset is removed successfully'); + }; + return ( { const db = new Dexie('LlamacppWebui') as Dexie & { conversations: Table; messages: Table; + userConfigurations: Table; }; // Define database schema @@ -59,6 +61,8 @@ db.version(1).stores({ conversations: '&id, lastModified', // Index messages by 'id' (unique), 'convId', composite key '[convId+id]', and 'timestamp' messages: '&id, convId, [convId+id], timestamp', + // Index userConfigurations by 'id' (unique) and 'name' + userConfigurations: '&id, name', }); // --- Main Storage Utility Functions --- @@ -435,6 +439,51 @@ const StorageUtils = { localStorage.setItem('theme', theme); } }, + + /** + * Retrieves the user's configuration presets. + * @returns The array of configuration preset. + */ + async getPresets() { + const presets = await db.transaction( + 'r', + db.userConfigurations, + async () => { + return await db.userConfigurations.toArray(); + } + ); + return presets; + }, + + /** + * Saves the user's configuration preset to localStorage, replacing the existing one. + * @param name The preset name to save. + * @param config The Configuration object to save. + */ + async savePreset(name: string, config: Configuration, id?: string) { + const now = Date.now(); + const newPreset: ConfigurationPreset = { + id: id || `config-${now}`, + name, + createdAt: now, + config, + }; + return await db.transaction('rw', db.userConfigurations, async () => { + await db.userConfigurations.where('name').equals(name).delete(); + await db.userConfigurations.add(newPreset); + return newPreset; + }); + }, + + /** + * Removes the user's configuration preset. + * @param name The preset name to remove. + */ + async removePreset(name: string) { + return await db.transaction('rw', db.userConfigurations, async () => { + return await db.userConfigurations.where('name').equals(name).delete(); + }); + }, }; export default StorageUtils; diff --git a/src/utils/types.ts b/src/utils/types.ts index 3d77d40..91ceff4 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -65,6 +65,13 @@ export interface Configuration { } export type ConfigurationKey = keyof Configuration; +export interface ConfigurationPreset { + id: string; + name: string; + createdAt: number; + config: Partial; +} + export interface InferenceProviders { [key: string]: { baseUrl: string;