diff --git a/.assets/demo.gif b/.assets/demo.gif new file mode 100644 index 0000000..d94ed86 Binary files /dev/null and b/.assets/demo.gif differ diff --git a/.assets/perplexica-preview.gif b/.assets/perplexica-preview.gif deleted file mode 100644 index 5dae084..0000000 Binary files a/.assets/perplexica-preview.gif and /dev/null differ diff --git a/.assets/sponsers/warp.png b/.assets/sponsers/warp.png new file mode 100644 index 0000000..3cefad3 Binary files /dev/null and b/.assets/sponsers/warp.png differ diff --git a/README.md b/README.md index 33e62e6..0e666c4 100644 --- a/README.md +++ b/README.md @@ -1,74 +1,57 @@ -# 🚀 Perplexica - An AI-powered search engine 🔎 - -
- Special thanks to: -
-
- - Warp sponsorship - - -### [Warp, the AI Devtool that lives in your terminal](https://www.warp.dev/perplexica) - -[Available for MacOS, Linux, & Windows](https://www.warp.dev/perplexica) - -
- -
+# Perplexica 🔍 +[![GitHub Repo stars](https://img.shields.io/github/stars/ItzCrazyKns/Perplexica?style=social)](https://github.com/ItzCrazyKns/Perplexica/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/ItzCrazyKns/Perplexica?style=social)](https://github.com/ItzCrazyKns/Perplexica/network/members) +[![GitHub watchers](https://img.shields.io/github/watchers/ItzCrazyKns/Perplexica?style=social)](https://github.com/ItzCrazyKns/Perplexica/watchers) +[![Docker Pulls](https://img.shields.io/docker/pulls/itzcrazykns1337/perplexica?color=blue)](https://hub.docker.com/r/itzcrazykns1337/perplexica) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/ItzCrazyKns/Perplexica/blob/master/LICENSE) +[![GitHub last commit](https://img.shields.io/github/last-commit/ItzCrazyKns/Perplexica?color=green)](https://github.com/ItzCrazyKns/Perplexica/commits/master) [![Discord](https://dcbadge.limes.pink/api/server/26aArMy8tT?style=flat)](https://discord.gg/26aArMy8tT) -![preview](.assets/perplexica-screenshot.png?) +Perplexica is a **privacy-focused AI answering engine** that runs entirely on your own hardware. It combines knowledge from the vast internet with support for **local LLMs** (Ollama) and cloud providers (OpenAI, Claude, Groq), delivering accurate answers with **cited sources** while keeping your searches completely private. -## Table of Contents - -- [Overview](#overview) -- [Preview](#preview) -- [Features](#features) -- [Installation](#installation) - - [Getting Started with Docker (Recommended)](#getting-started-with-docker-recommended) - - [Non-Docker Installation](#non-docker-installation) - - [Ollama Connection Errors](#ollama-connection-errors) - - [Lemonade Connection Errors](#lemonade-connection-errors) -- [Using as a Search Engine](#using-as-a-search-engine) -- [Using Perplexica's API](#using-perplexicas-api) -- [Expose Perplexica to a network](#expose-perplexica-to-network) -- [One-Click Deployment](#one-click-deployment) -- [Upcoming Features](#upcoming-features) -- [Support Us](#support-us) - - [Donations](#donations) -- [Contribution](#contribution) -- [Help and Support](#help-and-support) - -## Overview - -Perplexica is an open-source AI-powered searching tool or an AI-powered search engine that goes deep into the internet to find answers. Inspired by Perplexity AI, it's an open-source option that not just searches the web but understands your questions. It uses advanced machine learning algorithms like similarity searching and embeddings to refine results and provides clear answers with sources cited. - -Using SearxNG to stay current and fully open source, Perplexica ensures you always get the most up-to-date information without compromising your privacy. +![preview](.assets/perplexica-screenshot.png) Want to know more about its architecture and how it works? You can read it [here](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/architecture/README.md). -## Preview +## ✨ Features -![video-preview](.assets/perplexica-preview.gif) +🤖 **Support for all major AI providers** - Use local LLMs through Ollama or connect to OpenAI, Anthropic Claude, Google Gemini, Groq, and more. Mix and match models based on your needs. -## Features +⚡ **Smart search modes** - Choose Balanced Mode for everyday searches, Fast Mode when you need quick answers, or wait for Quality Mode (coming soon) for deep research. -- **Local LLMs**: You can utilize local LLMs such as Qwen, DeepSeek, Llama, and Mistral. -- **Two Main Modes:** - - **Copilot Mode:** (In development) Boosts search by generating different queries to find more relevant internet sources. Like normal search instead of just using the context by SearxNG, it visits the top matches and tries to find relevant sources to the user's query directly from the page. - - **Normal Mode:** Processes your query and performs a web search. -- **Focus Modes:** Special modes to better answer specific types of questions. Perplexica currently has 6 focus modes: - - **All Mode:** Searches the entire web to find the best results. - - **Writing Assistant Mode:** Helpful for writing tasks that do not require searching the web. - - **Academic Search Mode:** Finds articles and papers, ideal for academic research. - - **YouTube Search Mode:** Finds YouTube videos based on the search query. - - **Wolfram Alpha Search Mode:** Answers queries that need calculations or data analysis using Wolfram Alpha. - - **Reddit Search Mode:** Searches Reddit for discussions and opinions related to the query. -- **Current Information:** Some search tools might give you outdated info because they use data from crawling bots and convert them into embeddings and store them in a index. Unlike them, Perplexica uses SearxNG, a metasearch engine to get the results and rerank and get the most relevant source out of it, ensuring you always get the latest information without the overhead of daily data updates. -- **API**: Integrate Perplexica into your existing applications and make use of its capibilities. +🎯 **Six specialized focus modes** - Get better results with modes designed for specific tasks: Academic papers, YouTube videos, Reddit discussions, Wolfram Alpha calculations, writing assistance, or general web search. -It has many more features like image and video search. Some of the planned features are mentioned in [upcoming features](#upcoming-features). +🔍 **Web search powered by SearxNG** - Access multiple search engines while keeping your identity private. Support for Tavily and Exa coming soon for even better results. + +📷 **Image and video search** - Find visual content alongside text results. Search isn't limited to just articles anymore. + +📄 **File uploads** - Upload documents and ask questions about them. PDFs, text files, images - Perplexica understands them all. + +🌐 **Search specific domains** - Limit your search to specific websites when you know where to look. Perfect for technical documentation or research papers. + +💡 **Smart suggestions** - Get intelligent search suggestions as you type, helping you formulate better queries. + +📚 **Discover** - Browse interesting articles and trending content throughout the day. Stay informed without even searching. + +🕒 **Search history** - Every search is saved locally so you can revisit your discoveries anytime. Your research is never lost. + +✨ **More coming soon** - We're actively developing new features based on community feedback. Join our Discord to help shape Perplexica's future! + +## Sponsors + +Perplexica's development is powered by the generous support of our sponsors. Their contributions help keep this project free, open-source, and accessible to everyone. + +
+ + + + Warp Terminal + + +**[Warp](https://www.warp.dev/perplexica)** - The AI-powered terminal revolutionizing development workflows + +
## Installation diff --git a/src/components/Settings/Sections/Models/AddModelDialog.tsx b/src/components/Settings/Sections/Models/AddModelDialog.tsx index 7df98de..009e1f2 100644 --- a/src/components/Settings/Sections/Models/AddModelDialog.tsx +++ b/src/components/Settings/Sections/Models/AddModelDialog.tsx @@ -97,7 +97,7 @@ const AddModel = ({ >
-

+

Add new {type === 'chat' ? 'chat' : 'embedding'} model

@@ -115,7 +115,7 @@ const AddModel = ({ setModelName(e.target.value)} - className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60" + className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 text-[13px] text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60" placeholder="e.g., GPT-4" type="text" required @@ -128,7 +128,7 @@ const AddModel = ({ setModelKey(e.target.value)} - className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60" + className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 text-[13px] text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60" placeholder="e.g., gpt-4" type="text" required @@ -140,7 +140,7 @@ const AddModel = ({ {open && ( @@ -119,8 +119,8 @@ const AddProvider = ({
-

- Add new provider +

+ Add new connection

@@ -128,7 +128,7 @@ const AddProvider = ({
setName(e.target.value)} className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 pr-10 text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60" - placeholder={'Provider Name'} + placeholder={'e.g., My OpenAI Connection'} type="text" required={true} /> @@ -178,7 +178,7 @@ const AddProvider = ({ [field.key]: event.target.value, })) } - className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 pr-10 text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60" + className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 pr-10 text-[13px] text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60" placeholder={ (field as StringUIConfigField).placeholder } @@ -194,12 +194,12 @@ const AddProvider = ({
diff --git a/src/components/Settings/Sections/Models/DeleteProviderDialog.tsx b/src/components/Settings/Sections/Models/DeleteProviderDialog.tsx index e05a449..c9a4c72 100644 --- a/src/components/Settings/Sections/Models/DeleteProviderDialog.tsx +++ b/src/components/Settings/Sections/Models/DeleteProviderDialog.tsx @@ -34,10 +34,10 @@ const DeleteProvider = ({ return prev.filter((p) => p.id !== modelProvider.id); }); - toast.success('Provider deleted successfully.'); + toast.success('Connection deleted successfully.'); } catch (error) { console.error('Error deleting provider:', error); - toast.error('Failed to delete provider.'); + toast.error('Failed to delete connection.'); } finally { setLoading(false); } @@ -51,7 +51,7 @@ const DeleteProvider = ({ setOpen(true); }} className="group p-1.5 rounded-md hover:bg-light-200 hover:dark:bg-dark-200 transition-colors group" - title="Delete provider" + title="Delete connection" >

- Delete provider + Delete connection

-

- Are you sure you want to delete the provider " +

+ Are you sure you want to delete the connection " {modelProvider.name}"? This action cannot be undone. + All associated models will also be removed.

diff --git a/src/components/Settings/Sections/Models/ModelProvider.tsx b/src/components/Settings/Sections/Models/ModelProvider.tsx index 79928c2..3ccbc14 100644 --- a/src/components/Settings/Sections/Models/ModelProvider.tsx +++ b/src/components/Settings/Sections/Models/ModelProvider.tsx @@ -1,7 +1,7 @@ import { UIConfigField, ConfigModelProvider } from '@/lib/config/types'; import { cn } from '@/lib/utils'; import { AnimatePresence, motion } from 'framer-motion'; -import { AlertCircle, ChevronDown, Pencil, Trash2, X } from 'lucide-react'; +import { AlertCircle, Plug2, Plus, Pencil, Trash2, X } from 'lucide-react'; import { useState } from 'react'; import { toast } from 'sonner'; import AddModel from './AddModelDialog'; @@ -17,7 +17,7 @@ const ModelProvider = ({ fields: UIConfigField[]; setProviders: React.Dispatch>; }) => { - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(true); const handleModelDelete = async ( type: 'chat' | 'embedding', @@ -66,146 +66,157 @@ const ModelProvider = ({ } }; + const modelCount = + modelProvider.chatModels.filter((m) => m.key !== 'error').length + + modelProvider.embeddingModels.filter((m) => m.key !== 'error').length; + const hasError = + modelProvider.chatModels.some((m) => m.key === 'error') || + modelProvider.embeddingModels.some((m) => m.key === 'error'); + return (
-
setOpen(!open)} - > -

- {modelProvider.name} -

-
-
- - +
+
+
+
- +

+ {modelProvider.name} +

+ {modelCount > 0 && ( +

+ {modelCount} model{modelCount !== 1 ? 's' : ''} configured +

)} +
+
+
+ +
- - {open && ( - -
-
-
-
-

- Chat models -

- -
-
- {modelProvider.chatModels.some((m) => m.key === 'error') ? ( -
- - - { - modelProvider.chatModels.find( - (m) => m.key === 'error', - )?.name - } - -
- ) : ( -
- {modelProvider.chatModels.map((model, index) => ( -
- {model.name} - -
- ))} -
- )} -
+
+
+
+

+ Chat Models +

+ {!modelProvider.chatModels.some((m) => m.key === 'error') && ( + + )} +
+
+ {modelProvider.chatModels.some((m) => m.key === 'error') ? ( +
+ + + { + modelProvider.chatModels.find((m) => m.key === 'error') + ?.name + } +
-
-
-

- Embedding models -

- -
-
- {modelProvider.embeddingModels.some( - (m) => m.key === 'error', - ) ? ( -
- - - { - modelProvider.embeddingModels.find( - (m) => m.key === 'error', - )?.name - } - -
- ) : ( -
- {modelProvider.embeddingModels.map((model, index) => ( -
- {model.name} - -
- ))} -
- )} -
+ ) : modelProvider.chatModels.filter((m) => m.key !== 'error') + .length === 0 && !hasError ? ( +
+

+ No chat models configured +

-
- - )} - + ) : modelProvider.chatModels.filter((m) => m.key !== 'error') + .length > 0 ? ( +
+ {modelProvider.chatModels.map((model, index) => ( +
+ {model.name} + +
+ ))} +
+ ) : null} +
+
+ +
+
+

+ Embedding Models +

+ {!modelProvider.embeddingModels.some((m) => m.key === 'error') && ( + + )} +
+
+ {modelProvider.embeddingModels.some((m) => m.key === 'error') ? ( +
+ + + { + modelProvider.embeddingModels.find((m) => m.key === 'error') + ?.name + } + +
+ ) : modelProvider.embeddingModels.filter((m) => m.key !== 'error') + .length === 0 && !hasError ? ( +
+

+ No embedding models configured +

+
+ ) : modelProvider.embeddingModels.filter((m) => m.key !== 'error') + .length > 0 ? ( +
+ {modelProvider.embeddingModels.map((model, index) => ( +
+ {model.name} + +
+ ))} +
+ ) : null} +
+
+
); }; diff --git a/src/components/Settings/Sections/Models/ModelSelect.tsx b/src/components/Settings/Sections/Models/ModelSelect.tsx index 75117b3..b5bc182 100644 --- a/src/components/Settings/Sections/Models/ModelSelect.tsx +++ b/src/components/Settings/Sections/Models/ModelSelect.tsx @@ -59,13 +59,13 @@ const ModelSelect = ({
-

+

Select {type === 'chat' ? 'Chat Model' : 'Embedding Model'}

{type === 'chat' - ? 'Select the model to use for chat responses' - : 'Select the model to use for embeddings'} + ? 'Choose which model to use for generating responses' + : 'Choose which model to use for generating embeddings'}

setName(event.target.value)} className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 pr-10 text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60" - placeholder={'Provider Name'} + placeholder={'Connection Name'} type="text" required={true} /> @@ -150,7 +150,7 @@ const UpdateProvider = ({ [field.key]: event.target.value, })) } - className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 pr-10 text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60" + className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 pr-10 text-[13px] text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60" placeholder={ (field as StringUIConfigField).placeholder } @@ -166,12 +166,12 @@ const UpdateProvider = ({
diff --git a/src/components/Settings/Sections/Personalization.tsx b/src/components/Settings/Sections/Personalization.tsx new file mode 100644 index 0000000..c0f0ede --- /dev/null +++ b/src/components/Settings/Sections/Personalization.tsx @@ -0,0 +1,29 @@ +import { UIConfigField } from '@/lib/config/types'; +import SettingsField from '../SettingsField'; + +const Personalization = ({ + fields, + values, +}: { + fields: UIConfigField[]; + values: Record; +}) => { + return ( +
+ {fields.map((field) => ( + + ))} +
+ ); +}; + +export default Personalization; diff --git a/src/components/Settings/Sections/General.tsx b/src/components/Settings/Sections/Preferences.tsx similarity index 87% rename from src/components/Settings/Sections/General.tsx rename to src/components/Settings/Sections/Preferences.tsx index 18b77bc..e14f763 100644 --- a/src/components/Settings/Sections/General.tsx +++ b/src/components/Settings/Sections/Preferences.tsx @@ -1,7 +1,7 @@ import { UIConfigField } from '@/lib/config/types'; import SettingsField from '../SettingsField'; -const General = ({ +const Preferences = ({ fields, values, }: { @@ -19,11 +19,11 @@ const General = ({ ? localStorage.getItem(field.key) : values[field.key]) ?? field.default } - dataAdd="general" + dataAdd="preferences" /> ))}
); }; -export default General; +export default Preferences; diff --git a/src/components/Settings/SettingsDialogue.tsx b/src/components/Settings/SettingsDialogue.tsx index 7950954..ba097a9 100644 --- a/src/components/Settings/SettingsDialogue.tsx +++ b/src/components/Settings/SettingsDialogue.tsx @@ -4,9 +4,10 @@ import { BrainCog, ChevronLeft, Search, - Settings, + Sliders, + ToggleRight, } from 'lucide-react'; -import General from './Sections/General'; +import Preferences from './Sections/Preferences'; import { motion } from 'framer-motion'; import { useEffect, useState } from 'react'; import { toast } from 'sonner'; @@ -15,20 +16,29 @@ import { cn } from '@/lib/utils'; import Models from './Sections/Models/Section'; import SearchSection from './Sections/Search'; import Select from '@/components/ui/Select'; +import Personalization from './Sections/Personalization'; const sections = [ { - key: 'general', - name: 'General', - description: 'Adjust common settings.', - icon: Settings, - component: General, - dataAdd: 'general', + key: 'preferences', + name: 'Preferences', + description: 'Customize your application preferences.', + icon: Sliders, + component: Preferences, + dataAdd: 'preferences', + }, + { + key: 'personalization', + name: 'Personalization', + description: 'Customize the behavior and tone of the model.', + icon: ToggleRight, + component: Personalization, + dataAdd: 'personalization', }, { key: 'models', name: 'Models', - description: 'Configure model settings.', + description: 'Connect to AI services and manage connections.', icon: BrainCog, component: Models, dataAdd: 'modelProviders', @@ -166,7 +176,7 @@ const SettingsDialogue = ({
-

+

{selectedSection.name}

diff --git a/src/components/Settings/SettingsField.tsx b/src/components/Settings/SettingsField.tsx index 8b2fc41..55aa640 100644 --- a/src/components/Settings/SettingsField.tsx +++ b/src/components/Settings/SettingsField.tsx @@ -1,6 +1,7 @@ import { SelectUIConfigField, StringUIConfigField, + SwitchUIConfigField, TextareaUIConfigField, UIConfigField, } from '@/lib/config/types'; @@ -9,6 +10,7 @@ import Select from '../ui/Select'; import { toast } from 'sonner'; import { useTheme } from 'next-themes'; import { Loader2 } from 'lucide-react'; +import { Switch } from '@headlessui/react'; const SettingsSelect = ({ field, @@ -62,7 +64,7 @@ const SettingsSelect = ({

-

+

{field.name}

@@ -133,7 +135,7 @@ const SettingsInput = ({

-

+

{field.name}

@@ -145,7 +147,7 @@ const SettingsInput = ({ value={value ?? field.default ?? ''} onChange={(event) => setValue(event.target.value)} onBlur={(event) => handleSave(event.target.value)} - className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-3 py-2 lg:px-4 lg:py-3 pr-10 !text-xs lg:!text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60" + className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-3 py-2 lg:px-4 lg:py-3 pr-10 !text-xs lg:!text-[13px] text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60" placeholder={field.placeholder} type="text" disabled={loading} @@ -209,7 +211,7 @@ const SettingsTextarea = ({

-

+

{field.name}

@@ -221,7 +223,7 @@ const SettingsTextarea = ({ value={value ?? field.default ?? ''} onChange={(event) => setValue(event.target.value)} onBlur={(event) => handleSave(event.target.value)} - className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-3 py-2 lg:px-4 lg:py-3 pr-10 !text-xs lg:!text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60" + className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-3 py-2 lg:px-4 lg:py-3 pr-10 !text-xs lg:!text-[13px] text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60" placeholder={field.placeholder} rows={4} disabled={loading} @@ -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 ( +

+
+
+

+ {field.name} +

+

+ {field.description} +

+
+ + +
+
+ ); +}; + const SettingsField = ({ field, value, @@ -276,6 +351,15 @@ const SettingsField = ({ dataAdd={dataAdd} /> ); + case 'switch': + return ( + + ); default: return
Unsupported field type: {field.type}
; } diff --git a/src/components/Setup/SetupConfig.tsx b/src/components/Setup/SetupConfig.tsx index 334974f..4e17a92 100644 --- a/src/components/Setup/SetupConfig.tsx +++ b/src/components/Setup/SetupConfig.tsx @@ -63,7 +63,11 @@ const SetupConfig = ({ } }; - const hasProviders = providers.length > 0; + const visibleProviders = providers.filter( + (p) => p.name.toLowerCase() !== 'transformers', + ); + const hasProviders = + visibleProviders.filter((p) => p.chatModels.length > 0).length > 0; return (
@@ -81,10 +85,10 @@ const SetupConfig = ({

- Manage Providers + Manage Connections

- Add and configure your model providers + Add connections to access AI models

- ) : providers.length === 0 ? ( + ) : visibleProviders.length === 0 ? (

- No providers configured + No connections configured +

+

+ Click "Add Connection" above to get started

) : ( - providers.map((provider) => ( + visibleProviders.map((provider) => ( { export const getTheme = () => getClientConfig('theme', 'dark'); -export const getAutoImageSearch = () => - Boolean(getClientConfig('autoImageSearch', 'true')); - -export const getAutoVideoSearch = () => - Boolean(getClientConfig('autoVideoSearch', 'true')); +export const getAutoMediaSearch = () => + getClientConfig('autoMediaSearch', 'true') === 'true'; export const getSystemInstructions = () => getClientConfig('systemInstructions', ''); diff --git a/src/lib/config/index.ts b/src/lib/config/index.ts index 0487a11..9b69c8a 100644 --- a/src/lib/config/index.ts +++ b/src/lib/config/index.ts @@ -13,14 +13,15 @@ class ConfigManager { currentConfig: Config = { version: this.configVersion, setupComplete: false, - general: {}, + preferences: {}, + personalization: {}, modelProviders: [], search: { searxngURL: '', }, }; uiConfigSections: UIConfigSections = { - general: [ + preferences: [ { name: 'Theme', key: 'theme', @@ -40,16 +41,6 @@ class ConfigManager { default: 'dark', 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', key: 'measureUnit', @@ -69,6 +60,27 @@ class ConfigManager { default: 'Metric', 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: [], search: [ diff --git a/src/lib/config/types.ts b/src/lib/config/types.ts index 8497cb5..6eaa70c 100644 --- a/src/lib/config/types.ts +++ b/src/lib/config/types.ts @@ -38,11 +38,17 @@ type TextareaUIConfigField = BaseUIConfigField & { default?: string; }; +type SwitchUIConfigField = BaseUIConfigField & { + type: 'switch'; + default?: boolean; +}; + type UIConfigField = | StringUIConfigField | SelectUIConfigField | PasswordUIConfigField - | TextareaUIConfigField; + | TextareaUIConfigField + | SwitchUIConfigField; type ConfigModelProvider = { id: string; @@ -57,7 +63,10 @@ type ConfigModelProvider = { type Config = { version: number; setupComplete: boolean; - general: { + preferences: { + [key: string]: any; + }; + personalization: { [key: string]: any; }; modelProviders: ConfigModelProvider[]; @@ -80,7 +89,8 @@ type ModelProviderUISection = { }; type UIConfigSections = { - general: UIConfigField[]; + preferences: UIConfigField[]; + personalization: UIConfigField[]; modelProviders: ModelProviderUISection[]; search: UIConfigField[]; }; @@ -95,4 +105,5 @@ export type { ModelProviderUISection, ConfigModelProvider, TextareaUIConfigField, + SwitchUIConfigField, }; diff --git a/src/lib/hooks/useChat.tsx b/src/lib/hooks/useChat.tsx index 8ef57ef..ee7e9c7 100644 --- a/src/lib/hooks/useChat.tsx +++ b/src/lib/hooks/useChat.tsx @@ -21,6 +21,7 @@ import { useParams, useSearchParams } from 'next/navigation'; import { toast } from 'sonner'; import { getSuggestions } from '../actions'; import { MinimalProvider } from '../models/types'; +import { getAutoMediaSearch } from '../config/clientRegistry'; export type Section = { userMessage: UserMessage; @@ -94,17 +95,6 @@ const checkConfig = async ( '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`, { headers: { 'Content-Type': 'application/json', @@ -624,16 +614,13 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => { const lastMsg = messagesRef.current[messagesRef.current.length - 1]; - const autoImageSearch = localStorage.getItem('autoImageSearch'); - const autoVideoSearch = localStorage.getItem('autoVideoSearch'); + const autoMediaSearch = getAutoMediaSearch(); - if (autoImageSearch === 'true') { + if (autoMediaSearch) { document .getElementById(`search-images-${lastMsg.messageId}`) ?.click(); - } - if (autoVideoSearch === 'true') { document .getElementById(`search-videos-${lastMsg.messageId}`) ?.click(); diff --git a/src/lib/models/providers/gemini.ts b/src/lib/models/providers/gemini.ts index 6cf3584..6cfd913 100644 --- a/src/lib/models/providers/gemini.ts +++ b/src/lib/models/providers/gemini.ts @@ -48,7 +48,12 @@ class GeminiProvider extends BaseModelProvider { let defaultChatModels: Model[] = []; data.models.forEach((m: any) => { - if (m.supportedGenerationMethods.includes('embedText')) { + if ( + m.supportedGenerationMethods.some( + (genMethod: string) => + genMethod === 'embedText' || genMethod === 'embedContent', + ) + ) { defaultEmbeddingModels.push({ key: m.name, name: m.displayName,