fix: better working ai chat
This commit is contained in:
@@ -319,25 +319,28 @@ export class ChatEngine {
|
||||
* Get the built-in default system prompt
|
||||
*/
|
||||
private getBuiltInSystemPrompt(): string {
|
||||
return `You are an AI assistant for the Blogging Desktop Server (bDS) application.
|
||||
You help users manage their blog posts and media files.
|
||||
return `You are an AI assistant integrated into the Blogging Desktop Server (bDS) application.
|
||||
Your role is to help users manage their blog posts and media files using ONLY the tools provided to you.
|
||||
|
||||
You have access to tools that allow you to:
|
||||
- Search for posts using full-text search with optional category/tag filters
|
||||
- Read individual post content and metadata
|
||||
- List and filter posts by status, category, or tags
|
||||
- View information about media files (images)
|
||||
- Update metadata for posts and media files
|
||||
- List all tags with post counts
|
||||
- List all categories with post counts
|
||||
IMPORTANT: You do NOT have access to the internet, real-time data, or any external services.
|
||||
You can ONLY access information through the tools listed below. Do not claim otherwise.
|
||||
|
||||
When answering questions about the user's blog content:
|
||||
1. Use the search or list tools to find relevant posts
|
||||
2. Read specific posts to get detailed content
|
||||
3. Use list_tags and list_categories to understand the taxonomy
|
||||
4. Provide helpful summaries and suggestions
|
||||
Available Tools:
|
||||
- search_posts: Search blog posts using full-text search. Supports category/tag filters.
|
||||
- read_post: Read the full content and metadata of a specific post by ID.
|
||||
- list_posts: List posts with optional filtering by status, category, or tags.
|
||||
- get_media: Get information about a specific media file by ID.
|
||||
- list_media: List media files with optional MIME type filtering.
|
||||
- update_post_metadata: Update a post's title, excerpt, tags, or categories.
|
||||
- update_media_metadata: Update a media file's alt text, caption, or tags.
|
||||
- list_tags: List all tags with post counts.
|
||||
- list_categories: List all categories with post counts.
|
||||
|
||||
Be concise but thorough in your responses. When displaying post information, format it clearly.`;
|
||||
When answering questions:
|
||||
1. USE THE TOOLS to find information. Never make up data about posts or media.
|
||||
2. If asked about something outside your tools (weather, news, websites), explain that you can only access the user's local blog content.
|
||||
3. Be concise and helpful. Format post information clearly when displaying it.
|
||||
4. If a search returns no results, suggest alternative queries or filters.`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -396,8 +396,10 @@ export class OpenCodeManager {
|
||||
const toolUseBlocks = (data.content as AnthropicContentBlock[]).filter(
|
||||
(b: AnthropicContentBlock) => b.type === 'tool_use'
|
||||
);
|
||||
|
||||
// Capture text from any block type that has a text field (text, thinking, etc.)
|
||||
const textBlocks = (data.content as AnthropicContentBlock[]).filter(
|
||||
(b: AnthropicContentBlock) => b.type === 'text'
|
||||
(b: AnthropicContentBlock) => b.text
|
||||
);
|
||||
|
||||
// Accumulate and stream text content to frontend
|
||||
@@ -536,10 +538,22 @@ export class OpenCodeManager {
|
||||
textContent = content;
|
||||
} else if (Array.isArray(content)) {
|
||||
// Handle array of content parts (some models return this format)
|
||||
// Accept any part that has a text field, regardless of type
|
||||
textContent = content
|
||||
.filter((part: { type?: string; text?: string }) => part.type === 'text' && part.text)
|
||||
.filter((part: { type?: string; text?: string }) => part.text)
|
||||
.map((part: { text: string }) => part.text)
|
||||
.join('');
|
||||
|
||||
// Log what types we're seeing for debugging
|
||||
const types = content.map((p: { type?: string }) => p.type).filter(Boolean);
|
||||
if (types.length > 0) {
|
||||
console.log('[OpenCodeManager:OpenAI] Content block types:', types);
|
||||
}
|
||||
} else if (content && typeof content === 'object') {
|
||||
// Handle single object with text field
|
||||
if ('text' in content && typeof content.text === 'string') {
|
||||
textContent = content.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (textContent) {
|
||||
@@ -892,17 +906,17 @@ export class OpenCodeManager {
|
||||
private async generateConversationTitle(
|
||||
conversationId: string,
|
||||
userMessage: string,
|
||||
assistantResponse: string
|
||||
_assistantResponse: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const body = {
|
||||
model: 'claude-haiku-4-5',
|
||||
max_tokens: 100,
|
||||
system: 'Generate a short, concise title (max 6 words) for this conversation. Only output the title, nothing else.',
|
||||
max_tokens: 20,
|
||||
system: 'Generate an ultra-short title (2-3 words, max 25 characters) for this conversation. Focus ONLY on the topic. Ignore any capability disclaimers. Output ONLY the title text.',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: `User: ${userMessage.substring(0, 200)}\nAssistant: ${assistantResponse.substring(0, 200)}`,
|
||||
content: `Topic: ${userMessage.substring(0, 100)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -930,7 +944,14 @@ export class OpenCodeManager {
|
||||
title = data.content || '';
|
||||
}
|
||||
|
||||
title = title.trim().replace(/^["']|["']$/g, '');
|
||||
// Clean up and truncate title
|
||||
title = title.trim().replace(/^["']|["']$/g, '').replace(/[.!?]+$/, '');
|
||||
|
||||
// Hard limit on title length
|
||||
const MAX_TITLE_LENGTH = 30;
|
||||
if (title.length > MAX_TITLE_LENGTH) {
|
||||
title = title.substring(0, MAX_TITLE_LENGTH - 1) + '…';
|
||||
}
|
||||
|
||||
if (title) {
|
||||
await this.chatEngine.updateConversation(conversationId, { title });
|
||||
|
||||
@@ -297,10 +297,10 @@ export interface ElectronAPI {
|
||||
getApiKey: () => Promise<{ hasKey: boolean; maskedKey: string }>;
|
||||
|
||||
// Settings
|
||||
getAvailableModels: () => Promise<Array<{ id: string; name: string }>>;
|
||||
setDefaultModel: (modelId: string) => Promise<void>;
|
||||
getSystemPrompt: () => Promise<string | null>;
|
||||
setSystemPrompt: (prompt: string) => Promise<void>;
|
||||
getAvailableModels: () => Promise<{ success: boolean; models?: Array<{ id: string; name: string }>; selectedModel?: string; error?: string }>;
|
||||
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<unknown[]>;
|
||||
|
||||
@@ -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