Add DeepSeek and LMStudio providers

- Integrate DeepSeek and LMStudio AI providers
- Add message processing utilities for improved handling
- Implement reasoning panel for message actions
- Add logging functionality to UI
- Update configurations and dependencies
This commit is contained in:
haddadrm
2025-02-25 08:53:53 +04:00
parent 4d24d73161
commit a6e4402616
18 changed files with 8270 additions and 592 deletions

View File

@@ -19,6 +19,7 @@ interface SettingsType {
groqApiKey: string;
anthropicApiKey: string;
geminiApiKey: string;
deepseekApiKey: string;
ollamaApiUrl: string;
customOpenaiApiKey: string;
customOpenaiApiUrl: string;
@@ -791,6 +792,25 @@ const Page = () => {
onSave={(value) => saveConfig('geminiApiKey', value)}
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
DeepSeek API Key
</p>
<Input
type="text"
placeholder="DeepSeek API key"
value={config.deepseekApiKey}
isSaving={savingStates['deepseekApiKey']}
onChange={(e) => {
setConfig((prev) => ({
...prev!,
deepseekApiKey: e.target.value,
}));
}}
onSave={(value) => saveConfig('deepseekApiKey', value)}
/>
</div>
</div>
</SettingsSection>
</div>

View File

@@ -0,0 +1,120 @@
'use client';
import * as React from 'react';
import { Brain, ChevronDown, Maximize2, Minimize2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import Markdown from 'markdown-to-jsx';
import logger from '@/lib/logger';
interface ReasoningPanelProps {
thinking: string;
className?: string;
isExpanded?: boolean;
}
const ReasoningPanel = ({ thinking, className, isExpanded: propExpanded }: ReasoningPanelProps): React.ReactElement => {
const [isExpanded, setIsExpanded] = React.useState(true);
const [detailsRefs, setDetailsRefs] = React.useState<HTMLDetailsElement[]>([]);
logger.info('ReasoningPanel rendering with:', {
thinking: thinking,
isExpanded: propExpanded,
detailsRefsCount: detailsRefs.length
});
React.useEffect(() => {
if (propExpanded !== undefined) {
logger.info('Updating expansion state:', propExpanded);
setIsExpanded(propExpanded);
}
}, [propExpanded]);
const addDetailsRef = React.useCallback((element: HTMLDetailsElement | null) => {
if (element) {
setDetailsRefs(refs => {
if (!refs.includes(element)) {
logger.info('Adding new details ref');
return [...refs, element];
}
return refs;
});
}
}, []);
const expandAll = () => {
logger.info('Expanding all details');
detailsRefs.forEach(ref => ref.open = true);
};
const collapseAll = () => {
logger.info('Collapsing all details');
detailsRefs.forEach(ref => ref.open = false);
};
return (
<div className={cn("flex flex-col space-y-2 mb-4", className)}>
<button
onClick={() => setIsExpanded(!isExpanded)}
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"
type="button"
>
<Brain size={20} />
<h3 className="font-medium text-xl">Reasoning</h3>
<ChevronDown
size={16}
className={cn(
"transition-transform duration-200",
isExpanded ? "rotate-180" : ""
)}
/>
</button>
{isExpanded && (
<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;
// Extract content without the bullet prefix
const content = paragraph.replace(/^[•\-\d.]\s*/, '');
logger.info(`Processing paragraph ${index}:`, content);
return (
<div key={index} className="mb-2 last:mb-0">
<details
ref={addDetailsRef}
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>
{/* Content is shown in the summary when expanded - no need to render it again */}
</details>
</div>
);
})}
<div className="flex justify-end space-x-2 mt-4 text-sm text-black/70 dark:text-white/70">
<button
onClick={expandAll}
className="flex items-center space-x-1 hover:text-[#24A0ED] transition-colors"
>
<Maximize2 size={10} />
<span className="text-xs">Expand all</span>
</button>
<span></span>
<button
onClick={collapseAll}
className="flex items-center space-x-1 hover:text-[#24A0ED] transition-colors"
>
<Minimize2 size={10} />
<span className="text-xs">Collapse all</span>
</button>
</div>
</div>
)}
</div>
);
};
export default ReasoningPanel;

View File

@@ -4,6 +4,7 @@
import React, { MutableRefObject, useEffect, useState } from 'react';
import { Message } from './ChatWindow';
import { cn } from '@/lib/utils';
import logger from '@/lib/logger';
import {
BookCopy,
Disc3,
@@ -12,6 +13,7 @@ import {
Layers3,
Plus,
} from 'lucide-react';
import ReasoningPanel from './MessageActions/ReasoningPanel';
import Markdown from 'markdown-to-jsx';
import Copy from './MessageActions/Copy';
import Rewrite from './MessageActions/Rewrite';
@@ -41,26 +43,66 @@ const MessageBox = ({
}) => {
const [parsedMessage, setParsedMessage] = useState(message.content);
const [speechMessage, setSpeechMessage] = useState(message.content);
const [thinking, setThinking] = useState<string>('');
const [isThinkingExpanded, setIsThinkingExpanded] = useState(true);
useEffect(() => {
logger.info(`Processing message:`, {
content: message.content,
role: message.role,
messageId: message.messageId
});
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);
logger.info(`Think tag match:`, match);
if (match) {
const [_, thinkingContent, answerContent] = match;
// Set thinking content even if </think> hasn't appeared yet
if (thinkingContent) {
logger.info(`Found thinking content:`, thinkingContent.trim());
setThinking(thinkingContent.trim());
setIsThinkingExpanded(true); // Expand when thinking starts
}
// Only set answer content if we have it (after </think>)
if (answerContent) {
logger.info(`Found answer content:`, answerContent.trim());
setIsThinkingExpanded(false); // Collapse when thinking is done
// 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 });
@@ -81,6 +123,7 @@ const MessageBox = ({
ref={dividerRef}
className="flex flex-col space-y-6 w-full lg:w-9/12"
>
{thinking && <ReasoningPanel thinking={thinking} isExpanded={isThinkingExpanded} />}
{message.sources && message.sources.length > 0 && (
<div className="flex flex-col space-y-2">
<div className="flex flex-row items-center space-x-2">

13
ui/lib/logger.ts Normal file
View File

@@ -0,0 +1,13 @@
const logger = {
info: (...args: any[]) => {
console.log('[INFO]', ...args);
},
warn: (...args: any[]) => {
console.warn('[WARN]', ...args);
},
error: (...args: any[]) => {
console.error('[ERROR]', ...args);
}
};
export default logger;

6961
ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
{
"compilerOptions": {
"target": "es2018",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,

File diff suppressed because it is too large Load Diff