fix: better working ai chat
This commit is contained in:
@@ -319,25 +319,28 @@ export class ChatEngine {
|
|||||||
* Get the built-in default system prompt
|
* Get the built-in default system prompt
|
||||||
*/
|
*/
|
||||||
private getBuiltInSystemPrompt(): string {
|
private getBuiltInSystemPrompt(): string {
|
||||||
return `You are an AI assistant for the Blogging Desktop Server (bDS) application.
|
return `You are an AI assistant integrated into the Blogging Desktop Server (bDS) application.
|
||||||
You help users manage their blog posts and media files.
|
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:
|
IMPORTANT: You do NOT have access to the internet, real-time data, or any external services.
|
||||||
- Search for posts using full-text search with optional category/tag filters
|
You can ONLY access information through the tools listed below. Do not claim otherwise.
|
||||||
- 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
|
|
||||||
|
|
||||||
When answering questions about the user's blog content:
|
Available Tools:
|
||||||
1. Use the search or list tools to find relevant posts
|
- search_posts: Search blog posts using full-text search. Supports category/tag filters.
|
||||||
2. Read specific posts to get detailed content
|
- read_post: Read the full content and metadata of a specific post by ID.
|
||||||
3. Use list_tags and list_categories to understand the taxonomy
|
- list_posts: List posts with optional filtering by status, category, or tags.
|
||||||
4. Provide helpful summaries and suggestions
|
- 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(
|
const toolUseBlocks = (data.content as AnthropicContentBlock[]).filter(
|
||||||
(b: AnthropicContentBlock) => b.type === 'tool_use'
|
(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(
|
const textBlocks = (data.content as AnthropicContentBlock[]).filter(
|
||||||
(b: AnthropicContentBlock) => b.type === 'text'
|
(b: AnthropicContentBlock) => b.text
|
||||||
);
|
);
|
||||||
|
|
||||||
// Accumulate and stream text content to frontend
|
// Accumulate and stream text content to frontend
|
||||||
@@ -536,10 +538,22 @@ export class OpenCodeManager {
|
|||||||
textContent = content;
|
textContent = content;
|
||||||
} else if (Array.isArray(content)) {
|
} else if (Array.isArray(content)) {
|
||||||
// Handle array of content parts (some models return this format)
|
// Handle array of content parts (some models return this format)
|
||||||
|
// Accept any part that has a text field, regardless of type
|
||||||
textContent = content
|
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)
|
.map((part: { text: string }) => part.text)
|
||||||
.join('');
|
.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) {
|
if (textContent) {
|
||||||
@@ -892,17 +906,17 @@ export class OpenCodeManager {
|
|||||||
private async generateConversationTitle(
|
private async generateConversationTitle(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
userMessage: string,
|
userMessage: string,
|
||||||
assistantResponse: string
|
_assistantResponse: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const body = {
|
const body = {
|
||||||
model: 'claude-haiku-4-5',
|
model: 'claude-haiku-4-5',
|
||||||
max_tokens: 100,
|
max_tokens: 20,
|
||||||
system: 'Generate a short, concise title (max 6 words) for this conversation. Only output the title, nothing else.',
|
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: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: 'user',
|
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 = 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) {
|
if (title) {
|
||||||
await this.chatEngine.updateConversation(conversationId, { title });
|
await this.chatEngine.updateConversation(conversationId, { title });
|
||||||
|
|||||||
@@ -297,10 +297,10 @@ export interface ElectronAPI {
|
|||||||
getApiKey: () => Promise<{ hasKey: boolean; maskedKey: string }>;
|
getApiKey: () => Promise<{ hasKey: boolean; maskedKey: string }>;
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
getAvailableModels: () => Promise<Array<{ id: string; name: string }>>;
|
getAvailableModels: () => Promise<{ success: boolean; models?: Array<{ id: string; name: string }>; selectedModel?: string; error?: string }>;
|
||||||
setDefaultModel: (modelId: string) => Promise<void>;
|
setDefaultModel: (modelId: string) => Promise<{ success: boolean; error?: string }>;
|
||||||
getSystemPrompt: () => Promise<string | null>;
|
getSystemPrompt: () => Promise<{ success: boolean; prompt?: string; error?: string }>;
|
||||||
setSystemPrompt: (prompt: string) => Promise<void>;
|
setSystemPrompt: (prompt: string) => Promise<{ success: boolean; error?: string }>;
|
||||||
|
|
||||||
// Conversations
|
// Conversations
|
||||||
getConversations: () => Promise<unknown[]>;
|
getConversations: () => Promise<unknown[]>;
|
||||||
|
|||||||
@@ -414,4 +414,61 @@
|
|||||||
color: var(--vscode-input-placeholderForeground);
|
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';
|
import './SettingsView.css';
|
||||||
|
|
||||||
// Export category IDs for sidebar navigation
|
// 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
|
// Scroll to a settings section by category ID
|
||||||
export const scrollToSettingsSection = (category: SettingsCategory) => {
|
export const scrollToSettingsSection = (category: SettingsCategory) => {
|
||||||
@@ -113,6 +113,15 @@ export const SettingsView: React.FC = () => {
|
|||||||
const [postCategories, setPostCategories] = useState<string[]>(DEFAULT_POST_CATEGORIES);
|
const [postCategories, setPostCategories] = useState<string[]>(DEFAULT_POST_CATEGORIES);
|
||||||
const [newCategoryInput, setNewCategoryInput] = useState('');
|
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
|
// Check if a section has any matching settings
|
||||||
const sectionHasMatches = useCallback((sectionKeywords: string[]) => {
|
const sectionHasMatches = useCallback((sectionKeywords: string[]) => {
|
||||||
if (!searchQuery) return true;
|
if (!searchQuery) return true;
|
||||||
@@ -147,6 +156,28 @@ export const SettingsView: React.FC = () => {
|
|||||||
setPostCategories(DEFAULT_POST_CATEGORIES);
|
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
|
// Check Dropbox status
|
||||||
const dbxConfigured = await window.electronAPI?.dropbox?.isConfigured();
|
const dbxConfigured = await window.electronAPI?.dropbox?.isConfigured();
|
||||||
setDropboxConfigured(dbxConfigured || false);
|
setDropboxConfigured(dbxConfigured || false);
|
||||||
@@ -270,6 +301,7 @@ export const SettingsView: React.FC = () => {
|
|||||||
const projectKeywords = ['project', 'name', 'description', 'blog', 'site'];
|
const projectKeywords = ['project', 'name', 'description', 'blog', 'site'];
|
||||||
const editorKeywords = ['editor', 'mode', 'wysiwyg', 'markdown', 'preview', 'visual'];
|
const editorKeywords = ['editor', 'mode', 'wysiwyg', 'markdown', 'preview', 'visual'];
|
||||||
const contentKeywords = ['content', 'categories', 'post', 'article', 'picture', 'aside', 'page'];
|
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 syncKeywords = ['sync', 'dropbox', 'file', 'backup', 'token', 'remote'];
|
||||||
const publishingKeywords = ['publishing', 'ftp', 'ssh', 'deploy', 'server', 'host', 'upload'];
|
const publishingKeywords = ['publishing', 'ftp', 'ssh', 'deploy', 'server', 'host', 'upload'];
|
||||||
const dataKeywords = ['data', 'database', 'rebuild', 'maintenance', 'posts', 'media', 'links', 'folder', 'filesystem'];
|
const dataKeywords = ['data', 'database', 'rebuild', 'maintenance', 'posts', 'media', 'links', 'folder', 'filesystem'];
|
||||||
@@ -459,6 +491,168 @@ export const SettingsView: React.FC = () => {
|
|||||||
</SettingSection>
|
</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 = () => (
|
const renderSyncSettings = () => (
|
||||||
<>
|
<>
|
||||||
<SettingSection
|
<SettingSection
|
||||||
@@ -778,6 +972,7 @@ export const SettingsView: React.FC = () => {
|
|||||||
sectionHasMatches(projectKeywords) ||
|
sectionHasMatches(projectKeywords) ||
|
||||||
sectionHasMatches(editorKeywords) ||
|
sectionHasMatches(editorKeywords) ||
|
||||||
sectionHasMatches(contentKeywords) ||
|
sectionHasMatches(contentKeywords) ||
|
||||||
|
sectionHasMatches(aiKeywords) ||
|
||||||
sectionHasMatches(syncKeywords) ||
|
sectionHasMatches(syncKeywords) ||
|
||||||
sectionHasMatches(publishingKeywords) ||
|
sectionHasMatches(publishingKeywords) ||
|
||||||
sectionHasMatches(dataKeywords);
|
sectionHasMatches(dataKeywords);
|
||||||
@@ -813,6 +1008,7 @@ export const SettingsView: React.FC = () => {
|
|||||||
{renderProjectSettings()}
|
{renderProjectSettings()}
|
||||||
{renderEditorSettings()}
|
{renderEditorSettings()}
|
||||||
{renderContentSettings()}
|
{renderContentSettings()}
|
||||||
|
{renderAISettings()}
|
||||||
{renderSyncSettings()}
|
{renderSyncSettings()}
|
||||||
{renderPublishingSettings()}
|
{renderPublishingSettings()}
|
||||||
{renderDataSettings()}
|
{renderDataSettings()}
|
||||||
|
|||||||
@@ -82,8 +82,8 @@
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 120px;
|
min-width: 100px;
|
||||||
max-width: 200px;
|
max-width: 160px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: var(--vscode-tab-inactiveBackground, #2d2d2d);
|
background-color: var(--vscode-tab-inactiveBackground, #2d2d2d);
|
||||||
border-right: 1px solid var(--vscode-tab-border, #252526);
|
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 { useAppStore, Tab } from '../../store';
|
||||||
import './TabBar.css';
|
import './TabBar.css';
|
||||||
|
|
||||||
const MAX_CHAT_TITLE_LENGTH = 25;
|
const MAX_CHAT_TITLE_LENGTH = 18;
|
||||||
|
|
||||||
const getTabTitle = (
|
const getTabTitle = (
|
||||||
tab: Tab,
|
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
|
// Settings
|
||||||
getAvailableModels: () => Promise<{ success: boolean; models?: ChatModel[]; selectedModel?: string; error?: string }>;
|
getAvailableModels: () => Promise<{ success: boolean; models?: ChatModel[]; selectedModel?: string; error?: string }>;
|
||||||
setDefaultModel: (modelId: string) => Promise<void>;
|
setDefaultModel: (modelId: string) => Promise<{ success: boolean; error?: string }>;
|
||||||
getSystemPrompt: () => Promise<string | null>;
|
getSystemPrompt: () => Promise<{ success: boolean; prompt?: string; error?: string }>;
|
||||||
setSystemPrompt: (prompt: string) => Promise<void>;
|
setSystemPrompt: (prompt: string) => Promise<{ success: boolean; error?: string }>;
|
||||||
|
|
||||||
// Conversations
|
// Conversations
|
||||||
getConversations: () => Promise<ChatConversation[]>;
|
getConversations: () => Promise<ChatConversation[]>;
|
||||||
|
|||||||
Reference in New Issue
Block a user