feat: added a view image ai command

This commit is contained in:
2026-02-11 22:20:34 +01:00
parent 8c82cf5b29
commit 77b5b0ec26
3 changed files with 133 additions and 13 deletions

View File

@@ -292,7 +292,8 @@ export class ChatEngine {
args: [],
});
if (result.rows.length > 0) {
// Return saved prompt if it exists and is non-empty
if (result.rows.length > 0 && result.rows[0].value) {
return result.rows[0].value as string;
}
@@ -300,7 +301,8 @@ export class ChatEngine {
}
/**
* Set default system prompt for new conversations
* Set default system prompt for new conversations.
* Pass empty string to reset to built-in default.
*/
async setDefaultSystemPrompt(prompt: string): Promise<void> {
const client = this.db.getLocalClient();
@@ -308,6 +310,15 @@ export class ChatEngine {
throw new Error('Database not initialized');
}
// If empty string, delete the setting to use built-in default
if (!prompt || prompt.trim() === '') {
await client.execute({
sql: `DELETE FROM settings WHERE key = ?`,
args: ['chat_system_prompt'],
});
return;
}
const now = Date.now();
await client.execute({
sql: `INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, ?)`,
@@ -331,6 +342,7 @@ Available Tools:
- 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.
- view_image: View an image to analyze its visual content. Use this when you need to describe or analyze what an image looks like.
- 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.
@@ -340,7 +352,8 @@ 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.`;
4. If a search returns no results, suggest alternative queries or filters.
5. When asked to describe or analyze an image, use the view_image tool to see the actual image content.`;
}
/**

View File

@@ -99,7 +99,22 @@ interface AnthropicContentBlock {
name?: string;
input?: unknown;
tool_use_id?: string;
content?: string;
content?: string | AnthropicToolResultContent[];
source?: {
type: 'base64';
media_type: string;
data: string;
};
}
interface AnthropicToolResultContent {
type: 'text' | 'image';
text?: string;
source?: {
type: 'base64';
media_type: string;
data: string;
};
}
interface HttpResponse {
@@ -441,11 +456,44 @@ export class OpenCodeManager {
callbacks.onToolResult({ name: toolName, result });
}
toolResults.push({
type: 'tool_result',
tool_use_id: toolUseId,
content: JSON.stringify(result),
});
// Check if this is an image result that needs special formatting
if (result && typeof result === 'object' && (result as Record<string, unknown>).__isImageResult) {
const imageResult = result as {
__isImageResult: boolean;
success: boolean;
mediaType: string;
base64: string;
metadata: Record<string, unknown>;
};
// Format as Anthropic multimodal content (image + text)
const imageContent: AnthropicToolResultContent[] = [
{
type: 'image',
source: {
type: 'base64',
media_type: imageResult.mediaType,
data: imageResult.base64,
},
},
{
type: 'text',
text: JSON.stringify({ success: true, metadata: imageResult.metadata }),
},
];
toolResults.push({
type: 'tool_result',
tool_use_id: toolUseId,
content: imageContent,
});
} else {
toolResults.push({
type: 'tool_result',
tool_use_id: toolUseId,
content: JSON.stringify(result),
});
}
}
// Add assistant response and tool results to messages for next round
@@ -702,6 +750,22 @@ export class OpenCodeManager {
properties: {},
},
},
{
name: 'view_image',
description: 'View an image to analyze its visual content. Returns the actual image for visual inspection. Use this when you need to see, describe, or analyze what an image looks like. Only works with image files (not PDFs or other media types).',
input_schema: {
type: 'object',
properties: {
mediaId: { type: 'string', description: 'The unique ID of the image to view' },
size: {
type: 'string',
enum: ['small', 'medium', 'large'],
description: 'Image size: small (150px) for quick reference, medium (400px, default) for details, large (800px) for full analysis',
},
},
required: ['mediaId'],
},
},
];
}
@@ -872,6 +936,49 @@ export class OpenCodeManager {
};
}
case 'view_image': {
const mediaId = args.mediaId as string;
const size = (args.size as 'small' | 'medium' | 'large') || 'medium';
// Get media metadata first
const mediaItem = await this.mediaEngine.getMedia(mediaId);
if (!mediaItem) {
return { success: false, error: 'Image not found' };
}
// Verify it's an image (not PDF, video, etc)
if (!mediaItem.mimeType.startsWith('image/')) {
return { success: false, error: `Cannot view this file type: ${mediaItem.mimeType}. Only images are supported.` };
}
// Get thumbnail data URL (base64)
const dataUrl = await this.mediaEngine.getThumbnailDataUrl(mediaId, size);
if (!dataUrl) {
return { success: false, error: 'Thumbnail not available. Try regenerating thumbnails from Settings.' };
}
// Extract base64 data (remove data:image/webp;base64, prefix)
const base64Data = dataUrl.replace(/^data:image\/\w+;base64,/, '');
// Return special image result format
return {
__isImageResult: true,
success: true,
mediaType: 'image/webp',
base64: base64Data,
metadata: {
id: mediaItem.id,
filename: mediaItem.filename,
originalName: mediaItem.originalName,
width: mediaItem.width,
height: mediaItem.height,
alt: mediaItem.alt,
caption: mediaItem.caption,
size: size,
},
};
}
default:
return { success: false, error: `Unknown tool: ${name}` };
}

View File

@@ -159,8 +159,8 @@ export const SettingsView: React.FC = () => {
// Load AI settings
try {
const promptResult = await window.electronAPI?.chat.getSystemPrompt();
if (promptResult?.success && promptResult.prompt) {
setAiSystemPrompt(promptResult.prompt);
if (promptResult?.success) {
setAiSystemPrompt(promptResult.prompt || '');
}
const keyResult = await window.electronAPI?.chat.getApiKey();
@@ -512,8 +512,8 @@ export const SettingsView: React.FC = () => {
// 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);
if (result?.success) {
setAiSystemPrompt(result.prompt || '');
setAiSystemPromptModified(false);
showToast.success('System prompt reset to default');
}