mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-06-24 18:58:31 +00:00
Compare commits
11 Commits
docker-tes
...
6b72114010
Author | SHA1 | Date | |
---|---|---|---|
6b72114010 | |||
5cf0598985 | |||
b38413e92d | |||
0c908618c9 | |||
42fa951fd6 | |||
ece1a60f4d | |||
802627afcd | |||
f12c39f41e | |||
65692f1d52 | |||
c4f818f602 | |||
6edac6938c |
@ -37,6 +37,7 @@ services:
|
|||||||
args:
|
args:
|
||||||
- NEXT_PUBLIC_API_URL=http://127.0.0.1:3001/api
|
- NEXT_PUBLIC_API_URL=http://127.0.0.1:3001/api
|
||||||
- NEXT_PUBLIC_WS_URL=ws://127.0.0.1:3001
|
- NEXT_PUBLIC_WS_URL=ws://127.0.0.1:3001
|
||||||
|
network: host
|
||||||
image: itzcrazykns1337/perplexica-frontend:main
|
image: itzcrazykns1337/perplexica-frontend:main
|
||||||
depends_on:
|
depends_on:
|
||||||
- perplexica-backend
|
- perplexica-backend
|
||||||
|
6912
package-lock.json
generated
Normal file
6912
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -3,12 +3,28 @@ 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")
|
||||||
|
|
||||||
[API_KEYS]
|
[MODELS.OPENAI]
|
||||||
OPENAI = "" # OpenAI API key - sk-1234567890abcdef1234567890abcdef
|
API_KEY = ""
|
||||||
GROQ = "" # Groq API key - gsk_1234567890abcdef1234567890abcdef
|
|
||||||
ANTHROPIC = "" # Anthropic API key - sk-ant-1234567890abcdef1234567890abcdef
|
[MODELS.GROQ]
|
||||||
GEMINI = "" # Gemini API key - sk-1234567890abcdef1234567890abcdef
|
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_ENDPOINTS]
|
[API_ENDPOINTS]
|
||||||
SEARXNG = "http://localhost:32768" # SearxNG API URL
|
SEARXNG = "http://localhost:32768" # SearxNG API URL
|
||||||
OLLAMA = "" # Ollama API URL - http://host.docker.internal:11434
|
|
@ -10,15 +10,33 @@ interface Config {
|
|||||||
SIMILARITY_MEASURE: string;
|
SIMILARITY_MEASURE: string;
|
||||||
KEEP_ALIVE: string;
|
KEEP_ALIVE: string;
|
||||||
};
|
};
|
||||||
API_KEYS: {
|
MODELS: {
|
||||||
OPENAI: string;
|
OPENAI: {
|
||||||
GROQ: string;
|
API_KEY: string;
|
||||||
ANTHROPIC: string;
|
};
|
||||||
GEMINI: 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_ENDPOINTS: {
|
API_ENDPOINTS: {
|
||||||
SEARXNG: string;
|
SEARXNG: string;
|
||||||
OLLAMA: string;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,42 +56,63 @@ export const getSimilarityMeasure = () =>
|
|||||||
|
|
||||||
export const getKeepAlive = () => loadConfig().GENERAL.KEEP_ALIVE;
|
export const getKeepAlive = () => loadConfig().GENERAL.KEEP_ALIVE;
|
||||||
|
|
||||||
export const getOpenaiApiKey = () => loadConfig().API_KEYS.OPENAI;
|
export const getOpenaiApiKey = () => loadConfig().MODELS.OPENAI.API_KEY;
|
||||||
|
|
||||||
export const getGroqApiKey = () => loadConfig().API_KEYS.GROQ;
|
export const getGroqApiKey = () => loadConfig().MODELS.GROQ.API_KEY;
|
||||||
|
|
||||||
export const getAnthropicApiKey = () => loadConfig().API_KEYS.ANTHROPIC;
|
export const getAnthropicApiKey = () => loadConfig().MODELS.ANTHROPIC.API_KEY;
|
||||||
|
|
||||||
export const getGeminiApiKey = () => loadConfig().API_KEYS.GEMINI;
|
export const getGeminiApiKey = () => loadConfig().MODELS.GEMINI.API_KEY;
|
||||||
|
|
||||||
export const getSearxngApiEndpoint = () =>
|
export const getSearxngApiEndpoint = () =>
|
||||||
process.env.SEARXNG_API_URL || loadConfig().API_ENDPOINTS.SEARXNG;
|
process.env.SEARXNG_API_URL || loadConfig().API_ENDPOINTS.SEARXNG;
|
||||||
|
|
||||||
export const getOllamaApiEndpoint = () => loadConfig().API_ENDPOINTS.OLLAMA;
|
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;
|
||||||
|
|
||||||
|
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 updateConfig = (config: RecursivePartial<Config>) => {
|
export const updateConfig = (config: RecursivePartial<Config>) => {
|
||||||
const currentConfig = loadConfig();
|
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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(__dirname, `../${configFileName}`),
|
path.join(__dirname, `../${configFileName}`),
|
||||||
toml.stringify(config),
|
toml.stringify(mergedConfig),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
1
src/lib/chat/service.ts
Normal file
1
src/lib/chat/service.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
@ -19,6 +19,8 @@ class LineOutputParser extends BaseOutputParser<string> {
|
|||||||
lc_namespace = ['langchain', 'output_parsers', 'line_output_parser'];
|
lc_namespace = ['langchain', 'output_parsers', 'line_output_parser'];
|
||||||
|
|
||||||
async parse(text: string): Promise<string> {
|
async parse(text: string): Promise<string> {
|
||||||
|
text = text.trim() || '';
|
||||||
|
|
||||||
const regex = /^(\s*(-|\*|\d+\.\s|\d+\)\s|\u2022)\s*)+/;
|
const regex = /^(\s*(-|\*|\d+\.\s|\d+\)\s|\u2022)\s*)+/;
|
||||||
const startKeyIndex = text.indexOf(`<${this.key}>`);
|
const startKeyIndex = text.indexOf(`<${this.key}>`);
|
||||||
const endKeyIndex = text.indexOf(`</${this.key}>`);
|
const endKeyIndex = text.indexOf(`</${this.key}>`);
|
||||||
|
@ -19,11 +19,13 @@ class LineListOutputParser extends BaseOutputParser<string[]> {
|
|||||||
lc_namespace = ['langchain', 'output_parsers', 'line_list_output_parser'];
|
lc_namespace = ['langchain', 'output_parsers', 'line_list_output_parser'];
|
||||||
|
|
||||||
async parse(text: string): Promise<string[]> {
|
async parse(text: string): Promise<string[]> {
|
||||||
|
text = text.trim() || '';
|
||||||
|
|
||||||
const regex = /^(\s*(-|\*|\d+\.\s|\d+\)\s|\u2022)\s*)+/;
|
const regex = /^(\s*(-|\*|\d+\.\s|\d+\)\s|\u2022)\s*)+/;
|
||||||
const startKeyIndex = text.indexOf(`<${this.key}>`);
|
const startKeyIndex = text.indexOf(`<${this.key}>`);
|
||||||
const endKeyIndex = text.indexOf(`</${this.key}>`);
|
const endKeyIndex = text.indexOf(`</${this.key}>`);
|
||||||
|
|
||||||
if (startKeyIndex === -1 && endKeyIndex === -1) {
|
if (startKeyIndex === -1 || endKeyIndex === -1) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +36,22 @@ export const loadGeminiChatModels = async () => {
|
|||||||
apiKey: geminiApiKey,
|
apiKey: geminiApiKey,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
'gemini-2.0-flash-exp': {
|
||||||
|
displayName: 'Gemini 2.0 Flash Exp',
|
||||||
|
model: new ChatGoogleGenerativeAI({
|
||||||
|
modelName: 'gemini-2.0-flash-exp',
|
||||||
|
temperature: 0.7,
|
||||||
|
apiKey: geminiApiKey,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'gemini-2.0-flash-thinking-exp-01-21': {
|
||||||
|
displayName: 'Gemini 2.0 Flash Thinking Exp 01-21',
|
||||||
|
model: new ChatGoogleGenerativeAI({
|
||||||
|
modelName: 'gemini-2.0-flash-thinking-exp-01-21',
|
||||||
|
temperature: 0.7,
|
||||||
|
apiKey: geminiApiKey,
|
||||||
|
}),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return chatModels;
|
return chatModels;
|
||||||
|
@ -4,6 +4,13 @@ import { loadOpenAIChatModels, loadOpenAIEmbeddingsModels } from './openai';
|
|||||||
import { loadAnthropicChatModels } from './anthropic';
|
import { loadAnthropicChatModels } from './anthropic';
|
||||||
import { loadTransformersEmbeddingsModels } from './transformers';
|
import { loadTransformersEmbeddingsModels } from './transformers';
|
||||||
import { loadGeminiChatModels, loadGeminiEmbeddingsModels } from './gemini';
|
import { loadGeminiChatModels, loadGeminiEmbeddingsModels } from './gemini';
|
||||||
|
import { loadLMStudioChatModels, loadLMStudioEmbeddingsModels } from './lmstudio';
|
||||||
|
import {
|
||||||
|
getCustomOpenaiApiKey,
|
||||||
|
getCustomOpenaiApiUrl,
|
||||||
|
getCustomOpenaiModelName,
|
||||||
|
} from '../../config';
|
||||||
|
import { ChatOpenAI } from '@langchain/openai';
|
||||||
|
|
||||||
const chatModelProviders = {
|
const chatModelProviders = {
|
||||||
openai: loadOpenAIChatModels,
|
openai: loadOpenAIChatModels,
|
||||||
@ -11,6 +18,7 @@ const chatModelProviders = {
|
|||||||
ollama: loadOllamaChatModels,
|
ollama: loadOllamaChatModels,
|
||||||
anthropic: loadAnthropicChatModels,
|
anthropic: loadAnthropicChatModels,
|
||||||
gemini: loadGeminiChatModels,
|
gemini: loadGeminiChatModels,
|
||||||
|
lm_studio: loadLMStudioChatModels,
|
||||||
};
|
};
|
||||||
|
|
||||||
const embeddingModelProviders = {
|
const embeddingModelProviders = {
|
||||||
@ -18,6 +26,7 @@ const embeddingModelProviders = {
|
|||||||
local: loadTransformersEmbeddingsModels,
|
local: loadTransformersEmbeddingsModels,
|
||||||
ollama: loadOllamaEmbeddingsModels,
|
ollama: loadOllamaEmbeddingsModels,
|
||||||
gemini: loadGeminiEmbeddingsModels,
|
gemini: loadGeminiEmbeddingsModels,
|
||||||
|
lm_studio: loadLMStudioEmbeddingsModels,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAvailableChatModelProviders = async () => {
|
export const getAvailableChatModelProviders = async () => {
|
||||||
|
89
src/lib/providers/lmstudio.ts
Normal file
89
src/lib/providers/lmstudio.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { OpenAIEmbeddings } from '@langchain/openai';
|
||||||
|
import { ChatOpenAI } from '@langchain/openai';
|
||||||
|
import { getKeepAlive, getLMStudioApiEndpoint } from '../../config';
|
||||||
|
import logger from '../../utils/logger';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface LMStudioModel {
|
||||||
|
id: string;
|
||||||
|
// add other properties if LM Studio API provides them
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatModelConfig {
|
||||||
|
displayName: string;
|
||||||
|
model: ChatOpenAI;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadLMStudioChatModels = async (): Promise<Record<string, ChatModelConfig>> => {
|
||||||
|
const lmStudioEndpoint = getLMStudioApiEndpoint();
|
||||||
|
|
||||||
|
if (!lmStudioEndpoint) {
|
||||||
|
logger.debug('LM Studio endpoint not configured, skipping');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get<{ data: LMStudioModel[] }>(`${lmStudioEndpoint}/models`, {
|
||||||
|
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: 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 {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${lmStudioEndpoint}/models`, {
|
||||||
|
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: lmStudioEndpoint,
|
||||||
|
},
|
||||||
|
modelName: model.id,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return embeddingsModels;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Error loading LM Studio embeddings model: ${err}`);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
@ -6,10 +6,14 @@ import {
|
|||||||
import {
|
import {
|
||||||
getGroqApiKey,
|
getGroqApiKey,
|
||||||
getOllamaApiEndpoint,
|
getOllamaApiEndpoint,
|
||||||
|
getLMStudioApiEndpoint,
|
||||||
getAnthropicApiKey,
|
getAnthropicApiKey,
|
||||||
getGeminiApiKey,
|
getGeminiApiKey,
|
||||||
getOpenaiApiKey,
|
getOpenaiApiKey,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
|
getCustomOpenaiApiUrl,
|
||||||
|
getCustomOpenaiApiKey,
|
||||||
|
getCustomOpenaiModelName,
|
||||||
} from '../config';
|
} from '../config';
|
||||||
import logger from '../utils/logger';
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
@ -51,9 +55,13 @@ router.get('/', async (_, res) => {
|
|||||||
|
|
||||||
config['openaiApiKey'] = getOpenaiApiKey();
|
config['openaiApiKey'] = getOpenaiApiKey();
|
||||||
config['ollamaApiUrl'] = getOllamaApiEndpoint();
|
config['ollamaApiUrl'] = getOllamaApiEndpoint();
|
||||||
|
config['lmStudioApiUrl'] = getLMStudioApiEndpoint();
|
||||||
config['anthropicApiKey'] = getAnthropicApiKey();
|
config['anthropicApiKey'] = getAnthropicApiKey();
|
||||||
config['groqApiKey'] = getGroqApiKey();
|
config['groqApiKey'] = getGroqApiKey();
|
||||||
config['geminiApiKey'] = getGeminiApiKey();
|
config['geminiApiKey'] = getGeminiApiKey();
|
||||||
|
config['customOpenaiApiUrl'] = getCustomOpenaiApiUrl();
|
||||||
|
config['customOpenaiApiKey'] = getCustomOpenaiApiKey();
|
||||||
|
config['customOpenaiModelName'] = getCustomOpenaiModelName()
|
||||||
|
|
||||||
res.status(200).json(config);
|
res.status(200).json(config);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@ -66,14 +74,30 @@ router.post('/', async (req, res) => {
|
|||||||
const config = req.body;
|
const config = req.body;
|
||||||
|
|
||||||
const updatedConfig = {
|
const updatedConfig = {
|
||||||
API_KEYS: {
|
MODELS: {
|
||||||
OPENAI: config.openaiApiKey,
|
OPENAI: {
|
||||||
GROQ: config.groqApiKey,
|
API_KEY: config.openaiApiKey,
|
||||||
ANTHROPIC: config.anthropicApiKey,
|
},
|
||||||
GEMINI: config.geminiApiKey,
|
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_ENDPOINTS: {
|
|
||||||
OLLAMA: config.ollamaApiUrl,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,18 +1,27 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["ESNext"],
|
"target": "es2018",
|
||||||
"module": "Node16",
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"moduleResolution": "Node16",
|
"allowJs": true,
|
||||||
"target": "ESNext",
|
|
||||||
"outDir": "dist",
|
|
||||||
"sourceMap": false,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"skipDefaultLibCheck": true
|
"strict": true,
|
||||||
},
|
"noEmit": true,
|
||||||
"include": ["src"],
|
"esModuleInterop": true,
|
||||||
"exclude": ["node_modules", "**/*.spec.ts"]
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,8 @@ import {
|
|||||||
StopCircle,
|
StopCircle,
|
||||||
Layers3,
|
Layers3,
|
||||||
Plus,
|
Plus,
|
||||||
|
Brain,
|
||||||
|
ChevronDown,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import Markdown from 'markdown-to-jsx';
|
import Markdown from 'markdown-to-jsx';
|
||||||
import Copy from './MessageActions/Copy';
|
import Copy from './MessageActions/Copy';
|
||||||
@ -41,26 +43,58 @@ const MessageBox = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [parsedMessage, setParsedMessage] = useState(message.content);
|
const [parsedMessage, setParsedMessage] = useState(message.content);
|
||||||
const [speechMessage, setSpeechMessage] = 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(() => {
|
useEffect(() => {
|
||||||
const regex = /\[(\d+)\]/g;
|
const regex = /\[(\d+)\]/g;
|
||||||
|
const thinkRegex = /<think>(.*?)(?:<\/think>|$)(.*)/s;
|
||||||
|
|
||||||
if (
|
// Check for thinking content, including partial tags
|
||||||
message.role === 'assistant' &&
|
const match = message.content.match(thinkRegex);
|
||||||
message?.sources &&
|
if (match) {
|
||||||
message.sources.length > 0
|
const [_, thinkingContent, answerContent] = match;
|
||||||
) {
|
|
||||||
return setParsedMessage(
|
// 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(
|
message.content.replace(
|
||||||
regex,
|
regex,
|
||||||
(_, number) =>
|
(_, 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>`,
|
`<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 {
|
||||||
|
|
||||||
setSpeechMessage(message.content.replace(regex, ''));
|
|
||||||
setParsedMessage(message.content);
|
setParsedMessage(message.content);
|
||||||
|
}
|
||||||
|
setSpeechMessage(message.content.replace(regex, ''));
|
||||||
|
}
|
||||||
}, [message.content, message.sources, message.role]);
|
}, [message.content, message.sources, message.role]);
|
||||||
|
|
||||||
const { speechStatus, start, stop } = useSpeech({ text: speechMessage });
|
const { speechStatus, start, stop } = useSpeech({ text: speechMessage });
|
||||||
@ -92,6 +126,49 @@ const MessageBox = ({
|
|||||||
<MessageSources sources={message.sources} />
|
<MessageSources sources={message.sources} />
|
||||||
</div>
|
</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-col space-y-2">
|
||||||
<div className="flex flex-row items-center space-x-2">
|
<div className="flex flex-row items-center space-x-2">
|
||||||
<Disc3
|
<Disc3
|
||||||
@ -113,6 +190,7 @@ const MessageBox = ({
|
|||||||
>
|
>
|
||||||
{parsedMessage}
|
{parsedMessage}
|
||||||
</Markdown>
|
</Markdown>
|
||||||
|
</div>
|
||||||
{loading && isLast ? null : (
|
{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 justify-between w-full text-black dark:text-white py-4 -mx-2">
|
||||||
<div className="flex flex-row items-center space-x-1">
|
<div className="flex flex-row items-center space-x-1">
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"langchain": "^0.1.30",
|
"langchain": "^0.1.30",
|
||||||
"lucide-react": "^0.363.0",
|
"lucide-react": "^0.363.0",
|
||||||
"markdown-to-jsx": "^7.6.2",
|
"markdown-to-jsx": "^7.7.2",
|
||||||
"next": "14.1.4",
|
"next": "14.1.4",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
|
@ -2210,10 +2210,10 @@ lucide-react@^0.363.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.363.0.tgz#2bb1f9d09b830dda86f5118fcd097f87247fe0e3"
|
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.363.0.tgz#2bb1f9d09b830dda86f5118fcd097f87247fe0e3"
|
||||||
integrity sha512-AlsfPCsXQyQx7wwsIgzcKOL9LwC498LIMAo+c0Es5PkHJa33xwmYAkkSoKoJWWWSYQEStqu58/jT4tL2gi32uQ==
|
integrity sha512-AlsfPCsXQyQx7wwsIgzcKOL9LwC498LIMAo+c0Es5PkHJa33xwmYAkkSoKoJWWWSYQEStqu58/jT4tL2gi32uQ==
|
||||||
|
|
||||||
markdown-to-jsx@^7.6.2:
|
markdown-to-jsx@^7.7.2:
|
||||||
version "7.6.2"
|
version "7.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.6.2.tgz#254cbf7d412a37073486c0a2dd52266d2191a793"
|
resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.7.2.tgz#59c1dd64f48b53719311ab140be3cd51cdabccd3"
|
||||||
integrity sha512-gEcyiJXzBxmId2Y/kydLbD6KRNccDiUy/Src1cFGn3s2X0LZZ/hUiEc2VisFyA5kUE3SXclTCczjQiAuqKZiFQ==
|
integrity sha512-N3AKfYRvxNscvcIH6HDnDKILp4S8UWbebp+s92Y8SwIq0CuSbLW4Jgmrbjku3CWKjTQO0OyIMS6AhzqrwjEa3g==
|
||||||
|
|
||||||
md5@^2.3.0:
|
md5@^2.3.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
|
Reference in New Issue
Block a user