mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-09-16 14:21:32 +00:00
Compare commits
5 Commits
develop/v1
...
5bf3221349
Author | SHA1 | Date | |
---|---|---|---|
|
5bf3221349 | ||
|
b84e4e4ce6 | ||
|
467905d9f2 | ||
|
18b6f5b674 | ||
|
2bdcbf20fb |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,7 +2,6 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
package-lock.json
|
|
||||||
|
|
||||||
# Build output
|
# Build output
|
||||||
/.next/
|
/.next/
|
||||||
@@ -38,6 +37,3 @@ Thumbs.db
|
|||||||
# Db
|
# Db
|
||||||
db.sqlite
|
db.sqlite
|
||||||
/searxng
|
/searxng
|
||||||
|
|
||||||
# Dev
|
|
||||||
docker-compose-dev.yaml
|
|
||||||
|
@@ -43,7 +43,7 @@ Want to know more about its architecture and how it works? You can read it [here
|
|||||||
- **Normal Mode:** Processes your query and performs a web search.
|
- **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:
|
- **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.
|
- **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.
|
- **Writing Assistant Mode:** Helpful for writing tasks that does not require searching the web.
|
||||||
- **Academic Search Mode:** Finds articles and papers, ideal for academic research.
|
- **Academic Search Mode:** Finds articles and papers, ideal for academic research.
|
||||||
- **YouTube Search Mode:** Finds YouTube videos based on the search query.
|
- **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.
|
- **Wolfram Alpha Search Mode:** Answers queries that need calculations or data analysis using Wolfram Alpha.
|
||||||
@@ -142,7 +142,6 @@ You can access Perplexica over your home network by following our networking gui
|
|||||||
|
|
||||||
## One-Click Deployment
|
## One-Click Deployment
|
||||||
|
|
||||||
[](https://usw.sealos.io/?openapp=system-template%3FtemplateName%3Dperplexica)
|
|
||||||
[](https://repocloud.io/details/?app_id=267)
|
[](https://repocloud.io/details/?app_id=267)
|
||||||
|
|
||||||
## Upcoming Features
|
## Upcoming Features
|
||||||
|
@@ -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
|
||||||
|
@@ -30,8 +30,8 @@
|
|||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
"@langchain/anthropic": "^0.2.3",
|
"@langchain/anthropic": "^0.2.3",
|
||||||
"@langchain/community": "^0.2.16",
|
"@langchain/community": "^0.2.16",
|
||||||
"@langchain/google-genai": "^0.0.23",
|
|
||||||
"@langchain/openai": "^0.0.25",
|
"@langchain/openai": "^0.0.25",
|
||||||
|
"@langchain/google-genai": "^0.0.23",
|
||||||
"@xenova/transformers": "^2.17.1",
|
"@xenova/transformers": "^2.17.1",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"better-sqlite3": "^11.0.0",
|
"better-sqlite3": "^11.0.0",
|
||||||
|
@@ -3,12 +3,6 @@ PORT = 3001 # Port to run the server on
|
|||||||
SIMILARITY_MEASURE = "cosine" # "cosine" or "dot"
|
SIMILARITY_MEASURE = "cosine" # "cosine" or "dot"
|
||||||
KEEP_ALIVE = "5m" # How long to keep Ollama models loaded into memory. (Instead of using -1 use "-1m")
|
KEEP_ALIVE = "5m" # How long to keep Ollama models loaded into memory. (Instead of using -1 use "-1m")
|
||||||
|
|
||||||
[SEARCH_ENGINE_BACKENDS] # "google" | "searxng" | "bing" | "brave" | "yacy"
|
|
||||||
SEARCH = "searxng"
|
|
||||||
IMAGE = "searxng"
|
|
||||||
VIDEO = "searxng"
|
|
||||||
NEWS = "searxng"
|
|
||||||
|
|
||||||
[MODELS.OPENAI]
|
[MODELS.OPENAI]
|
||||||
API_KEY = ""
|
API_KEY = ""
|
||||||
|
|
||||||
@@ -28,18 +22,5 @@ API_URL = ""
|
|||||||
[MODELS.OLLAMA]
|
[MODELS.OLLAMA]
|
||||||
API_URL = "" # Ollama API URL - http://host.docker.internal:11434
|
API_URL = "" # Ollama API URL - http://host.docker.internal:11434
|
||||||
|
|
||||||
[SEARCH_ENGINES.GOOGLE]
|
[API_ENDPOINTS]
|
||||||
API_KEY = ""
|
SEARXNG = "http://localhost:32768" # SearxNG API URL
|
||||||
CSE_ID = ""
|
|
||||||
|
|
||||||
[SEARCH_ENGINES.SEARXNG]
|
|
||||||
ENDPOINT = ""
|
|
||||||
|
|
||||||
[SEARCH_ENGINES.BING]
|
|
||||||
SUBSCRIPTION_KEY = ""
|
|
||||||
|
|
||||||
[SEARCH_ENGINES.BRAVE]
|
|
||||||
API_KEY = ""
|
|
||||||
|
|
||||||
[SEARCH_ENGINES.YACY]
|
|
||||||
ENDPOINT = ""
|
|
@@ -15,5 +15,3 @@ server:
|
|||||||
engines:
|
engines:
|
||||||
- name: wolframalpha
|
- name: wolframalpha
|
||||||
disabled: false
|
disabled: false
|
||||||
- name: qwant
|
|
||||||
disabled: true
|
|
||||||
|
@@ -7,12 +7,7 @@ import { PromptTemplate } from '@langchain/core/prompts';
|
|||||||
import formatChatHistoryAsString from '../utils/formatHistory';
|
import formatChatHistoryAsString from '../utils/formatHistory';
|
||||||
import { BaseMessage } from '@langchain/core/messages';
|
import { BaseMessage } from '@langchain/core/messages';
|
||||||
import { StringOutputParser } from '@langchain/core/output_parsers';
|
import { StringOutputParser } from '@langchain/core/output_parsers';
|
||||||
import { searchSearxng } from '../lib/searchEngines/searxng';
|
import { searchSearxng } from '../lib/searxng';
|
||||||
import { searchGooglePSE } from '../lib/searchEngines/google_pse';
|
|
||||||
import { searchBraveAPI } from '../lib/searchEngines/brave';
|
|
||||||
import { searchYaCy } from '../lib/searchEngines/yacy';
|
|
||||||
import { searchBingAPI } from '../lib/searchEngines/bing';
|
|
||||||
import { getImageSearchEngineBackend } from '../config';
|
|
||||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
|
||||||
const imageSearchChainPrompt = `
|
const imageSearchChainPrompt = `
|
||||||
@@ -41,103 +36,6 @@ type ImageSearchChainInput = {
|
|||||||
query: string;
|
query: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function performImageSearch(query: string) {
|
|
||||||
const searchEngine = getImageSearchEngineBackend();
|
|
||||||
let images = [];
|
|
||||||
|
|
||||||
switch (searchEngine) {
|
|
||||||
case 'google': {
|
|
||||||
const googleResult = await searchGooglePSE(query);
|
|
||||||
images = googleResult.results
|
|
||||||
.map((result) => {
|
|
||||||
if (result.img_src && result.url && result.title) {
|
|
||||||
return {
|
|
||||||
img_src: result.img_src,
|
|
||||||
url: result.url,
|
|
||||||
title: result.title,
|
|
||||||
source: result.displayLink,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'searxng': {
|
|
||||||
const searxResult = await searchSearxng(query, {
|
|
||||||
engines: ['google images', 'bing images'],
|
|
||||||
pageno: 1,
|
|
||||||
});
|
|
||||||
searxResult.results.forEach((result) => {
|
|
||||||
if (result.img_src && result.url && result.title) {
|
|
||||||
images.push({
|
|
||||||
img_src: result.img_src,
|
|
||||||
url: result.url,
|
|
||||||
title: result.title,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'brave': {
|
|
||||||
const braveResult = await searchBraveAPI(query);
|
|
||||||
images = braveResult.results
|
|
||||||
.map((result) => {
|
|
||||||
if (result.img_src && result.url && result.title) {
|
|
||||||
return {
|
|
||||||
img_src: result.img_src,
|
|
||||||
url: result.url,
|
|
||||||
title: result.title,
|
|
||||||
source: result.url,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'yacy': {
|
|
||||||
const yacyResult = await searchYaCy(query);
|
|
||||||
images = yacyResult.results
|
|
||||||
.map((result) => {
|
|
||||||
if (result.img_src && result.url && result.title) {
|
|
||||||
return {
|
|
||||||
img_src: result.img_src,
|
|
||||||
url: result.url,
|
|
||||||
title: result.title,
|
|
||||||
source: result.url,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'bing': {
|
|
||||||
const bingResult = await searchBingAPI(query);
|
|
||||||
images = bingResult.results
|
|
||||||
.map((result) => {
|
|
||||||
if (result.img_src && result.url && result.title) {
|
|
||||||
return {
|
|
||||||
img_src: result.img_src,
|
|
||||||
url: result.url,
|
|
||||||
title: result.title,
|
|
||||||
source: result.url,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown search engine ${searchEngine}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return images;
|
|
||||||
}
|
|
||||||
|
|
||||||
const strParser = new StringOutputParser();
|
const strParser = new StringOutputParser();
|
||||||
|
|
||||||
const createImageSearchChain = (llm: BaseChatModel) => {
|
const createImageSearchChain = (llm: BaseChatModel) => {
|
||||||
@@ -154,7 +52,22 @@ const createImageSearchChain = (llm: BaseChatModel) => {
|
|||||||
llm,
|
llm,
|
||||||
strParser,
|
strParser,
|
||||||
RunnableLambda.from(async (input: string) => {
|
RunnableLambda.from(async (input: string) => {
|
||||||
const images = await performImageSearch(input);
|
const res = await searchSearxng(input, {
|
||||||
|
engines: ['bing images', 'google images'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const images = [];
|
||||||
|
|
||||||
|
res.results.forEach((result) => {
|
||||||
|
if (result.img_src && result.url && result.title) {
|
||||||
|
images.push({
|
||||||
|
img_src: result.img_src,
|
||||||
|
url: result.url,
|
||||||
|
title: result.title,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return images.slice(0, 10);
|
return images.slice(0, 10);
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
@@ -7,11 +7,7 @@ import { PromptTemplate } from '@langchain/core/prompts';
|
|||||||
import formatChatHistoryAsString from '../utils/formatHistory';
|
import formatChatHistoryAsString from '../utils/formatHistory';
|
||||||
import { BaseMessage } from '@langchain/core/messages';
|
import { BaseMessage } from '@langchain/core/messages';
|
||||||
import { StringOutputParser } from '@langchain/core/output_parsers';
|
import { StringOutputParser } from '@langchain/core/output_parsers';
|
||||||
import { searchSearxng } from '../lib/searchEngines/searxng';
|
import { searchSearxng } from '../lib/searxng';
|
||||||
import { searchGooglePSE } from '../lib/searchEngines/google_pse';
|
|
||||||
import { searchBraveAPI } from '../lib/searchEngines/brave';
|
|
||||||
import { searchBingAPI } from '../lib/searchEngines/bing';
|
|
||||||
import { getVideoSearchEngineBackend } from '../config';
|
|
||||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
|
||||||
const VideoSearchChainPrompt = `
|
const VideoSearchChainPrompt = `
|
||||||
@@ -42,102 +38,6 @@ type VideoSearchChainInput = {
|
|||||||
|
|
||||||
const strParser = new StringOutputParser();
|
const strParser = new StringOutputParser();
|
||||||
|
|
||||||
async function performVideoSearch(query: string) {
|
|
||||||
const searchEngine = getVideoSearchEngineBackend();
|
|
||||||
const youtubeQuery = `${query} site:youtube.com`;
|
|
||||||
let videos = [];
|
|
||||||
|
|
||||||
switch (searchEngine) {
|
|
||||||
case 'google': {
|
|
||||||
const googleResult = await searchGooglePSE(youtubeQuery);
|
|
||||||
googleResult.results.forEach((result) => {
|
|
||||||
// Use .results instead of .originalres
|
|
||||||
if (result.img_src && result.url && result.title) {
|
|
||||||
const videoId = new URL(result.url).searchParams.get('v');
|
|
||||||
videos.push({
|
|
||||||
img_src: result.img_src,
|
|
||||||
url: result.url,
|
|
||||||
title: result.title,
|
|
||||||
iframe_src: videoId
|
|
||||||
? `https://www.youtube.com/embed/${videoId}`
|
|
||||||
: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'searxng': {
|
|
||||||
const searxResult = await searchSearxng(query, {
|
|
||||||
engines: ['youtube'],
|
|
||||||
});
|
|
||||||
searxResult.results.forEach((result) => {
|
|
||||||
if (
|
|
||||||
result.thumbnail &&
|
|
||||||
result.url &&
|
|
||||||
result.title &&
|
|
||||||
result.iframe_src
|
|
||||||
) {
|
|
||||||
videos.push({
|
|
||||||
img_src: result.thumbnail,
|
|
||||||
url: result.url,
|
|
||||||
title: result.title,
|
|
||||||
iframe_src: result.iframe_src,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'brave': {
|
|
||||||
const braveResult = await searchBraveAPI(youtubeQuery);
|
|
||||||
braveResult.results.forEach((result) => {
|
|
||||||
if (result.img_src && result.url && result.title) {
|
|
||||||
const videoId = new URL(result.url).searchParams.get('v');
|
|
||||||
videos.push({
|
|
||||||
img_src: result.img_src,
|
|
||||||
url: result.url,
|
|
||||||
title: result.title,
|
|
||||||
iframe_src: videoId
|
|
||||||
? `https://www.youtube.com/embed/${videoId}`
|
|
||||||
: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'yacy': {
|
|
||||||
console.log('Not available for yacy');
|
|
||||||
videos = [];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'bing': {
|
|
||||||
const bingResult = await searchBingAPI(youtubeQuery);
|
|
||||||
bingResult.results.forEach((result) => {
|
|
||||||
if (result.img_src && result.url && result.title) {
|
|
||||||
const videoId = new URL(result.url).searchParams.get('v');
|
|
||||||
videos.push({
|
|
||||||
img_src: result.img_src,
|
|
||||||
url: result.url,
|
|
||||||
title: result.title,
|
|
||||||
iframe_src: videoId
|
|
||||||
? `https://www.youtube.com/embed/${videoId}`
|
|
||||||
: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown search engine ${searchEngine}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return videos;
|
|
||||||
}
|
|
||||||
|
|
||||||
const createVideoSearchChain = (llm: BaseChatModel) => {
|
const createVideoSearchChain = (llm: BaseChatModel) => {
|
||||||
return RunnableSequence.from([
|
return RunnableSequence.from([
|
||||||
RunnableMap.from({
|
RunnableMap.from({
|
||||||
@@ -152,7 +52,28 @@ const createVideoSearchChain = (llm: BaseChatModel) => {
|
|||||||
llm,
|
llm,
|
||||||
strParser,
|
strParser,
|
||||||
RunnableLambda.from(async (input: string) => {
|
RunnableLambda.from(async (input: string) => {
|
||||||
const videos = await performVideoSearch(input);
|
const res = await searchSearxng(input, {
|
||||||
|
engines: ['youtube'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const videos = [];
|
||||||
|
|
||||||
|
res.results.forEach((result) => {
|
||||||
|
if (
|
||||||
|
result.thumbnail &&
|
||||||
|
result.url &&
|
||||||
|
result.title &&
|
||||||
|
result.iframe_src
|
||||||
|
) {
|
||||||
|
videos.push({
|
||||||
|
img_src: result.thumbnail,
|
||||||
|
url: result.url,
|
||||||
|
title: result.title,
|
||||||
|
iframe_src: result.iframe_src,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return videos.slice(0, 10);
|
return videos.slice(0, 10);
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
@@ -10,12 +10,6 @@ interface Config {
|
|||||||
SIMILARITY_MEASURE: string;
|
SIMILARITY_MEASURE: string;
|
||||||
KEEP_ALIVE: string;
|
KEEP_ALIVE: string;
|
||||||
};
|
};
|
||||||
SEARCH_ENGINE_BACKENDS: {
|
|
||||||
SEARCH: string;
|
|
||||||
IMAGE: string;
|
|
||||||
VIDEO: string;
|
|
||||||
NEWS: string;
|
|
||||||
};
|
|
||||||
MODELS: {
|
MODELS: {
|
||||||
OPENAI: {
|
OPENAI: {
|
||||||
API_KEY: string;
|
API_KEY: string;
|
||||||
@@ -38,23 +32,8 @@ interface Config {
|
|||||||
MODEL_NAME: string;
|
MODEL_NAME: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
SEARCH_ENGINES: {
|
API_ENDPOINTS: {
|
||||||
GOOGLE: {
|
SEARXNG: string;
|
||||||
API_KEY: string;
|
|
||||||
CSE_ID: string;
|
|
||||||
};
|
|
||||||
SEARXNG: {
|
|
||||||
ENDPOINT: string;
|
|
||||||
};
|
|
||||||
BING: {
|
|
||||||
SUBSCRIPTION_KEY: string;
|
|
||||||
};
|
|
||||||
BRAVE: {
|
|
||||||
API_KEY: string;
|
|
||||||
};
|
|
||||||
YACY: {
|
|
||||||
ENDPOINT: string;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,32 +61,8 @@ export const getAnthropicApiKey = () => loadConfig().MODELS.ANTHROPIC.API_KEY;
|
|||||||
|
|
||||||
export const getGeminiApiKey = () => loadConfig().MODELS.GEMINI.API_KEY;
|
export const getGeminiApiKey = () => loadConfig().MODELS.GEMINI.API_KEY;
|
||||||
|
|
||||||
export const getSearchEngineBackend = () =>
|
|
||||||
loadConfig().SEARCH_ENGINE_BACKENDS.SEARCH;
|
|
||||||
|
|
||||||
export const getImageSearchEngineBackend = () =>
|
|
||||||
loadConfig().SEARCH_ENGINE_BACKENDS.IMAGE || getSearchEngineBackend();
|
|
||||||
|
|
||||||
export const getVideoSearchEngineBackend = () =>
|
|
||||||
loadConfig().SEARCH_ENGINE_BACKENDS.VIDEO || getSearchEngineBackend();
|
|
||||||
|
|
||||||
export const getNewsSearchEngineBackend = () =>
|
|
||||||
loadConfig().SEARCH_ENGINE_BACKENDS.NEWS || getSearchEngineBackend();
|
|
||||||
|
|
||||||
export const getGoogleApiKey = () => loadConfig().SEARCH_ENGINES.GOOGLE.API_KEY;
|
|
||||||
|
|
||||||
export const getGoogleCseId = () => loadConfig().SEARCH_ENGINES.GOOGLE.CSE_ID;
|
|
||||||
|
|
||||||
export const getBraveApiKey = () => loadConfig().SEARCH_ENGINES.BRAVE.API_KEY;
|
|
||||||
|
|
||||||
export const getBingSubscriptionKey = () =>
|
|
||||||
loadConfig().SEARCH_ENGINES.BING.SUBSCRIPTION_KEY;
|
|
||||||
|
|
||||||
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().API_ENDPOINTS.SEARXNG;
|
||||||
|
|
||||||
export const getOllamaApiEndpoint = () => loadConfig().MODELS.OLLAMA.API_URL;
|
export const getOllamaApiEndpoint = () => loadConfig().MODELS.OLLAMA.API_URL;
|
||||||
|
|
||||||
|
@@ -1,105 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import { getBingSubscriptionKey } from '../../config';
|
|
||||||
|
|
||||||
interface BingAPISearchResult {
|
|
||||||
_type: string;
|
|
||||||
name: string;
|
|
||||||
url: string;
|
|
||||||
displayUrl: string;
|
|
||||||
snippet?: string;
|
|
||||||
dateLastCrawled?: string;
|
|
||||||
thumbnailUrl?: string;
|
|
||||||
contentUrl?: string;
|
|
||||||
hostPageUrl?: string;
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
accentColor?: string;
|
|
||||||
contentSize?: string;
|
|
||||||
datePublished?: string;
|
|
||||||
encodingFormat?: string;
|
|
||||||
hostPageDisplayUrl?: string;
|
|
||||||
id?: string;
|
|
||||||
isLicensed?: boolean;
|
|
||||||
isFamilyFriendly?: boolean;
|
|
||||||
language?: string;
|
|
||||||
mediaUrl?: string;
|
|
||||||
motionThumbnailUrl?: string;
|
|
||||||
publisher?: string;
|
|
||||||
viewCount?: number;
|
|
||||||
webSearchUrl?: string;
|
|
||||||
primaryImageOfPage?: {
|
|
||||||
thumbnailUrl?: string;
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
};
|
|
||||||
video?: {
|
|
||||||
allowHttpsEmbed?: boolean;
|
|
||||||
embedHtml?: string;
|
|
||||||
allowMobileEmbed?: boolean;
|
|
||||||
viewCount?: number;
|
|
||||||
duration?: string;
|
|
||||||
};
|
|
||||||
image?: {
|
|
||||||
thumbnail?: {
|
|
||||||
contentUrl?: string;
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
};
|
|
||||||
imageInsightsToken?: string;
|
|
||||||
imageId?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const searchBingAPI = async (query: string) => {
|
|
||||||
try {
|
|
||||||
const bingApiKey = await getBingSubscriptionKey();
|
|
||||||
const url = new URL(`https://api.cognitive.microsoft.com/bing/v7.0/search`);
|
|
||||||
url.searchParams.append('q', query);
|
|
||||||
url.searchParams.append('responseFilter', 'Webpages,Images,Videos');
|
|
||||||
|
|
||||||
const res = await axios.get(url.toString(), {
|
|
||||||
headers: {
|
|
||||||
'Ocp-Apim-Subscription-Key': bingApiKey,
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.data.error) {
|
|
||||||
throw new Error(`Bing API Error: ${res.data.error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalres = res.data;
|
|
||||||
|
|
||||||
// Extract web, image, and video results
|
|
||||||
const webResults = originalres.webPages?.value || [];
|
|
||||||
const imageResults = originalres.images?.value || [];
|
|
||||||
const videoResults = originalres.videos?.value || [];
|
|
||||||
|
|
||||||
const results = webResults.map((item: BingAPISearchResult) => ({
|
|
||||||
title: item.name,
|
|
||||||
url: item.url,
|
|
||||||
content: item.snippet,
|
|
||||||
img_src:
|
|
||||||
item.primaryImageOfPage?.thumbnailUrl ||
|
|
||||||
imageResults.find((img: any) => img.hostPageUrl === item.url)
|
|
||||||
?.thumbnailUrl ||
|
|
||||||
videoResults.find((vid: any) => vid.hostPageUrl === item.url)
|
|
||||||
?.thumbnailUrl,
|
|
||||||
...(item.video && {
|
|
||||||
videoData: {
|
|
||||||
duration: item.video.duration,
|
|
||||||
embedUrl: item.video.embedHtml?.match(/src="(.*?)"/)?.[1],
|
|
||||||
},
|
|
||||||
publisher: item.publisher,
|
|
||||||
datePublished: item.datePublished,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return { results, originalres };
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error.response?.data
|
|
||||||
? JSON.stringify(error.response.data, null, 2)
|
|
||||||
: error.message || 'Unknown error';
|
|
||||||
throw new Error(`Bing API Error: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
};
|
|
@@ -1,102 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import { getBraveApiKey } from '../../config';
|
|
||||||
|
|
||||||
interface BraveSearchResult {
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
content?: string;
|
|
||||||
img_src?: string;
|
|
||||||
age?: string;
|
|
||||||
family_friendly?: boolean;
|
|
||||||
language?: string;
|
|
||||||
video?: {
|
|
||||||
embedUrl?: string;
|
|
||||||
duration?: string;
|
|
||||||
};
|
|
||||||
rating?: {
|
|
||||||
value: number;
|
|
||||||
scale: number;
|
|
||||||
};
|
|
||||||
products?: Array<{
|
|
||||||
name: string;
|
|
||||||
price?: string;
|
|
||||||
}>;
|
|
||||||
recipe?: {
|
|
||||||
ingredients?: string[];
|
|
||||||
cookTime?: string;
|
|
||||||
};
|
|
||||||
meta?: {
|
|
||||||
fetched?: string;
|
|
||||||
lastCrawled?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const searchBraveAPI = async (
|
|
||||||
query: string,
|
|
||||||
numResults: number = 20,
|
|
||||||
): Promise<{ results: BraveSearchResult[]; originalres: any }> => {
|
|
||||||
try {
|
|
||||||
const braveApiKey = await getBraveApiKey();
|
|
||||||
const url = new URL(`https://api.search.brave.com/res/v1/web/search`);
|
|
||||||
|
|
||||||
url.searchParams.append('q', query);
|
|
||||||
url.searchParams.append('count', numResults.toString());
|
|
||||||
|
|
||||||
const res = await axios.get(url.toString(), {
|
|
||||||
headers: {
|
|
||||||
'X-Subscription-Token': braveApiKey,
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.data.error) {
|
|
||||||
throw new Error(`Brave API Error: ${res.data.error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalres = res.data;
|
|
||||||
const webResults = originalres.web?.results || [];
|
|
||||||
|
|
||||||
const results: BraveSearchResult[] = webResults.map((item: any) => ({
|
|
||||||
title: item.title,
|
|
||||||
url: item.url,
|
|
||||||
content: item.description,
|
|
||||||
img_src: item.thumbnail?.src || item.deep_results?.images?.[0]?.src,
|
|
||||||
age: item.age,
|
|
||||||
family_friendly: item.family_friendly,
|
|
||||||
language: item.language,
|
|
||||||
video: item.video
|
|
||||||
? {
|
|
||||||
embedUrl: item.video.embed_url,
|
|
||||||
duration: item.video.duration,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
rating: item.rating
|
|
||||||
? {
|
|
||||||
value: item.rating.value,
|
|
||||||
scale: item.rating.scale_max,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
products: item.deep_results?.product_cluster?.map((p: any) => ({
|
|
||||||
name: p.name,
|
|
||||||
price: p.price,
|
|
||||||
})),
|
|
||||||
recipe: item.recipe
|
|
||||||
? {
|
|
||||||
ingredients: item.recipe.ingredients,
|
|
||||||
cookTime: item.recipe.cook_time,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
meta: {
|
|
||||||
fetched: item.meta?.fetched,
|
|
||||||
lastCrawled: item.meta?.last_crawled,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
return { results, originalres };
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error.response?.data
|
|
||||||
? JSON.stringify(error.response.data, null, 2)
|
|
||||||
: error.message || 'Unknown error';
|
|
||||||
throw new Error(`Brave API Error: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
};
|
|
@@ -1,85 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import { getGoogleApiKey, getGoogleCseId } from '../../config';
|
|
||||||
|
|
||||||
interface GooglePSESearchResult {
|
|
||||||
kind: string;
|
|
||||||
title: string;
|
|
||||||
htmlTitle: string;
|
|
||||||
link: string;
|
|
||||||
displayLink: string;
|
|
||||||
snippet?: string;
|
|
||||||
htmlSnippet?: string;
|
|
||||||
cacheId?: string;
|
|
||||||
formattedUrl: string;
|
|
||||||
htmlFormattedUrl: string;
|
|
||||||
pagemap?: {
|
|
||||||
videoobject: any;
|
|
||||||
cse_thumbnail?: Array<{
|
|
||||||
src: string;
|
|
||||||
width: string;
|
|
||||||
height: string;
|
|
||||||
}>;
|
|
||||||
metatags?: Array<{
|
|
||||||
[key: string]: string;
|
|
||||||
author?: string;
|
|
||||||
}>;
|
|
||||||
cse_image?: Array<{
|
|
||||||
src: string;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
fileFormat?: string;
|
|
||||||
image?: {
|
|
||||||
contextLink: string;
|
|
||||||
thumbnailLink: string;
|
|
||||||
};
|
|
||||||
mime?: string;
|
|
||||||
labels?: Array<{
|
|
||||||
name: string;
|
|
||||||
displayName: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const searchGooglePSE = async (query: string) => {
|
|
||||||
try {
|
|
||||||
const [googleApiKey, googleCseID] = await Promise.all([
|
|
||||||
getGoogleApiKey(),
|
|
||||||
getGoogleCseId(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const url = new URL(`https://www.googleapis.com/customsearch/v1`);
|
|
||||||
url.searchParams.append('q', query);
|
|
||||||
url.searchParams.append('cx', googleCseID);
|
|
||||||
url.searchParams.append('key', googleApiKey);
|
|
||||||
|
|
||||||
const res = await axios.get(url.toString());
|
|
||||||
|
|
||||||
if (res.data.error) {
|
|
||||||
throw new Error(`Google PSE Error: ${res.data.error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalres = res.data.items;
|
|
||||||
|
|
||||||
const results = originalres.map((item: GooglePSESearchResult) => ({
|
|
||||||
title: item.title,
|
|
||||||
url: item.link,
|
|
||||||
content: item.snippet,
|
|
||||||
img_src:
|
|
||||||
item.pagemap?.cse_image?.[0]?.src ||
|
|
||||||
item.pagemap?.cse_thumbnail?.[0]?.src ||
|
|
||||||
item.image?.thumbnailLink,
|
|
||||||
...(item.pagemap?.videoobject?.[0] && {
|
|
||||||
videoData: {
|
|
||||||
duration: item.pagemap.videoobject[0].duration,
|
|
||||||
embedUrl: item.pagemap.videoobject[0].embedurl,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return { results, originalres };
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error.response?.data
|
|
||||||
? JSON.stringify(error.response.data, null, 2)
|
|
||||||
: error.message || 'Unknown error';
|
|
||||||
throw new Error(`Google PSE Error: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
};
|
|
@@ -1,79 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import { getYacyJsonEndpoint } from '../../config';
|
|
||||||
|
|
||||||
interface YaCySearchResult {
|
|
||||||
channels: {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
link: string;
|
|
||||||
image: {
|
|
||||||
url: string;
|
|
||||||
title: string;
|
|
||||||
link: string;
|
|
||||||
};
|
|
||||||
startIndex: string;
|
|
||||||
itemsPerPage: string;
|
|
||||||
searchTerms: string;
|
|
||||||
items: {
|
|
||||||
title: string;
|
|
||||||
link: string;
|
|
||||||
code: string;
|
|
||||||
description: string;
|
|
||||||
pubDate: string;
|
|
||||||
image?: string;
|
|
||||||
size: string;
|
|
||||||
sizename: string;
|
|
||||||
guid: string;
|
|
||||||
faviconUrl: string;
|
|
||||||
host: string;
|
|
||||||
path: string;
|
|
||||||
file: string;
|
|
||||||
urlhash: string;
|
|
||||||
ranking: string;
|
|
||||||
}[];
|
|
||||||
navigation: {
|
|
||||||
facetname: string;
|
|
||||||
displayname: string;
|
|
||||||
type: string;
|
|
||||||
min: string;
|
|
||||||
max: string;
|
|
||||||
mean: string;
|
|
||||||
elements: {
|
|
||||||
name: string;
|
|
||||||
count: string;
|
|
||||||
modifier: string;
|
|
||||||
url: string;
|
|
||||||
}[];
|
|
||||||
}[];
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const searchYaCy = async (query: string, numResults: number = 20) => {
|
|
||||||
try {
|
|
||||||
const yacyBaseUrl = getYacyJsonEndpoint();
|
|
||||||
|
|
||||||
const url = new URL(`${yacyBaseUrl}/yacysearch.json`);
|
|
||||||
url.searchParams.append('query', query);
|
|
||||||
url.searchParams.append('count', numResults.toString());
|
|
||||||
|
|
||||||
const res = await axios.get(url.toString());
|
|
||||||
|
|
||||||
const originalres = res.data as YaCySearchResult;
|
|
||||||
|
|
||||||
const results = originalres.channels[0].items.map((item) => ({
|
|
||||||
title: item.title,
|
|
||||||
url: item.link,
|
|
||||||
content: item.description,
|
|
||||||
img_src: item.image || null,
|
|
||||||
pubDate: item.pubDate,
|
|
||||||
host: item.host,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return { results, originalres };
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error.response?.data
|
|
||||||
? JSON.stringify(error.response.data, null, 2)
|
|
||||||
: error.message || 'Unknown error';
|
|
||||||
throw new Error(`YaCy Error: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
};
|
|
@@ -1,5 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { getSearxngApiEndpoint } from '../../config';
|
import { getSearxngApiEndpoint } from '../config';
|
||||||
|
|
||||||
interface SearxngSearchOptions {
|
interface SearxngSearchOptions {
|
||||||
categories?: string[];
|
categories?: string[];
|
@@ -6,7 +6,11 @@ import {
|
|||||||
redditSearchResponsePrompt,
|
redditSearchResponsePrompt,
|
||||||
redditSearchRetrieverPrompt,
|
redditSearchRetrieverPrompt,
|
||||||
} from './redditSearch';
|
} from './redditSearch';
|
||||||
import { webSearchResponsePrompt, webSearchRetrieverPrompt } from './webSearch';
|
import {
|
||||||
|
webSearchResponsePrompt,
|
||||||
|
webSearchRetrieverPrompt,
|
||||||
|
preciseWebSearchResponsePrompt,
|
||||||
|
} from './webSearch';
|
||||||
import {
|
import {
|
||||||
wolframAlphaSearchResponsePrompt,
|
wolframAlphaSearchResponsePrompt,
|
||||||
wolframAlphaSearchRetrieverPrompt,
|
wolframAlphaSearchRetrieverPrompt,
|
||||||
@@ -20,6 +24,7 @@ import {
|
|||||||
export default {
|
export default {
|
||||||
webSearchResponsePrompt,
|
webSearchResponsePrompt,
|
||||||
webSearchRetrieverPrompt,
|
webSearchRetrieverPrompt,
|
||||||
|
preciseWebSearchResponsePrompt,
|
||||||
academicSearchResponsePrompt,
|
academicSearchResponsePrompt,
|
||||||
academicSearchRetrieverPrompt,
|
academicSearchRetrieverPrompt,
|
||||||
redditSearchResponsePrompt,
|
redditSearchResponsePrompt,
|
||||||
|
@@ -104,3 +104,41 @@ export const webSearchResponsePrompt = `
|
|||||||
|
|
||||||
Current date & time in ISO format (UTC timezone) is: {date}.
|
Current date & time in ISO format (UTC timezone) is: {date}.
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const preciseWebSearchResponsePrompt = `
|
||||||
|
You are Perplexica, an AI model skilled in web search and crafting accurate, concise, and well-structured answers. You excel at breaking down long form content into brief summaries or specific answers.
|
||||||
|
|
||||||
|
Your task is to provide answers that are:
|
||||||
|
- **Informative and relevant**: Precisely address the user's query using the given context.
|
||||||
|
- **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically.
|
||||||
|
- **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included.
|
||||||
|
- **Brief and Accurate**: If a direct answer is available, provide it succinctly without unnecessary elaboration.
|
||||||
|
|
||||||
|
### Formatting Instructions
|
||||||
|
- **Structure**: Use a well-organized format with proper headings. Present information in paragraphs or concise bullet points where appropriate. You should never need more than one heading.
|
||||||
|
- **Tone and Style**: Maintain a matter-of-fact tone and focus on delivering accurate information. Avoid overly complex language or unnecessary jargon.
|
||||||
|
- **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability.
|
||||||
|
- **Length and Depth**: Be brief. Provide concise answers. Avoid superficial responses and strive for accuracy without unnecessary repetition.
|
||||||
|
- **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title.
|
||||||
|
- **Conclusion or Summary**: Do not include a conclusion unless the context specifically requires it.
|
||||||
|
|
||||||
|
### Citation Requirements
|
||||||
|
- Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`.
|
||||||
|
- Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]."
|
||||||
|
- Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context.
|
||||||
|
- Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]."
|
||||||
|
- Always prioritize credibility and accuracy by linking all statements back to their respective context sources.
|
||||||
|
- Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation.
|
||||||
|
|
||||||
|
### Special Instructions
|
||||||
|
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
|
||||||
|
- If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query.
|
||||||
|
- Do not provide additional commentary or personal opinions unless specifically asked for in the context.
|
||||||
|
- Do not include plesantries or greetings in your response.
|
||||||
|
|
||||||
|
<context>
|
||||||
|
{context}
|
||||||
|
</context>
|
||||||
|
|
||||||
|
Current date & time in ISO format (UTC timezone) is: {date}.
|
||||||
|
`;
|
||||||
|
@@ -13,16 +13,6 @@ 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';
|
||||||
|
|
||||||
@@ -71,21 +61,6 @@ 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.' });
|
||||||
@@ -119,30 +94,6 @@ 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);
|
||||||
|
@@ -1,125 +1,42 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { searchSearxng } from '../lib/searchEngines/searxng';
|
import { searchSearxng } from '../lib/searxng';
|
||||||
import { searchGooglePSE } from '../lib/searchEngines/google_pse';
|
|
||||||
import { searchBraveAPI } from '../lib/searchEngines/brave';
|
|
||||||
import { searchYaCy } from '../lib/searchEngines/yacy';
|
|
||||||
import { searchBingAPI } from '../lib/searchEngines/bing';
|
|
||||||
import { getNewsSearchEngineBackend } from '../config';
|
|
||||||
import logger from '../utils/logger';
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
async function performSearch(query: string, site: string) {
|
|
||||||
const searchEngine = getNewsSearchEngineBackend();
|
|
||||||
switch (searchEngine) {
|
|
||||||
case 'google': {
|
|
||||||
const googleResult = await searchGooglePSE(query);
|
|
||||||
|
|
||||||
return googleResult.originalres.map((item) => {
|
|
||||||
const imageSources = [
|
|
||||||
item.pagemap?.cse_image?.[0]?.src,
|
|
||||||
item.pagemap?.cse_thumbnail?.[0]?.src,
|
|
||||||
item.pagemap?.metatags?.[0]?.['og:image'],
|
|
||||||
item.pagemap?.metatags?.[0]?.['twitter:image'],
|
|
||||||
item.pagemap?.metatags?.[0]?.['image'],
|
|
||||||
].filter(Boolean); // Remove undefined values
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: item.title,
|
|
||||||
url: item.link,
|
|
||||||
content: item.snippet,
|
|
||||||
thumbnail: imageSources[0], // First available image
|
|
||||||
img_src: imageSources[0], // Same as thumbnail for consistency
|
|
||||||
iframe_src: null,
|
|
||||||
author: item.pagemap?.metatags?.[0]?.['og:site_name'] || site,
|
|
||||||
publishedDate:
|
|
||||||
item.pagemap?.metatags?.[0]?.['article:published_time'],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'searxng': {
|
|
||||||
const searxResult = await searchSearxng(query, {
|
|
||||||
engines: ['bing news'],
|
|
||||||
pageno: 1,
|
|
||||||
});
|
|
||||||
return searxResult.results;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'brave': {
|
|
||||||
const braveResult = await searchBraveAPI(query);
|
|
||||||
return braveResult.results.map((item) => ({
|
|
||||||
title: item.title,
|
|
||||||
url: item.url,
|
|
||||||
content: item.content,
|
|
||||||
thumbnail: item.img_src,
|
|
||||||
img_src: item.img_src,
|
|
||||||
iframe_src: null,
|
|
||||||
author: item.meta?.fetched || site,
|
|
||||||
publishedDate: item.meta?.lastCrawled,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'yacy': {
|
|
||||||
const yacyResult = await searchYaCy(query);
|
|
||||||
return yacyResult.results.map((item) => ({
|
|
||||||
title: item.title,
|
|
||||||
url: item.url,
|
|
||||||
content: item.content,
|
|
||||||
thumbnail: item.img_src,
|
|
||||||
img_src: item.img_src,
|
|
||||||
iframe_src: null,
|
|
||||||
author: item?.host || site,
|
|
||||||
publishedDate: item?.pubDate,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'bing': {
|
|
||||||
const bingResult = await searchBingAPI(query);
|
|
||||||
return bingResult.results.map((item) => ({
|
|
||||||
title: item.title,
|
|
||||||
url: item.url,
|
|
||||||
content: item.content,
|
|
||||||
thumbnail: item.img_src,
|
|
||||||
img_src: item.img_src,
|
|
||||||
iframe_src: null,
|
|
||||||
author: item?.publisher || site,
|
|
||||||
publishedDate: item?.datePublished,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown search engine ${searchEngine}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const queries = [
|
|
||||||
{ site: 'businessinsider.com', topic: 'AI' },
|
|
||||||
{ site: 'www.exchangewire.com', topic: 'AI' },
|
|
||||||
{ site: 'yahoo.com', topic: 'AI' },
|
|
||||||
{ site: 'businessinsider.com', topic: 'tech' },
|
|
||||||
{ site: 'www.exchangewire.com', topic: 'tech' },
|
|
||||||
{ site: 'yahoo.com', topic: 'tech' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const data = (
|
const data = (
|
||||||
await Promise.all(
|
await Promise.all([
|
||||||
queries.map(async ({ site, topic }) => {
|
searchSearxng('site:businessinsider.com AI', {
|
||||||
try {
|
engines: ['bing news'],
|
||||||
const query = `site:${site} ${topic}`;
|
pageno: 1,
|
||||||
return await performSearch(query, site);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error searching ${site}: ${error.message}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
|
searchSearxng('site:www.exchangewire.com AI', {
|
||||||
|
engines: ['bing news'],
|
||||||
|
pageno: 1,
|
||||||
|
}),
|
||||||
|
searchSearxng('site:yahoo.com AI', {
|
||||||
|
engines: ['bing news'],
|
||||||
|
pageno: 1,
|
||||||
|
}),
|
||||||
|
searchSearxng('site:businessinsider.com tech', {
|
||||||
|
engines: ['bing news'],
|
||||||
|
pageno: 1,
|
||||||
|
}),
|
||||||
|
searchSearxng('site:www.exchangewire.com tech', {
|
||||||
|
engines: ['bing news'],
|
||||||
|
pageno: 1,
|
||||||
|
}),
|
||||||
|
searchSearxng('site:yahoo.com tech', {
|
||||||
|
engines: ['bing news'],
|
||||||
|
pageno: 1,
|
||||||
|
}),
|
||||||
|
])
|
||||||
)
|
)
|
||||||
)
|
.map((result) => result.results)
|
||||||
.flat()
|
.flat()
|
||||||
.sort(() => Math.random() - 0.5)
|
.sort(() => Math.random() - 0.5);
|
||||||
.filter((item) => item.title && item.url && item.content);
|
|
||||||
|
|
||||||
return res.json({ blogs: data });
|
return res.json({ blogs: data });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
@@ -5,6 +5,7 @@ import { getAvailableChatModelProviders } from '../lib/providers';
|
|||||||
import { HumanMessage, AIMessage } from '@langchain/core/messages';
|
import { HumanMessage, AIMessage } from '@langchain/core/messages';
|
||||||
import logger from '../utils/logger';
|
import logger from '../utils/logger';
|
||||||
import { ChatOpenAI } from '@langchain/openai';
|
import { ChatOpenAI } from '@langchain/openai';
|
||||||
|
import { ChatOllama } from '@langchain/community/chat_models/ollama';
|
||||||
import {
|
import {
|
||||||
getCustomOpenaiApiKey,
|
getCustomOpenaiApiKey,
|
||||||
getCustomOpenaiApiUrl,
|
getCustomOpenaiApiUrl,
|
||||||
@@ -16,6 +17,7 @@ const router = express.Router();
|
|||||||
interface ChatModel {
|
interface ChatModel {
|
||||||
provider: string;
|
provider: string;
|
||||||
model: string;
|
model: string;
|
||||||
|
ollamaContextWindow?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImageSearchBody {
|
interface ImageSearchBody {
|
||||||
@@ -61,6 +63,10 @@ router.post('/', async (req, res) => {
|
|||||||
) {
|
) {
|
||||||
llm = chatModelProviders[chatModelProvider][chatModel]
|
llm = chatModelProviders[chatModelProvider][chatModel]
|
||||||
.model as unknown as BaseChatModel | undefined;
|
.model as unknown as BaseChatModel | undefined;
|
||||||
|
|
||||||
|
if (llm instanceof ChatOllama) {
|
||||||
|
llm.numCtx = body.chatModel?.ollamaContextWindow || 2048;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!llm) {
|
if (!llm) {
|
||||||
|
@@ -15,12 +15,14 @@ import {
|
|||||||
getCustomOpenaiApiUrl,
|
getCustomOpenaiApiUrl,
|
||||||
getCustomOpenaiModelName,
|
getCustomOpenaiModelName,
|
||||||
} from '../config';
|
} from '../config';
|
||||||
|
import { ChatOllama } from '@langchain/community/chat_models/ollama';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
interface chatModel {
|
interface chatModel {
|
||||||
provider: string;
|
provider: string;
|
||||||
model: string;
|
model: string;
|
||||||
|
ollamaContextWindow?: number;
|
||||||
customOpenAIKey?: string;
|
customOpenAIKey?: string;
|
||||||
customOpenAIBaseURL?: string;
|
customOpenAIBaseURL?: string;
|
||||||
}
|
}
|
||||||
@@ -78,6 +80,7 @@ router.post('/', async (req, res) => {
|
|||||||
const embeddingModel =
|
const embeddingModel =
|
||||||
body.embeddingModel?.model ||
|
body.embeddingModel?.model ||
|
||||||
Object.keys(embeddingModelProviders[embeddingModelProvider])[0];
|
Object.keys(embeddingModelProviders[embeddingModelProvider])[0];
|
||||||
|
const ollamaContextWindow = body.chatModel?.ollamaContextWindow || 2048;
|
||||||
|
|
||||||
let llm: BaseChatModel | undefined;
|
let llm: BaseChatModel | undefined;
|
||||||
let embeddings: Embeddings | undefined;
|
let embeddings: Embeddings | undefined;
|
||||||
@@ -99,6 +102,9 @@ router.post('/', async (req, res) => {
|
|||||||
) {
|
) {
|
||||||
llm = chatModelProviders[chatModelProvider][chatModel]
|
llm = chatModelProviders[chatModelProvider][chatModel]
|
||||||
.model as unknown as BaseChatModel | undefined;
|
.model as unknown as BaseChatModel | undefined;
|
||||||
|
if (llm instanceof ChatOllama) {
|
||||||
|
llm.numCtx = ollamaContextWindow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@@ -10,12 +10,14 @@ import {
|
|||||||
getCustomOpenaiApiUrl,
|
getCustomOpenaiApiUrl,
|
||||||
getCustomOpenaiModelName,
|
getCustomOpenaiModelName,
|
||||||
} from '../config';
|
} from '../config';
|
||||||
|
import { ChatOllama } from '@langchain/community/chat_models/ollama';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
interface ChatModel {
|
interface ChatModel {
|
||||||
provider: string;
|
provider: string;
|
||||||
model: string;
|
model: string;
|
||||||
|
ollamaContextWindow?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SuggestionsBody {
|
interface SuggestionsBody {
|
||||||
@@ -60,6 +62,9 @@ router.post('/', async (req, res) => {
|
|||||||
) {
|
) {
|
||||||
llm = chatModelProviders[chatModelProvider][chatModel]
|
llm = chatModelProviders[chatModelProvider][chatModel]
|
||||||
.model as unknown as BaseChatModel | undefined;
|
.model as unknown as BaseChatModel | undefined;
|
||||||
|
if (llm instanceof ChatOllama) {
|
||||||
|
llm.numCtx = body.chatModel?.ollamaContextWindow || 2048;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!llm) {
|
if (!llm) {
|
||||||
|
@@ -10,12 +10,14 @@ import {
|
|||||||
getCustomOpenaiApiUrl,
|
getCustomOpenaiApiUrl,
|
||||||
getCustomOpenaiModelName,
|
getCustomOpenaiModelName,
|
||||||
} from '../config';
|
} from '../config';
|
||||||
|
import { ChatOllama } from '@langchain/community/chat_models/ollama';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
interface ChatModel {
|
interface ChatModel {
|
||||||
provider: string;
|
provider: string;
|
||||||
model: string;
|
model: string;
|
||||||
|
ollamaContextWindow?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VideoSearchBody {
|
interface VideoSearchBody {
|
||||||
@@ -61,6 +63,10 @@ router.post('/', async (req, res) => {
|
|||||||
) {
|
) {
|
||||||
llm = chatModelProviders[chatModelProvider][chatModel]
|
llm = chatModelProviders[chatModelProvider][chatModel]
|
||||||
.model as unknown as BaseChatModel | undefined;
|
.model as unknown as BaseChatModel | undefined;
|
||||||
|
|
||||||
|
if (llm instanceof ChatOllama) {
|
||||||
|
llm.numCtx = body.chatModel?.ollamaContextWindow || 2048;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!llm) {
|
if (!llm) {
|
||||||
|
@@ -17,12 +17,7 @@ import LineListOutputParser from '../lib/outputParsers/listLineOutputParser';
|
|||||||
import LineOutputParser from '../lib/outputParsers/lineOutputParser';
|
import LineOutputParser from '../lib/outputParsers/lineOutputParser';
|
||||||
import { getDocumentsFromLinks } from '../utils/documents';
|
import { getDocumentsFromLinks } from '../utils/documents';
|
||||||
import { Document } from 'langchain/document';
|
import { Document } from 'langchain/document';
|
||||||
import { searchSearxng } from '../lib/searchEngines/searxng';
|
import { searchSearxng } from '../lib/searxng';
|
||||||
import { searchGooglePSE } from '../lib/searchEngines/google_pse';
|
|
||||||
import { searchBingAPI } from '../lib/searchEngines/bing';
|
|
||||||
import { searchBraveAPI } from '../lib/searchEngines/brave';
|
|
||||||
import { searchYaCy } from '../lib/searchEngines/yacy';
|
|
||||||
import { getSearchEngineBackend } from '../config';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import computeSimilarity from '../utils/computeSimilarity';
|
import computeSimilarity from '../utils/computeSimilarity';
|
||||||
@@ -39,6 +34,7 @@ export interface MetaSearchAgentType {
|
|||||||
embeddings: Embeddings,
|
embeddings: Embeddings,
|
||||||
optimizationMode: 'speed' | 'balanced' | 'quality',
|
optimizationMode: 'speed' | 'balanced' | 'quality',
|
||||||
fileIds: string[],
|
fileIds: string[],
|
||||||
|
isCompact?: boolean,
|
||||||
) => Promise<eventEmitter>;
|
) => Promise<eventEmitter>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +45,7 @@ interface Config {
|
|||||||
rerankThreshold: number;
|
rerankThreshold: number;
|
||||||
queryGeneratorPrompt: string;
|
queryGeneratorPrompt: string;
|
||||||
responsePrompt: string;
|
responsePrompt: string;
|
||||||
|
preciseResponsePrompt: string;
|
||||||
activeEngines: string[];
|
activeEngines: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,37 +205,10 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|||||||
|
|
||||||
return { query: question, docs: docs };
|
return { query: question, docs: docs };
|
||||||
} else {
|
} else {
|
||||||
const searchEngine = getSearchEngineBackend();
|
const res = await searchSearxng(question, {
|
||||||
|
|
||||||
let res;
|
|
||||||
switch (searchEngine) {
|
|
||||||
case 'searxng':
|
|
||||||
res = await searchSearxng(question, {
|
|
||||||
language: 'en',
|
language: 'en',
|
||||||
engines: this.config.activeEngines,
|
engines: this.config.activeEngines,
|
||||||
});
|
});
|
||||||
break;
|
|
||||||
case 'google':
|
|
||||||
res = await searchGooglePSE(question);
|
|
||||||
break;
|
|
||||||
case 'bing':
|
|
||||||
res = await searchBingAPI(question);
|
|
||||||
break;
|
|
||||||
case 'brave':
|
|
||||||
res = await searchBraveAPI(question);
|
|
||||||
break;
|
|
||||||
case 'yacy':
|
|
||||||
res = await searchYaCy(question);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown search engine ${searchEngine}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!res?.results) {
|
|
||||||
throw new Error(
|
|
||||||
`No results found for search engine: ${searchEngine}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const documents = res.results.map(
|
const documents = res.results.map(
|
||||||
(result) =>
|
(result) =>
|
||||||
@@ -267,6 +237,7 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|||||||
fileIds: string[],
|
fileIds: string[],
|
||||||
embeddings: Embeddings,
|
embeddings: Embeddings,
|
||||||
optimizationMode: 'speed' | 'balanced' | 'quality',
|
optimizationMode: 'speed' | 'balanced' | 'quality',
|
||||||
|
isCompact?: boolean,
|
||||||
) {
|
) {
|
||||||
return RunnableSequence.from([
|
return RunnableSequence.from([
|
||||||
RunnableMap.from({
|
RunnableMap.from({
|
||||||
@@ -310,7 +281,12 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|||||||
.pipe(this.processDocs),
|
.pipe(this.processDocs),
|
||||||
}),
|
}),
|
||||||
ChatPromptTemplate.fromMessages([
|
ChatPromptTemplate.fromMessages([
|
||||||
['system', this.config.responsePrompt],
|
[
|
||||||
|
'system',
|
||||||
|
isCompact
|
||||||
|
? this.config.preciseResponsePrompt
|
||||||
|
: this.config.responsePrompt,
|
||||||
|
],
|
||||||
new MessagesPlaceholder('chat_history'),
|
new MessagesPlaceholder('chat_history'),
|
||||||
['user', '{query}'],
|
['user', '{query}'],
|
||||||
]),
|
]),
|
||||||
@@ -497,6 +473,7 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|||||||
embeddings: Embeddings,
|
embeddings: Embeddings,
|
||||||
optimizationMode: 'speed' | 'balanced' | 'quality',
|
optimizationMode: 'speed' | 'balanced' | 'quality',
|
||||||
fileIds: string[],
|
fileIds: string[],
|
||||||
|
isCompact?: boolean,
|
||||||
) {
|
) {
|
||||||
const emitter = new eventEmitter();
|
const emitter = new eventEmitter();
|
||||||
|
|
||||||
@@ -505,6 +482,7 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|||||||
fileIds,
|
fileIds,
|
||||||
embeddings,
|
embeddings,
|
||||||
optimizationMode,
|
optimizationMode,
|
||||||
|
isCompact,
|
||||||
);
|
);
|
||||||
|
|
||||||
const stream = answeringChain.streamEvents(
|
const stream = answeringChain.streamEvents(
|
||||||
|
@@ -14,6 +14,7 @@ import {
|
|||||||
getCustomOpenaiApiUrl,
|
getCustomOpenaiApiUrl,
|
||||||
getCustomOpenaiModelName,
|
getCustomOpenaiModelName,
|
||||||
} from '../config';
|
} from '../config';
|
||||||
|
import { ChatOllama } from '@langchain/community/chat_models/ollama';
|
||||||
|
|
||||||
export const handleConnection = async (
|
export const handleConnection = async (
|
||||||
ws: WebSocket,
|
ws: WebSocket,
|
||||||
@@ -42,6 +43,8 @@ export const handleConnection = async (
|
|||||||
searchParams.get('embeddingModel') ||
|
searchParams.get('embeddingModel') ||
|
||||||
Object.keys(embeddingModelProviders[embeddingModelProvider])[0];
|
Object.keys(embeddingModelProviders[embeddingModelProvider])[0];
|
||||||
|
|
||||||
|
const ollamaContextWindow = searchParams.get('ollamaContextWindow');
|
||||||
|
|
||||||
let llm: BaseChatModel | undefined;
|
let llm: BaseChatModel | undefined;
|
||||||
let embeddings: Embeddings | undefined;
|
let embeddings: Embeddings | undefined;
|
||||||
|
|
||||||
@@ -52,6 +55,9 @@ export const handleConnection = async (
|
|||||||
) {
|
) {
|
||||||
llm = chatModelProviders[chatModelProvider][chatModel]
|
llm = chatModelProviders[chatModelProvider][chatModel]
|
||||||
.model as unknown as BaseChatModel | undefined;
|
.model as unknown as BaseChatModel | undefined;
|
||||||
|
if (llm instanceof ChatOllama) {
|
||||||
|
llm.numCtx = ollamaContextWindow ? parseInt(ollamaContextWindow) : 2048;
|
||||||
|
}
|
||||||
} else if (chatModelProvider == 'custom_openai') {
|
} else if (chatModelProvider == 'custom_openai') {
|
||||||
const customOpenaiApiKey = getCustomOpenaiApiKey();
|
const customOpenaiApiKey = getCustomOpenaiApiKey();
|
||||||
const customOpenaiApiUrl = getCustomOpenaiApiUrl();
|
const customOpenaiApiUrl = getCustomOpenaiApiUrl();
|
||||||
|
@@ -26,6 +26,7 @@ type WSMessage = {
|
|||||||
focusMode: string;
|
focusMode: string;
|
||||||
history: Array<[string, string]>;
|
history: Array<[string, string]>;
|
||||||
files: Array<string>;
|
files: Array<string>;
|
||||||
|
isCompact?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const searchHandlers = {
|
export const searchHandlers = {
|
||||||
@@ -33,6 +34,7 @@ export const searchHandlers = {
|
|||||||
activeEngines: [],
|
activeEngines: [],
|
||||||
queryGeneratorPrompt: prompts.webSearchRetrieverPrompt,
|
queryGeneratorPrompt: prompts.webSearchRetrieverPrompt,
|
||||||
responsePrompt: prompts.webSearchResponsePrompt,
|
responsePrompt: prompts.webSearchResponsePrompt,
|
||||||
|
preciseResponsePrompt: prompts.preciseWebSearchResponsePrompt,
|
||||||
rerank: true,
|
rerank: true,
|
||||||
rerankThreshold: 0.3,
|
rerankThreshold: 0.3,
|
||||||
searchWeb: true,
|
searchWeb: true,
|
||||||
@@ -42,6 +44,7 @@ export const searchHandlers = {
|
|||||||
activeEngines: ['arxiv', 'google scholar', 'pubmed'],
|
activeEngines: ['arxiv', 'google scholar', 'pubmed'],
|
||||||
queryGeneratorPrompt: prompts.academicSearchRetrieverPrompt,
|
queryGeneratorPrompt: prompts.academicSearchRetrieverPrompt,
|
||||||
responsePrompt: prompts.academicSearchResponsePrompt,
|
responsePrompt: prompts.academicSearchResponsePrompt,
|
||||||
|
preciseResponsePrompt: prompts.preciseWebSearchResponsePrompt,
|
||||||
rerank: true,
|
rerank: true,
|
||||||
rerankThreshold: 0,
|
rerankThreshold: 0,
|
||||||
searchWeb: true,
|
searchWeb: true,
|
||||||
@@ -51,6 +54,7 @@ export const searchHandlers = {
|
|||||||
activeEngines: [],
|
activeEngines: [],
|
||||||
queryGeneratorPrompt: '',
|
queryGeneratorPrompt: '',
|
||||||
responsePrompt: prompts.writingAssistantPrompt,
|
responsePrompt: prompts.writingAssistantPrompt,
|
||||||
|
preciseResponsePrompt: prompts.preciseWebSearchResponsePrompt,
|
||||||
rerank: true,
|
rerank: true,
|
||||||
rerankThreshold: 0,
|
rerankThreshold: 0,
|
||||||
searchWeb: false,
|
searchWeb: false,
|
||||||
@@ -60,6 +64,7 @@ export const searchHandlers = {
|
|||||||
activeEngines: ['wolframalpha'],
|
activeEngines: ['wolframalpha'],
|
||||||
queryGeneratorPrompt: prompts.wolframAlphaSearchRetrieverPrompt,
|
queryGeneratorPrompt: prompts.wolframAlphaSearchRetrieverPrompt,
|
||||||
responsePrompt: prompts.wolframAlphaSearchResponsePrompt,
|
responsePrompt: prompts.wolframAlphaSearchResponsePrompt,
|
||||||
|
preciseResponsePrompt: prompts.preciseWebSearchResponsePrompt,
|
||||||
rerank: false,
|
rerank: false,
|
||||||
rerankThreshold: 0,
|
rerankThreshold: 0,
|
||||||
searchWeb: true,
|
searchWeb: true,
|
||||||
@@ -69,6 +74,7 @@ export const searchHandlers = {
|
|||||||
activeEngines: ['youtube'],
|
activeEngines: ['youtube'],
|
||||||
queryGeneratorPrompt: prompts.youtubeSearchRetrieverPrompt,
|
queryGeneratorPrompt: prompts.youtubeSearchRetrieverPrompt,
|
||||||
responsePrompt: prompts.youtubeSearchResponsePrompt,
|
responsePrompt: prompts.youtubeSearchResponsePrompt,
|
||||||
|
preciseResponsePrompt: prompts.preciseWebSearchResponsePrompt,
|
||||||
rerank: true,
|
rerank: true,
|
||||||
rerankThreshold: 0.3,
|
rerankThreshold: 0.3,
|
||||||
searchWeb: true,
|
searchWeb: true,
|
||||||
@@ -78,6 +84,7 @@ export const searchHandlers = {
|
|||||||
activeEngines: ['reddit'],
|
activeEngines: ['reddit'],
|
||||||
queryGeneratorPrompt: prompts.redditSearchRetrieverPrompt,
|
queryGeneratorPrompt: prompts.redditSearchRetrieverPrompt,
|
||||||
responsePrompt: prompts.redditSearchResponsePrompt,
|
responsePrompt: prompts.redditSearchResponsePrompt,
|
||||||
|
preciseResponsePrompt: prompts.preciseWebSearchResponsePrompt,
|
||||||
rerank: true,
|
rerank: true,
|
||||||
rerankThreshold: 0.3,
|
rerankThreshold: 0.3,
|
||||||
searchWeb: true,
|
searchWeb: true,
|
||||||
@@ -116,6 +123,7 @@ const handleEmitterEvents = (
|
|||||||
sources = parsedData.data;
|
sources = parsedData.data;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('end', () => {
|
emitter.on('end', () => {
|
||||||
ws.send(JSON.stringify({ type: 'messageEnd', messageId: messageId }));
|
ws.send(JSON.stringify({ type: 'messageEnd', messageId: messageId }));
|
||||||
|
|
||||||
@@ -132,6 +140,7 @@ const handleEmitterEvents = (
|
|||||||
})
|
})
|
||||||
.execute();
|
.execute();
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('error', (data) => {
|
emitter.on('error', (data) => {
|
||||||
const parsedData = JSON.parse(data);
|
const parsedData = JSON.parse(data);
|
||||||
ws.send(
|
ws.send(
|
||||||
@@ -197,6 +206,7 @@ export const handleMessage = async (
|
|||||||
embeddings,
|
embeddings,
|
||||||
parsedWSMessage.optimizationMode,
|
parsedWSMessage.optimizationMode,
|
||||||
parsedWSMessage.files,
|
parsedWSMessage.files,
|
||||||
|
parsedWSMessage.isCompact,
|
||||||
);
|
);
|
||||||
|
|
||||||
handleEmitterEvents(emitter, ws, aiMessageId, parsedMessage.chatId);
|
handleEmitterEvents(emitter, ws, aiMessageId, parsedMessage.chatId);
|
||||||
|
@@ -23,18 +23,7 @@ interface SettingsType {
|
|||||||
customOpenaiApiKey: string;
|
customOpenaiApiKey: string;
|
||||||
customOpenaiApiUrl: string;
|
customOpenaiApiUrl: string;
|
||||||
customOpenaiModelName: string;
|
customOpenaiModelName: string;
|
||||||
searchEngineBackends: {
|
ollamaContextWindow: number;
|
||||||
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> {
|
||||||
@@ -124,12 +113,11 @@ 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({
|
const [contextWindowSize, setContextWindowSize] = useState(2048);
|
||||||
search: '',
|
const [isCustomContextWindow, setIsCustomContextWindow] = useState(false);
|
||||||
image: '',
|
const predefinedContextSizes = [
|
||||||
video: '',
|
1024, 2048, 3072, 4096, 8192, 16384, 32768, 65536, 131072,
|
||||||
news: '',
|
];
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchConfig = async () => {
|
const fetchConfig = async () => {
|
||||||
@@ -141,17 +129,8 @@ const Page = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const data = (await res.json()) as SettingsType;
|
const data = (await res.json()) as SettingsType;
|
||||||
setConfig(data);
|
|
||||||
|
|
||||||
// Set search engine backends if they exist in the response
|
setConfig(data);
|
||||||
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(
|
||||||
@@ -199,6 +178,13 @@ const Page = () => {
|
|||||||
setAutomaticVideoSearch(
|
setAutomaticVideoSearch(
|
||||||
localStorage.getItem('autoVideoSearch') === 'true',
|
localStorage.getItem('autoVideoSearch') === 'true',
|
||||||
);
|
);
|
||||||
|
const storedContextWindow = parseInt(
|
||||||
|
localStorage.getItem('ollamaContextWindow') ?? '2048',
|
||||||
|
);
|
||||||
|
setContextWindowSize(storedContextWindow);
|
||||||
|
setIsCustomContextWindow(
|
||||||
|
!predefinedContextSizes.includes(storedContextWindow),
|
||||||
|
);
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
@@ -359,8 +345,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') {
|
} else if (key === 'ollamaContextWindow') {
|
||||||
localStorage.setItem('searchEngineBackends', value);
|
localStorage.setItem('ollamaContextWindow', value.toString());
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to save:', err);
|
console.error('Failed to save:', err);
|
||||||
@@ -578,6 +564,78 @@ const Page = () => {
|
|||||||
];
|
];
|
||||||
})()}
|
})()}
|
||||||
/>
|
/>
|
||||||
|
{selectedChatModelProvider === 'ollama' && (
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
|
Chat Context Window Size
|
||||||
|
</p>
|
||||||
|
<Select
|
||||||
|
value={
|
||||||
|
isCustomContextWindow
|
||||||
|
? 'custom'
|
||||||
|
: contextWindowSize.toString()
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value === 'custom') {
|
||||||
|
setIsCustomContextWindow(true);
|
||||||
|
} else {
|
||||||
|
setIsCustomContextWindow(false);
|
||||||
|
const numValue = parseInt(value);
|
||||||
|
setContextWindowSize(numValue);
|
||||||
|
setConfig((prev) => ({
|
||||||
|
...prev!,
|
||||||
|
ollamaContextWindow: numValue,
|
||||||
|
}));
|
||||||
|
saveConfig('ollamaContextWindow', numValue);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={[
|
||||||
|
...predefinedContextSizes.map((size) => ({
|
||||||
|
value: size.toString(),
|
||||||
|
label: `${size.toLocaleString()} tokens`,
|
||||||
|
})),
|
||||||
|
{ value: 'custom', label: 'Custom...' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{isCustomContextWindow && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={512}
|
||||||
|
value={contextWindowSize}
|
||||||
|
placeholder="Custom context window size (minimum 512)"
|
||||||
|
isSaving={savingStates['ollamaContextWindow']}
|
||||||
|
onChange={(e) => {
|
||||||
|
// Allow any value to be typed
|
||||||
|
const value =
|
||||||
|
parseInt(e.target.value) ||
|
||||||
|
contextWindowSize;
|
||||||
|
setContextWindowSize(value);
|
||||||
|
}}
|
||||||
|
onSave={(value) => {
|
||||||
|
// Validate only when saving
|
||||||
|
const numValue = Math.max(
|
||||||
|
512,
|
||||||
|
parseInt(value) || 2048,
|
||||||
|
);
|
||||||
|
setContextWindowSize(numValue);
|
||||||
|
setConfig((prev) => ({
|
||||||
|
...prev!,
|
||||||
|
ollamaContextWindow: numValue,
|
||||||
|
}));
|
||||||
|
saveConfig('ollamaContextWindow', numValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
|
||||||
|
{isCustomContextWindow
|
||||||
|
? 'Adjust the context window size for Ollama models (minimum 512 tokens)'
|
||||||
|
: 'Adjust the context window size for Ollama models'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -593,16 +651,12 @@ const Page = () => {
|
|||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Model name"
|
placeholder="Model name"
|
||||||
value={config.customOpenaiModelName}
|
defaultValue={config.customOpenaiModelName}
|
||||||
isSaving={savingStates['customOpenaiModelName']}
|
onChange={(e) =>
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
setConfig({
|
||||||
setConfig((prev) => ({
|
...config,
|
||||||
...prev!,
|
|
||||||
customOpenaiModelName: e.target.value,
|
customOpenaiModelName: e.target.value,
|
||||||
}));
|
})
|
||||||
}}
|
|
||||||
onSave={(value) =>
|
|
||||||
saveConfig('customOpenaiModelName', value)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -613,16 +667,12 @@ const Page = () => {
|
|||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Custom OpenAI API Key"
|
placeholder="Custom OpenAI API Key"
|
||||||
value={config.customOpenaiApiKey}
|
defaultValue={config.customOpenaiApiKey}
|
||||||
isSaving={savingStates['customOpenaiApiKey']}
|
onChange={(e) =>
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
setConfig({
|
||||||
setConfig((prev) => ({
|
...config,
|
||||||
...prev!,
|
|
||||||
customOpenaiApiKey: e.target.value,
|
customOpenaiApiKey: e.target.value,
|
||||||
}));
|
})
|
||||||
}}
|
|
||||||
onSave={(value) =>
|
|
||||||
saveConfig('customOpenaiApiKey', value)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -633,16 +683,12 @@ const Page = () => {
|
|||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Custom OpenAI Base URL"
|
placeholder="Custom OpenAI Base URL"
|
||||||
value={config.customOpenaiApiUrl}
|
defaultValue={config.customOpenaiApiUrl}
|
||||||
isSaving={savingStates['customOpenaiApiUrl']}
|
onChange={(e) =>
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
setConfig({
|
||||||
setConfig((prev) => ({
|
...config,
|
||||||
...prev!,
|
|
||||||
customOpenaiApiUrl: e.target.value,
|
customOpenaiApiUrl: e.target.value,
|
||||||
}));
|
})
|
||||||
}}
|
|
||||||
onSave={(value) =>
|
|
||||||
saveConfig('customOpenaiApiUrl', value)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -823,234 +869,6 @@ 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>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
@@ -16,9 +16,17 @@ const Chat = ({
|
|||||||
setFileIds,
|
setFileIds,
|
||||||
files,
|
files,
|
||||||
setFiles,
|
setFiles,
|
||||||
|
isCompact,
|
||||||
|
setIsCompact,
|
||||||
|
optimizationMode,
|
||||||
|
setOptimizationMode,
|
||||||
}: {
|
}: {
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
sendMessage: (message: string) => void;
|
sendMessage: (
|
||||||
|
message: string,
|
||||||
|
messageId?: string,
|
||||||
|
options?: { isCompact?: boolean },
|
||||||
|
) => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
messageAppeared: boolean;
|
messageAppeared: boolean;
|
||||||
rewrite: (messageId: string) => void;
|
rewrite: (messageId: string) => void;
|
||||||
@@ -26,6 +34,10 @@ const Chat = ({
|
|||||||
setFileIds: (fileIds: string[]) => void;
|
setFileIds: (fileIds: string[]) => void;
|
||||||
files: File[];
|
files: File[];
|
||||||
setFiles: (files: File[]) => void;
|
setFiles: (files: File[]) => void;
|
||||||
|
isCompact: boolean;
|
||||||
|
setIsCompact: (isCompact: boolean) => void;
|
||||||
|
optimizationMode: string;
|
||||||
|
setOptimizationMode: (mode: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [dividerWidth, setDividerWidth] = useState(0);
|
const [dividerWidth, setDividerWidth] = useState(0);
|
||||||
const dividerRef = useRef<HTMLDivElement | null>(null);
|
const dividerRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -71,6 +83,7 @@ const Chat = ({
|
|||||||
dividerRef={isLast ? dividerRef : undefined}
|
dividerRef={isLast ? dividerRef : undefined}
|
||||||
isLast={isLast}
|
isLast={isLast}
|
||||||
rewrite={rewrite}
|
rewrite={rewrite}
|
||||||
|
isCompact={isCompact}
|
||||||
sendMessage={sendMessage}
|
sendMessage={sendMessage}
|
||||||
/>
|
/>
|
||||||
{!isLast && msg.role === 'assistant' && (
|
{!isLast && msg.role === 'assistant' && (
|
||||||
@@ -83,7 +96,7 @@ const Chat = ({
|
|||||||
<div ref={messageEnd} className="h-0" />
|
<div ref={messageEnd} className="h-0" />
|
||||||
{dividerWidth > 0 && (
|
{dividerWidth > 0 && (
|
||||||
<div
|
<div
|
||||||
className="bottom-24 lg:bottom-10 fixed z-40"
|
className="bottom-24 lg:bottom-10 fixed"
|
||||||
style={{ width: dividerWidth }}
|
style={{ width: dividerWidth }}
|
||||||
>
|
>
|
||||||
<MessageInput
|
<MessageInput
|
||||||
@@ -93,6 +106,10 @@ const Chat = ({
|
|||||||
setFileIds={setFileIds}
|
setFileIds={setFileIds}
|
||||||
files={files}
|
files={files}
|
||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
|
isCompact={isCompact}
|
||||||
|
setIsCompact={setIsCompact}
|
||||||
|
optimizationMode={optimizationMode}
|
||||||
|
setOptimizationMode={setOptimizationMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@@ -197,6 +197,11 @@ const useSocket = (
|
|||||||
'openAIBaseURL',
|
'openAIBaseURL',
|
||||||
localStorage.getItem('openAIBaseURL')!,
|
localStorage.getItem('openAIBaseURL')!,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
searchParams.append(
|
||||||
|
'ollamaContextWindow',
|
||||||
|
localStorage.getItem('ollamaContextWindow') || '2048',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
searchParams.append('embeddingModel', embeddingModel!);
|
searchParams.append('embeddingModel', embeddingModel!);
|
||||||
@@ -394,6 +399,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
|
|
||||||
const [focusMode, setFocusMode] = useState('webSearch');
|
const [focusMode, setFocusMode] = useState('webSearch');
|
||||||
const [optimizationMode, setOptimizationMode] = useState('speed');
|
const [optimizationMode, setOptimizationMode] = useState('speed');
|
||||||
|
const [isCompact, setIsCompact] = useState(false);
|
||||||
|
|
||||||
const [isMessagesLoaded, setIsMessagesLoaded] = useState(false);
|
const [isMessagesLoaded, setIsMessagesLoaded] = useState(false);
|
||||||
|
|
||||||
@@ -401,6 +407,21 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
|
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedCompactMode = localStorage.getItem('compactMode');
|
||||||
|
const savedOptimizationMode = localStorage.getItem('optimizationMode');
|
||||||
|
|
||||||
|
if (savedCompactMode !== null) {
|
||||||
|
setIsCompact(savedCompactMode === 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedOptimizationMode !== null) {
|
||||||
|
setOptimizationMode(savedOptimizationMode);
|
||||||
|
} else {
|
||||||
|
localStorage.setItem('optimizationMode', optimizationMode);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
chatId &&
|
chatId &&
|
||||||
@@ -451,7 +472,11 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
}
|
}
|
||||||
}, [isMessagesLoaded, isWSReady]);
|
}, [isMessagesLoaded, isWSReady]);
|
||||||
|
|
||||||
const sendMessage = async (message: string, messageId?: string) => {
|
const sendMessage = async (
|
||||||
|
message: string,
|
||||||
|
messageId?: string,
|
||||||
|
options?: { isCompact?: boolean },
|
||||||
|
) => {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
toast.error('Cannot send message while disconnected');
|
toast.error('Cannot send message while disconnected');
|
||||||
@@ -466,9 +491,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
let added = false;
|
let added = false;
|
||||||
|
|
||||||
messageId = messageId ?? crypto.randomBytes(7).toString('hex');
|
messageId = messageId ?? crypto.randomBytes(7).toString('hex');
|
||||||
|
let messageData = {
|
||||||
ws.send(
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'message',
|
type: 'message',
|
||||||
message: {
|
message: {
|
||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
@@ -479,8 +502,10 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
focusMode: focusMode,
|
focusMode: focusMode,
|
||||||
optimizationMode: optimizationMode,
|
optimizationMode: optimizationMode,
|
||||||
history: [...chatHistory, ['human', message]],
|
history: [...chatHistory, ['human', message]],
|
||||||
}),
|
isCompact: options?.isCompact ?? isCompact,
|
||||||
);
|
};
|
||||||
|
//console.log('sending:', messageData, JSON.stringify(messageData));
|
||||||
|
ws.send(JSON.stringify(messageData));
|
||||||
|
|
||||||
setMessages((prevMessages) => [
|
setMessages((prevMessages) => [
|
||||||
...prevMessages,
|
...prevMessages,
|
||||||
@@ -610,12 +635,12 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)];
|
return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)];
|
||||||
});
|
});
|
||||||
|
|
||||||
sendMessage(message.content, message.messageId);
|
sendMessage(message.content, message.messageId, { isCompact });
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isReady && initialMessage && ws?.readyState === 1) {
|
if (isReady && initialMessage && ws?.readyState === 1) {
|
||||||
sendMessage(initialMessage);
|
sendMessage(initialMessage, undefined, { isCompact });
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [ws?.readyState, isReady, initialMessage, isWSReady]);
|
}, [ws?.readyState, isReady, initialMessage, isWSReady]);
|
||||||
@@ -655,6 +680,10 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
setFileIds={setFileIds}
|
setFileIds={setFileIds}
|
||||||
files={files}
|
files={files}
|
||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
|
isCompact={isCompact}
|
||||||
|
setIsCompact={setIsCompact}
|
||||||
|
optimizationMode={optimizationMode}
|
||||||
|
setOptimizationMode={setOptimizationMode}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -668,6 +697,8 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
setFileIds={setFileIds}
|
setFileIds={setFileIds}
|
||||||
files={files}
|
files={files}
|
||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
|
isCompact={isCompact}
|
||||||
|
setIsCompact={setIsCompact}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -14,6 +14,8 @@ const EmptyChat = ({
|
|||||||
setFileIds,
|
setFileIds,
|
||||||
files,
|
files,
|
||||||
setFiles,
|
setFiles,
|
||||||
|
isCompact,
|
||||||
|
setIsCompact,
|
||||||
}: {
|
}: {
|
||||||
sendMessage: (message: string) => void;
|
sendMessage: (message: string) => void;
|
||||||
focusMode: string;
|
focusMode: string;
|
||||||
@@ -24,6 +26,8 @@ const EmptyChat = ({
|
|||||||
setFileIds: (fileIds: string[]) => void;
|
setFileIds: (fileIds: string[]) => void;
|
||||||
files: File[];
|
files: File[];
|
||||||
setFiles: (files: File[]) => void;
|
setFiles: (files: File[]) => void;
|
||||||
|
isCompact: boolean;
|
||||||
|
setIsCompact: (isCompact: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
|
|
||||||
@@ -48,6 +52,8 @@ const EmptyChat = ({
|
|||||||
setFileIds={setFileIds}
|
setFileIds={setFileIds}
|
||||||
files={files}
|
files={files}
|
||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
|
isCompact={isCompact}
|
||||||
|
setIsCompact={setIsCompact}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -17,8 +17,14 @@ const EmptyChatMessageInput = ({
|
|||||||
setFileIds,
|
setFileIds,
|
||||||
files,
|
files,
|
||||||
setFiles,
|
setFiles,
|
||||||
|
isCompact,
|
||||||
|
setIsCompact,
|
||||||
}: {
|
}: {
|
||||||
sendMessage: (message: string) => void;
|
sendMessage: (
|
||||||
|
message: string,
|
||||||
|
messageId?: string,
|
||||||
|
options?: { isCompact?: boolean },
|
||||||
|
) => void;
|
||||||
focusMode: string;
|
focusMode: string;
|
||||||
setFocusMode: (mode: string) => void;
|
setFocusMode: (mode: string) => void;
|
||||||
optimizationMode: string;
|
optimizationMode: string;
|
||||||
@@ -27,6 +33,8 @@ const EmptyChatMessageInput = ({
|
|||||||
setFileIds: (fileIds: string[]) => void;
|
setFileIds: (fileIds: string[]) => void;
|
||||||
files: File[];
|
files: File[];
|
||||||
setFiles: (files: File[]) => void;
|
setFiles: (files: File[]) => void;
|
||||||
|
isCompact: boolean;
|
||||||
|
setIsCompact: (isCompact: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [copilotEnabled, setCopilotEnabled] = useState(false);
|
const [copilotEnabled, setCopilotEnabled] = useState(false);
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
@@ -61,13 +69,13 @@ const EmptyChatMessageInput = ({
|
|||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendMessage(message);
|
sendMessage(message, undefined, { isCompact });
|
||||||
setMessage('');
|
setMessage('');
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendMessage(message);
|
sendMessage(message, undefined, { isCompact });
|
||||||
setMessage('');
|
setMessage('');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -97,6 +105,8 @@ const EmptyChatMessageInput = ({
|
|||||||
<Optimization
|
<Optimization
|
||||||
optimizationMode={optimizationMode}
|
optimizationMode={optimizationMode}
|
||||||
setOptimizationMode={setOptimizationMode}
|
setOptimizationMode={setOptimizationMode}
|
||||||
|
isCompact={isCompact}
|
||||||
|
setIsCompact={setIsCompact}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
disabled={message.trim().length === 0}
|
disabled={message.trim().length === 0}
|
||||||
|
@@ -28,6 +28,7 @@ const MessageBox = ({
|
|||||||
dividerRef,
|
dividerRef,
|
||||||
isLast,
|
isLast,
|
||||||
rewrite,
|
rewrite,
|
||||||
|
isCompact,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
}: {
|
}: {
|
||||||
message: Message;
|
message: Message;
|
||||||
@@ -37,7 +38,12 @@ const MessageBox = ({
|
|||||||
dividerRef?: MutableRefObject<HTMLDivElement | null>;
|
dividerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
isLast: boolean;
|
isLast: boolean;
|
||||||
rewrite: (messageId: string) => void;
|
rewrite: (messageId: string) => void;
|
||||||
sendMessage: (message: string) => void;
|
isCompact: boolean;
|
||||||
|
sendMessage: (
|
||||||
|
message: string,
|
||||||
|
messageId?: string,
|
||||||
|
options?: { isCompact?: boolean },
|
||||||
|
) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [parsedMessage, setParsedMessage] = useState(message.content);
|
const [parsedMessage, setParsedMessage] = useState(message.content);
|
||||||
const [speechMessage, setSpeechMessage] = useState(message.content);
|
const [speechMessage, setSpeechMessage] = useState(message.content);
|
||||||
@@ -65,16 +71,14 @@ const MessageBox = ({
|
|||||||
|
|
||||||
const { speechStatus, start, stop } = useSpeech({ text: speechMessage });
|
const { speechStatus, start, stop } = useSpeech({ text: speechMessage });
|
||||||
|
|
||||||
|
const handleSuggestionClick = (suggestion: string) => {
|
||||||
|
sendMessage(suggestion, undefined, { isCompact });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{message.role === 'user' && (
|
{message.role === 'user' && (
|
||||||
<div
|
<div className={cn('w-full', messageIndex === 0 ? 'pt-16' : 'pt-8')}>
|
||||||
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>
|
||||||
@@ -169,7 +173,7 @@ const MessageBox = ({
|
|||||||
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
|
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sendMessage(suggestion);
|
handleSuggestionClick(suggestion);
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer flex flex-row justify-between font-medium space-x-2 items-center"
|
className="cursor-pointer flex flex-row justify-between font-medium space-x-2 items-center"
|
||||||
>
|
>
|
||||||
|
@@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from 'react';
|
|||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
import Attach from './MessageInputActions/Attach';
|
import Attach from './MessageInputActions/Attach';
|
||||||
import CopilotToggle from './MessageInputActions/Copilot';
|
import CopilotToggle from './MessageInputActions/Copilot';
|
||||||
|
import Optimization from './MessageInputActions/Optimization';
|
||||||
import { File } from './ChatWindow';
|
import { File } from './ChatWindow';
|
||||||
import AttachSmall from './MessageInputActions/AttachSmall';
|
import AttachSmall from './MessageInputActions/AttachSmall';
|
||||||
|
|
||||||
@@ -14,13 +15,25 @@ const MessageInput = ({
|
|||||||
setFileIds,
|
setFileIds,
|
||||||
files,
|
files,
|
||||||
setFiles,
|
setFiles,
|
||||||
|
isCompact,
|
||||||
|
setIsCompact,
|
||||||
|
optimizationMode,
|
||||||
|
setOptimizationMode,
|
||||||
}: {
|
}: {
|
||||||
sendMessage: (message: string) => void;
|
sendMessage: (
|
||||||
|
message: string,
|
||||||
|
messageId?: string,
|
||||||
|
options?: { isCompact?: boolean },
|
||||||
|
) => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
fileIds: string[];
|
fileIds: string[];
|
||||||
setFileIds: (fileIds: string[]) => void;
|
setFileIds: (fileIds: string[]) => void;
|
||||||
files: File[];
|
files: File[];
|
||||||
setFiles: (files: File[]) => void;
|
setFiles: (files: File[]) => void;
|
||||||
|
isCompact: boolean;
|
||||||
|
setIsCompact: (isCompact: boolean) => void;
|
||||||
|
optimizationMode: string;
|
||||||
|
setOptimizationMode: (mode: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [copilotEnabled, setCopilotEnabled] = useState(false);
|
const [copilotEnabled, setCopilotEnabled] = useState(false);
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
@@ -40,20 +53,16 @@ const MessageInput = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
const activeElement = document.activeElement;
|
const activeElement = document.activeElement;
|
||||||
|
|
||||||
const isInputFocused =
|
const isInputFocused =
|
||||||
activeElement?.tagName === 'INPUT' ||
|
activeElement?.tagName === 'INPUT' ||
|
||||||
activeElement?.tagName === 'TEXTAREA' ||
|
activeElement?.tagName === 'TEXTAREA' ||
|
||||||
activeElement?.hasAttribute('contenteditable');
|
activeElement?.hasAttribute('contenteditable');
|
||||||
|
|
||||||
if (e.key === '/' && !isInputFocused) {
|
if (e.key === '/' && !isInputFocused) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', handleKeyDown);
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
};
|
};
|
||||||
@@ -64,28 +73,36 @@ const MessageInput = ({
|
|||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendMessage(message);
|
sendMessage(message, undefined, { isCompact });
|
||||||
setMessage('');
|
setMessage('');
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey && !loading) {
|
if (e.key === 'Enter' && !e.shiftKey && !loading) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendMessage(message);
|
sendMessage(message, undefined, { isCompact });
|
||||||
setMessage('');
|
setMessage('');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-light-secondary dark:bg-dark-secondary p-4 flex items-center overflow-hidden border border-light-200 dark:border-dark-200',
|
'bg-light-secondary dark:bg-dark-secondary p-4 flex items-center border border-light-200 dark:border-dark-200',
|
||||||
mode === 'multi' ? 'flex-col rounded-lg' : 'flex-row rounded-full',
|
mode === 'multi' ? 'flex-col rounded-lg' : 'flex-row rounded-full',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{mode === 'single' && (
|
{mode === 'single' && (
|
||||||
|
<div className="flex flex-row items-center space-x-2">
|
||||||
<AttachSmall
|
<AttachSmall
|
||||||
fileIds={fileIds}
|
fileIds={fileIds}
|
||||||
setFileIds={setFileIds}
|
setFileIds={setFileIds}
|
||||||
files={files}
|
files={files}
|
||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
/>
|
/>
|
||||||
|
<Optimization
|
||||||
|
optimizationMode={optimizationMode}
|
||||||
|
setOptimizationMode={setOptimizationMode}
|
||||||
|
isCompact={isCompact}
|
||||||
|
setIsCompact={setIsCompact}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<TextareaAutosize
|
<TextareaAutosize
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
@@ -113,12 +130,20 @@ const MessageInput = ({
|
|||||||
)}
|
)}
|
||||||
{mode === 'multi' && (
|
{mode === 'multi' && (
|
||||||
<div className="flex flex-row items-center justify-between w-full pt-2">
|
<div className="flex flex-row items-center justify-between w-full pt-2">
|
||||||
|
<div className="flex flex-row items-center space-x-2">
|
||||||
<AttachSmall
|
<AttachSmall
|
||||||
fileIds={fileIds}
|
fileIds={fileIds}
|
||||||
setFileIds={setFileIds}
|
setFileIds={setFileIds}
|
||||||
files={files}
|
files={files}
|
||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
/>
|
/>
|
||||||
|
<Optimization
|
||||||
|
optimizationMode={optimizationMode}
|
||||||
|
setOptimizationMode={setOptimizationMode}
|
||||||
|
isCompact={isCompact}
|
||||||
|
setIsCompact={setIsCompact}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex flex-row items-center space-x-4">
|
<div className="flex flex-row items-center space-x-4">
|
||||||
<CopilotToggle
|
<CopilotToggle
|
||||||
copilotEnabled={copilotEnabled}
|
copilotEnabled={copilotEnabled}
|
||||||
|
@@ -110,7 +110,7 @@ const Attach = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => fileInputRef.current.click()}
|
onClick={() => fileInputRef.current.click()}
|
||||||
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
|
className="flex flex-row items-center space-x-1 text-white/70 hover:text-white transition duration-200"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@@ -128,7 +128,7 @@ const Attach = ({
|
|||||||
setFiles([]);
|
setFiles([]);
|
||||||
setFileIds([]);
|
setFileIds([]);
|
||||||
}}
|
}}
|
||||||
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
|
className="flex flex-row items-center space-x-1 text-white/70 hover:text-white transition duration-200"
|
||||||
>
|
>
|
||||||
<Trash size={14} />
|
<Trash size={14} />
|
||||||
<p className="text-xs">Clear</p>
|
<p className="text-xs">Clear</p>
|
||||||
@@ -145,7 +145,7 @@ const Attach = ({
|
|||||||
<div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md">
|
<div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md">
|
||||||
<File size={16} className="text-white/70" />
|
<File size={16} className="text-white/70" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-white/70 text-sm">
|
||||||
{file.fileName.length > 25
|
{file.fileName.length > 25
|
||||||
? file.fileName.replace(/\.\w+$/, '').substring(0, 25) +
|
? file.fileName.replace(/\.\w+$/, '').substring(0, 25) +
|
||||||
'...' +
|
'...' +
|
||||||
|
@@ -82,7 +82,7 @@ const AttachSmall = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => fileInputRef.current.click()}
|
onClick={() => fileInputRef.current.click()}
|
||||||
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
|
className="flex flex-row items-center space-x-1 text-white/70 hover:text-white transition duration-200"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@@ -100,7 +100,7 @@ const AttachSmall = ({
|
|||||||
setFiles([]);
|
setFiles([]);
|
||||||
setFileIds([]);
|
setFileIds([]);
|
||||||
}}
|
}}
|
||||||
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
|
className="flex flex-row items-center space-x-1 text-white/70 hover:text-white transition duration-200"
|
||||||
>
|
>
|
||||||
<Trash size={14} />
|
<Trash size={14} />
|
||||||
<p className="text-xs">Clear</p>
|
<p className="text-xs">Clear</p>
|
||||||
@@ -117,7 +117,7 @@ const AttachSmall = ({
|
|||||||
<div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md">
|
<div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md">
|
||||||
<File size={16} className="text-white/70" />
|
<File size={16} className="text-white/70" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-white/70 text-sm">
|
||||||
{file.fileName.length > 25
|
{file.fileName.length > 25
|
||||||
? file.fileName.replace(/\.\w+$/, '').substring(0, 25) +
|
? file.fileName.replace(/\.\w+$/, '').substring(0, 25) +
|
||||||
'...' +
|
'...' +
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { ChevronDown, Sliders, Star, Zap } from 'lucide-react';
|
import { ChevronDown, Minimize2, Sliders, Star, Zap } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
@@ -6,8 +6,7 @@ import {
|
|||||||
PopoverPanel,
|
PopoverPanel,
|
||||||
Transition,
|
Transition,
|
||||||
} from '@headlessui/react';
|
} from '@headlessui/react';
|
||||||
import { Fragment } from 'react';
|
import { Fragment, useEffect } from 'react';
|
||||||
|
|
||||||
const OptimizationModes = [
|
const OptimizationModes = [
|
||||||
{
|
{
|
||||||
key: 'speed',
|
key: 'speed',
|
||||||
@@ -37,10 +36,33 @@ const OptimizationModes = [
|
|||||||
const Optimization = ({
|
const Optimization = ({
|
||||||
optimizationMode,
|
optimizationMode,
|
||||||
setOptimizationMode,
|
setOptimizationMode,
|
||||||
|
isCompact,
|
||||||
|
setIsCompact,
|
||||||
}: {
|
}: {
|
||||||
optimizationMode: string;
|
optimizationMode: string;
|
||||||
setOptimizationMode: (mode: string) => void;
|
setOptimizationMode: (mode: string) => void;
|
||||||
|
isCompact: boolean;
|
||||||
|
setIsCompact: (isCompact: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const savedCompactMode = localStorage.getItem('compactMode');
|
||||||
|
if (savedCompactMode === null) {
|
||||||
|
localStorage.setItem('compactMode', String(isCompact));
|
||||||
|
} else {
|
||||||
|
setIsCompact(savedCompactMode === 'true');
|
||||||
|
}
|
||||||
|
}, [setIsCompact]);
|
||||||
|
|
||||||
|
const handleCompactChange = (checked: boolean) => {
|
||||||
|
setIsCompact(checked);
|
||||||
|
localStorage.setItem('compactMode', String(checked));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOptimizationChange = (mode: string) => {
|
||||||
|
setOptimizationMode(mode);
|
||||||
|
localStorage.setItem('optimizationMode', mode);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
|
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
|
||||||
<PopoverButton
|
<PopoverButton
|
||||||
@@ -48,6 +70,12 @@ const Optimization = ({
|
|||||||
className="p-2 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white"
|
className="p-2 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white"
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center space-x-1">
|
<div className="flex flex-row items-center space-x-1">
|
||||||
|
{isCompact && (
|
||||||
|
<Minimize2
|
||||||
|
size={16}
|
||||||
|
className="text-gray-600 dark:text-gray-400"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{
|
{
|
||||||
OptimizationModes.find((mode) => mode.key === optimizationMode)
|
OptimizationModes.find((mode) => mode.key === optimizationMode)
|
||||||
?.icon
|
?.icon
|
||||||
@@ -70,11 +98,11 @@ const Optimization = ({
|
|||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<PopoverPanel className="absolute z-10 w-64 md:w-[250px] right-0">
|
<PopoverPanel className="absolute z-10 w-64 md:w-[250px] right-0 bottom-[100%] mb-2">
|
||||||
<div className="flex flex-col gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto">
|
<div className="flex flex-col gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto">
|
||||||
{OptimizationModes.map((mode, i) => (
|
{OptimizationModes.map((mode, i) => (
|
||||||
<PopoverButton
|
<PopoverButton
|
||||||
onClick={() => setOptimizationMode(mode.key)}
|
onClick={() => handleOptimizationChange(mode.key)}
|
||||||
key={i}
|
key={i}
|
||||||
disabled={mode.key === 'quality'}
|
disabled={mode.key === 'quality'}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -94,6 +122,30 @@ const Optimization = ({
|
|||||||
</p>
|
</p>
|
||||||
</PopoverButton>
|
</PopoverButton>
|
||||||
))}
|
))}
|
||||||
|
<div className="border-t border-light-200 dark:border-dark-200 pt-2 mt-1">
|
||||||
|
<label className="flex items-center space-x-2 p-2 rounded-lg cursor-pointer hover:bg-light-secondary dark:hover:bg-dark-secondary">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isCompact}
|
||||||
|
onChange={(e) => handleCompactChange(e.target.checked)}
|
||||||
|
className="form-checkbox h-4 w-4 text-blue-600 transition duration-150 ease-in-out"
|
||||||
|
/>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Minimize2
|
||||||
|
size={16}
|
||||||
|
className="text-gray-600 dark:text-gray-400"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-black dark:text-white">
|
||||||
|
Compact Mode
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-black/70 dark:text-white/70">
|
||||||
|
Generate more concise responses
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PopoverPanel>
|
</PopoverPanel>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
@@ -33,9 +33,10 @@ const SearchImages = ({
|
|||||||
|
|
||||||
const chatModelProvider = localStorage.getItem('chatModelProvider');
|
const chatModelProvider = localStorage.getItem('chatModelProvider');
|
||||||
const chatModel = localStorage.getItem('chatModel');
|
const chatModel = localStorage.getItem('chatModel');
|
||||||
|
|
||||||
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
|
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
|
||||||
const customOpenAIKey = localStorage.getItem('openAIApiKey');
|
const customOpenAIKey = localStorage.getItem('openAIApiKey');
|
||||||
|
const ollamaContextWindow =
|
||||||
|
localStorage.getItem('ollamaContextWindow') || '2048';
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${process.env.NEXT_PUBLIC_API_URL}/images`,
|
`${process.env.NEXT_PUBLIC_API_URL}/images`,
|
||||||
@@ -54,6 +55,9 @@ const SearchImages = ({
|
|||||||
customOpenAIBaseURL: customOpenAIBaseURL,
|
customOpenAIBaseURL: customOpenAIBaseURL,
|
||||||
customOpenAIKey: customOpenAIKey,
|
customOpenAIKey: customOpenAIKey,
|
||||||
}),
|
}),
|
||||||
|
...(chatModelProvider === 'ollama' && {
|
||||||
|
ollamaContextWindow: parseInt(ollamaContextWindow),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
@@ -48,9 +48,10 @@ const Searchvideos = ({
|
|||||||
|
|
||||||
const chatModelProvider = localStorage.getItem('chatModelProvider');
|
const chatModelProvider = localStorage.getItem('chatModelProvider');
|
||||||
const chatModel = localStorage.getItem('chatModel');
|
const chatModel = localStorage.getItem('chatModel');
|
||||||
|
|
||||||
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
|
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
|
||||||
const customOpenAIKey = localStorage.getItem('openAIApiKey');
|
const customOpenAIKey = localStorage.getItem('openAIApiKey');
|
||||||
|
const ollamaContextWindow =
|
||||||
|
localStorage.getItem('ollamaContextWindow') || '2048';
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${process.env.NEXT_PUBLIC_API_URL}/videos`,
|
`${process.env.NEXT_PUBLIC_API_URL}/videos`,
|
||||||
@@ -69,6 +70,9 @@ const Searchvideos = ({
|
|||||||
customOpenAIBaseURL: customOpenAIBaseURL,
|
customOpenAIBaseURL: customOpenAIBaseURL,
|
||||||
customOpenAIKey: customOpenAIKey,
|
customOpenAIKey: customOpenAIKey,
|
||||||
}),
|
}),
|
||||||
|
...(chatModelProvider === 'ollama' && {
|
||||||
|
ollamaContextWindow: parseInt(ollamaContextWindow),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
@@ -6,6 +6,8 @@ export const getSuggestions = async (chatHisory: Message[]) => {
|
|||||||
|
|
||||||
const customOpenAIKey = localStorage.getItem('openAIApiKey');
|
const customOpenAIKey = localStorage.getItem('openAIApiKey');
|
||||||
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
|
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
|
||||||
|
const ollamaContextWindow =
|
||||||
|
localStorage.getItem('ollamaContextWindow') || '2048';
|
||||||
|
|
||||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/suggestions`, {
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/suggestions`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -21,6 +23,9 @@ export const getSuggestions = async (chatHisory: Message[]) => {
|
|||||||
customOpenAIKey,
|
customOpenAIKey,
|
||||||
customOpenAIBaseURL,
|
customOpenAIBaseURL,
|
||||||
}),
|
}),
|
||||||
|
...(chatModelProvider === 'ollama' && {
|
||||||
|
ollamaContextWindow: parseInt(ollamaContextWindow),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user