Compare commits

..

5 Commits

Author SHA1 Message Date
ItzCrazyKns
3da53aed03 Merge branch 'master' of https://github.com/ItzCrazyKns/Perplexica 2025-10-30 11:36:30 +05:30
ItzCrazyKns
244675759c feat(config): add getAutoMediaSearch, update uses 2025-10-30 11:29:14 +05:30
ItzCrazyKns
ce6a37aaff feat(settingsFields): add switch field 2025-10-30 11:28:15 +05:30
ItzCrazyKns
c3abba8462 feat(settings): separate personalization & preferences 2025-10-29 23:13:51 +05:30
ItzCrazyKns
f709aa8224 feat(config): add new switch config field 2025-10-29 23:12:09 +05:30
8 changed files with 177 additions and 47 deletions

View File

@@ -0,0 +1,29 @@
import { UIConfigField } from '@/lib/config/types';
import SettingsField from '../SettingsField';
const Personalization = ({
fields,
values,
}: {
fields: UIConfigField[];
values: Record<string, any>;
}) => {
return (
<div className="flex-1 space-y-6 overflow-y-auto px-6 py-6">
{fields.map((field) => (
<SettingsField
key={field.key}
field={field}
value={
(field.scope === 'client'
? localStorage.getItem(field.key)
: values[field.key]) ?? field.default
}
dataAdd="personalization"
/>
))}
</div>
);
};
export default Personalization;

View File

@@ -1,7 +1,7 @@
import { UIConfigField } from '@/lib/config/types'; import { UIConfigField } from '@/lib/config/types';
import SettingsField from '../SettingsField'; import SettingsField from '../SettingsField';
const General = ({ const Preferences = ({
fields, fields,
values, values,
}: { }: {
@@ -19,11 +19,11 @@ const General = ({
? localStorage.getItem(field.key) ? localStorage.getItem(field.key)
: values[field.key]) ?? field.default : values[field.key]) ?? field.default
} }
dataAdd="general" dataAdd="preferences"
/> />
))} ))}
</div> </div>
); );
}; };
export default General; export default Preferences;

View File

@@ -4,9 +4,10 @@ import {
BrainCog, BrainCog,
ChevronLeft, ChevronLeft,
Search, Search,
Settings, Sliders,
ToggleRight,
} from 'lucide-react'; } from 'lucide-react';
import General from './Sections/General'; import Preferences from './Sections/Preferences';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
@@ -15,15 +16,24 @@ import { cn } from '@/lib/utils';
import Models from './Sections/Models/Section'; import Models from './Sections/Models/Section';
import SearchSection from './Sections/Search'; import SearchSection from './Sections/Search';
import Select from '@/components/ui/Select'; import Select from '@/components/ui/Select';
import Personalization from './Sections/Personalization';
const sections = [ const sections = [
{ {
key: 'general', key: 'preferences',
name: 'General', name: 'Preferences',
description: 'Adjust common settings.', description: 'Customize your application preferences.',
icon: Settings, icon: Sliders,
component: General, component: Preferences,
dataAdd: 'general', dataAdd: 'preferences',
},
{
key: 'personalization',
name: 'Personalization',
description: 'Customize the behavior and tone of the model.',
icon: ToggleRight,
component: Personalization,
dataAdd: 'personalization',
}, },
{ {
key: 'models', key: 'models',

View File

@@ -1,6 +1,7 @@
import { import {
SelectUIConfigField, SelectUIConfigField,
StringUIConfigField, StringUIConfigField,
SwitchUIConfigField,
TextareaUIConfigField, TextareaUIConfigField,
UIConfigField, UIConfigField,
} from '@/lib/config/types'; } from '@/lib/config/types';
@@ -9,6 +10,7 @@ import Select from '../ui/Select';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
import { Loader2 } from 'lucide-react'; import { Loader2 } from 'lucide-react';
import { Switch } from '@headlessui/react';
const SettingsSelect = ({ const SettingsSelect = ({
field, field,
@@ -237,6 +239,79 @@ const SettingsTextarea = ({
); );
}; };
const SettingsSwitch = ({
field,
value,
setValue,
dataAdd,
}: {
field: SwitchUIConfigField;
value?: any;
setValue: (value: any) => void;
dataAdd: string;
}) => {
const [loading, setLoading] = useState(false);
const handleSave = async (newValue: boolean) => {
setLoading(true);
setValue(newValue);
try {
if (field.scope === 'client') {
localStorage.setItem(field.key, String(newValue));
} else {
const res = await fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
key: `${dataAdd}.${field.key}`,
value: newValue,
}),
});
if (!res.ok) {
console.error('Failed to save config:', await res.text());
throw new Error('Failed to save configuration');
}
}
} catch (error) {
console.error('Error saving config:', error);
toast.error('Failed to save configuration.');
} finally {
setTimeout(() => setLoading(false), 150);
}
};
const isChecked = value === true || value === 'true';
return (
<section className="rounded-xl border border-light-200 bg-light-primary/80 p-4 lg:p-6 transition-colors dark:border-dark-200 dark:bg-dark-primary/80">
<div className="flex flex-row items-center space-x-3 lg:space-x-5 w-full justify-between">
<div>
<h4 className="text-sm lg:text-base text-black dark:text-white">
{field.name}
</h4>
<p className="text-[11px] lg:text-xs text-black/50 dark:text-white/50">
{field.description}
</p>
</div>
<Switch
checked={isChecked}
onChange={handleSave}
disabled={loading}
className="group relative flex h-6 w-12 shrink-0 cursor-pointer rounded-full bg-white/10 p-1 duration-200 ease-in-out focus:outline-none transition-colors disabled:opacity-60 disabled:cursor-not-allowed data-[checked]:bg-sky-500"
>
<span
aria-hidden="true"
className="pointer-events-none inline-block size-4 translate-x-0 rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out group-data-[checked]:translate-x-6"
/>
</Switch>
</div>
</section>
);
};
const SettingsField = ({ const SettingsField = ({
field, field,
value, value,
@@ -276,6 +351,15 @@ const SettingsField = ({
dataAdd={dataAdd} dataAdd={dataAdd}
/> />
); );
case 'switch':
return (
<SettingsSwitch
field={field}
value={val}
setValue={setVal}
dataAdd={dataAdd}
/>
);
default: default:
return <div>Unsupported field type: {field.type}</div>; return <div>Unsupported field type: {field.type}</div>;
} }

View File

@@ -6,11 +6,8 @@ const getClientConfig = (key: string, defaultVal?: any) => {
export const getTheme = () => getClientConfig('theme', 'dark'); export const getTheme = () => getClientConfig('theme', 'dark');
export const getAutoImageSearch = () => export const getAutoMediaSearch = () =>
Boolean(getClientConfig('autoImageSearch', 'true')); getClientConfig('autoMediaSearch', 'true') === 'true';
export const getAutoVideoSearch = () =>
Boolean(getClientConfig('autoVideoSearch', 'true'));
export const getSystemInstructions = () => export const getSystemInstructions = () =>
getClientConfig('systemInstructions', ''); getClientConfig('systemInstructions', '');

View File

@@ -13,14 +13,15 @@ class ConfigManager {
currentConfig: Config = { currentConfig: Config = {
version: this.configVersion, version: this.configVersion,
setupComplete: false, setupComplete: false,
general: {}, preferences: {},
personalization: {},
modelProviders: [], modelProviders: [],
search: { search: {
searxngURL: '', searxngURL: '',
}, },
}; };
uiConfigSections: UIConfigSections = { uiConfigSections: UIConfigSections = {
general: [ preferences: [
{ {
name: 'Theme', name: 'Theme',
key: 'theme', key: 'theme',
@@ -40,16 +41,6 @@ class ConfigManager {
default: 'dark', default: 'dark',
scope: 'client', scope: 'client',
}, },
{
name: 'System Instructions',
key: 'systemInstructions',
type: 'textarea',
required: false,
description: 'Add custom behavior or tone for the model.',
placeholder:
'e.g., "Respond in a friendly and concise tone" or "Use British English and format answers as bullet points."',
scope: 'client',
},
{ {
name: 'Measurement Unit', name: 'Measurement Unit',
key: 'measureUnit', key: 'measureUnit',
@@ -69,6 +60,27 @@ class ConfigManager {
default: 'Metric', default: 'Metric',
scope: 'client', scope: 'client',
}, },
{
name: 'Auto video & image search',
key: 'autoMediaSearch',
type: 'switch',
required: false,
description: 'Automatically search for relevant images and videos.',
default: true,
scope: 'client',
},
],
personalization: [
{
name: 'System Instructions',
key: 'systemInstructions',
type: 'textarea',
required: false,
description: 'Add custom behavior or tone for the model.',
placeholder:
'e.g., "Respond in a friendly and concise tone" or "Use British English and format answers as bullet points."',
scope: 'client',
},
], ],
modelProviders: [], modelProviders: [],
search: [ search: [

View File

@@ -38,11 +38,17 @@ type TextareaUIConfigField = BaseUIConfigField & {
default?: string; default?: string;
}; };
type SwitchUIConfigField = BaseUIConfigField & {
type: 'switch';
default?: boolean;
};
type UIConfigField = type UIConfigField =
| StringUIConfigField | StringUIConfigField
| SelectUIConfigField | SelectUIConfigField
| PasswordUIConfigField | PasswordUIConfigField
| TextareaUIConfigField; | TextareaUIConfigField
| SwitchUIConfigField;
type ConfigModelProvider = { type ConfigModelProvider = {
id: string; id: string;
@@ -57,7 +63,10 @@ type ConfigModelProvider = {
type Config = { type Config = {
version: number; version: number;
setupComplete: boolean; setupComplete: boolean;
general: { preferences: {
[key: string]: any;
};
personalization: {
[key: string]: any; [key: string]: any;
}; };
modelProviders: ConfigModelProvider[]; modelProviders: ConfigModelProvider[];
@@ -80,7 +89,8 @@ type ModelProviderUISection = {
}; };
type UIConfigSections = { type UIConfigSections = {
general: UIConfigField[]; preferences: UIConfigField[];
personalization: UIConfigField[];
modelProviders: ModelProviderUISection[]; modelProviders: ModelProviderUISection[];
search: UIConfigField[]; search: UIConfigField[];
}; };
@@ -95,4 +105,5 @@ export type {
ModelProviderUISection, ModelProviderUISection,
ConfigModelProvider, ConfigModelProvider,
TextareaUIConfigField, TextareaUIConfigField,
SwitchUIConfigField,
}; };

View File

@@ -21,6 +21,7 @@ import { useParams, useSearchParams } from 'next/navigation';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { getSuggestions } from '../actions'; import { getSuggestions } from '../actions';
import { MinimalProvider } from '../models/types'; import { MinimalProvider } from '../models/types';
import { getAutoMediaSearch } from '../config/clientRegistry';
export type Section = { export type Section = {
userMessage: UserMessage; userMessage: UserMessage;
@@ -94,17 +95,6 @@ const checkConfig = async (
'embeddingModelProviderId', 'embeddingModelProviderId',
); );
const autoImageSearch = localStorage.getItem('autoImageSearch');
const autoVideoSearch = localStorage.getItem('autoVideoSearch');
if (!autoImageSearch) {
localStorage.setItem('autoImageSearch', 'true');
}
if (!autoVideoSearch) {
localStorage.setItem('autoVideoSearch', 'false');
}
const res = await fetch(`/api/providers`, { const res = await fetch(`/api/providers`, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -624,16 +614,13 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
const lastMsg = messagesRef.current[messagesRef.current.length - 1]; const lastMsg = messagesRef.current[messagesRef.current.length - 1];
const autoImageSearch = localStorage.getItem('autoImageSearch'); const autoMediaSearch = getAutoMediaSearch();
const autoVideoSearch = localStorage.getItem('autoVideoSearch');
if (autoImageSearch === 'true') { if (autoMediaSearch) {
document document
.getElementById(`search-images-${lastMsg.messageId}`) .getElementById(`search-images-${lastMsg.messageId}`)
?.click(); ?.click();
}
if (autoVideoSearch === 'true') {
document document
.getElementById(`search-videos-${lastMsg.messageId}`) .getElementById(`search-videos-${lastMsg.messageId}`)
?.click(); ?.click();