mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-06-22 01:38:47 +00:00
Compare commits
7 Commits
develop/v1
...
c690c20417
Author | SHA1 | Date | |
---|---|---|---|
c690c20417 | |||
7dfcac07cd | |||
76ed952aa2 | |||
b20ea70089 | |||
5220abae05 | |||
9668056554 | |||
1d6ab2c90c |
4
.gitignore
vendored
4
.gitignore
vendored
@ -2,7 +2,6 @@
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
package-lock.json
|
||||
|
||||
# Build output
|
||||
/.next/
|
||||
@ -38,6 +37,3 @@ Thumbs.db
|
||||
# Db
|
||||
db.sqlite
|
||||
/searxng
|
||||
|
||||
# Dev
|
||||
docker-compose-dev.yaml
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
[](https://discord.gg/26aArMy8tT)
|
||||
|
||||
|
||||

|
||||
|
||||
## Table of Contents <!-- omit in toc -->
|
||||
@ -43,7 +44,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.
|
||||
- **Focus Modes:** Special modes to better answer specific types of questions. Perplexica currently has 6 focus modes:
|
||||
- **All Mode:** Searches the entire web to find the best results.
|
||||
- **Writing Assistant Mode:** Helpful for writing tasks that do not require searching the web.
|
||||
- **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.
|
||||
- **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.
|
||||
@ -142,7 +143,6 @@ You can access Perplexica over your home network by following our networking gui
|
||||
|
||||
## One-Click Deployment
|
||||
|
||||
[](https://usw.sealos.io/?openapp=system-template%3FtemplateName%3Dperplexica)
|
||||
[](https://repocloud.io/details/?app_id=267)
|
||||
|
||||
## Upcoming Features
|
||||
|
@ -4,7 +4,7 @@ services:
|
||||
volumes:
|
||||
- ./searxng:/etc/searxng:rw
|
||||
ports:
|
||||
- '4000:8080'
|
||||
- 4000:8080
|
||||
networks:
|
||||
- perplexica-network
|
||||
restart: unless-stopped
|
||||
@ -19,7 +19,7 @@ services:
|
||||
depends_on:
|
||||
- searxng
|
||||
ports:
|
||||
- '3001:3001'
|
||||
- 3001:3001
|
||||
volumes:
|
||||
- backend-dbstore:/home/perplexica/data
|
||||
- uploads:/home/perplexica/uploads
|
||||
@ -37,11 +37,12 @@ services:
|
||||
args:
|
||||
- NEXT_PUBLIC_API_URL=http://127.0.0.1:3001/api
|
||||
- NEXT_PUBLIC_WS_URL=ws://127.0.0.1:3001
|
||||
network: host
|
||||
image: itzcrazykns1337/perplexica-frontend:main
|
||||
depends_on:
|
||||
- perplexica-backend
|
||||
ports:
|
||||
- '3000:3000'
|
||||
- 3000:3000
|
||||
networks:
|
||||
- perplexica-network
|
||||
restart: unless-stopped
|
||||
|
6912
package-lock.json
generated
Normal file
6912
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -30,8 +30,8 @@
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@langchain/anthropic": "^0.2.3",
|
||||
"@langchain/community": "^0.2.16",
|
||||
"@langchain/google-genai": "^0.0.23",
|
||||
"@langchain/openai": "^0.0.25",
|
||||
"@langchain/google-genai": "^0.0.23",
|
||||
"@xenova/transformers": "^2.17.1",
|
||||
"axios": "^1.6.8",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
|
@ -3,12 +3,6 @@ PORT = 3001 # Port to run the server on
|
||||
SIMILARITY_MEASURE = "cosine" # "cosine" or "dot"
|
||||
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]
|
||||
API_KEY = ""
|
||||
|
||||
@ -21,25 +15,16 @@ API_KEY = ""
|
||||
[MODELS.GEMINI]
|
||||
API_KEY = ""
|
||||
|
||||
[MODELS.CUSTOM_OPENAI]
|
||||
API_KEY = ""
|
||||
API_URL = ""
|
||||
|
||||
[MODELS.OLLAMA]
|
||||
API_URL = "" # Ollama API URL - http://host.docker.internal:11434
|
||||
|
||||
[SEARCH_ENGINES.GOOGLE]
|
||||
[MODELS.LMSTUDIO]
|
||||
API_URL = "" # LM STUDIO API URL - http://host.docker.internal:1234
|
||||
|
||||
[MODELS.CUSTOM_OPENAI]
|
||||
API_KEY = ""
|
||||
CSE_ID = ""
|
||||
API_URL = ""
|
||||
MODEL_NAME = ""
|
||||
|
||||
[SEARCH_ENGINES.SEARXNG]
|
||||
ENDPOINT = ""
|
||||
|
||||
[SEARCH_ENGINES.BING]
|
||||
SUBSCRIPTION_KEY = ""
|
||||
|
||||
[SEARCH_ENGINES.BRAVE]
|
||||
API_KEY = ""
|
||||
|
||||
[SEARCH_ENGINES.YACY]
|
||||
ENDPOINT = ""
|
||||
[API_ENDPOINTS]
|
||||
SEARXNG = "http://localhost:32768" # SearxNG API URL
|
@ -15,5 +15,3 @@ server:
|
||||
engines:
|
||||
- name: wolframalpha
|
||||
disabled: false
|
||||
- name: qwant
|
||||
disabled: true
|
||||
|
@ -7,12 +7,7 @@ import { PromptTemplate } from '@langchain/core/prompts';
|
||||
import formatChatHistoryAsString from '../utils/formatHistory';
|
||||
import { BaseMessage } from '@langchain/core/messages';
|
||||
import { StringOutputParser } from '@langchain/core/output_parsers';
|
||||
import { searchSearxng } from '../lib/searchEngines/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 { searchSearxng } from '../lib/searxng';
|
||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
|
||||
const imageSearchChainPrompt = `
|
||||
@ -41,103 +36,6 @@ type ImageSearchChainInput = {
|
||||
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 createImageSearchChain = (llm: BaseChatModel) => {
|
||||
@ -154,7 +52,22 @@ const createImageSearchChain = (llm: BaseChatModel) => {
|
||||
llm,
|
||||
strParser,
|
||||
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);
|
||||
}),
|
||||
]);
|
||||
|
@ -7,30 +7,26 @@ import { PromptTemplate } from '@langchain/core/prompts';
|
||||
import formatChatHistoryAsString from '../utils/formatHistory';
|
||||
import { BaseMessage } from '@langchain/core/messages';
|
||||
import { StringOutputParser } from '@langchain/core/output_parsers';
|
||||
import { searchSearxng } from '../lib/searchEngines/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 { searchSearxng } from '../lib/searxng';
|
||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
|
||||
const VideoSearchChainPrompt = `
|
||||
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search Youtube for videos.
|
||||
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation.
|
||||
|
||||
|
||||
Example:
|
||||
1. Follow up question: How does a car work?
|
||||
Rephrased: How does a car work?
|
||||
|
||||
|
||||
2. Follow up question: What is the theory of relativity?
|
||||
Rephrased: What is theory of relativity
|
||||
|
||||
|
||||
3. Follow up question: How does an AC work?
|
||||
Rephrased: How does an AC work
|
||||
|
||||
|
||||
Conversation:
|
||||
{chat_history}
|
||||
|
||||
|
||||
Follow up question: {query}
|
||||
Rephrased question:
|
||||
`;
|
||||
@ -42,102 +38,6 @@ type VideoSearchChainInput = {
|
||||
|
||||
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) => {
|
||||
return RunnableSequence.from([
|
||||
RunnableMap.from({
|
||||
@ -152,7 +52,28 @@ const createVideoSearchChain = (llm: BaseChatModel) => {
|
||||
llm,
|
||||
strParser,
|
||||
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);
|
||||
}),
|
||||
]);
|
||||
|
@ -10,12 +10,6 @@ interface Config {
|
||||
SIMILARITY_MEASURE: string;
|
||||
KEEP_ALIVE: string;
|
||||
};
|
||||
SEARCH_ENGINE_BACKENDS: {
|
||||
SEARCH: string;
|
||||
IMAGE: string;
|
||||
VIDEO: string;
|
||||
NEWS: string;
|
||||
};
|
||||
MODELS: {
|
||||
OPENAI: {
|
||||
API_KEY: string;
|
||||
@ -32,29 +26,17 @@ interface Config {
|
||||
OLLAMA: {
|
||||
API_URL: string;
|
||||
};
|
||||
LMSTUDIO: {
|
||||
API_URL: string;
|
||||
};
|
||||
CUSTOM_OPENAI: {
|
||||
API_URL: string;
|
||||
API_KEY: string;
|
||||
MODEL_NAME: string;
|
||||
};
|
||||
};
|
||||
SEARCH_ENGINES: {
|
||||
GOOGLE: {
|
||||
API_KEY: string;
|
||||
CSE_ID: string;
|
||||
};
|
||||
SEARXNG: {
|
||||
ENDPOINT: string;
|
||||
};
|
||||
BING: {
|
||||
SUBSCRIPTION_KEY: string;
|
||||
};
|
||||
BRAVE: {
|
||||
API_KEY: string;
|
||||
};
|
||||
YACY: {
|
||||
ENDPOINT: string;
|
||||
};
|
||||
API_ENDPOINTS: {
|
||||
SEARXNG: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -82,35 +64,13 @@ export const getAnthropicApiKey = () => loadConfig().MODELS.ANTHROPIC.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 = () =>
|
||||
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 getLMStudioApiEndpoint = () => loadConfig().MODELS.LMSTUDIO.API_URL;
|
||||
|
||||
export const getCustomOpenaiApiKey = () =>
|
||||
loadConfig().MODELS.CUSTOM_OPENAI.API_KEY;
|
||||
|
||||
@ -121,10 +81,6 @@ export const getCustomOpenaiModelName = () =>
|
||||
loadConfig().MODELS.CUSTOM_OPENAI.MODEL_NAME;
|
||||
|
||||
const mergeConfigs = (current: any, update: any): any => {
|
||||
if (update === null || update === undefined) {
|
||||
return current;
|
||||
}
|
||||
|
||||
if (typeof current !== 'object' || current === null) {
|
||||
return update;
|
||||
}
|
||||
|
1
src/lib/chat/service.ts
Normal file
1
src/lib/chat/service.ts
Normal file
@ -0,0 +1 @@
|
||||
|
@ -4,6 +4,7 @@ import { loadOpenAIChatModels, loadOpenAIEmbeddingsModels } from './openai';
|
||||
import { loadAnthropicChatModels } from './anthropic';
|
||||
import { loadTransformersEmbeddingsModels } from './transformers';
|
||||
import { loadGeminiChatModels, loadGeminiEmbeddingsModels } from './gemini';
|
||||
import { loadLMStudioChatModels, loadLMStudioEmbeddingsModels } from './lmstudio';
|
||||
import {
|
||||
getCustomOpenaiApiKey,
|
||||
getCustomOpenaiApiUrl,
|
||||
@ -17,6 +18,7 @@ const chatModelProviders = {
|
||||
ollama: loadOllamaChatModels,
|
||||
anthropic: loadAnthropicChatModels,
|
||||
gemini: loadGeminiChatModels,
|
||||
lm_studio: loadLMStudioChatModels,
|
||||
};
|
||||
|
||||
const embeddingModelProviders = {
|
||||
@ -24,6 +26,7 @@ const embeddingModelProviders = {
|
||||
local: loadTransformersEmbeddingsModels,
|
||||
ollama: loadOllamaEmbeddingsModels,
|
||||
gemini: loadGeminiEmbeddingsModels,
|
||||
lm_studio: loadLMStudioEmbeddingsModels,
|
||||
};
|
||||
|
||||
export const getAvailableChatModelProviders = async () => {
|
||||
|
125
src/lib/providers/lmstudio.ts
Normal file
125
src/lib/providers/lmstudio.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { OpenAIEmbeddings } from '@langchain/openai';
|
||||
import { ChatOpenAI } from '@langchain/openai';
|
||||
import { getKeepAlive, getLMStudioApiEndpoint } from '../../config';
|
||||
import logger from '../../utils/logger';
|
||||
import axios from 'axios';
|
||||
|
||||
const ensureV1Endpoint = (endpoint: string): string => {
|
||||
return endpoint.endsWith('/v1') ? endpoint : `${endpoint}/v1`;
|
||||
};
|
||||
|
||||
interface LMStudioModel {
|
||||
id: string;
|
||||
// add other properties if LM Studio API provides them
|
||||
}
|
||||
|
||||
interface ChatModelConfig {
|
||||
displayName: string;
|
||||
model: ChatOpenAI;
|
||||
}
|
||||
|
||||
const checkLMStudioAvailability = async (endpoint: string): Promise<boolean> => {
|
||||
const v1Endpoint = ensureV1Endpoint(endpoint);
|
||||
try {
|
||||
await axios.get(`${v1Endpoint}/models`, {
|
||||
timeout: 1000, // 1 second timeout
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
logger.debug(`LM Studio server not available at ${endpoint}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const loadLMStudioChatModels = async (): Promise<Record<string, ChatModelConfig>> => {
|
||||
const lmStudioEndpoint = getLMStudioApiEndpoint();
|
||||
|
||||
if (!lmStudioEndpoint) {
|
||||
logger.debug('LM Studio endpoint not configured, skipping');
|
||||
return {};
|
||||
}
|
||||
|
||||
// Check if server is available before attempting to load models
|
||||
const isAvailable = await checkLMStudioAvailability(lmStudioEndpoint);
|
||||
if (!isAvailable) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const v1Endpoint = ensureV1Endpoint(lmStudioEndpoint);
|
||||
const response = await axios.get<{ data: LMStudioModel[] }>(`${v1Endpoint}/models`, {
|
||||
timeout: 5000, // 5 second timeout for model loading
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const lmStudioModels = response.data.data;
|
||||
|
||||
const chatModels = lmStudioModels.reduce<Record<string, ChatModelConfig>>((acc, model) => {
|
||||
acc[model.id] = {
|
||||
displayName: model.id,
|
||||
model: new ChatOpenAI({
|
||||
openAIApiKey: 'lm-studio',
|
||||
configuration: {
|
||||
baseURL: ensureV1Endpoint(lmStudioEndpoint),
|
||||
},
|
||||
modelName: model.id,
|
||||
temperature: 0.7,
|
||||
}),
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return chatModels;
|
||||
} catch (err) {
|
||||
logger.error(`Error loading LM Studio models: ${err}`);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const loadLMStudioEmbeddingsModels = async () => {
|
||||
const lmStudioEndpoint = getLMStudioApiEndpoint();
|
||||
|
||||
if (!lmStudioEndpoint) return {};
|
||||
|
||||
// Check if server is available before attempting to load models
|
||||
const isAvailable = await checkLMStudioAvailability(lmStudioEndpoint);
|
||||
if (!isAvailable) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const v1Endpoint = ensureV1Endpoint(lmStudioEndpoint);
|
||||
const response = await axios.get(`${v1Endpoint}/models`, {
|
||||
timeout: 5000, // 5 second timeout for model loading
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const lmStudioModels = response.data.data;
|
||||
|
||||
const embeddingsModels = lmStudioModels.reduce((acc, model) => {
|
||||
acc[model.id] = {
|
||||
displayName: model.id,
|
||||
model: new OpenAIEmbeddings({
|
||||
openAIApiKey: 'lm-studio', // Dummy key required by LangChain
|
||||
configuration: {
|
||||
baseURL: ensureV1Endpoint(lmStudioEndpoint),
|
||||
},
|
||||
modelName: model.id,
|
||||
}),
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return embeddingsModels;
|
||||
} catch (err) {
|
||||
logger.error(`Error loading LM Studio embeddings model: ${err}`);
|
||||
return {};
|
||||
}
|
||||
};
|
@ -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 { getSearxngApiEndpoint } from '../../config';
|
||||
import { getSearxngApiEndpoint } from '../config';
|
||||
|
||||
interface SearxngSearchOptions {
|
||||
categories?: string[];
|
@ -6,6 +6,7 @@ import {
|
||||
import {
|
||||
getGroqApiKey,
|
||||
getOllamaApiEndpoint,
|
||||
getLMStudioApiEndpoint,
|
||||
getAnthropicApiKey,
|
||||
getGeminiApiKey,
|
||||
getOpenaiApiKey,
|
||||
@ -13,16 +14,6 @@ import {
|
||||
getCustomOpenaiApiUrl,
|
||||
getCustomOpenaiApiKey,
|
||||
getCustomOpenaiModelName,
|
||||
getSearchEngineBackend,
|
||||
getImageSearchEngineBackend,
|
||||
getVideoSearchEngineBackend,
|
||||
getNewsSearchEngineBackend,
|
||||
getSearxngApiEndpoint,
|
||||
getGoogleApiKey,
|
||||
getGoogleCseId,
|
||||
getBingSubscriptionKey,
|
||||
getBraveApiKey,
|
||||
getYacyJsonEndpoint,
|
||||
} from '../config';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
@ -64,27 +55,13 @@ router.get('/', async (_, res) => {
|
||||
|
||||
config['openaiApiKey'] = getOpenaiApiKey();
|
||||
config['ollamaApiUrl'] = getOllamaApiEndpoint();
|
||||
config['lmStudioApiUrl'] = getLMStudioApiEndpoint();
|
||||
config['anthropicApiKey'] = getAnthropicApiKey();
|
||||
config['groqApiKey'] = getGroqApiKey();
|
||||
config['geminiApiKey'] = getGeminiApiKey();
|
||||
config['customOpenaiApiUrl'] = getCustomOpenaiApiUrl();
|
||||
config['customOpenaiApiKey'] = getCustomOpenaiApiKey();
|
||||
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();
|
||||
config['customOpenaiModelName'] = getCustomOpenaiModelName()
|
||||
|
||||
res.status(200).json(config);
|
||||
} catch (err: any) {
|
||||
@ -113,36 +90,15 @@ router.post('/', async (req, res) => {
|
||||
OLLAMA: {
|
||||
API_URL: config.ollamaApiUrl,
|
||||
},
|
||||
LMSTUDIO: {
|
||||
API_URL: config.lmStudioApiUrl,
|
||||
},
|
||||
CUSTOM_OPENAI: {
|
||||
API_URL: config.customOpenaiApiUrl,
|
||||
API_KEY: config.customOpenaiApiKey,
|
||||
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);
|
||||
|
@ -1,125 +1,42 @@
|
||||
import express from 'express';
|
||||
import { searchSearxng } from '../lib/searchEngines/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 { searchSearxng } from '../lib/searxng';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
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) => {
|
||||
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 = (
|
||||
await Promise.all(
|
||||
queries.map(async ({ site, topic }) => {
|
||||
try {
|
||||
const query = `site:${site} ${topic}`;
|
||||
return await performSearch(query, site);
|
||||
} catch (error) {
|
||||
logger.error(`Error searching ${site}: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
await Promise.all([
|
||||
searchSearxng('site:businessinsider.com AI', {
|
||||
engines: ['bing news'],
|
||||
pageno: 1,
|
||||
}),
|
||||
)
|
||||
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()
|
||||
.sort(() => Math.random() - 0.5)
|
||||
.filter((item) => item.title && item.url && item.content);
|
||||
.sort(() => Math.random() - 0.5);
|
||||
|
||||
return res.json({ blogs: data });
|
||||
} catch (err: any) {
|
||||
|
@ -17,12 +17,7 @@ import LineListOutputParser from '../lib/outputParsers/listLineOutputParser';
|
||||
import LineOutputParser from '../lib/outputParsers/lineOutputParser';
|
||||
import { getDocumentsFromLinks } from '../utils/documents';
|
||||
import { Document } from 'langchain/document';
|
||||
import { searchSearxng } from '../lib/searchEngines/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 { searchSearxng } from '../lib/searxng';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import computeSimilarity from '../utils/computeSimilarity';
|
||||
@ -137,7 +132,7 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
||||
You are a web search summarizer, tasked with summarizing a piece of text retrieved from a web search. Your job is to summarize the
|
||||
text into a detailed, 2-4 paragraph explanation that captures the main ideas and provides a comprehensive answer to the query.
|
||||
If the query is \"summarize\", you should provide a detailed summary of the text. If the query is a specific question, you should answer it in the summary.
|
||||
|
||||
|
||||
- **Journalistic tone**: The summary should sound professional and journalistic, not too casual or vague.
|
||||
- **Thorough and detailed**: Ensure that every key point from the text is captured and that the summary directly answers the query.
|
||||
- **Not too lengthy, but detailed**: The summary should be informative but not excessively long. Focus on providing detailed information in a concise format.
|
||||
@ -208,37 +203,10 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
||||
|
||||
return { query: question, docs: docs };
|
||||
} else {
|
||||
const searchEngine = getSearchEngineBackend();
|
||||
|
||||
let res;
|
||||
switch (searchEngine) {
|
||||
case 'searxng':
|
||||
res = await searchSearxng(question, {
|
||||
language: 'en',
|
||||
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 res = await searchSearxng(question, {
|
||||
language: 'en',
|
||||
engines: this.config.activeEngines,
|
||||
});
|
||||
|
||||
const documents = res.results.map(
|
||||
(result) =>
|
||||
|
@ -23,18 +23,6 @@ interface SettingsType {
|
||||
customOpenaiApiKey: string;
|
||||
customOpenaiApiUrl: string;
|
||||
customOpenaiModelName: string;
|
||||
searchEngineBackends: {
|
||||
search: string;
|
||||
image: string;
|
||||
video: string;
|
||||
news: string;
|
||||
};
|
||||
searxngEndpoint: string;
|
||||
googleApiKey: string;
|
||||
googleCseId: string;
|
||||
bingSubscriptionKey: string;
|
||||
braveApiKey: string;
|
||||
yacyEndpoint: string;
|
||||
}
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
@ -124,12 +112,6 @@ const Page = () => {
|
||||
const [automaticImageSearch, setAutomaticImageSearch] = useState(false);
|
||||
const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false);
|
||||
const [savingStates, setSavingStates] = useState<Record<string, boolean>>({});
|
||||
const [searchEngineBackends, setSearchEngineBackends] = useState({
|
||||
search: '',
|
||||
image: '',
|
||||
video: '',
|
||||
news: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
@ -143,16 +125,6 @@ const Page = () => {
|
||||
const data = (await res.json()) as SettingsType;
|
||||
setConfig(data);
|
||||
|
||||
// Set search engine backends if they exist in the response
|
||||
if (data.searchEngineBackends) {
|
||||
setSearchEngineBackends({
|
||||
search: data.searchEngineBackends.search || '',
|
||||
image: data.searchEngineBackends.image || '',
|
||||
video: data.searchEngineBackends.video || '',
|
||||
news: data.searchEngineBackends.news || '',
|
||||
});
|
||||
}
|
||||
|
||||
const chatModelProvidersKeys = Object.keys(data.chatModelProviders || {});
|
||||
const embeddingModelProvidersKeys = Object.keys(
|
||||
data.embeddingModelProviders || {},
|
||||
@ -359,8 +331,6 @@ const Page = () => {
|
||||
localStorage.setItem('embeddingModelProvider', value);
|
||||
} else if (key === 'embeddingModel') {
|
||||
localStorage.setItem('embeddingModel', value);
|
||||
} else if (key === 'searchEngineBackends') {
|
||||
localStorage.setItem('searchEngineBackends', value);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to save:', err);
|
||||
@ -823,234 +793,6 @@ const Page = () => {
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
)
|
||||
)}
|
||||
|
@ -11,6 +11,8 @@ import {
|
||||
StopCircle,
|
||||
Layers3,
|
||||
Plus,
|
||||
Brain,
|
||||
ChevronDown,
|
||||
} from 'lucide-react';
|
||||
import Markdown from 'markdown-to-jsx';
|
||||
import Copy from './MessageActions/Copy';
|
||||
@ -41,26 +43,58 @@ const MessageBox = ({
|
||||
}) => {
|
||||
const [parsedMessage, setParsedMessage] = useState(message.content);
|
||||
const [speechMessage, setSpeechMessage] = useState(message.content);
|
||||
const [thinking, setThinking] = useState<string>('');
|
||||
const [answer, setAnswer] = useState<string>('');
|
||||
const [isThinkingExpanded, setIsThinkingExpanded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const regex = /\[(\d+)\]/g;
|
||||
const thinkRegex = /<think>(.*?)(?:<\/think>|$)(.*)/s;
|
||||
|
||||
if (
|
||||
message.role === 'assistant' &&
|
||||
message?.sources &&
|
||||
message.sources.length > 0
|
||||
) {
|
||||
return setParsedMessage(
|
||||
message.content.replace(
|
||||
regex,
|
||||
(_, number) =>
|
||||
`<a href="${message.sources?.[number - 1]?.metadata?.url}" target="_blank" className="bg-light-secondary dark:bg-dark-secondary px-1 rounded ml-1 no-underline text-xs text-black/70 dark:text-white/70 relative">${number}</a>`,
|
||||
),
|
||||
);
|
||||
// Check for thinking content, including partial tags
|
||||
const match = message.content.match(thinkRegex);
|
||||
if (match) {
|
||||
const [_, thinkingContent, answerContent] = match;
|
||||
|
||||
// Set thinking content even if </think> hasn't appeared yet
|
||||
if (thinkingContent) {
|
||||
setThinking(thinkingContent.trim());
|
||||
setIsThinkingExpanded(true); // Auto-expand when thinking starts
|
||||
}
|
||||
|
||||
// Only set answer content if we have it (after </think>)
|
||||
if (answerContent) {
|
||||
setAnswer(answerContent.trim());
|
||||
|
||||
// Process the answer part for sources if needed
|
||||
if (message.role === 'assistant' && message?.sources && message.sources.length > 0) {
|
||||
setParsedMessage(
|
||||
answerContent.trim().replace(
|
||||
regex,
|
||||
(_, number) =>
|
||||
`<a href="${message.sources?.[number - 1]?.metadata?.url}" target="_blank" className="bg-light-secondary dark:bg-dark-secondary px-1 rounded ml-1 no-underline text-xs text-black/70 dark:text-white/70 relative">${number}</a>`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setParsedMessage(answerContent.trim());
|
||||
}
|
||||
setSpeechMessage(answerContent.trim().replace(regex, ''));
|
||||
}
|
||||
} else {
|
||||
// No thinking content - process as before
|
||||
if (message.role === 'assistant' && message?.sources && message.sources.length > 0) {
|
||||
setParsedMessage(
|
||||
message.content.replace(
|
||||
regex,
|
||||
(_, number) =>
|
||||
`<a href="${message.sources?.[number - 1]?.metadata?.url}" target="_blank" className="bg-light-secondary dark:bg-dark-secondary px-1 rounded ml-1 no-underline text-xs text-black/70 dark:text-white/70 relative">${number}</a>`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setParsedMessage(message.content);
|
||||
}
|
||||
setSpeechMessage(message.content.replace(regex, ''));
|
||||
}
|
||||
|
||||
setSpeechMessage(message.content.replace(regex, ''));
|
||||
setParsedMessage(message.content);
|
||||
}, [message.content, message.sources, message.role]);
|
||||
|
||||
const { speechStatus, start, stop } = useSpeech({ text: speechMessage });
|
||||
@ -68,13 +102,7 @@ const MessageBox = ({
|
||||
return (
|
||||
<div>
|
||||
{message.role === 'user' && (
|
||||
<div
|
||||
className={cn(
|
||||
'w-full',
|
||||
messageIndex === 0 ? 'pt-16' : 'pt-8',
|
||||
'break-words',
|
||||
)}
|
||||
>
|
||||
<div className={cn('w-full', messageIndex === 0 ? 'pt-16' : 'pt-8')}>
|
||||
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
|
||||
{message.content}
|
||||
</h2>
|
||||
@ -98,27 +126,71 @@ const MessageBox = ({
|
||||
<MessageSources sources={message.sources} />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<Disc3
|
||||
className={cn(
|
||||
'text-black dark:text-white',
|
||||
isLast && loading ? 'animate-spin' : 'animate-none',
|
||||
<div className="flex flex-col space-y-4">
|
||||
{thinking && (
|
||||
<div className="flex flex-col space-y-2 mb-4">
|
||||
<button
|
||||
onClick={() => setIsThinkingExpanded(!isThinkingExpanded)}
|
||||
className="flex flex-row items-center space-x-2 group text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white transition duration-200"
|
||||
>
|
||||
<Brain size={20} />
|
||||
<h3 className="font-medium text-xl">Reasoning</h3>
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className={cn(
|
||||
"transition-transform duration-200",
|
||||
isThinkingExpanded ? "rotate-180" : ""
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isThinkingExpanded && (
|
||||
<div className="rounded-lg bg-light-secondary/50 dark:bg-dark-secondary/50 p-4">
|
||||
{thinking.split('\n\n').map((paragraph, index) => {
|
||||
if (!paragraph.trim()) return null;
|
||||
|
||||
const content = paragraph.replace(/^[•\-\d.]\s*/, '');
|
||||
|
||||
return (
|
||||
<div key={index} className="mb-2 last:mb-0">
|
||||
<details className="group [&_summary::-webkit-details-marker]:hidden">
|
||||
<summary className="flex items-center cursor-pointer list-none text-sm text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white">
|
||||
<span className="arrow mr-2 inline-block transition-transform duration-200 group-open:rotate-90 group-open:self-start group-open:mt-1">▸</span>
|
||||
<p className="relative whitespace-normal line-clamp-1 group-open:line-clamp-none after:content-['...'] after:inline group-open:after:hidden transition-all duration-200 text-ellipsis overflow-hidden group-open:overflow-visible">
|
||||
{content}
|
||||
</p>
|
||||
</summary>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
size={20}
|
||||
/>
|
||||
<h3 className="text-black dark:text-white font-medium text-xl">
|
||||
Answer
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<Disc3
|
||||
className={cn(
|
||||
'text-black dark:text-white',
|
||||
isLast && loading ? 'animate-spin' : 'animate-none',
|
||||
)}
|
||||
size={20}
|
||||
/>
|
||||
<h3 className="text-black dark:text-white font-medium text-xl">
|
||||
Answer
|
||||
</h3>
|
||||
</div>
|
||||
<Markdown
|
||||
className={cn(
|
||||
'prose prose-h1:mb-3 prose-h2:mb-2 prose-h2:mt-6 prose-h2:font-[800] prose-h3:mt-4 prose-h3:mb-1.5 prose-h3:font-[600] dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 font-[400]',
|
||||
'max-w-none break-words text-black dark:text-white',
|
||||
)}
|
||||
>
|
||||
{parsedMessage}
|
||||
</Markdown>
|
||||
</div>
|
||||
<Markdown
|
||||
className={cn(
|
||||
'prose prose-h1:mb-3 prose-h2:mb-2 prose-h2:mt-6 prose-h2:font-[800] prose-h3:mt-4 prose-h3:mb-1.5 prose-h3:font-[600] dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 font-[400]',
|
||||
'max-w-none break-words text-black dark:text-white',
|
||||
)}
|
||||
>
|
||||
{parsedMessage}
|
||||
</Markdown>
|
||||
{loading && isLast ? null : (
|
||||
<div className="flex flex-row items-center justify-between w-full text-black dark:text-white py-4 -mx-2">
|
||||
<div className="flex flex-row items-center space-x-1">
|
||||
|
@ -110,7 +110,7 @@ const Attach = ({
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
type="file"
|
||||
@ -128,7 +128,7 @@ const Attach = ({
|
||||
setFiles([]);
|
||||
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} />
|
||||
<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">
|
||||
<File size={16} className="text-white/70" />
|
||||
</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.replace(/\.\w+$/, '').substring(0, 25) +
|
||||
'...' +
|
||||
|
@ -82,7 +82,7 @@ const AttachSmall = ({
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
type="file"
|
||||
@ -100,7 +100,7 @@ const AttachSmall = ({
|
||||
setFiles([]);
|
||||
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} />
|
||||
<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">
|
||||
<File size={16} className="text-white/70" />
|
||||
</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.replace(/\.\w+$/, '').substring(0, 25) +
|
||||
'...' +
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2018",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
Reference in New Issue
Block a user