mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-09-15 22:01:33 +00:00
Compare commits
5 Commits
6b72114010
...
303992c82c
Author | SHA1 | Date | |
---|---|---|---|
|
303992c82c | ||
|
7ec201d011 | ||
|
3582695054 | ||
|
46541e6c0c | ||
|
f37686189e |
@@ -37,7 +37,6 @@ 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
|
||||
|
6912
package-lock.json
generated
6912
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,28 +3,12 @@ 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")
|
||||
|
||||
[MODELS.OPENAI]
|
||||
API_KEY = ""
|
||||
|
||||
[MODELS.GROQ]
|
||||
API_KEY = ""
|
||||
|
||||
[MODELS.ANTHROPIC]
|
||||
API_KEY = ""
|
||||
|
||||
[MODELS.GEMINI]
|
||||
API_KEY = ""
|
||||
|
||||
[MODELS.OLLAMA]
|
||||
API_URL = "" # Ollama API URL - http://host.docker.internal:11434
|
||||
|
||||
[MODELS.LMSTUDIO]
|
||||
API_URL = "" # LM STUDIO API URL - http://host.docker.internal:1234/v1
|
||||
|
||||
[MODELS.CUSTOM_OPENAI]
|
||||
API_KEY = ""
|
||||
API_URL = ""
|
||||
MODEL_NAME = ""
|
||||
[API_KEYS]
|
||||
OPENAI = "" # OpenAI API key - sk-1234567890abcdef1234567890abcdef
|
||||
GROQ = "" # Groq API key - gsk_1234567890abcdef1234567890abcdef
|
||||
ANTHROPIC = "" # Anthropic API key - sk-ant-1234567890abcdef1234567890abcdef
|
||||
GEMINI = "" # Gemini API key - sk-1234567890abcdef1234567890abcdef
|
||||
|
||||
[API_ENDPOINTS]
|
||||
SEARXNG = "http://localhost:32768" # SearxNG API URL
|
||||
OLLAMA = "" # Ollama API URL - http://host.docker.internal:11434
|
123
src/config.ts
123
src/config.ts
@@ -10,32 +10,15 @@ interface Config {
|
||||
SIMILARITY_MEASURE: string;
|
||||
KEEP_ALIVE: string;
|
||||
};
|
||||
MODELS: {
|
||||
OPENAI: {
|
||||
API_KEY: string;
|
||||
};
|
||||
GROQ: {
|
||||
API_KEY: string;
|
||||
};
|
||||
ANTHROPIC: {
|
||||
API_KEY: string;
|
||||
};
|
||||
GEMINI: {
|
||||
API_KEY: string;
|
||||
};
|
||||
OLLAMA: {
|
||||
API_URL: string;
|
||||
};
|
||||
LMSTUDIO: {
|
||||
API_URL: string;
|
||||
};
|
||||
CUSTOM_OPENAI: {
|
||||
API_URL: string;
|
||||
API_KEY: string;
|
||||
MODEL_NAME: string;
|
||||
};
|
||||
API_KEYS: {
|
||||
OPENAI: string;
|
||||
GROQ: string;
|
||||
ANTHROPIC: string;
|
||||
GEMINI: string;
|
||||
};
|
||||
API_ENDPOINTS: {
|
||||
API_ENDPOINTS: {
|
||||
OLLAMA: string;
|
||||
LMSTUDIO: string;
|
||||
SEARXNG: string;
|
||||
};
|
||||
}
|
||||
@@ -56,63 +39,65 @@ export const getSimilarityMeasure = () =>
|
||||
|
||||
export const getKeepAlive = () => loadConfig().GENERAL.KEEP_ALIVE;
|
||||
|
||||
export const getOpenaiApiKey = () => loadConfig().MODELS.OPENAI.API_KEY;
|
||||
export const getOpenaiApiKey = () => loadConfig().API_KEYS.OPENAI;
|
||||
|
||||
export const getGroqApiKey = () => loadConfig().MODELS.GROQ.API_KEY;
|
||||
export const getGroqApiKey = () => loadConfig().API_KEYS.GROQ;
|
||||
|
||||
export const getAnthropicApiKey = () => loadConfig().MODELS.ANTHROPIC.API_KEY;
|
||||
export const getAnthropicApiKey = () => loadConfig().API_KEYS.ANTHROPIC;
|
||||
|
||||
export const getGeminiApiKey = () => loadConfig().MODELS.GEMINI.API_KEY;
|
||||
export const getGeminiApiKey = () => loadConfig().API_KEYS.GEMINI;
|
||||
|
||||
export const getSearxngApiEndpoint = () =>
|
||||
process.env.SEARXNG_API_URL || loadConfig().API_ENDPOINTS.SEARXNG;
|
||||
|
||||
export const getOllamaApiEndpoint = () => loadConfig().MODELS.OLLAMA.API_URL;
|
||||
export const getOllamaApiEndpoint = () => loadConfig().API_ENDPOINTS.OLLAMA;
|
||||
|
||||
export const getLMStudioApiEndpoint = () => loadConfig().MODELS.LMSTUDIO.API_URL;
|
||||
|
||||
export const getCustomOpenaiApiKey = () =>
|
||||
loadConfig().MODELS.CUSTOM_OPENAI.API_KEY;
|
||||
|
||||
export const getCustomOpenaiApiUrl = () =>
|
||||
loadConfig().MODELS.CUSTOM_OPENAI.API_URL;
|
||||
|
||||
export const getCustomOpenaiModelName = () =>
|
||||
loadConfig().MODELS.CUSTOM_OPENAI.MODEL_NAME;
|
||||
|
||||
const mergeConfigs = (current: any, update: any): any => {
|
||||
if (typeof current !== 'object' || current === null) {
|
||||
return update;
|
||||
}
|
||||
|
||||
const result = { ...current };
|
||||
|
||||
for (const key in update) {
|
||||
if (Object.prototype.hasOwnProperty.call(update, key)) {
|
||||
const updateValue = update[key];
|
||||
|
||||
if (
|
||||
typeof updateValue === 'object' &&
|
||||
updateValue !== null &&
|
||||
typeof result[key] === 'object' &&
|
||||
result[key] !== null
|
||||
) {
|
||||
result[key] = mergeConfigs(result[key], updateValue);
|
||||
} else if (updateValue !== undefined) {
|
||||
result[key] = updateValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
export const getLMStudioApiEndpoint = () => loadConfig().API_ENDPOINTS.LMSTUDIO;
|
||||
|
||||
export const updateConfig = (config: RecursivePartial<Config>) => {
|
||||
const currentConfig = loadConfig();
|
||||
const mergedConfig = mergeConfigs(currentConfig, config);
|
||||
|
||||
for (const key in currentConfig) {
|
||||
if (!config[key]) config[key] = {};
|
||||
|
||||
if (typeof currentConfig[key] === 'object' && currentConfig[key] !== null) {
|
||||
for (const nestedKey in currentConfig[key]) {
|
||||
if (
|
||||
!config[key][nestedKey] &&
|
||||
currentConfig[key][nestedKey] &&
|
||||
config[key][nestedKey] !== ''
|
||||
) {
|
||||
config[key][nestedKey] = currentConfig[key][nestedKey];
|
||||
}
|
||||
}
|
||||
} else if (currentConfig[key] && config[key] !== '') {
|
||||
config[key] = currentConfig[key];
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
export const updateConfig = (config: RecursivePartial<Config>) => {
|
||||
const currentConfig = loadConfig();
|
||||
|
||||
// Merge existing config with new values
|
||||
const mergedConfig: RecursivePartial<Config> = {
|
||||
GENERAL: {
|
||||
...currentConfig.GENERAL,
|
||||
...config.GENERAL,
|
||||
},
|
||||
API_KEYS: {
|
||||
...currentConfig.API_KEYS,
|
||||
...config.API_KEYS,
|
||||
},
|
||||
API_ENDPOINTS: {
|
||||
...currentConfig.API_ENDPOINTS,
|
||||
...config.API_ENDPOINTS,
|
||||
},
|
||||
};
|
||||
*/
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, `../${configFileName}`),
|
||||
toml.stringify(mergedConfig),
|
||||
toml.stringify(config),
|
||||
);
|
||||
};
|
||||
|
@@ -1 +0,0 @@
|
||||
|
@@ -5,12 +5,6 @@ import { loadAnthropicChatModels } from './anthropic';
|
||||
import { loadTransformersEmbeddingsModels } from './transformers';
|
||||
import { loadGeminiChatModels, loadGeminiEmbeddingsModels } from './gemini';
|
||||
import { loadLMStudioChatModels, loadLMStudioEmbeddingsModels } from './lmstudio';
|
||||
import {
|
||||
getCustomOpenaiApiKey,
|
||||
getCustomOpenaiApiUrl,
|
||||
getCustomOpenaiModelName,
|
||||
} from '../../config';
|
||||
import { ChatOpenAI } from '@langchain/openai';
|
||||
|
||||
const chatModelProviders = {
|
||||
openai: loadOpenAIChatModels,
|
||||
|
@@ -11,9 +11,6 @@ import {
|
||||
getGeminiApiKey,
|
||||
getOpenaiApiKey,
|
||||
updateConfig,
|
||||
getCustomOpenaiApiUrl,
|
||||
getCustomOpenaiApiKey,
|
||||
getCustomOpenaiModelName,
|
||||
} from '../config';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
@@ -59,9 +56,6 @@ router.get('/', async (_, res) => {
|
||||
config['anthropicApiKey'] = getAnthropicApiKey();
|
||||
config['groqApiKey'] = getGroqApiKey();
|
||||
config['geminiApiKey'] = getGeminiApiKey();
|
||||
config['customOpenaiApiUrl'] = getCustomOpenaiApiUrl();
|
||||
config['customOpenaiApiKey'] = getCustomOpenaiApiKey();
|
||||
config['customOpenaiModelName'] = getCustomOpenaiModelName()
|
||||
|
||||
res.status(200).json(config);
|
||||
} catch (err: any) {
|
||||
@@ -74,30 +68,15 @@ router.post('/', async (req, res) => {
|
||||
const config = req.body;
|
||||
|
||||
const updatedConfig = {
|
||||
MODELS: {
|
||||
OPENAI: {
|
||||
API_KEY: config.openaiApiKey,
|
||||
},
|
||||
GROQ: {
|
||||
API_KEY: config.groqApiKey,
|
||||
},
|
||||
ANTHROPIC: {
|
||||
API_KEY: config.anthropicApiKey,
|
||||
},
|
||||
GEMINI: {
|
||||
API_KEY: config.geminiApiKey,
|
||||
},
|
||||
OLLAMA: {
|
||||
API_URL: config.ollamaApiUrl,
|
||||
},
|
||||
LMSTUDIO: {
|
||||
API_URL: config.lmStudioApiUrl,
|
||||
},
|
||||
CUSTOM_OPENAI: {
|
||||
API_URL: config.customOpenaiApiUrl,
|
||||
API_KEY: config.customOpenaiApiKey,
|
||||
MODEL_NAME: config.customOpenaiModelName,
|
||||
},
|
||||
API_KEYS: {
|
||||
OPENAI: config.openaiApiKey,
|
||||
GROQ: config.groqApiKey,
|
||||
ANTHROPIC: config.anthropicApiKey,
|
||||
GEMINI: config.geminiApiKey,
|
||||
},
|
||||
API_ENDPOINTS: {
|
||||
OLLAMA: config.ollamaApiUrl,
|
||||
LMSTUDIO: config.lmStudioApiUrl,
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -1,27 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2018",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"lib": ["ESNext"],
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"target": "ESNext",
|
||||
"outDir": "dist",
|
||||
"sourceMap": false,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "**/*.spec.ts"]
|
||||
}
|
||||
|
@@ -49,37 +49,27 @@ const MessageBox = ({
|
||||
|
||||
useEffect(() => {
|
||||
const regex = /\[(\d+)\]/g;
|
||||
const thinkRegex = /<think>(.*?)(?:<\/think>|$)(.*)/s;
|
||||
|
||||
// Check for thinking content, including partial tags
|
||||
const match = message.content.match(thinkRegex);
|
||||
// First check for thinking content
|
||||
const match = message.content.match(/<think>(.*?)<\/think>(.*)/s);
|
||||
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
|
||||
}
|
||||
setThinking(thinkingContent.trim());
|
||||
setAnswer(answerContent.trim());
|
||||
|
||||
// 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, ''));
|
||||
// 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) {
|
||||
@@ -115,6 +105,37 @@ const MessageBox = ({
|
||||
ref={dividerRef}
|
||||
className="flex flex-col space-y-6 w-full lg:w-9/12"
|
||||
>
|
||||
{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">View Thinking</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">
|
||||
<Markdown
|
||||
className={cn(
|
||||
'prose dark:prose-invert text-sm leading-relaxed',
|
||||
'max-w-none break-words'
|
||||
)}
|
||||
>
|
||||
{thinking}
|
||||
</Markdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{message.sources && message.sources.length > 0 && (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
@@ -126,71 +147,27 @@ const MessageBox = ({
|
||||
<MessageSources sources={message.sources} />
|
||||
</div>
|
||||
)}
|
||||
<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>
|
||||
)}
|
||||
</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
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<Disc3
|
||||
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',
|
||||
'text-black dark:text-white',
|
||||
isLast && loading ? 'animate-spin' : 'animate-none',
|
||||
)}
|
||||
>
|
||||
{parsedMessage}
|
||||
</Markdown>
|
||||
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>
|
||||
{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">
|
||||
@@ -277,4 +254,3 @@ const MessageBox = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageBox;
|
||||
|
Reference in New Issue
Block a user