mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-10-30 19:08:15 +00:00
Compare commits
12 Commits
v1.11.2
...
feat/impro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3da53aed03 | ||
|
|
244675759c | ||
|
|
ce6a37aaff | ||
|
|
c3abba8462 | ||
|
|
f709aa8224 | ||
|
|
22695f4ef6 | ||
|
|
75ef2e0282 | ||
|
|
b0d97c4c83 | ||
|
|
6527388e25 | ||
|
|
7397e33f29 | ||
|
|
f6ffa9ebe0 | ||
|
|
f9e675823b |
BIN
.assets/demo.gif
Normal file
BIN
.assets/demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 MiB |
BIN
.assets/sponsers/warp.png
Normal file
BIN
.assets/sponsers/warp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 433 KiB |
103
README.md
103
README.md
@@ -1,74 +1,57 @@
|
||||
# 🚀 Perplexica - An AI-powered search engine 🔎 <!-- omit in toc -->
|
||||
|
||||
<div align="center" markdown="1">
|
||||
<sup>Special thanks to:</sup>
|
||||
<br>
|
||||
<br>
|
||||
<a href="https://www.warp.dev/perplexica">
|
||||
<img alt="Warp sponsorship" width="400" src="https://github.com/user-attachments/assets/775dd593-9b5f-40f1-bf48-479faff4c27b">
|
||||
</a>
|
||||
|
||||
### [Warp, the AI Devtool that lives in your terminal](https://www.warp.dev/perplexica)
|
||||
|
||||
[Available for MacOS, Linux, & Windows](https://www.warp.dev/perplexica)
|
||||
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
# Perplexica 🔍
|
||||
|
||||
[](https://github.com/ItzCrazyKns/Perplexica/stargazers)
|
||||
[](https://github.com/ItzCrazyKns/Perplexica/network/members)
|
||||
[](https://github.com/ItzCrazyKns/Perplexica/watchers)
|
||||
[](https://hub.docker.com/r/itzcrazykns1337/perplexica)
|
||||
[](https://github.com/ItzCrazyKns/Perplexica/blob/master/LICENSE)
|
||||
[](https://github.com/ItzCrazyKns/Perplexica/commits/master)
|
||||
[](https://discord.gg/26aArMy8tT)
|
||||
|
||||

|
||||
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 <!-- omit in toc -->
|
||||
|
||||
- [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.
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
🤖 **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.
|
||||
|
||||
<div align="center">
|
||||
|
||||
|
||||
<a href="https://www.warp.dev/perplexica">
|
||||
<img alt="Warp Terminal" src=".assets/sponsers/warp.png" width="100%">
|
||||
</a>
|
||||
|
||||
**[Warp](https://www.warp.dev/perplexica)** - The AI-powered terminal revolutionizing development workflows
|
||||
|
||||
</div>
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -82,10 +82,10 @@ const AddProvider = ({
|
||||
|
||||
setProviders((prev) => [...prev, data]);
|
||||
|
||||
toast.success('Provider added successfully.');
|
||||
toast.success('Connection added successfully.');
|
||||
} catch (error) {
|
||||
console.error('Error adding provider:', error);
|
||||
toast.error('Failed to add provider.');
|
||||
toast.error('Failed to add connection.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setOpen(false);
|
||||
@@ -99,7 +99,7 @@ const AddProvider = ({
|
||||
className="px-3 md:px-4 py-1.5 md:py-2 rounded-lg text-xs sm:text-sm border border-light-200 dark:border-dark-200 text-black dark:text-white bg-light-secondary/50 dark:bg-dark-secondary/50 hover:bg-light-secondary hover:dark:bg-dark-secondary hover:border-light-300 hover:dark:border-dark-300 flex flex-row items-center space-x-1 active:scale-95 transition duration-200"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5 md:w-4 md:h-4" />
|
||||
<span>Add Provider</span>
|
||||
<span>Add Connection</span>
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
@@ -120,7 +120,7 @@ const AddProvider = ({
|
||||
<form onSubmit={handleSubmit} className="flex flex-col flex-1">
|
||||
<div className="px-6 pt-6 pb-4">
|
||||
<h3 className="text-black/90 dark:text-white/90 font-medium">
|
||||
Add new provider
|
||||
Add new connection
|
||||
</h3>
|
||||
</div>
|
||||
<div className="border-t border-light-200 dark:border-dark-200" />
|
||||
@@ -128,7 +128,7 @@ const AddProvider = ({
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex flex-col items-start space-y-2">
|
||||
<label className="text-xs text-black/70 dark:text-white/70">
|
||||
Select provider type
|
||||
Select connection type
|
||||
</label>
|
||||
<Select
|
||||
value={selectedProvider ?? ''}
|
||||
@@ -149,13 +149,13 @@ const AddProvider = ({
|
||||
className="flex flex-col items-start space-y-2"
|
||||
>
|
||||
<label className="text-xs text-black/70 dark:text-white/70">
|
||||
Name*
|
||||
Connection Name*
|
||||
</label>
|
||||
<input
|
||||
value={name}
|
||||
onChange={(e) => 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}
|
||||
/>
|
||||
@@ -199,7 +199,7 @@ const AddProvider = ({
|
||||
{loading ? (
|
||||
<Loader2 className="animate-spin" size={16} />
|
||||
) : (
|
||||
'Add Provider'
|
||||
'Add Connection'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<Trash2
|
||||
size={14}
|
||||
@@ -76,14 +76,15 @@ const DeleteProvider = ({
|
||||
<DialogPanel className="w-full mx-4 lg:w-[600px] max-h-[85vh] flex flex-col border bg-light-primary dark:bg-dark-primary border-light-secondary dark:border-dark-secondary rounded-lg">
|
||||
<div className="px-6 pt-6 pb-4">
|
||||
<h3 className="text-black/90 dark:text-white/90 font-medium">
|
||||
Delete provider
|
||||
Delete connection
|
||||
</h3>
|
||||
</div>
|
||||
<div className="border-t border-light-200 dark:border-dark-200" />
|
||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||
<p className="text-SM text-black/60 dark:text-white/60">
|
||||
Are you sure you want to delete the provider "
|
||||
<p className="text-sm text-black/60 dark:text-white/60">
|
||||
Are you sure you want to delete the connection "
|
||||
{modelProvider.name}"? This action cannot be undone.
|
||||
All associated models will also be removed.
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-6 py-6 flex justify-end space-x-2">
|
||||
|
||||
@@ -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<React.SetStateAction<ConfigModelProvider[]>>;
|
||||
}) => {
|
||||
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 (
|
||||
<div
|
||||
key={modelProvider.id}
|
||||
className="border border-light-200 dark:border-dark-200 rounded-lg overflow-hidden"
|
||||
className="border border-light-200 dark:border-dark-200 rounded-lg overflow-hidden bg-light-primary dark:bg-dark-primary"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'group px-5 py-4 flex flex-row justify-between w-full cursor-pointer hover:bg-light-secondary hover:dark:bg-dark-secondary transition duration-200 items-center',
|
||||
!open && 'rounded-lg',
|
||||
)}
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
<p className="text-sm lg:text-base text-black dark:text-white font-medium">
|
||||
{modelProvider.name}
|
||||
</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex flex-row items-center">
|
||||
<UpdateProvider
|
||||
fields={fields}
|
||||
modelProvider={modelProvider}
|
||||
setProviders={setProviders}
|
||||
/>
|
||||
<DeleteProvider
|
||||
modelProvider={modelProvider}
|
||||
setProviders={setProviders}
|
||||
/>
|
||||
<div className="px-5 py-3.5 flex flex-row justify-between w-full items-center border-b border-light-200 dark:border-dark-200 bg-light-secondary/30 dark:bg-dark-secondary/30">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="p-1.5 rounded-md bg-sky-500/10 dark:bg-sky-500/10">
|
||||
<Plug2 size={14} className="text-sky-500" />
|
||||
</div>
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className={cn(
|
||||
open ? 'rotate-180' : '',
|
||||
'transition duration-200 text-black/70 dark:text-white/70 group-hover:text-sky-500',
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm lg:text-base text-black dark:text-white font-medium">
|
||||
{modelProvider.name}
|
||||
</p>
|
||||
{modelCount > 0 && (
|
||||
<p className="text-[10px] lg:text-xs text-black/50 dark:text-white/50">
|
||||
{modelCount} model{modelCount !== 1 ? 's' : ''} configured
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<UpdateProvider
|
||||
fields={fields}
|
||||
modelProvider={modelProvider}
|
||||
setProviders={setProviders}
|
||||
/>
|
||||
<DeleteProvider
|
||||
modelProvider={modelProvider}
|
||||
setProviders={setProviders}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
>
|
||||
<div className="border-t border-light-200 dark:border-dark-200" />
|
||||
<div className="flex flex-col gap-y-4 px-5 py-4">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<div className="flex flex-row w-full justify-between items-center">
|
||||
<p className="text-[11px] lg:text-xs text-black/70 dark:text-white/70">
|
||||
Chat models
|
||||
</p>
|
||||
<AddModel
|
||||
providerId={modelProvider.id}
|
||||
setProviders={setProviders}
|
||||
type="chat"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{modelProvider.chatModels.some((m) => m.key === 'error') ? (
|
||||
<div className="flex flex-row items-center gap-2 text-xs lg:text-sm text-red-500 dark:text-red-400 rounded-lg bg-red-50 dark:bg-red-950/20 px-3 py-2 border border-red-200 dark:border-red-900/30">
|
||||
<AlertCircle size={16} className="shrink-0" />
|
||||
<span className="break-words">
|
||||
{
|
||||
modelProvider.chatModels.find(
|
||||
(m) => m.key === 'error',
|
||||
)?.name
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-row flex-wrap gap-2">
|
||||
{modelProvider.chatModels.map((model, index) => (
|
||||
<div
|
||||
key={`${modelProvider.id}-chat-${model.key}-${index}`}
|
||||
className="flex flex-row items-center space-x-1 text-xs lg:text-sm text-black/70 dark:text-white/70 rounded-lg bg-light-secondary dark:bg-dark-secondary px-3 py-1.5"
|
||||
>
|
||||
<span>{model.name}</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
handleModelDelete('chat', model.key);
|
||||
}}
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-4 px-5 py-4">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<div className="flex flex-row w-full justify-between items-center">
|
||||
<p className="text-[11px] lg:text-xs font-medium text-black/70 dark:text-white/70 uppercase tracking-wide">
|
||||
Chat Models
|
||||
</p>
|
||||
{!modelProvider.chatModels.some((m) => m.key === 'error') && (
|
||||
<AddModel
|
||||
providerId={modelProvider.id}
|
||||
setProviders={setProviders}
|
||||
type="chat"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{modelProvider.chatModels.some((m) => m.key === 'error') ? (
|
||||
<div className="flex flex-row items-center gap-2 text-xs lg:text-sm text-red-500 dark:text-red-400 rounded-lg bg-red-50 dark:bg-red-950/20 px-3 py-2 border border-red-200 dark:border-red-900/30">
|
||||
<AlertCircle size={16} className="shrink-0" />
|
||||
<span className="break-words">
|
||||
{
|
||||
modelProvider.chatModels.find((m) => m.key === 'error')
|
||||
?.name
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<div className="flex flex-row w-full justify-between items-center">
|
||||
<p className="text-[11px] lg:text-xs text-black/70 dark:text-white/70">
|
||||
Embedding models
|
||||
</p>
|
||||
<AddModel
|
||||
providerId={modelProvider.id}
|
||||
setProviders={setProviders}
|
||||
type="embedding"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{modelProvider.embeddingModels.some(
|
||||
(m) => m.key === 'error',
|
||||
) ? (
|
||||
<div className="flex flex-row items-center gap-2 text-xs lg:text-sm text-red-500 dark:text-red-400 rounded-lg bg-red-50 dark:bg-red-950/20 px-3 py-2 border border-red-200 dark:border-red-900/30">
|
||||
<AlertCircle size={16} className="shrink-0" />
|
||||
<span className="break-words">
|
||||
{
|
||||
modelProvider.embeddingModels.find(
|
||||
(m) => m.key === 'error',
|
||||
)?.name
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-row flex-wrap gap-2">
|
||||
{modelProvider.embeddingModels.map((model, index) => (
|
||||
<div
|
||||
key={`${modelProvider.id}-embedding-${model.key}-${index}`}
|
||||
className="flex flex-row items-center space-x-1 text-xs lg:text-sm text-black/70 dark:text-white/70 rounded-lg bg-light-secondary dark:bg-dark-secondary px-3 py-1.5"
|
||||
>
|
||||
<span>{model.name}</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
handleModelDelete('embedding', model.key);
|
||||
}}
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : modelProvider.chatModels.filter((m) => m.key !== 'error')
|
||||
.length === 0 && !hasError ? (
|
||||
<div className="flex flex-col items-center justify-center py-4 px-4 rounded-lg border-2 border-dashed border-light-200 dark:border-dark-200 bg-light-secondary/20 dark:bg-dark-secondary/20">
|
||||
<p className="text-xs text-black/50 dark:text-white/50 text-center">
|
||||
No chat models configured
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
) : modelProvider.chatModels.filter((m) => m.key !== 'error')
|
||||
.length > 0 ? (
|
||||
<div className="flex flex-row flex-wrap gap-2">
|
||||
{modelProvider.chatModels.map((model, index) => (
|
||||
<div
|
||||
key={`${modelProvider.id}-chat-${model.key}-${index}`}
|
||||
className="flex flex-row items-center space-x-1.5 text-xs lg:text-sm text-black/70 dark:text-white/70 rounded-lg bg-light-secondary dark:bg-dark-secondary px-3 py-1.5 border border-light-200 dark:border-dark-200"
|
||||
>
|
||||
<span>{model.name}</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
handleModelDelete('chat', model.key);
|
||||
}}
|
||||
className="hover:text-red-500 dark:hover:text-red-400 transition-colors"
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<div className="flex flex-row w-full justify-between items-center">
|
||||
<p className="text-[11px] lg:text-xs font-medium text-black/70 dark:text-white/70 uppercase tracking-wide">
|
||||
Embedding Models
|
||||
</p>
|
||||
{!modelProvider.embeddingModels.some((m) => m.key === 'error') && (
|
||||
<AddModel
|
||||
providerId={modelProvider.id}
|
||||
setProviders={setProviders}
|
||||
type="embedding"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{modelProvider.embeddingModels.some((m) => m.key === 'error') ? (
|
||||
<div className="flex flex-row items-center gap-2 text-xs lg:text-sm text-red-500 dark:text-red-400 rounded-lg bg-red-50 dark:bg-red-950/20 px-3 py-2 border border-red-200 dark:border-red-900/30">
|
||||
<AlertCircle size={16} className="shrink-0" />
|
||||
<span className="break-words">
|
||||
{
|
||||
modelProvider.embeddingModels.find((m) => m.key === 'error')
|
||||
?.name
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
) : modelProvider.embeddingModels.filter((m) => m.key !== 'error')
|
||||
.length === 0 && !hasError ? (
|
||||
<div className="flex flex-col items-center justify-center py-4 px-4 rounded-lg border-2 border-dashed border-light-200 dark:border-dark-200 bg-light-secondary/20 dark:bg-dark-secondary/20">
|
||||
<p className="text-xs text-black/50 dark:text-white/50 text-center">
|
||||
No embedding models configured
|
||||
</p>
|
||||
</div>
|
||||
) : modelProvider.embeddingModels.filter((m) => m.key !== 'error')
|
||||
.length > 0 ? (
|
||||
<div className="flex flex-row flex-wrap gap-2">
|
||||
{modelProvider.embeddingModels.map((model, index) => (
|
||||
<div
|
||||
key={`${modelProvider.id}-embedding-${model.key}-${index}`}
|
||||
className="flex flex-row items-center space-x-1.5 text-xs lg:text-sm text-black/70 dark:text-white/70 rounded-lg bg-light-secondary dark:bg-dark-secondary px-3 py-1.5 border border-light-200 dark:border-dark-200"
|
||||
>
|
||||
<span>{model.name}</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
handleModelDelete('embedding', model.key);
|
||||
}}
|
||||
className="hover:text-red-500 dark:hover:text-red-400 transition-colors"
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -64,8 +64,8 @@ const ModelSelect = ({
|
||||
</h4>
|
||||
<p className="text-[11px] lg:text-xs text-black/50 dark:text-white/50">
|
||||
{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'}
|
||||
</p>
|
||||
</div>
|
||||
<Select
|
||||
|
||||
@@ -39,22 +39,50 @@ const Models = ({
|
||||
<div className="border-t border-light-200 dark:border-dark-200" />
|
||||
<div className="flex flex-row justify-between items-center px-6 ">
|
||||
<p className="text-xs lg:text-sm text-black/70 dark:text-white/70">
|
||||
Manage model provider
|
||||
Manage connections
|
||||
</p>
|
||||
<AddProvider modelProviders={fields} setProviders={setProviders} />
|
||||
</div>
|
||||
<div className="flex flex-col px-6 gap-y-4">
|
||||
{providers.map((provider) => (
|
||||
<ModelProvider
|
||||
key={`provider-${provider.id}`}
|
||||
fields={
|
||||
(fields.find((f) => f.key === provider.type)?.fields ??
|
||||
[]) as UIConfigField[]
|
||||
}
|
||||
modelProvider={provider}
|
||||
setProviders={setProviders}
|
||||
/>
|
||||
))}
|
||||
{providers.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 px-4 rounded-lg border-2 border-dashed border-light-200 dark:border-dark-200 bg-light-secondary/10 dark:bg-dark-secondary/10">
|
||||
<div className="p-3 rounded-full bg-sky-500/10 dark:bg-sky-500/10 mb-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-8 h-8 text-sky-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-black/70 dark:text-white/70 mb-1">
|
||||
No connections yet
|
||||
</p>
|
||||
<p className="text-xs text-black/50 dark:text-white/50 text-center max-w-sm mb-4">
|
||||
Add your first connection to start using AI models. Connect to
|
||||
OpenAI, Anthropic, Ollama, and more.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
providers.map((provider) => (
|
||||
<ModelProvider
|
||||
key={`provider-${provider.id}`}
|
||||
fields={
|
||||
(fields.find((f) => f.key === provider.type)?.fields ??
|
||||
[]) as UIConfigField[]
|
||||
}
|
||||
modelProvider={provider}
|
||||
setProviders={setProviders}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -67,10 +67,10 @@ const UpdateProvider = ({
|
||||
});
|
||||
});
|
||||
|
||||
toast.success('Provider updated successfully.');
|
||||
toast.success('Connection updated successfully.');
|
||||
} catch (error) {
|
||||
console.error('Error updating provider:', error);
|
||||
toast.error('Failed to update provider.');
|
||||
toast.error('Failed to update connection.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setOpen(false);
|
||||
@@ -110,7 +110,7 @@ const UpdateProvider = ({
|
||||
<form onSubmit={handleSubmit} className="flex flex-col flex-1">
|
||||
<div className="px-6 pt-6 pb-4">
|
||||
<h3 className="text-black/90 dark:text-white/90 font-medium">
|
||||
Update provider
|
||||
Update connection
|
||||
</h3>
|
||||
</div>
|
||||
<div className="border-t border-light-200 dark:border-dark-200" />
|
||||
@@ -121,13 +121,13 @@ const UpdateProvider = ({
|
||||
className="flex flex-col items-start space-y-2"
|
||||
>
|
||||
<label className="text-xs text-black/70 dark:text-white/70">
|
||||
Name*
|
||||
Connection Name*
|
||||
</label>
|
||||
<input
|
||||
value={name}
|
||||
onChange={(event) => 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}
|
||||
/>
|
||||
@@ -171,7 +171,7 @@ const UpdateProvider = ({
|
||||
{loading ? (
|
||||
<Loader2 className="animate-spin" size={16} />
|
||||
) : (
|
||||
'Update Provider'
|
||||
'Update Connection'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
29
src/components/Settings/Sections/Personalization.tsx
Normal file
29
src/components/Settings/Sections/Personalization.tsx
Normal 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;
|
||||
@@ -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"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default General;
|
||||
export default Preferences;
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
@@ -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 = ({
|
||||
field,
|
||||
value,
|
||||
@@ -276,6 +351,15 @@ const SettingsField = ({
|
||||
dataAdd={dataAdd}
|
||||
/>
|
||||
);
|
||||
case 'switch':
|
||||
return (
|
||||
<SettingsSwitch
|
||||
field={field}
|
||||
value={val}
|
||||
setValue={setVal}
|
||||
dataAdd={dataAdd}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <div>Unsupported field type: {field.type}</div>;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="w-[95vw] md:w-[80vw] lg:w-[65vw] mx-auto px-2 sm:px-4 md:px-6 flex flex-col space-y-6">
|
||||
@@ -81,10 +85,10 @@ const SetupConfig = ({
|
||||
<div className="flex flex-row justify-between items-center mb-4 md:mb-6 pb-3 md:pb-4 border-b border-light-200 dark:border-dark-200">
|
||||
<div>
|
||||
<p className="text-xs sm:text-sm font-medium text-black dark:text-white">
|
||||
Manage Providers
|
||||
Manage Connections
|
||||
</p>
|
||||
<p className="text-[10px] sm:text-xs text-black/50 dark:text-white/50 mt-0.5">
|
||||
Add and configure your model providers
|
||||
Add connections to access AI models
|
||||
</p>
|
||||
</div>
|
||||
<AddProvider
|
||||
@@ -100,14 +104,17 @@ const SetupConfig = ({
|
||||
Loading providers...
|
||||
</p>
|
||||
</div>
|
||||
) : providers.length === 0 ? (
|
||||
) : visibleProviders.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 md:py-12 text-center">
|
||||
<p className="text-xs sm:text-sm font-medium text-black/70 dark:text-white/70">
|
||||
No providers configured
|
||||
No connections configured
|
||||
</p>
|
||||
<p className="text-[10px] sm:text-xs text-black/50 dark:text-white/50 mt-1">
|
||||
Click "Add Connection" above to get started
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
providers.map((provider) => (
|
||||
visibleProviders.map((provider) => (
|
||||
<ModelProvider
|
||||
key={`provider-${provider.id}`}
|
||||
fields={
|
||||
|
||||
@@ -6,11 +6,8 @@ const getClientConfig = (key: string, defaultVal?: any) => {
|
||||
|
||||
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', '');
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -48,7 +48,7 @@ class GeminiProvider extends BaseModelProvider<GeminiConfig> {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user