From b5795867a83186d85c46a1aae3230365011644b1 Mon Sep 17 00:00:00 2001 From: hugo Date: Sun, 15 Feb 2026 09:09:48 +0100 Subject: [PATCH] feat: added field "title" and switched to it to free up caption for its normal use --- drizzle/0002_rainy_luckman.sql | 1 + drizzle/meta/0002_snapshot.json | 759 ++++++++++++++++++ drizzle/meta/_journal.json | 7 + src/main/database/schema.ts | 1 + src/main/engine/ChatEngine.ts | 2 +- src/main/engine/ImportExecutionEngine.ts | 2 +- src/main/engine/MediaEngine.ts | 23 +- src/main/engine/OpenCodeManager.ts | 19 +- src/main/ipc/chatHandlers.ts | 2 +- src/renderer/components/Editor/Editor.tsx | 17 +- .../components/InsertModal/InsertModal.tsx | 14 +- .../LinkedMediaPanel/LinkedMediaPanel.tsx | 10 +- .../components/SettingsView/SettingsView.tsx | 2 +- src/renderer/components/Sidebar/Sidebar.tsx | 10 +- src/renderer/store/appStore.ts | 1 + src/renderer/types/electron.d.ts | 4 +- .../engine/ImportExecutionEngine.e2e.test.ts | 8 +- tests/engine/ImportExecutionEngine.test.ts | 4 +- tests/engine/MediaEngine.test.ts | 41 +- tests/utils/factories.ts | 1 + 20 files changed, 886 insertions(+), 42 deletions(-) create mode 100644 drizzle/0002_rainy_luckman.sql create mode 100644 drizzle/meta/0002_snapshot.json diff --git a/drizzle/0002_rainy_luckman.sql b/drizzle/0002_rainy_luckman.sql new file mode 100644 index 0000000..14e7227 --- /dev/null +++ b/drizzle/0002_rainy_luckman.sql @@ -0,0 +1 @@ +ALTER TABLE `media` ADD `title` text; \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..3da9582 --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,759 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "26aa5345-b6d8-4426-a144-0199140a896a", + "prevId": "c9e34b7f-92a5-4549-99c9-e5a680004bfc", + "tables": { + "chat_conversations": { + "name": "chat_conversations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "copilot_session_id": { + "name": "copilot_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chat_messages": { + "name": "chat_messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "import_definitions": { + "name": "import_definitions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "wxr_file_path": { + "name": "wxr_file_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uploads_folder_path": { + "name": "uploads_folder_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_analysis_result": { + "name": "last_analysis_result", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "media": { + "name": "media", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "alt": { + "name": "alt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "caption": { + "name": "caption", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sidecar_path": { + "name": "sidecar_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "post_links": { + "name": "post_links", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "source_post_id": { + "name": "source_post_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_post_id": { + "name": "target_post_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "link_text": { + "name": "link_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "post_media": { + "name": "post_media", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "post_id": { + "name": "post_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "media_id": { + "name": "media_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "post_media_post_media_idx": { + "name": "post_media_post_media_idx", + "columns": [ + "post_id", + "media_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "posts": { + "name": "posts", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "excerpt": { + "name": "excerpt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "categories": { + "name": "categories", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "published_title": { + "name": "published_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "published_content": { + "name": "published_content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "published_tags": { + "name": "published_tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "published_categories": { + "name": "published_categories", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "published_excerpt": { + "name": "published_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "posts_project_slug_idx": { + "name": "posts_project_slug_idx", + "columns": [ + "project_id", + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "data_path": { + "name": "data_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "projects_slug_unique": { + "name": "projects_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tags": { + "name": "tags", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tags_project_name_idx": { + "name": "tags_project_name_idx", + "columns": [ + "project_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index ca0270d..26566d8 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1771088786493, "tag": "0001_narrow_black_bolt", "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1771141922712, + "tag": "0002_rainy_luckman", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/main/database/schema.ts b/src/main/database/schema.ts index 6a8e8b5..a877cae 100644 --- a/src/main/database/schema.ts +++ b/src/main/database/schema.ts @@ -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(), diff --git a/src/main/engine/ChatEngine.ts b/src/main/engine/ChatEngine.ts index 84175e9..2a0aa07 100644 --- a/src/main/engine/ChatEngine.ts +++ b/src/main/engine/ChatEngine.ts @@ -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. diff --git a/src/main/engine/ImportExecutionEngine.ts b/src/main/engine/ImportExecutionEngine.ts index 04a932d..2695de6 100644 --- a/src/main/engine/ImportExecutionEngine.ts +++ b/src/main/engine/ImportExecutionEngine.ts @@ -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: [], diff --git a/src/main/engine/MediaEngine.ts b/src/main/engine/MediaEngine.ts index 13178d0..b27a888 100644 --- a/src/main/engine/MediaEngine.ts +++ b/src/main/engine/MediaEngine.ts @@ -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, }); diff --git a/src/main/engine/OpenCodeManager.ts b/src/main/engine/OpenCodeManager.ts index 98b6fd6..f472a1f 100644 --- a/src/main/engine/OpenCodeManager.ts +++ b/src/main/engine/OpenCodeManager.ts @@ -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 = {}; + 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, }; diff --git a/src/main/ipc/chatHandlers.ts b/src/main/ipc/chatHandlers.ts index aa965ed..ce90d9e 100644 --- a/src/main/ipc/chatHandlers.ts +++ b/src/main/ipc/chatHandlers.ts @@ -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(); diff --git a/src/renderer/components/Editor/Editor.tsx b/src/renderer/components/Editor/Editor.tsx index 2ac6cde..d939103 100644 --- a/src/renderer/components/Editor/Editor.tsx +++ b/src/renderer/components/Editor/Editor.tsx @@ -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 }) => { > 🤖 - AI: Generate Alt & Caption - Uses Claude Sonnet 4.5 to analyze the image + AI: Generate Title, Alt & Caption + Analyzes the image to suggest metadata @@ -1755,6 +1759,15 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => { )} +
+ + setTitle(e.target.value)} + placeholder="Title for lists and search results" + /> +
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 = ({ 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 (
diff --git a/src/renderer/components/LinkedMediaPanel/LinkedMediaPanel.tsx b/src/renderer/components/LinkedMediaPanel/LinkedMediaPanel.tsx index 403840d..d7ef568 100644 --- a/src/renderer/components/LinkedMediaPanel/LinkedMediaPanel.tsx +++ b/src/renderer/components/LinkedMediaPanel/LinkedMediaPanel.tsx @@ -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; } diff --git a/src/renderer/components/SettingsView/SettingsView.tsx b/src/renderer/components/SettingsView/SettingsView.tsx index cab4a13..9cda8af 100644 --- a/src/renderer/components/SettingsView/SettingsView.tsx +++ b/src/renderer/components/SettingsView/SettingsView.tsx @@ -329,7 +329,7 @@ export const SettingsView: React.FC = () => {