Compare commits

..

3 Commits

Author SHA1 Message Date
ItzCrazyKns
59ab10110a Merge branch 'master' of https://github.com/HadiCherkaoui/Perplexica into pr/658 2025-03-02 15:35:14 +05:30
ItzCrazyKns
10f9cd2f79 feat(app): lint & beautify 2025-03-02 15:35:08 +05:30
Hadi Cherkaoui
82e1dd73b0 add config to ui 2025-03-02 11:00:56 +01:00
14 changed files with 442 additions and 107 deletions

View File

@@ -2,7 +2,6 @@
[![Discord](https://dcbadge.vercel.app/api/server/26aArMy8tT?style=flat&compact=true)](https://discord.gg/26aArMy8tT) [![Discord](https://dcbadge.vercel.app/api/server/26aArMy8tT?style=flat&compact=true)](https://discord.gg/26aArMy8tT)
![preview](.assets/perplexica-screenshot.png?) ![preview](.assets/perplexica-screenshot.png?)
## Table of Contents <!-- omit in toc --> ## Table of Contents <!-- omit in toc -->

View File

@@ -4,7 +4,7 @@ services:
volumes: volumes:
- ./searxng:/etc/searxng:rw - ./searxng:/etc/searxng:rw
ports: ports:
- "4000:8080" - '4000:8080'
networks: networks:
- perplexica-network - perplexica-network
restart: unless-stopped restart: unless-stopped
@@ -19,7 +19,7 @@ services:
depends_on: depends_on:
- searxng - searxng
ports: ports:
- "3001:3001" - '3001:3001'
volumes: volumes:
- backend-dbstore:/home/perplexica/data - backend-dbstore:/home/perplexica/data
- uploads:/home/perplexica/uploads - uploads:/home/perplexica/uploads
@@ -41,7 +41,7 @@ services:
depends_on: depends_on:
- perplexica-backend - perplexica-backend
ports: ports:
- "3000:3000" - '3000:3000'
networks: networks:
- perplexica-network - perplexica-network
restart: unless-stopped restart: unless-stopped

View File

@@ -48,16 +48,18 @@ async function performImageSearch(query: string) {
switch (searchEngine) { switch (searchEngine) {
case 'google': { case 'google': {
const googleResult = await searchGooglePSE(query); const googleResult = await searchGooglePSE(query);
images = googleResult.results.map((result) => { images = googleResult.results
if (result.img_src && result.url && result.title) { .map((result) => {
return { if (result.img_src && result.url && result.title) {
img_src: result.img_src, return {
url: result.url, img_src: result.img_src,
title: result.title, url: result.url,
source: result.displayLink title: result.title,
}; source: result.displayLink,
} };
}).filter(Boolean); }
})
.filter(Boolean);
break; break;
} }
@@ -80,46 +82,52 @@ async function performImageSearch(query: string) {
case 'brave': { case 'brave': {
const braveResult = await searchBraveAPI(query); const braveResult = await searchBraveAPI(query);
images = braveResult.results.map((result) => { images = braveResult.results
if (result.img_src && result.url && result.title) { .map((result) => {
return { if (result.img_src && result.url && result.title) {
img_src: result.img_src, return {
url: result.url, img_src: result.img_src,
title: result.title, url: result.url,
source: result.url title: result.title,
}; source: result.url,
} };
}).filter(Boolean); }
})
.filter(Boolean);
break; break;
} }
case 'yacy': { case 'yacy': {
const yacyResult = await searchYaCy(query); const yacyResult = await searchYaCy(query);
images = yacyResult.results.map((result) => { images = yacyResult.results
if (result.img_src && result.url && result.title) { .map((result) => {
return { if (result.img_src && result.url && result.title) {
img_src: result.img_src, return {
url: result.url, img_src: result.img_src,
title: result.title, url: result.url,
source: result.url title: result.title,
source: result.url,
};
} }
} })
}).filter(Boolean); .filter(Boolean);
break; break;
} }
case 'bing': { case 'bing': {
const bingResult = await searchBingAPI(query); const bingResult = await searchBingAPI(query);
images = bingResult.results.map((result) => { images = bingResult.results
if (result.img_src && result.url && result.title) { .map((result) => {
return { if (result.img_src && result.url && result.title) {
img_src: result.img_src, return {
url: result.url, img_src: result.img_src,
title: result.title, url: result.url,
source: result.url title: result.title,
source: result.url,
};
} }
} })
}).filter(Boolean); .filter(Boolean);
break; break;
} }

View File

@@ -50,14 +50,17 @@ async function performVideoSearch(query: string) {
switch (searchEngine) { switch (searchEngine) {
case 'google': { case 'google': {
const googleResult = await searchGooglePSE(youtubeQuery); const googleResult = await searchGooglePSE(youtubeQuery);
googleResult.results.forEach((result) => { // Use .results instead of .originalres googleResult.results.forEach((result) => {
// Use .results instead of .originalres
if (result.img_src && result.url && result.title) { if (result.img_src && result.url && result.title) {
const videoId = new URL(result.url).searchParams.get('v'); const videoId = new URL(result.url).searchParams.get('v');
videos.push({ videos.push({
img_src: result.img_src, img_src: result.img_src,
url: result.url, url: result.url,
title: result.title, title: result.title,
iframe_src: videoId ? `https://www.youtube.com/embed/${videoId}` : null iframe_src: videoId
? `https://www.youtube.com/embed/${videoId}`
: null,
}); });
} }
}); });
@@ -95,7 +98,9 @@ async function performVideoSearch(query: string) {
img_src: result.img_src, img_src: result.img_src,
url: result.url, url: result.url,
title: result.title, title: result.title,
iframe_src: videoId ? `https://www.youtube.com/embed/${videoId}` : null iframe_src: videoId
? `https://www.youtube.com/embed/${videoId}`
: null,
}); });
} }
}); });
@@ -117,7 +122,9 @@ async function performVideoSearch(query: string) {
img_src: result.img_src, img_src: result.img_src,
url: result.url, url: result.url,
title: result.title, title: result.title,
iframe_src: videoId ? `https://www.youtube.com/embed/${videoId}` : null iframe_src: videoId
? `https://www.youtube.com/embed/${videoId}`
: null,
}); });
} }
}); });

View File

@@ -15,7 +15,7 @@ interface Config {
IMAGE: string; IMAGE: string;
VIDEO: string; VIDEO: string;
NEWS: string; NEWS: string;
} };
MODELS: { MODELS: {
OPENAI: { OPENAI: {
API_KEY: string; API_KEY: string;
@@ -103,7 +103,8 @@ export const getBraveApiKey = () => loadConfig().SEARCH_ENGINES.BRAVE.API_KEY;
export const getBingSubscriptionKey = () => export const getBingSubscriptionKey = () =>
loadConfig().SEARCH_ENGINES.BING.SUBSCRIPTION_KEY; loadConfig().SEARCH_ENGINES.BING.SUBSCRIPTION_KEY;
export const getYacyJsonEndpoint = () => loadConfig().SEARCH_ENGINES.YACY.ENDPOINT; export const getYacyJsonEndpoint = () =>
loadConfig().SEARCH_ENGINES.YACY.ENDPOINT;
export const getSearxngApiEndpoint = () => export const getSearxngApiEndpoint = () =>
process.env.SEARXNG_API_URL || loadConfig().SEARCH_ENGINES.SEARXNG.ENDPOINT; process.env.SEARXNG_API_URL || loadConfig().SEARCH_ENGINES.SEARXNG.ENDPOINT;

View File

@@ -60,8 +60,8 @@ export const searchBingAPI = async (query: string) => {
const res = await axios.get(url.toString(), { const res = await axios.get(url.toString(), {
headers: { headers: {
'Ocp-Apim-Subscription-Key': bingApiKey, 'Ocp-Apim-Subscription-Key': bingApiKey,
'Accept': 'application/json' Accept: 'application/json',
} },
}); });
if (res.data.error) { if (res.data.error) {
@@ -79,17 +79,20 @@ export const searchBingAPI = async (query: string) => {
title: item.name, title: item.name,
url: item.url, url: item.url,
content: item.snippet, content: item.snippet,
img_src: item.primaryImageOfPage?.thumbnailUrl img_src:
|| imageResults.find((img: any) => img.hostPageUrl === item.url)?.thumbnailUrl item.primaryImageOfPage?.thumbnailUrl ||
|| videoResults.find((vid: any) => vid.hostPageUrl === item.url)?.thumbnailUrl, imageResults.find((img: any) => img.hostPageUrl === item.url)
?.thumbnailUrl ||
videoResults.find((vid: any) => vid.hostPageUrl === item.url)
?.thumbnailUrl,
...(item.video && { ...(item.video && {
videoData: { videoData: {
duration: item.video.duration, duration: item.video.duration,
embedUrl: item.video.embedHtml?.match(/src="(.*?)"/)?.[1] embedUrl: item.video.embedHtml?.match(/src="(.*?)"/)?.[1],
}, },
publisher: item.publisher, publisher: item.publisher,
datePublished: item.datePublished datePublished: item.datePublished,
}) }),
})); }));
return { results, originalres }; return { results, originalres };

View File

@@ -33,7 +33,7 @@ interface BraveSearchResult {
export const searchBraveAPI = async ( export const searchBraveAPI = async (
query: string, query: string,
numResults: number = 20 numResults: number = 20,
): Promise<{ results: BraveSearchResult[]; originalres: any }> => { ): Promise<{ results: BraveSearchResult[]; originalres: any }> => {
try { try {
const braveApiKey = await getBraveApiKey(); const braveApiKey = await getBraveApiKey();
@@ -45,8 +45,8 @@ export const searchBraveAPI = async (
const res = await axios.get(url.toString(), { const res = await axios.get(url.toString(), {
headers: { headers: {
'X-Subscription-Token': braveApiKey, 'X-Subscription-Token': braveApiKey,
'Accept': 'application/json' Accept: 'application/json',
} },
}); });
if (res.data.error) { if (res.data.error) {
@@ -64,26 +64,32 @@ export const searchBraveAPI = async (
age: item.age, age: item.age,
family_friendly: item.family_friendly, family_friendly: item.family_friendly,
language: item.language, language: item.language,
video: item.video ? { video: item.video
embedUrl: item.video.embed_url, ? {
duration: item.video.duration embedUrl: item.video.embed_url,
} : undefined, duration: item.video.duration,
rating: item.rating ? { }
value: item.rating.value, : undefined,
scale: item.rating.scale_max rating: item.rating
} : undefined, ? {
value: item.rating.value,
scale: item.rating.scale_max,
}
: undefined,
products: item.deep_results?.product_cluster?.map((p: any) => ({ products: item.deep_results?.product_cluster?.map((p: any) => ({
name: p.name, name: p.name,
price: p.price price: p.price,
})), })),
recipe: item.recipe ? { recipe: item.recipe
ingredients: item.recipe.ingredients, ? {
cookTime: item.recipe.cook_time ingredients: item.recipe.ingredients,
} : undefined, cookTime: item.recipe.cook_time,
}
: undefined,
meta: { meta: {
fetched: item.meta?.fetched, fetched: item.meta?.fetched,
lastCrawled: item.meta?.last_crawled lastCrawled: item.meta?.last_crawled,
} },
})); }));
return { results, originalres }; return { results, originalres };

View File

@@ -21,7 +21,7 @@ interface GooglePSESearchResult {
}>; }>;
metatags?: Array<{ metatags?: Array<{
[key: string]: string; [key: string]: string;
'author'?: string; author?: string;
}>; }>;
cse_image?: Array<{ cse_image?: Array<{
src: string; src: string;
@@ -43,7 +43,7 @@ export const searchGooglePSE = async (query: string) => {
try { try {
const [googleApiKey, googleCseID] = await Promise.all([ const [googleApiKey, googleCseID] = await Promise.all([
getGoogleApiKey(), getGoogleApiKey(),
getGoogleCseId() getGoogleCseId(),
]); ]);
const url = new URL(`https://www.googleapis.com/customsearch/v1`); const url = new URL(`https://www.googleapis.com/customsearch/v1`);
@@ -63,15 +63,16 @@ export const searchGooglePSE = async (query: string) => {
title: item.title, title: item.title,
url: item.link, url: item.link,
content: item.snippet, content: item.snippet,
img_src: item.pagemap?.cse_image?.[0]?.src img_src:
|| item.pagemap?.cse_thumbnail?.[0]?.src item.pagemap?.cse_image?.[0]?.src ||
|| item.image?.thumbnailLink, item.pagemap?.cse_thumbnail?.[0]?.src ||
item.image?.thumbnailLink,
...(item.pagemap?.videoobject?.[0] && { ...(item.pagemap?.videoobject?.[0] && {
videoData: { videoData: {
duration: item.pagemap.videoobject[0].duration, duration: item.pagemap.videoobject[0].duration,
embedUrl: item.pagemap.videoobject[0].embedurl embedUrl: item.pagemap.videoobject[0].embedurl,
} },
}) }),
})); }));
return { results, originalres }; return { results, originalres };

View File

@@ -48,11 +48,7 @@ interface YaCySearchResult {
}[]; }[];
} }
export const searchYaCy = async (query: string, numResults: number = 20) => {
export const searchYaCy = async (
query: string,
numResults: number = 20
) => {
try { try {
const yacyBaseUrl = getYacyJsonEndpoint(); const yacyBaseUrl = getYacyJsonEndpoint();
@@ -64,7 +60,7 @@ export const searchYaCy = async (
const originalres = res.data as YaCySearchResult; const originalres = res.data as YaCySearchResult;
const results = originalres.channels[0].items.map(item => ({ const results = originalres.channels[0].items.map((item) => ({
title: item.title, title: item.title,
url: item.link, url: item.link,
content: item.description, content: item.description,

View File

@@ -13,6 +13,16 @@ import {
getCustomOpenaiApiUrl, getCustomOpenaiApiUrl,
getCustomOpenaiApiKey, getCustomOpenaiApiKey,
getCustomOpenaiModelName, getCustomOpenaiModelName,
getSearchEngineBackend,
getImageSearchEngineBackend,
getVideoSearchEngineBackend,
getNewsSearchEngineBackend,
getSearxngApiEndpoint,
getGoogleApiKey,
getGoogleCseId,
getBingSubscriptionKey,
getBraveApiKey,
getYacyJsonEndpoint,
} from '../config'; } from '../config';
import logger from '../utils/logger'; import logger from '../utils/logger';
@@ -61,6 +71,21 @@ router.get('/', async (_, res) => {
config['customOpenaiApiKey'] = getCustomOpenaiApiKey(); config['customOpenaiApiKey'] = getCustomOpenaiApiKey();
config['customOpenaiModelName'] = getCustomOpenaiModelName(); config['customOpenaiModelName'] = getCustomOpenaiModelName();
// Add search engine configuration
config['searchEngineBackends'] = {
search: getSearchEngineBackend(),
image: getImageSearchEngineBackend(),
video: getVideoSearchEngineBackend(),
news: getNewsSearchEngineBackend(),
};
config['searxngEndpoint'] = getSearxngApiEndpoint();
config['googleApiKey'] = getGoogleApiKey();
config['googleCseId'] = getGoogleCseId();
config['bingSubscriptionKey'] = getBingSubscriptionKey();
config['braveApiKey'] = getBraveApiKey();
config['yacyEndpoint'] = getYacyJsonEndpoint();
res.status(200).json(config); res.status(200).json(config);
} catch (err: any) { } catch (err: any) {
res.status(500).json({ message: 'An error has occurred.' }); res.status(500).json({ message: 'An error has occurred.' });
@@ -94,6 +119,30 @@ router.post('/', async (req, res) => {
MODEL_NAME: config.customOpenaiModelName, MODEL_NAME: config.customOpenaiModelName,
}, },
}, },
SEARCH_ENGINE_BACKENDS: config.searchEngineBackends ? {
SEARCH: config.searchEngineBackends.search,
IMAGE: config.searchEngineBackends.image,
VIDEO: config.searchEngineBackends.video,
NEWS: config.searchEngineBackends.news,
} : undefined,
SEARCH_ENGINES: {
GOOGLE: {
API_KEY: config.googleApiKey,
CSE_ID: config.googleCseId,
},
SEARXNG: {
ENDPOINT: config.searxngEndpoint,
},
BING: {
SUBSCRIPTION_KEY: config.bingSubscriptionKey,
},
BRAVE: {
API_KEY: config.braveApiKey,
},
YACY: {
ENDPOINT: config.yacyEndpoint,
},
},
}; };
updateConfig(updatedConfig); updateConfig(updatedConfig);

View File

@@ -15,7 +15,7 @@ async function performSearch(query: string, site: string) {
case 'google': { case 'google': {
const googleResult = await searchGooglePSE(query); const googleResult = await searchGooglePSE(query);
return googleResult.originalres.map(item => { return googleResult.originalres.map((item) => {
const imageSources = [ const imageSources = [
item.pagemap?.cse_image?.[0]?.src, item.pagemap?.cse_image?.[0]?.src,
item.pagemap?.cse_thumbnail?.[0]?.src, item.pagemap?.cse_thumbnail?.[0]?.src,
@@ -29,10 +29,11 @@ async function performSearch(query: string, site: string) {
url: item.link, url: item.link,
content: item.snippet, content: item.snippet,
thumbnail: imageSources[0], // First available image thumbnail: imageSources[0], // First available image
img_src: imageSources[0], // Same as thumbnail for consistency img_src: imageSources[0], // Same as thumbnail for consistency
iframe_src: null, iframe_src: null,
author: item.pagemap?.metatags?.[0]?.['og:site_name'] || site, author: item.pagemap?.metatags?.[0]?.['og:site_name'] || site,
publishedDate: item.pagemap?.metatags?.[0]?.['article:published_time'] publishedDate:
item.pagemap?.metatags?.[0]?.['article:published_time'],
}; };
}); });
} }
@@ -47,7 +48,7 @@ async function performSearch(query: string, site: string) {
case 'brave': { case 'brave': {
const braveResult = await searchBraveAPI(query); const braveResult = await searchBraveAPI(query);
return braveResult.results.map(item => ({ return braveResult.results.map((item) => ({
title: item.title, title: item.title,
url: item.url, url: item.url,
content: item.content, content: item.content,
@@ -55,7 +56,7 @@ async function performSearch(query: string, site: string) {
img_src: item.img_src, img_src: item.img_src,
iframe_src: null, iframe_src: null,
author: item.meta?.fetched || site, author: item.meta?.fetched || site,
publishedDate: item.meta?.lastCrawled publishedDate: item.meta?.lastCrawled,
})); }));
} }
@@ -69,13 +70,13 @@ async function performSearch(query: string, site: string) {
img_src: item.img_src, img_src: item.img_src,
iframe_src: null, iframe_src: null,
author: item?.host || site, author: item?.host || site,
publishedDate: item?.pubDate publishedDate: item?.pubDate,
})) }));
} }
case 'bing': { case 'bing': {
const bingResult = await searchBingAPI(query); const bingResult = await searchBingAPI(query);
return bingResult.results.map(item => ({ return bingResult.results.map((item) => ({
title: item.title, title: item.title,
url: item.url, url: item.url,
content: item.content, content: item.content,
@@ -83,8 +84,8 @@ async function performSearch(query: string, site: string) {
img_src: item.img_src, img_src: item.img_src,
iframe_src: null, iframe_src: null,
author: item?.publisher || site, author: item?.publisher || site,
publishedDate: item?.datePublished publishedDate: item?.datePublished,
})) }));
} }
default: default:
@@ -92,7 +93,6 @@ async function performSearch(query: string, site: string) {
} }
} }
router.get('/', async (req, res) => { router.get('/', async (req, res) => {
try { try {
const queries = [ const queries = [
@@ -114,12 +114,12 @@ router.get('/', async (req, res) => {
logger.error(`Error searching ${site}: ${error.message}`); logger.error(`Error searching ${site}: ${error.message}`);
return []; return [];
} }
}) }),
) )
) )
.flat() .flat()
.sort(() => Math.random() - 0.5) .sort(() => Math.random() - 0.5)
.filter(item => item.title && item.url && item.content); .filter((item) => item.title && item.url && item.content);
return res.json({ blogs: data }); return res.json({ blogs: data });
} catch (err: any) { } catch (err: any) {

View File

@@ -208,7 +208,6 @@ class MetaSearchAgent implements MetaSearchAgentType {
return { query: question, docs: docs }; return { query: question, docs: docs };
} else { } else {
const searchEngine = getSearchEngineBackend(); const searchEngine = getSearchEngineBackend();
let res; let res;
@@ -236,7 +235,9 @@ class MetaSearchAgent implements MetaSearchAgentType {
} }
if (!res?.results) { if (!res?.results) {
throw new Error(`No results found for search engine: ${searchEngine}`); throw new Error(
`No results found for search engine: ${searchEngine}`,
);
} }
const documents = res.results.map( const documents = res.results.map(

View File

@@ -23,6 +23,18 @@ interface SettingsType {
customOpenaiApiKey: string; customOpenaiApiKey: string;
customOpenaiApiUrl: string; customOpenaiApiUrl: string;
customOpenaiModelName: string; customOpenaiModelName: string;
searchEngineBackends: {
search: string;
image: string;
video: string;
news: string;
};
searxngEndpoint: string;
googleApiKey: string;
googleCseId: string;
bingSubscriptionKey: string;
braveApiKey: string;
yacyEndpoint: string;
} }
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
@@ -112,6 +124,12 @@ const Page = () => {
const [automaticImageSearch, setAutomaticImageSearch] = useState(false); const [automaticImageSearch, setAutomaticImageSearch] = useState(false);
const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false); const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false);
const [savingStates, setSavingStates] = useState<Record<string, boolean>>({}); const [savingStates, setSavingStates] = useState<Record<string, boolean>>({});
const [searchEngineBackends, setSearchEngineBackends] = useState({
search: '',
image: '',
video: '',
news: '',
});
useEffect(() => { useEffect(() => {
const fetchConfig = async () => { const fetchConfig = async () => {
@@ -125,6 +143,16 @@ const Page = () => {
const data = (await res.json()) as SettingsType; const data = (await res.json()) as SettingsType;
setConfig(data); setConfig(data);
// Set search engine backends if they exist in the response
if (data.searchEngineBackends) {
setSearchEngineBackends({
search: data.searchEngineBackends.search || '',
image: data.searchEngineBackends.image || '',
video: data.searchEngineBackends.video || '',
news: data.searchEngineBackends.news || '',
});
}
const chatModelProvidersKeys = Object.keys(data.chatModelProviders || {}); const chatModelProvidersKeys = Object.keys(data.chatModelProviders || {});
const embeddingModelProvidersKeys = Object.keys( const embeddingModelProvidersKeys = Object.keys(
data.embeddingModelProviders || {}, data.embeddingModelProviders || {},
@@ -331,6 +359,8 @@ const Page = () => {
localStorage.setItem('embeddingModelProvider', value); localStorage.setItem('embeddingModelProvider', value);
} else if (key === 'embeddingModel') { } else if (key === 'embeddingModel') {
localStorage.setItem('embeddingModel', value); localStorage.setItem('embeddingModel', value);
} else if (key === 'searchEngineBackends') {
localStorage.setItem('searchEngineBackends', value);
} }
} catch (err) { } catch (err) {
console.error('Failed to save:', err); console.error('Failed to save:', err);
@@ -793,6 +823,234 @@ const Page = () => {
</div> </div>
</div> </div>
</SettingsSection> </SettingsSection>
<SettingsSection title="Search Engine Settings">
<div className="flex flex-col space-y-4">
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Default Search Engine
</p>
<Select
value={searchEngineBackends.search}
onChange={(e) => {
const value = e.target.value;
setSearchEngineBackends((prev) => ({
...prev,
search: value,
}));
saveConfig('searchEngineBackends', {
...searchEngineBackends,
search: value,
});
}}
options={[
{ value: 'searxng', label: 'SearXNG' },
{ value: 'google', label: 'Google' },
{ value: 'bing', label: 'Bing' },
{ value: 'brave', label: 'Brave' },
{ value: 'yacy', label: 'YaCy' },
]}
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Image Search Engine
</p>
<Select
value={searchEngineBackends.image}
onChange={(e) => {
const value = e.target.value;
setSearchEngineBackends((prev) => ({
...prev,
image: value,
}));
saveConfig('searchEngineBackends', {
...searchEngineBackends,
image: value,
});
}}
options={[
{ value: '', label: 'Use Default Search Engine' },
{ value: 'searxng', label: 'SearXNG' },
{ value: 'google', label: 'Google' },
{ value: 'bing', label: 'Bing' },
{ value: 'brave', label: 'Brave' },
]}
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Video Search Engine
</p>
<Select
value={searchEngineBackends.video}
onChange={(e) => {
const value = e.target.value;
setSearchEngineBackends((prev) => ({
...prev,
video: value,
}));
saveConfig('searchEngineBackends', {
...searchEngineBackends,
video: value,
});
}}
options={[
{ value: '', label: 'Use Default Search Engine' },
{ value: 'searxng', label: 'SearXNG' },
{ value: 'google', label: 'Google' },
{ value: 'bing', label: 'Bing' },
{ value: 'brave', label: 'Brave' },
]}
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
News Search Engine
</p>
<Select
value={searchEngineBackends.news}
onChange={(e) => {
const value = e.target.value;
setSearchEngineBackends((prev) => ({
...prev,
news: value,
}));
saveConfig('searchEngineBackends', {
...searchEngineBackends,
news: value,
});
}}
options={[
{ value: '', label: 'Use Default Search Engine' },
{ value: 'searxng', label: 'SearXNG' },
{ value: 'google', label: 'Google' },
{ value: 'bing', label: 'Bing' },
{ value: 'brave', label: 'Brave' },
]}
/>
</div>
<div className="pt-4 border-t border-light-200 dark:border-dark-200">
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
SearXNG Endpoint
</p>
<Input
type="text"
placeholder="SearXNG API Endpoint"
value={config.searxngEndpoint || ''}
isSaving={savingStates['searxngEndpoint']}
onChange={(e) => {
setConfig((prev) => ({
...prev!,
searxngEndpoint: e.target.value,
}));
}}
onSave={(value) => saveConfig('searxngEndpoint', value)}
/>
</div>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Google API Key
</p>
<Input
type="text"
placeholder="Google API Key"
value={config.googleApiKey || ''}
isSaving={savingStates['googleApiKey']}
onChange={(e) => {
setConfig((prev) => ({
...prev!,
googleApiKey: e.target.value,
}));
}}
onSave={(value) => saveConfig('googleApiKey', value)}
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Google CSE ID
</p>
<Input
type="text"
placeholder="Google Custom Search Engine ID"
value={config.googleCseId || ''}
isSaving={savingStates['googleCseId']}
onChange={(e) => {
setConfig((prev) => ({
...prev!,
googleCseId: e.target.value,
}));
}}
onSave={(value) => saveConfig('googleCseId', value)}
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Bing Subscription Key
</p>
<Input
type="text"
placeholder="Bing Subscription Key"
value={config.bingSubscriptionKey || ''}
isSaving={savingStates['bingSubscriptionKey']}
onChange={(e) => {
setConfig((prev) => ({
...prev!,
bingSubscriptionKey: e.target.value,
}));
}}
onSave={(value) => saveConfig('bingSubscriptionKey', value)}
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Brave API Key
</p>
<Input
type="text"
placeholder="Brave API Key"
value={config.braveApiKey || ''}
isSaving={savingStates['braveApiKey']}
onChange={(e) => {
setConfig((prev) => ({
...prev!,
braveApiKey: e.target.value,
}));
}}
onSave={(value) => saveConfig('braveApiKey', value)}
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
YaCy Endpoint
</p>
<Input
type="text"
placeholder="YaCy API Endpoint"
value={config.yacyEndpoint || ''}
isSaving={savingStates['yacyEndpoint']}
onChange={(e) => {
setConfig((prev) => ({
...prev!,
yacyEndpoint: e.target.value,
}));
}}
onSave={(value) => saveConfig('yacyEndpoint', value)}
/>
</div>
</div>
</SettingsSection>
</div> </div>
) )
)} )}

View File

@@ -68,7 +68,13 @@ const MessageBox = ({
return ( return (
<div> <div>
{message.role === 'user' && ( {message.role === 'user' && (
<div className={cn('w-full', messageIndex === 0 ? 'pt-16' : 'pt-8', 'break-words')}> <div
className={cn(
'w-full',
messageIndex === 0 ? 'pt-16' : 'pt-8',
'break-words',
)}
>
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12"> <h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
{message.content} {message.content}
</h2> </h2>