fix: better working ai chat

This commit is contained in:
2026-02-11 21:03:50 +01:00
parent 53ebe91895
commit 226298dd15
8 changed files with 311 additions and 34 deletions

View File

@@ -414,4 +414,61 @@
color: var(--vscode-input-placeholderForeground);
}
/* AI Settings */
.system-prompt-textarea {
width: 100%;
min-height: 150px;
max-height: 400px;
padding: 10px 12px;
font-size: 12px;
font-family: var(--vscode-editor-font-family), monospace;
line-height: 1.5;
background-color: var(--vscode-input-background);
border: 1px solid var(--vscode-input-border);
color: var(--vscode-input-foreground);
border-radius: 4px;
outline: none;
resize: vertical;
}
.system-prompt-textarea:focus {
border-color: var(--vscode-focusBorder);
}
.system-prompt-textarea::placeholder {
color: var(--vscode-input-placeholderForeground);
}
.setting-inline-action {
display: flex;
gap: 8px;
align-items: center;
}
.setting-inline-action input {
flex: 1;
max-width: 300px;
}
.setting-row-full {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px 16px;
border-radius: 4px;
transition: background-color 0.15s;
}
.setting-row-full:hover {
background-color: var(--vscode-list-hoverBackground);
}
.setting-row-full .setting-control {
width: 100%;
}
.setting-row-full .setting-actions {
display: flex;
gap: 8px;
margin-top: 8px;
}

View File

@@ -4,7 +4,7 @@ import { showToast } from '../Toast';
import './SettingsView.css';
// Export category IDs for sidebar navigation
export type SettingsCategory = 'project' | 'editor' | 'content' | 'sync' | 'publishing' | 'data';
export type SettingsCategory = 'project' | 'editor' | 'content' | 'ai' | 'sync' | 'publishing' | 'data';
// Scroll to a settings section by category ID
export const scrollToSettingsSection = (category: SettingsCategory) => {
@@ -113,6 +113,15 @@ export const SettingsView: React.FC = () => {
const [postCategories, setPostCategories] = useState<string[]>(DEFAULT_POST_CATEGORIES);
const [newCategoryInput, setNewCategoryInput] = useState('');
// AI Assistant settings
const [aiSystemPrompt, setAiSystemPrompt] = useState('');
const [aiSystemPromptModified, setAiSystemPromptModified] = useState(false);
const [aiApiKeyMasked, setAiApiKeyMasked] = useState('');
const [aiHasApiKey, setAiHasApiKey] = useState(false);
const [newApiKey, setNewApiKey] = useState('');
const [availableModels, setAvailableModels] = useState<{id: string; name: string}[]>([]);
const [selectedModel, setSelectedModel] = useState('');
// Check if a section has any matching settings
const sectionHasMatches = useCallback((sectionKeywords: string[]) => {
if (!searchQuery) return true;
@@ -147,6 +156,28 @@ export const SettingsView: React.FC = () => {
setPostCategories(DEFAULT_POST_CATEGORIES);
}
// Load AI settings
try {
const promptResult = await window.electronAPI?.chat.getSystemPrompt();
if (promptResult?.success && promptResult.prompt) {
setAiSystemPrompt(promptResult.prompt);
}
const keyResult = await window.electronAPI?.chat.getApiKey();
if (keyResult) {
setAiHasApiKey(keyResult.hasKey);
setAiApiKeyMasked(keyResult.maskedKey || '');
}
const modelsResult = await window.electronAPI?.chat.getAvailableModels();
if (modelsResult?.success && modelsResult.models) {
setAvailableModels(modelsResult.models);
setSelectedModel(modelsResult.selectedModel || '');
}
} catch (error) {
console.error('Failed to load AI settings:', error);
}
// Check Dropbox status
const dbxConfigured = await window.electronAPI?.dropbox?.isConfigured();
setDropboxConfigured(dbxConfigured || false);
@@ -270,6 +301,7 @@ export const SettingsView: React.FC = () => {
const projectKeywords = ['project', 'name', 'description', 'blog', 'site'];
const editorKeywords = ['editor', 'mode', 'wysiwyg', 'markdown', 'preview', 'visual'];
const contentKeywords = ['content', 'categories', 'post', 'article', 'picture', 'aside', 'page'];
const aiKeywords = ['ai', 'assistant', 'chat', 'model', 'prompt', 'system', 'api', 'key', 'claude', 'gpt', 'opencode'];
const syncKeywords = ['sync', 'dropbox', 'file', 'backup', 'token', 'remote'];
const publishingKeywords = ['publishing', 'ftp', 'ssh', 'deploy', 'server', 'host', 'upload'];
const dataKeywords = ['data', 'database', 'rebuild', 'maintenance', 'posts', 'media', 'links', 'folder', 'filesystem'];
@@ -459,6 +491,168 @@ export const SettingsView: React.FC = () => {
</SettingSection>
);
// AI Assistant handlers
const handleSaveSystemPrompt = async () => {
try {
const result = await window.electronAPI?.chat.setSystemPrompt(aiSystemPrompt);
if (result?.success) {
setAiSystemPromptModified(false);
showToast.success('System prompt saved');
} else {
showToast.error('Failed to save system prompt');
}
} catch (error) {
console.error('Failed to save system prompt:', error);
showToast.error('Failed to save system prompt');
}
};
const handleResetSystemPrompt = async () => {
try {
// Set to empty to use built-in default
await window.electronAPI?.chat.setSystemPrompt('');
const result = await window.electronAPI?.chat.getSystemPrompt();
if (result?.prompt) {
setAiSystemPrompt(result.prompt);
setAiSystemPromptModified(false);
showToast.success('System prompt reset to default');
}
} catch (error) {
console.error('Failed to reset system prompt:', error);
showToast.error('Failed to reset system prompt');
}
};
const handleSaveApiKey = async () => {
if (!newApiKey.trim()) return;
try {
const validateResult = await window.electronAPI?.chat.validateApiKey(newApiKey.trim());
if (validateResult?.isValid) {
await window.electronAPI?.chat.setApiKey(newApiKey.trim());
setAiHasApiKey(true);
setAiApiKeyMasked('•'.repeat(Math.max(0, newApiKey.length - 4)) + newApiKey.slice(-4));
setNewApiKey('');
showToast.success('API key saved and validated');
} else {
showToast.error('Invalid API key');
}
} catch (error) {
console.error('Failed to save API key:', error);
showToast.error('Failed to save API key');
}
};
const handleModelChange = async (modelId: string) => {
try {
const result = await window.electronAPI?.chat.setDefaultModel(modelId);
if (result?.success) {
setSelectedModel(modelId);
showToast.success('Default model updated');
}
} catch (error) {
console.error('Failed to set model:', error);
showToast.error('Failed to set default model');
}
};
const renderAISettings = () => (
<SettingSection
id="settings-section-ai"
title="AI Assistant"
description="Configure the AI chat assistant that helps you manage your blog content."
hidden={!sectionHasMatches(aiKeywords)}
>
<SettingRow
id="ai-api-key"
label="OpenCode API Key"
description="Your API key for the OpenCode Zen gateway. Required to use AI features."
>
<div className="setting-input-group">
{aiHasApiKey ? (
<>
<input
id="ai-api-key"
type="text"
value={aiApiKeyMasked}
disabled
placeholder="API key configured"
/>
<span className="setting-status-badge success"> Configured</span>
</>
) : (
<>
<input
id="ai-api-key"
type="password"
value={newApiKey}
onChange={(e) => setNewApiKey(e.target.value)}
placeholder="Enter your API key..."
/>
<button className="primary" onClick={handleSaveApiKey} disabled={!newApiKey.trim()}>
Save Key
</button>
</>
)}
</div>
{aiHasApiKey && (
<div className="setting-inline-action">
<button className="text-button" onClick={() => { setAiHasApiKey(false); setAiApiKeyMasked(''); }}>
Change API Key
</button>
</div>
)}
</SettingRow>
<SettingRow
id="ai-model"
label="Default Model"
description="The AI model to use for new chat conversations."
>
<select
id="ai-model"
value={selectedModel}
onChange={(e) => handleModelChange(e.target.value)}
disabled={!aiHasApiKey}
>
{availableModels.length === 0 && <option value="">No models available</option>}
{availableModels.map(model => (
<option key={model.id} value={model.id}>{model.name}</option>
))}
</select>
</SettingRow>
<SettingRow
id="ai-system-prompt"
label="System Prompt"
description="Instructions given to the AI at the start of each conversation. This defines how the assistant behaves and what tools it knows about."
>
<textarea
id="ai-system-prompt"
value={aiSystemPrompt}
onChange={(e) => {
setAiSystemPrompt(e.target.value);
setAiSystemPromptModified(true);
}}
placeholder="Enter system instructions for the AI assistant..."
rows={12}
className="system-prompt-textarea"
/>
<div className="setting-actions">
<button
className="primary"
onClick={handleSaveSystemPrompt}
disabled={!aiSystemPromptModified}
>
Save Prompt
</button>
<button className="secondary" onClick={handleResetSystemPrompt}>
Reset to Default
</button>
</div>
</SettingRow>
</SettingSection>
);
const renderSyncSettings = () => (
<>
<SettingSection
@@ -778,6 +972,7 @@ export const SettingsView: React.FC = () => {
sectionHasMatches(projectKeywords) ||
sectionHasMatches(editorKeywords) ||
sectionHasMatches(contentKeywords) ||
sectionHasMatches(aiKeywords) ||
sectionHasMatches(syncKeywords) ||
sectionHasMatches(publishingKeywords) ||
sectionHasMatches(dataKeywords);
@@ -813,6 +1008,7 @@ export const SettingsView: React.FC = () => {
{renderProjectSettings()}
{renderEditorSettings()}
{renderContentSettings()}
{renderAISettings()}
{renderSyncSettings()}
{renderPublishingSettings()}
{renderDataSettings()}

View File

@@ -82,8 +82,8 @@
gap: 4px;
padding: 0 10px;
height: 100%;
min-width: 120px;
max-width: 200px;
min-width: 100px;
max-width: 160px;
cursor: pointer;
background-color: var(--vscode-tab-inactiveBackground, #2d2d2d);
border-right: 1px solid var(--vscode-tab-border, #252526);

View File

@@ -2,7 +2,7 @@ import React, { useRef, useState, useEffect, useCallback } from 'react';
import { useAppStore, Tab } from '../../store';
import './TabBar.css';
const MAX_CHAT_TITLE_LENGTH = 25;
const MAX_CHAT_TITLE_LENGTH = 18;
const getTabTitle = (
tab: Tab,

View File

@@ -342,9 +342,9 @@ export interface ElectronAPI {
// Settings
getAvailableModels: () => Promise<{ success: boolean; models?: ChatModel[]; selectedModel?: string; error?: string }>;
setDefaultModel: (modelId: string) => Promise<void>;
getSystemPrompt: () => Promise<string | null>;
setSystemPrompt: (prompt: string) => Promise<void>;
setDefaultModel: (modelId: string) => Promise<{ success: boolean; error?: string }>;
getSystemPrompt: () => Promise<{ success: boolean; prompt?: string; error?: string }>;
setSystemPrompt: (prompt: string) => Promise<{ success: boolean; error?: string }>;
// Conversations
getConversations: () => Promise<ChatConversation[]>;