fix: better working ai chat
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
6
src/renderer/types/electron.d.ts
vendored
6
src/renderer/types/electron.d.ts
vendored
@@ -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[]>;
|
||||
|
||||
Reference in New Issue
Block a user