This commit is contained in:
haddadrm 2025-02-07 15:38:49 +08:00 committed by GitHub
commit 0d7d5940c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 191 additions and 18 deletions

View file

@ -17,8 +17,9 @@ interface Config {
GEMINI: string; GEMINI: string;
}; };
API_ENDPOINTS: { API_ENDPOINTS: {
SEARXNG: string;
OLLAMA: string; OLLAMA: string;
LMSTUDIO: string;
SEARXNG: string;
}; };
} }
@ -51,6 +52,8 @@ export const getSearxngApiEndpoint = () =>
export const getOllamaApiEndpoint = () => loadConfig().API_ENDPOINTS.OLLAMA; export const getOllamaApiEndpoint = () => loadConfig().API_ENDPOINTS.OLLAMA;
export const getLMStudioApiEndpoint = () => loadConfig().API_ENDPOINTS.LMSTUDIO;
export const updateConfig = (config: RecursivePartial<Config>) => { export const updateConfig = (config: RecursivePartial<Config>) => {
const currentConfig = loadConfig(); const currentConfig = loadConfig();
@ -72,6 +75,27 @@ export const updateConfig = (config: RecursivePartial<Config>) => {
} }
} }
/*
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( fs.writeFileSync(
path.join(__dirname, `../${configFileName}`), path.join(__dirname, `../${configFileName}`),
toml.stringify(config), toml.stringify(config),

View file

@ -4,6 +4,7 @@ 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';
const chatModelProviders = { const chatModelProviders = {
openai: loadOpenAIChatModels, openai: loadOpenAIChatModels,
@ -11,6 +12,7 @@ const chatModelProviders = {
ollama: loadOllamaChatModels, ollama: loadOllamaChatModels,
anthropic: loadAnthropicChatModels, anthropic: loadAnthropicChatModels,
gemini: loadGeminiChatModels, gemini: loadGeminiChatModels,
lm_studio: loadLMStudioChatModels,
}; };
const embeddingModelProviders = { const embeddingModelProviders = {
@ -18,6 +20,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 () => {

View 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 {};
}
};

View file

@ -6,6 +6,7 @@ import {
import { import {
getGroqApiKey, getGroqApiKey,
getOllamaApiEndpoint, getOllamaApiEndpoint,
getLMStudioApiEndpoint,
getAnthropicApiKey, getAnthropicApiKey,
getGeminiApiKey, getGeminiApiKey,
getOpenaiApiKey, getOpenaiApiKey,
@ -51,6 +52,7 @@ 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();
@ -74,6 +76,7 @@ router.post('/', async (req, res) => {
}, },
API_ENDPOINTS: { API_ENDPOINTS: {
OLLAMA: config.ollamaApiUrl, OLLAMA: config.ollamaApiUrl,
LMSTUDIO: config.lmStudioApiUrl,
}, },
}; };

View file

@ -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,48 @@ 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;
if ( // First check for thinking content
message.role === 'assistant' && const match = message.content.match(/<think>(.*?)<\/think>(.*)/s);
message?.sources && if (match) {
message.sources.length > 0 const [_, thinkingContent, answerContent] = match;
) { setThinking(thinkingContent.trim());
return setParsedMessage( 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 });
@ -81,6 +105,37 @@ const MessageBox = ({
ref={dividerRef} ref={dividerRef}
className="flex flex-col space-y-6 w-full lg:w-9/12" 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 && ( {message.sources && message.sources.length > 0 && (
<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">
@ -199,4 +254,3 @@ const MessageBox = ({
); );
}; };
export default MessageBox;