feat: implemented the v1 browser embedded LLM support (Gemma 4 E4B based)
This commit is contained in:
@@ -3,7 +3,8 @@ import './ChatBox.css';
|
||||
import { FaPlus, FaBan, FaDownload } from 'react-icons/fa';
|
||||
import { UserMessage, AssistantMessage } from './chat';
|
||||
import { AgentCore } from '../agent/core/AgentCore';
|
||||
import { LLMProvider } from '../agent/llm/LLMProvider';
|
||||
import { OpenAICompatibleLLMProvider, type LLMProvider } from '../agent/llm/LLMProvider';
|
||||
import { LocalBrowserLLMProvider } from '../agent/llm/LocalBrowserLLMProvider';
|
||||
import { ConfigManager } from '../core/config/ConfigManager';
|
||||
import { useProjectStore } from '../stores/projectStore';
|
||||
import { SystemPrompts } from '../agent/core/SystemPrompts';
|
||||
@@ -13,6 +14,8 @@ import { useStreamProcessor } from '../hooks/useStreamProcessor';
|
||||
import { createMessage, addWelcomeMessage } from '../utils/chatMessageUtils';
|
||||
import { formatLocalDateTime } from '../util/timeUtil';
|
||||
import { downloadBlob, buildTimestampSuffix } from '../util/miscUtil';
|
||||
import { LocalLLMModelManager, type LocalLLMModelState } from '../util/localLLMModelManager';
|
||||
import { LOCAL_LLM_DISPLAY_NAME, LOCAL_LLM_PROVIDER_KEY } from '../util/localLLMConfig';
|
||||
import KGDropdown from './common/KGDropdown';
|
||||
|
||||
import type { ChatMessage } from '../types/projectTypes';
|
||||
@@ -32,6 +35,8 @@ const createLLMProviderFromConfig = (): LLMProvider => {
|
||||
let baseURL: string | undefined;
|
||||
|
||||
switch (providerType) {
|
||||
case LOCAL_LLM_PROVIDER_KEY:
|
||||
return new LocalBrowserLLMProvider();
|
||||
case 'openai':
|
||||
apiKey = configManager.get('general.openai.api_key') as string;
|
||||
model = configManager.get('general.openai.model') as string;
|
||||
@@ -50,7 +55,7 @@ const createLLMProviderFromConfig = (): LLMProvider => {
|
||||
break;
|
||||
}
|
||||
|
||||
return new LLMProvider(apiKey, model, baseURL);
|
||||
return new OpenAICompatibleLLMProvider(apiKey, model, baseURL);
|
||||
};
|
||||
|
||||
interface ChatBoxProps {
|
||||
@@ -64,6 +69,8 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [lastUserMessage, setLastUserMessage] = useState<string>('');
|
||||
const [localModelState, setLocalModelState] = useState<LocalLLMModelState>(LocalLLMModelManager.getState());
|
||||
const [activeProvider, setActiveProvider] = useState<string>('openai');
|
||||
|
||||
// Track if this is the first message (for system prompt logging)
|
||||
const [isFirstMessage, setIsFirstMessage] = useState(true);
|
||||
@@ -180,9 +187,11 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
||||
}
|
||||
|
||||
const applyProviderFromConfig = () => {
|
||||
const providerType = (configManager.get('general.llm_provider') as string) || 'openai';
|
||||
const provider = createLLMProviderFromConfig();
|
||||
const agentCore = AgentCore.instance();
|
||||
agentCore.setLLMProvider(provider);
|
||||
setActiveProvider(providerType);
|
||||
console.log('LLM provider configured');
|
||||
};
|
||||
|
||||
@@ -204,6 +213,7 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
||||
registerClearChatUICallback(clearChatUI);
|
||||
|
||||
const maybeUnsubscribePromise = initializeProvider();
|
||||
const unsubscribeLocalModel = LocalLLMModelManager.subscribe(setLocalModelState);
|
||||
|
||||
(async () => {
|
||||
if (hasShownWelcomeOnceInRuntime) return;
|
||||
@@ -216,6 +226,7 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
||||
})();
|
||||
|
||||
return () => {
|
||||
unsubscribeLocalModel();
|
||||
Promise.resolve(maybeUnsubscribePromise).then((cleanup) => {
|
||||
if (typeof cleanup === 'function') cleanup();
|
||||
}).catch(() => {});
|
||||
@@ -266,7 +277,10 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
||||
// Log system prompt only for first message
|
||||
if (isFirstMessage) {
|
||||
try {
|
||||
const systemPrompt = await SystemPrompts.getSystemPromptWithContext();
|
||||
const provider = AgentCore.instance().getLLMProvider();
|
||||
const systemPrompt = await SystemPrompts.getSystemPromptWithContext(
|
||||
provider?.getPreferredSystemPromptPath?.(),
|
||||
);
|
||||
console.log('------------ SYSTEM ------------');
|
||||
console.log(systemPrompt);
|
||||
console.log('--------------------------------');
|
||||
@@ -372,6 +386,45 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeProvider === LOCAL_LLM_PROVIDER_KEY && (
|
||||
<div style={{ padding: '10px 14px 0 14px' }}>
|
||||
<div className="settings-group" style={{ marginBottom: '12px', padding: '14px' }}>
|
||||
<h4 style={{ marginBottom: '10px' }}>{LOCAL_LLM_DISPLAY_NAME} Local Runtime</h4>
|
||||
{!localModelState.runtimeSupport.supported && (
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#d0a56b', marginBottom: '8px' }}>
|
||||
{localModelState.runtimeSupport.reason}
|
||||
</div>
|
||||
)}
|
||||
{!localModelState.isCached && !localModelState.isDownloading && localModelState.runtimeSupport.supported && (
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#b0b0b0', marginBottom: '8px' }}>
|
||||
The local language model has not been downloaded yet. It will be downloaded automatically the next time you send a chat request with this provider.
|
||||
</div>
|
||||
)}
|
||||
{(localModelState.isChecking || localModelState.isDownloading || localModelState.progressText) && (
|
||||
<div className="settings-progress-block">
|
||||
<div
|
||||
className="settings-progress-track"
|
||||
role="progressbar"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={Math.max(0, Math.min(100, localModelState.progressPercent))}
|
||||
>
|
||||
<div className="settings-progress-fill" style={{ width: `${Math.max(0, Math.min(100, localModelState.progressPercent))}%` }} />
|
||||
</div>
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#b0b0b0', marginTop: '6px' }}>
|
||||
{localModelState.isChecking ? 'Checking local model cache...' : localModelState.progressText}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{localModelState.error && (
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#d45a5a' }}>
|
||||
{localModelState.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="chatbox-messages">
|
||||
{messages.map((message) => (
|
||||
message.role === 'user' ? (
|
||||
|
||||
@@ -293,6 +293,25 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.settings-progress-block {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.settings-progress-track {
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
background-color: #3a3a3a;
|
||||
border: 1px solid #4a4a4a;
|
||||
}
|
||||
|
||||
.settings-progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #5a9fd4 0%, #76c28f 100%);
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
/* Settings Help Links */
|
||||
.settings-help-links {
|
||||
display: flex;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { ConfigManager } from '../../../core/config/ConfigManager';
|
||||
import { LocalLLMModelManager, type LocalLLMModelState } from '../../../util/localLLMModelManager';
|
||||
import { LOCAL_LLM_DISPLAY_NAME, LOCAL_LLM_PROVIDER_KEY } from '../../../util/localLLMConfig';
|
||||
|
||||
const GeneralSettings: React.FC = () => {
|
||||
const [llmProvider, setLlmProvider] = useState<string>('openai');
|
||||
const [llmProvider, setLlmProvider] = useState<string>(LOCAL_LLM_PROVIDER_KEY);
|
||||
const [openaiKey, setOpenaiKey] = useState<string>('');
|
||||
const [openaiModel, setOpenaiModel] = useState<string>('');
|
||||
const [geminiKey, setGeminiKey] = useState<string>('');
|
||||
@@ -22,6 +24,7 @@ const GeneralSettings: React.FC = () => {
|
||||
const [kgoneBaseUrl, setKgoneBaseUrl] = useState<string>('');
|
||||
const [kgoneServerManaged, setKgoneServerManaged] = useState<boolean>(false);
|
||||
const [soundfontServerManaged, setSoundfontServerManaged] = useState<boolean>(false);
|
||||
const [localModelState, setLocalModelState] = useState<LocalLLMModelState>(LocalLLMModelManager.getState());
|
||||
|
||||
const configManager = ConfigManager.instance();
|
||||
|
||||
@@ -46,7 +49,7 @@ const GeneralSettings: React.FC = () => {
|
||||
await configManager.initialize();
|
||||
}
|
||||
|
||||
setLlmProvider((configManager.get('general.llm_provider') as string) || 'openai');
|
||||
setLlmProvider((configManager.get('general.llm_provider') as string) || LOCAL_LLM_PROVIDER_KEY);
|
||||
setOpenaiKey((configManager.get('general.openai.api_key') as string) || '');
|
||||
setOpenaiModel((configManager.get('general.openai.model') as string) || '');
|
||||
setOpenaiFlex((configManager.get('general.openai.flex') as boolean) ?? false);
|
||||
@@ -69,6 +72,8 @@ const GeneralSettings: React.FC = () => {
|
||||
};
|
||||
|
||||
loadConfig();
|
||||
const unsubscribe = LocalLLMModelManager.subscribe(setLocalModelState);
|
||||
return unsubscribe;
|
||||
}, [configManager]);
|
||||
|
||||
// Debounced save function for text inputs
|
||||
@@ -197,6 +202,14 @@ const GeneralSettings: React.FC = () => {
|
||||
debouncedSave('general.kgone.base_url', value);
|
||||
};
|
||||
|
||||
const handleDeleteLocalModel = async () => {
|
||||
try {
|
||||
await LocalLLMModelManager.deleteCachedModel();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete local language model cache:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// NOTE: Gemini and Claude are not supported yet due to CORS issues.
|
||||
return (
|
||||
<div className="settings-section">
|
||||
@@ -217,6 +230,7 @@ const GeneralSettings: React.FC = () => {
|
||||
value={llmProvider}
|
||||
onChange={(e) => handleLlmProviderChange(e.target.value)}
|
||||
>
|
||||
<option value={LOCAL_LLM_PROVIDER_KEY}>Local LLM (Browser)</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
{/* <option value="gemini">Gemini</option>
|
||||
<option value="claude">Claude</option> */}
|
||||
@@ -243,6 +257,69 @@ const GeneralSettings: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-group">
|
||||
<h4>{LOCAL_LLM_DISPLAY_NAME} Local Runtime</h4>
|
||||
|
||||
{!localModelState.runtimeSupport.supported && (
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#d0a56b', marginTop: '4px', marginBottom: '8px' }}>
|
||||
{localModelState.runtimeSupport.reason}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label">
|
||||
Cached Model Status
|
||||
</label>
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
|
||||
{localModelState.isChecking
|
||||
? 'Checking local model cache...'
|
||||
: localModelState.isCached
|
||||
? 'Downloaded in browser cache.'
|
||||
: 'Not downloaded yet.'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!localModelState.isCached && !localModelState.isDownloading && localModelState.runtimeSupport.supported && (
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px', marginBottom: '8px' }}>
|
||||
The local model downloads automatically the next time you chat with `Local LLM (Browser)`.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(localModelState.isDownloading || localModelState.progressText) && (
|
||||
<div className="settings-progress-block">
|
||||
<div
|
||||
className="settings-progress-track"
|
||||
role="progressbar"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={Math.max(0, Math.min(100, localModelState.progressPercent))}
|
||||
>
|
||||
<div className="settings-progress-fill" style={{ width: `${Math.max(0, Math.min(100, localModelState.progressPercent))}%` }} />
|
||||
</div>
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '6px' }}>
|
||||
{localModelState.progressText}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{localModelState.error && (
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#d45a5a', marginTop: '8px' }}>
|
||||
{localModelState.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="settings-item" style={{ marginTop: '12px' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="settings-btn settings-btn-danger"
|
||||
onClick={() => void handleDeleteLocalModel()}
|
||||
disabled={localModelState.isDeleting || localModelState.isDownloading || !localModelState.isCached}
|
||||
>
|
||||
{localModelState.isDeleting ? 'Deleting...' : 'Delete Cached Model'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-group">
|
||||
<h4>OpenAI</h4>
|
||||
|
||||
@@ -600,4 +677,4 @@ const GeneralSettings: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneralSettings;
|
||||
export default GeneralSettings;
|
||||
|
||||
Reference in New Issue
Block a user