feat: added field "title" and switched to it to free up caption for its normal use
This commit is contained in:
@@ -54,6 +54,7 @@ export const media = sqliteTable('media', {
|
||||
size: integer('size').notNull(),
|
||||
width: integer('width'),
|
||||
height: integer('height'),
|
||||
title: text('title'),
|
||||
alt: text('alt'),
|
||||
caption: text('caption'),
|
||||
filePath: text('file_path').notNull(),
|
||||
|
||||
@@ -313,7 +313,7 @@ Available Tools:
|
||||
- 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.
|
||||
- update_media_metadata: Update a media file's title, alt text, caption, or tags.
|
||||
- list_tags: List all tags with post counts.
|
||||
- list_categories: List all categories with post counts.
|
||||
- get_post_backlinks: Get posts that link TO a given post (backlinks). Use to discover what references a post.
|
||||
|
||||
@@ -573,7 +573,7 @@ export class ImportExecutionEngine extends EventEmitter {
|
||||
// Import the media file
|
||||
const mediaEngine = getMediaEngine();
|
||||
await mediaEngine.importMedia(sourcePath, {
|
||||
caption: wxrMedia.title || undefined,
|
||||
title: wxrMedia.title || undefined,
|
||||
alt: wxrMedia.description || undefined,
|
||||
mimeType: wxrMedia.mimeType,
|
||||
tags: [],
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface MediaData {
|
||||
size: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
title?: string;
|
||||
alt?: string;
|
||||
caption?: string;
|
||||
createdAt: Date;
|
||||
@@ -42,6 +43,7 @@ export interface MediaMetadata {
|
||||
size: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
title?: string;
|
||||
alt?: string;
|
||||
caption?: string;
|
||||
createdAt: string;
|
||||
@@ -61,7 +63,7 @@ export interface MediaFilter {
|
||||
export interface MediaSearchResult {
|
||||
id: string;
|
||||
originalName: string;
|
||||
caption?: string;
|
||||
title?: string;
|
||||
mimeType: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -92,12 +94,13 @@ export class MediaEngine extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Update the FTS index for a media item.
|
||||
* Stores stemmed content from original_name, alt, caption, and tags.
|
||||
* Stores stemmed content from original_name, title, alt, caption, and tags.
|
||||
*/
|
||||
private async updateFTSIndex(item: {
|
||||
id: string;
|
||||
projectId: string;
|
||||
originalName: string;
|
||||
title?: string;
|
||||
alt?: string;
|
||||
caption?: string;
|
||||
tags: string[];
|
||||
@@ -111,6 +114,7 @@ export class MediaEngine extends EventEmitter {
|
||||
// Combine all searchable fields and stem them
|
||||
const allText = [
|
||||
item.originalName,
|
||||
item.title || '',
|
||||
item.alt || '',
|
||||
item.caption || '',
|
||||
item.tags.join(' '),
|
||||
@@ -300,6 +304,7 @@ export class MediaEngine extends EventEmitter {
|
||||
size: mediaData.size,
|
||||
width: mediaData.width,
|
||||
height: mediaData.height,
|
||||
title: mediaData.title,
|
||||
alt: mediaData.alt,
|
||||
caption: mediaData.caption,
|
||||
createdAt: mediaData.createdAt.toISOString(),
|
||||
@@ -319,6 +324,7 @@ export class MediaEngine extends EventEmitter {
|
||||
|
||||
if (metadata.width) lines.push(`width: ${metadata.width}`);
|
||||
if (metadata.height) lines.push(`height: ${metadata.height}`);
|
||||
if (metadata.title) lines.push(`title: "${metadata.title}"`);
|
||||
if (metadata.alt) lines.push(`alt: "${metadata.alt}"`);
|
||||
if (metadata.caption) lines.push(`caption: "${metadata.caption}"`);
|
||||
|
||||
@@ -385,6 +391,9 @@ export class MediaEngine extends EventEmitter {
|
||||
case 'height':
|
||||
metadata.height = parseInt(value, 10);
|
||||
break;
|
||||
case 'title':
|
||||
metadata.title = value;
|
||||
break;
|
||||
case 'alt':
|
||||
metadata.alt = value;
|
||||
break;
|
||||
@@ -492,6 +501,7 @@ export class MediaEngine extends EventEmitter {
|
||||
size: sourceBuffer.length,
|
||||
width,
|
||||
height,
|
||||
title: metadata?.title,
|
||||
alt: metadata?.alt,
|
||||
caption: metadata?.caption,
|
||||
createdAt,
|
||||
@@ -518,6 +528,7 @@ export class MediaEngine extends EventEmitter {
|
||||
size: mediaData.size,
|
||||
width: mediaData.width,
|
||||
height: mediaData.height,
|
||||
title: mediaData.title,
|
||||
alt: mediaData.alt,
|
||||
caption: mediaData.caption,
|
||||
filePath: destPath,
|
||||
@@ -535,6 +546,7 @@ export class MediaEngine extends EventEmitter {
|
||||
id: mediaData.id,
|
||||
projectId: this.currentProjectId,
|
||||
originalName: mediaData.originalName,
|
||||
title: mediaData.title,
|
||||
alt: mediaData.alt,
|
||||
caption: mediaData.caption,
|
||||
tags: mediaData.tags,
|
||||
@@ -566,6 +578,7 @@ export class MediaEngine extends EventEmitter {
|
||||
|
||||
await db.update(media)
|
||||
.set({
|
||||
title: updated.title,
|
||||
alt: updated.alt,
|
||||
caption: updated.caption,
|
||||
updatedAt: updated.updatedAt,
|
||||
@@ -578,6 +591,7 @@ export class MediaEngine extends EventEmitter {
|
||||
id: updated.id,
|
||||
projectId: this.currentProjectId,
|
||||
originalName: updated.originalName,
|
||||
title: updated.title,
|
||||
alt: updated.alt,
|
||||
caption: updated.caption,
|
||||
tags: updated.tags,
|
||||
@@ -641,6 +655,7 @@ export class MediaEngine extends EventEmitter {
|
||||
size: dbMedia.size,
|
||||
width: dbMedia.width || undefined,
|
||||
height: dbMedia.height || undefined,
|
||||
title: dbMedia.title || undefined,
|
||||
alt: dbMedia.alt || undefined,
|
||||
caption: dbMedia.caption || undefined,
|
||||
createdAt: dbMedia.createdAt,
|
||||
@@ -666,6 +681,7 @@ export class MediaEngine extends EventEmitter {
|
||||
size: dbMedia.size,
|
||||
width: dbMedia.width || undefined,
|
||||
height: dbMedia.height || undefined,
|
||||
title: dbMedia.title || undefined,
|
||||
alt: dbMedia.alt || undefined,
|
||||
caption: dbMedia.caption || undefined,
|
||||
createdAt: dbMedia.createdAt,
|
||||
@@ -726,6 +742,7 @@ export class MediaEngine extends EventEmitter {
|
||||
size: dbMedia.size,
|
||||
width: dbMedia.width || undefined,
|
||||
height: dbMedia.height || undefined,
|
||||
title: dbMedia.title || undefined,
|
||||
alt: dbMedia.alt || undefined,
|
||||
caption: dbMedia.caption || undefined,
|
||||
createdAt: dbMedia.createdAt,
|
||||
@@ -770,7 +787,7 @@ export class MediaEngine extends EventEmitter {
|
||||
searchResults.push({
|
||||
id: item.id,
|
||||
originalName: item.originalName,
|
||||
caption: item.caption || undefined,
|
||||
title: item.title || undefined,
|
||||
mimeType: item.mimeType,
|
||||
createdAt: item.createdAt,
|
||||
});
|
||||
|
||||
@@ -739,11 +739,12 @@ export class OpenCodeManager {
|
||||
},
|
||||
{
|
||||
name: 'update_media_metadata',
|
||||
description: 'Update metadata for a media file (alt text, caption, tags).',
|
||||
description: 'Update metadata for a media file (title, alt text, caption, tags).',
|
||||
input_schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
mediaId: { type: 'string', description: 'The unique ID of the media to update' },
|
||||
title: { type: 'string', description: 'New title for display in lists and search results' },
|
||||
alt: { type: 'string', description: 'New alt text for the image' },
|
||||
caption: { type: 'string', description: 'New caption for the image' },
|
||||
tags: { type: 'array', items: { type: 'string' }, description: 'New tags for the media' },
|
||||
@@ -926,7 +927,7 @@ export class OpenCodeManager {
|
||||
id: media.id, filename: media.filename,
|
||||
originalName: media.originalName, mimeType: media.mimeType,
|
||||
size: media.size, width: media.width, height: media.height,
|
||||
alt: media.alt, caption: media.caption, tags: media.tags,
|
||||
title: media.title, alt: media.alt, caption: media.caption, tags: media.tags,
|
||||
createdAt: media.createdAt, updatedAt: media.updatedAt,
|
||||
},
|
||||
};
|
||||
@@ -945,7 +946,7 @@ export class OpenCodeManager {
|
||||
media: mediaList.map(m => ({
|
||||
id: m.id, filename: m.filename,
|
||||
originalName: m.originalName, mimeType: m.mimeType,
|
||||
alt: m.alt, tags: m.tags,
|
||||
title: m.title, alt: m.alt, tags: m.tags,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -967,6 +968,7 @@ export class OpenCodeManager {
|
||||
|
||||
case 'update_media_metadata': {
|
||||
const updates: Record<string, unknown> = {};
|
||||
if (args.title !== undefined) updates.title = args.title;
|
||||
if (args.alt !== undefined) updates.alt = args.alt;
|
||||
if (args.caption !== undefined) updates.caption = args.caption;
|
||||
if (args.tags !== undefined) updates.tags = args.tags;
|
||||
@@ -1033,6 +1035,7 @@ export class OpenCodeManager {
|
||||
originalName: mediaItem.originalName,
|
||||
width: mediaItem.width,
|
||||
height: mediaItem.height,
|
||||
title: mediaItem.title,
|
||||
alt: mediaItem.alt,
|
||||
caption: mediaItem.caption,
|
||||
size: size,
|
||||
@@ -1080,6 +1083,7 @@ export class OpenCodeManager {
|
||||
filename: link.media.filename,
|
||||
originalName: link.media.originalName,
|
||||
mimeType: link.media.mimeType,
|
||||
title: link.media.title,
|
||||
alt: link.media.alt,
|
||||
caption: link.media.caption,
|
||||
width: link.media.width,
|
||||
@@ -1451,11 +1455,12 @@ Remember: Only suggest mappings from NEW items to EXISTING items. Consider langu
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze a media image and generate alt text and caption using AI
|
||||
* Analyze a media image and generate title, alt text, and caption using AI
|
||||
* This is a one-shot request that looks at the image and suggests metadata
|
||||
*/
|
||||
async analyzeMediaImage(mediaId: string, language: string = 'en'): Promise<{
|
||||
success: boolean;
|
||||
title?: string;
|
||||
alt?: string;
|
||||
caption?: string;
|
||||
error?: string;
|
||||
@@ -1496,12 +1501,13 @@ Remember: Only suggest mappings from NEW items to EXISTING items. Consider langu
|
||||
};
|
||||
const languageName = languageNames[language] || language;
|
||||
|
||||
const systemPrompt = `Generate alt text and caption for this image in ${languageName}.
|
||||
const systemPrompt = `Generate title, alt text, and caption for this image in ${languageName}.
|
||||
|
||||
TITLE: A short, descriptive title for display in lists and search results (3-8 words). Should identify the main subject.
|
||||
ALT: Describe ONLY what is visually present in the image. Be factual, neutral, and concise (5-12 words max). No interpretations, emotions, or "Image of" prefix. Example: "Red bicycle leaning against white brick wall"
|
||||
CAPTION: Short, engaging blog caption (5-20 words).
|
||||
|
||||
Respond with JSON only: {"alt": "...", "caption": "..."}`;
|
||||
Respond with JSON only: {"title": "...", "alt": "...", "caption": "..."}`;
|
||||
|
||||
try {
|
||||
// Using Claude Sonnet 4.5 for best image analysis
|
||||
@@ -1570,6 +1576,7 @@ Respond with JSON only: {"alt": "...", "caption": "..."}`;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
title: result.title || undefined,
|
||||
alt: result.alt || undefined,
|
||||
caption: result.caption || undefined,
|
||||
};
|
||||
|
||||
@@ -333,7 +333,7 @@ export function registerChatHandlers(): void {
|
||||
|
||||
// ============ Media Analysis ============
|
||||
|
||||
// Analyze a media image and generate alt text and caption
|
||||
// Analyze a media image and generate title, alt text, and caption
|
||||
ipcMain.handle('chat:analyzeMediaImage', async (_, mediaId: string, language?: string) => {
|
||||
try {
|
||||
const manager = getOpenCodeManager();
|
||||
|
||||
@@ -1426,6 +1426,7 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
const { media, updateMedia, showErrorModal, showConfirmDeleteModal, openTab } = useAppStore();
|
||||
const item = media.find(m => m.id === mediaId);
|
||||
|
||||
const [title, setTitle] = useState(item?.title || '');
|
||||
const [alt, setAlt] = useState(item?.alt || '');
|
||||
const [caption, setCaption] = useState(item?.caption || '');
|
||||
const [tags, setTags] = useState(item?.tags.join(', ') || '');
|
||||
@@ -1474,6 +1475,7 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
const result = await window.electronAPI?.chat.analyzeMediaImage(item.id, projectLanguage);
|
||||
|
||||
if (result?.success) {
|
||||
if (result.title) setTitle(result.title);
|
||||
if (result.alt) setAlt(result.alt);
|
||||
if (result.caption) setCaption(result.caption);
|
||||
showToast.success('AI analysis complete');
|
||||
@@ -1581,6 +1583,7 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (item) {
|
||||
setTitle(item.title || '');
|
||||
setAlt(item.alt || '');
|
||||
setCaption(item.caption || '');
|
||||
setTags(item.tags.join(', '));
|
||||
@@ -1594,6 +1597,7 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const updated = await window.electronAPI?.media.update(item.id, {
|
||||
title,
|
||||
alt,
|
||||
caption,
|
||||
tags: tags.split(',').map(t => t.trim()).filter(t => t.length > 0),
|
||||
@@ -1696,8 +1700,8 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
>
|
||||
<span className="quick-action-icon">🤖</span>
|
||||
<span className="quick-action-text">
|
||||
<strong>AI: Generate Alt & Caption</strong>
|
||||
<small>Uses Claude Sonnet 4.5 to analyze the image</small>
|
||||
<strong>AI: Generate Title, Alt & Caption</strong>
|
||||
<small>Analyzes the image to suggest metadata</small>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1755,6 +1759,15 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="editor-field">
|
||||
<label>Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Title for lists and search results"
|
||||
/>
|
||||
</div>
|
||||
<div className="editor-field">
|
||||
<label>Alt Text</label>
|
||||
<input
|
||||
|
||||
@@ -11,17 +11,17 @@ interface PostSearchResult {
|
||||
interface MediaSearchResult {
|
||||
id: string;
|
||||
originalName: string;
|
||||
caption?: string;
|
||||
title?: string;
|
||||
mimeType: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/** Get display name for media: caption (truncated to 60 chars) or fallback to filename */
|
||||
/** Get display name for media: title (truncated to 60 chars) or fallback to filename */
|
||||
function getMediaDisplayName(media: MediaSearchResult): string {
|
||||
if (media.caption) {
|
||||
return media.caption.length > 60
|
||||
? media.caption.substring(0, 60) + '...'
|
||||
: media.caption;
|
||||
if (media.title) {
|
||||
return media.title.length > 60
|
||||
? media.title.substring(0, 60) + '...'
|
||||
: media.title;
|
||||
}
|
||||
return media.originalName;
|
||||
}
|
||||
@@ -187,7 +187,7 @@ export const InsertModal: React.FC<InsertModalProps> = ({
|
||||
const externalLabel = mode === 'link' ? 'External URL' : 'External Image';
|
||||
const searchPlaceholder = mode === 'link'
|
||||
? 'Search posts by title or content...'
|
||||
: 'Search media by name, caption, or alt text...';
|
||||
: 'Search media by name, title, or alt text...';
|
||||
|
||||
return (
|
||||
<div className="insert-modal-backdrop" onClick={handleBackdropClick}>
|
||||
|
||||
@@ -14,12 +14,12 @@ import { useAppStore, MediaData } from '../../store';
|
||||
import { showToast } from '../Toast';
|
||||
import './LinkedMediaPanel.css';
|
||||
|
||||
/** Get display name for media: caption (truncated to 60 chars) or fallback to filename */
|
||||
/** Get display name for media: title (truncated to 60 chars) or fallback to filename */
|
||||
function getMediaDisplayName(media: MediaData): string {
|
||||
if (media.caption) {
|
||||
return media.caption.length > 60
|
||||
? media.caption.substring(0, 60) + '...'
|
||||
: media.caption;
|
||||
if (media.title) {
|
||||
return media.title.length > 60
|
||||
? media.title.substring(0, 60) + '...'
|
||||
: media.title;
|
||||
}
|
||||
return media.originalName;
|
||||
}
|
||||
|
||||
@@ -329,7 +329,7 @@ export const SettingsView: React.FC = () => {
|
||||
<SettingRow
|
||||
id="project-language"
|
||||
label="Main Language"
|
||||
description="The primary language for your blog content. AI-generated alt text and captions will use this language."
|
||||
description="The primary language for your blog content. AI-generated titles, alt text, and captions will use this language."
|
||||
>
|
||||
<select
|
||||
id="project-language"
|
||||
|
||||
@@ -5,12 +5,12 @@ import { groupPostsByStatus } from '../../utils';
|
||||
import type { ChatConversation, ImportDefinitionData } from '../../types/electron';
|
||||
import './Sidebar.css';
|
||||
|
||||
/** Get display name for media: caption (truncated to 60 chars) or fallback to filename */
|
||||
/** Get display name for media: title (truncated to 60 chars) or fallback to filename */
|
||||
function getMediaDisplayName(media: MediaData): string {
|
||||
if (media.caption) {
|
||||
return media.caption.length > 60
|
||||
? media.caption.substring(0, 60) + '...'
|
||||
: media.caption;
|
||||
if (media.title) {
|
||||
return media.title.length > 60
|
||||
? media.title.substring(0, 60) + '...'
|
||||
: media.title;
|
||||
}
|
||||
return media.originalName;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ export interface MediaData {
|
||||
size: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
title?: string;
|
||||
alt?: string;
|
||||
caption?: string;
|
||||
createdAt: string;
|
||||
|
||||
4
src/renderer/types/electron.d.ts
vendored
4
src/renderer/types/electron.d.ts
vendored
@@ -85,6 +85,7 @@ export interface MediaData {
|
||||
size: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
title?: string;
|
||||
alt?: string;
|
||||
caption?: string;
|
||||
createdAt: string;
|
||||
@@ -101,6 +102,7 @@ export interface MediaFilter {
|
||||
export interface MediaSearchResult {
|
||||
id: string;
|
||||
originalName: string;
|
||||
title?: string;
|
||||
mimeType: string;
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -424,7 +426,7 @@ export interface ElectronAPI {
|
||||
analyzeTaxonomy: (categories: Array<{ name: string; slug: string; existsInProject: boolean }>, tags: Array<{ name: string; slug: string; existsInProject: boolean }>, modelId: string) => Promise<{ success: boolean; categoryMappings?: Record<string, string>; tagMappings?: Record<string, string>; error?: string }>;
|
||||
|
||||
// Media Analysis
|
||||
analyzeMediaImage: (mediaId: string, language?: string) => Promise<{ success: boolean; alt?: string; caption?: string; error?: string }>;
|
||||
analyzeMediaImage: (mediaId: string, language?: string) => Promise<{ success: boolean; title?: string; alt?: string; caption?: string; error?: string }>;
|
||||
|
||||
// Event listeners for streaming/progress
|
||||
onStreamDelta: (callback: (data: ChatStreamDelta) => void) => () => void;
|
||||
|
||||
Reference in New Issue
Block a user