feat: added field "title" and switched to it to free up caption for its normal use
This commit is contained in:
1
drizzle/0002_rainy_luckman.sql
Normal file
1
drizzle/0002_rainy_luckman.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `media` ADD `title` text;
|
||||||
759
drizzle/meta/0002_snapshot.json
Normal file
759
drizzle/meta/0002_snapshot.json
Normal file
@@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,13 @@
|
|||||||
"when": 1771088786493,
|
"when": 1771088786493,
|
||||||
"tag": "0001_narrow_black_bolt",
|
"tag": "0001_narrow_black_bolt",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1771141922712,
|
||||||
|
"tag": "0002_rainy_luckman",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -54,6 +54,7 @@ export const media = sqliteTable('media', {
|
|||||||
size: integer('size').notNull(),
|
size: integer('size').notNull(),
|
||||||
width: integer('width'),
|
width: integer('width'),
|
||||||
height: integer('height'),
|
height: integer('height'),
|
||||||
|
title: text('title'),
|
||||||
alt: text('alt'),
|
alt: text('alt'),
|
||||||
caption: text('caption'),
|
caption: text('caption'),
|
||||||
filePath: text('file_path').notNull(),
|
filePath: text('file_path').notNull(),
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ Available Tools:
|
|||||||
- list_media: List media files with optional MIME type filtering.
|
- 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.
|
- 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_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_tags: List all tags with post counts.
|
||||||
- list_categories: List all categories 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.
|
- 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
|
// Import the media file
|
||||||
const mediaEngine = getMediaEngine();
|
const mediaEngine = getMediaEngine();
|
||||||
await mediaEngine.importMedia(sourcePath, {
|
await mediaEngine.importMedia(sourcePath, {
|
||||||
caption: wxrMedia.title || undefined,
|
title: wxrMedia.title || undefined,
|
||||||
alt: wxrMedia.description || undefined,
|
alt: wxrMedia.description || undefined,
|
||||||
mimeType: wxrMedia.mimeType,
|
mimeType: wxrMedia.mimeType,
|
||||||
tags: [],
|
tags: [],
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export interface MediaData {
|
|||||||
size: number;
|
size: number;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
|
title?: string;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
@@ -42,6 +43,7 @@ export interface MediaMetadata {
|
|||||||
size: number;
|
size: number;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
|
title?: string;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -61,7 +63,7 @@ export interface MediaFilter {
|
|||||||
export interface MediaSearchResult {
|
export interface MediaSearchResult {
|
||||||
id: string;
|
id: string;
|
||||||
originalName: string;
|
originalName: string;
|
||||||
caption?: string;
|
title?: string;
|
||||||
mimeType: string;
|
mimeType: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
@@ -92,12 +94,13 @@ export class MediaEngine extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the FTS index for a media item.
|
* 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: {
|
private async updateFTSIndex(item: {
|
||||||
id: string;
|
id: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
originalName: string;
|
originalName: string;
|
||||||
|
title?: string;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
@@ -111,6 +114,7 @@ export class MediaEngine extends EventEmitter {
|
|||||||
// Combine all searchable fields and stem them
|
// Combine all searchable fields and stem them
|
||||||
const allText = [
|
const allText = [
|
||||||
item.originalName,
|
item.originalName,
|
||||||
|
item.title || '',
|
||||||
item.alt || '',
|
item.alt || '',
|
||||||
item.caption || '',
|
item.caption || '',
|
||||||
item.tags.join(' '),
|
item.tags.join(' '),
|
||||||
@@ -300,6 +304,7 @@ export class MediaEngine extends EventEmitter {
|
|||||||
size: mediaData.size,
|
size: mediaData.size,
|
||||||
width: mediaData.width,
|
width: mediaData.width,
|
||||||
height: mediaData.height,
|
height: mediaData.height,
|
||||||
|
title: mediaData.title,
|
||||||
alt: mediaData.alt,
|
alt: mediaData.alt,
|
||||||
caption: mediaData.caption,
|
caption: mediaData.caption,
|
||||||
createdAt: mediaData.createdAt.toISOString(),
|
createdAt: mediaData.createdAt.toISOString(),
|
||||||
@@ -319,6 +324,7 @@ export class MediaEngine extends EventEmitter {
|
|||||||
|
|
||||||
if (metadata.width) lines.push(`width: ${metadata.width}`);
|
if (metadata.width) lines.push(`width: ${metadata.width}`);
|
||||||
if (metadata.height) lines.push(`height: ${metadata.height}`);
|
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.alt) lines.push(`alt: "${metadata.alt}"`);
|
||||||
if (metadata.caption) lines.push(`caption: "${metadata.caption}"`);
|
if (metadata.caption) lines.push(`caption: "${metadata.caption}"`);
|
||||||
|
|
||||||
@@ -385,6 +391,9 @@ export class MediaEngine extends EventEmitter {
|
|||||||
case 'height':
|
case 'height':
|
||||||
metadata.height = parseInt(value, 10);
|
metadata.height = parseInt(value, 10);
|
||||||
break;
|
break;
|
||||||
|
case 'title':
|
||||||
|
metadata.title = value;
|
||||||
|
break;
|
||||||
case 'alt':
|
case 'alt':
|
||||||
metadata.alt = value;
|
metadata.alt = value;
|
||||||
break;
|
break;
|
||||||
@@ -492,6 +501,7 @@ export class MediaEngine extends EventEmitter {
|
|||||||
size: sourceBuffer.length,
|
size: sourceBuffer.length,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
title: metadata?.title,
|
||||||
alt: metadata?.alt,
|
alt: metadata?.alt,
|
||||||
caption: metadata?.caption,
|
caption: metadata?.caption,
|
||||||
createdAt,
|
createdAt,
|
||||||
@@ -518,6 +528,7 @@ export class MediaEngine extends EventEmitter {
|
|||||||
size: mediaData.size,
|
size: mediaData.size,
|
||||||
width: mediaData.width,
|
width: mediaData.width,
|
||||||
height: mediaData.height,
|
height: mediaData.height,
|
||||||
|
title: mediaData.title,
|
||||||
alt: mediaData.alt,
|
alt: mediaData.alt,
|
||||||
caption: mediaData.caption,
|
caption: mediaData.caption,
|
||||||
filePath: destPath,
|
filePath: destPath,
|
||||||
@@ -535,6 +546,7 @@ export class MediaEngine extends EventEmitter {
|
|||||||
id: mediaData.id,
|
id: mediaData.id,
|
||||||
projectId: this.currentProjectId,
|
projectId: this.currentProjectId,
|
||||||
originalName: mediaData.originalName,
|
originalName: mediaData.originalName,
|
||||||
|
title: mediaData.title,
|
||||||
alt: mediaData.alt,
|
alt: mediaData.alt,
|
||||||
caption: mediaData.caption,
|
caption: mediaData.caption,
|
||||||
tags: mediaData.tags,
|
tags: mediaData.tags,
|
||||||
@@ -566,6 +578,7 @@ export class MediaEngine extends EventEmitter {
|
|||||||
|
|
||||||
await db.update(media)
|
await db.update(media)
|
||||||
.set({
|
.set({
|
||||||
|
title: updated.title,
|
||||||
alt: updated.alt,
|
alt: updated.alt,
|
||||||
caption: updated.caption,
|
caption: updated.caption,
|
||||||
updatedAt: updated.updatedAt,
|
updatedAt: updated.updatedAt,
|
||||||
@@ -578,6 +591,7 @@ export class MediaEngine extends EventEmitter {
|
|||||||
id: updated.id,
|
id: updated.id,
|
||||||
projectId: this.currentProjectId,
|
projectId: this.currentProjectId,
|
||||||
originalName: updated.originalName,
|
originalName: updated.originalName,
|
||||||
|
title: updated.title,
|
||||||
alt: updated.alt,
|
alt: updated.alt,
|
||||||
caption: updated.caption,
|
caption: updated.caption,
|
||||||
tags: updated.tags,
|
tags: updated.tags,
|
||||||
@@ -641,6 +655,7 @@ export class MediaEngine extends EventEmitter {
|
|||||||
size: dbMedia.size,
|
size: dbMedia.size,
|
||||||
width: dbMedia.width || undefined,
|
width: dbMedia.width || undefined,
|
||||||
height: dbMedia.height || undefined,
|
height: dbMedia.height || undefined,
|
||||||
|
title: dbMedia.title || undefined,
|
||||||
alt: dbMedia.alt || undefined,
|
alt: dbMedia.alt || undefined,
|
||||||
caption: dbMedia.caption || undefined,
|
caption: dbMedia.caption || undefined,
|
||||||
createdAt: dbMedia.createdAt,
|
createdAt: dbMedia.createdAt,
|
||||||
@@ -666,6 +681,7 @@ export class MediaEngine extends EventEmitter {
|
|||||||
size: dbMedia.size,
|
size: dbMedia.size,
|
||||||
width: dbMedia.width || undefined,
|
width: dbMedia.width || undefined,
|
||||||
height: dbMedia.height || undefined,
|
height: dbMedia.height || undefined,
|
||||||
|
title: dbMedia.title || undefined,
|
||||||
alt: dbMedia.alt || undefined,
|
alt: dbMedia.alt || undefined,
|
||||||
caption: dbMedia.caption || undefined,
|
caption: dbMedia.caption || undefined,
|
||||||
createdAt: dbMedia.createdAt,
|
createdAt: dbMedia.createdAt,
|
||||||
@@ -726,6 +742,7 @@ export class MediaEngine extends EventEmitter {
|
|||||||
size: dbMedia.size,
|
size: dbMedia.size,
|
||||||
width: dbMedia.width || undefined,
|
width: dbMedia.width || undefined,
|
||||||
height: dbMedia.height || undefined,
|
height: dbMedia.height || undefined,
|
||||||
|
title: dbMedia.title || undefined,
|
||||||
alt: dbMedia.alt || undefined,
|
alt: dbMedia.alt || undefined,
|
||||||
caption: dbMedia.caption || undefined,
|
caption: dbMedia.caption || undefined,
|
||||||
createdAt: dbMedia.createdAt,
|
createdAt: dbMedia.createdAt,
|
||||||
@@ -770,7 +787,7 @@ export class MediaEngine extends EventEmitter {
|
|||||||
searchResults.push({
|
searchResults.push({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
originalName: item.originalName,
|
originalName: item.originalName,
|
||||||
caption: item.caption || undefined,
|
title: item.title || undefined,
|
||||||
mimeType: item.mimeType,
|
mimeType: item.mimeType,
|
||||||
createdAt: item.createdAt,
|
createdAt: item.createdAt,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -739,11 +739,12 @@ export class OpenCodeManager {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'update_media_metadata',
|
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: {
|
input_schema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
mediaId: { type: 'string', description: 'The unique ID of the media to update' },
|
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' },
|
alt: { type: 'string', description: 'New alt text for the image' },
|
||||||
caption: { type: 'string', description: 'New caption for the image' },
|
caption: { type: 'string', description: 'New caption for the image' },
|
||||||
tags: { type: 'array', items: { type: 'string' }, description: 'New tags for the media' },
|
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,
|
id: media.id, filename: media.filename,
|
||||||
originalName: media.originalName, mimeType: media.mimeType,
|
originalName: media.originalName, mimeType: media.mimeType,
|
||||||
size: media.size, width: media.width, height: media.height,
|
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,
|
createdAt: media.createdAt, updatedAt: media.updatedAt,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -945,7 +946,7 @@ export class OpenCodeManager {
|
|||||||
media: mediaList.map(m => ({
|
media: mediaList.map(m => ({
|
||||||
id: m.id, filename: m.filename,
|
id: m.id, filename: m.filename,
|
||||||
originalName: m.originalName, mimeType: m.mimeType,
|
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': {
|
case 'update_media_metadata': {
|
||||||
const updates: Record<string, unknown> = {};
|
const updates: Record<string, unknown> = {};
|
||||||
|
if (args.title !== undefined) updates.title = args.title;
|
||||||
if (args.alt !== undefined) updates.alt = args.alt;
|
if (args.alt !== undefined) updates.alt = args.alt;
|
||||||
if (args.caption !== undefined) updates.caption = args.caption;
|
if (args.caption !== undefined) updates.caption = args.caption;
|
||||||
if (args.tags !== undefined) updates.tags = args.tags;
|
if (args.tags !== undefined) updates.tags = args.tags;
|
||||||
@@ -1033,6 +1035,7 @@ export class OpenCodeManager {
|
|||||||
originalName: mediaItem.originalName,
|
originalName: mediaItem.originalName,
|
||||||
width: mediaItem.width,
|
width: mediaItem.width,
|
||||||
height: mediaItem.height,
|
height: mediaItem.height,
|
||||||
|
title: mediaItem.title,
|
||||||
alt: mediaItem.alt,
|
alt: mediaItem.alt,
|
||||||
caption: mediaItem.caption,
|
caption: mediaItem.caption,
|
||||||
size: size,
|
size: size,
|
||||||
@@ -1080,6 +1083,7 @@ export class OpenCodeManager {
|
|||||||
filename: link.media.filename,
|
filename: link.media.filename,
|
||||||
originalName: link.media.originalName,
|
originalName: link.media.originalName,
|
||||||
mimeType: link.media.mimeType,
|
mimeType: link.media.mimeType,
|
||||||
|
title: link.media.title,
|
||||||
alt: link.media.alt,
|
alt: link.media.alt,
|
||||||
caption: link.media.caption,
|
caption: link.media.caption,
|
||||||
width: link.media.width,
|
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
|
* This is a one-shot request that looks at the image and suggests metadata
|
||||||
*/
|
*/
|
||||||
async analyzeMediaImage(mediaId: string, language: string = 'en'): Promise<{
|
async analyzeMediaImage(mediaId: string, language: string = 'en'): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
title?: string;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
error?: 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 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"
|
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).
|
CAPTION: Short, engaging blog caption (5-20 words).
|
||||||
|
|
||||||
Respond with JSON only: {"alt": "...", "caption": "..."}`;
|
Respond with JSON only: {"title": "...", "alt": "...", "caption": "..."}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Using Claude Sonnet 4.5 for best image analysis
|
// Using Claude Sonnet 4.5 for best image analysis
|
||||||
@@ -1570,6 +1576,7 @@ Respond with JSON only: {"alt": "...", "caption": "..."}`;
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
title: result.title || undefined,
|
||||||
alt: result.alt || undefined,
|
alt: result.alt || undefined,
|
||||||
caption: result.caption || undefined,
|
caption: result.caption || undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ export function registerChatHandlers(): void {
|
|||||||
|
|
||||||
// ============ Media Analysis ============
|
// ============ 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) => {
|
ipcMain.handle('chat:analyzeMediaImage', async (_, mediaId: string, language?: string) => {
|
||||||
try {
|
try {
|
||||||
const manager = getOpenCodeManager();
|
const manager = getOpenCodeManager();
|
||||||
|
|||||||
@@ -1426,6 +1426,7 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
|||||||
const { media, updateMedia, showErrorModal, showConfirmDeleteModal, openTab } = useAppStore();
|
const { media, updateMedia, showErrorModal, showConfirmDeleteModal, openTab } = useAppStore();
|
||||||
const item = media.find(m => m.id === mediaId);
|
const item = media.find(m => m.id === mediaId);
|
||||||
|
|
||||||
|
const [title, setTitle] = useState(item?.title || '');
|
||||||
const [alt, setAlt] = useState(item?.alt || '');
|
const [alt, setAlt] = useState(item?.alt || '');
|
||||||
const [caption, setCaption] = useState(item?.caption || '');
|
const [caption, setCaption] = useState(item?.caption || '');
|
||||||
const [tags, setTags] = useState(item?.tags.join(', ') || '');
|
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);
|
const result = await window.electronAPI?.chat.analyzeMediaImage(item.id, projectLanguage);
|
||||||
|
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
|
if (result.title) setTitle(result.title);
|
||||||
if (result.alt) setAlt(result.alt);
|
if (result.alt) setAlt(result.alt);
|
||||||
if (result.caption) setCaption(result.caption);
|
if (result.caption) setCaption(result.caption);
|
||||||
showToast.success('AI analysis complete');
|
showToast.success('AI analysis complete');
|
||||||
@@ -1581,6 +1583,7 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (item) {
|
if (item) {
|
||||||
|
setTitle(item.title || '');
|
||||||
setAlt(item.alt || '');
|
setAlt(item.alt || '');
|
||||||
setCaption(item.caption || '');
|
setCaption(item.caption || '');
|
||||||
setTags(item.tags.join(', '));
|
setTags(item.tags.join(', '));
|
||||||
@@ -1594,6 +1597,7 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
|||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
try {
|
try {
|
||||||
const updated = await window.electronAPI?.media.update(item.id, {
|
const updated = await window.electronAPI?.media.update(item.id, {
|
||||||
|
title,
|
||||||
alt,
|
alt,
|
||||||
caption,
|
caption,
|
||||||
tags: tags.split(',').map(t => t.trim()).filter(t => t.length > 0),
|
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-icon">🤖</span>
|
||||||
<span className="quick-action-text">
|
<span className="quick-action-text">
|
||||||
<strong>AI: Generate Alt & Caption</strong>
|
<strong>AI: Generate Title, Alt & Caption</strong>
|
||||||
<small>Uses Claude Sonnet 4.5 to analyze the image</small>
|
<small>Analyzes the image to suggest metadata</small>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1755,6 +1759,15 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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">
|
<div className="editor-field">
|
||||||
<label>Alt Text</label>
|
<label>Alt Text</label>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -11,17 +11,17 @@ interface PostSearchResult {
|
|||||||
interface MediaSearchResult {
|
interface MediaSearchResult {
|
||||||
id: string;
|
id: string;
|
||||||
originalName: string;
|
originalName: string;
|
||||||
caption?: string;
|
title?: string;
|
||||||
mimeType: string;
|
mimeType: string;
|
||||||
createdAt: 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 {
|
function getMediaDisplayName(media: MediaSearchResult): string {
|
||||||
if (media.caption) {
|
if (media.title) {
|
||||||
return media.caption.length > 60
|
return media.title.length > 60
|
||||||
? media.caption.substring(0, 60) + '...'
|
? media.title.substring(0, 60) + '...'
|
||||||
: media.caption;
|
: media.title;
|
||||||
}
|
}
|
||||||
return media.originalName;
|
return media.originalName;
|
||||||
}
|
}
|
||||||
@@ -187,7 +187,7 @@ export const InsertModal: React.FC<InsertModalProps> = ({
|
|||||||
const externalLabel = mode === 'link' ? 'External URL' : 'External Image';
|
const externalLabel = mode === 'link' ? 'External URL' : 'External Image';
|
||||||
const searchPlaceholder = mode === 'link'
|
const searchPlaceholder = mode === 'link'
|
||||||
? 'Search posts by title or content...'
|
? 'Search posts by title or content...'
|
||||||
: 'Search media by name, caption, or alt text...';
|
: 'Search media by name, title, or alt text...';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="insert-modal-backdrop" onClick={handleBackdropClick}>
|
<div className="insert-modal-backdrop" onClick={handleBackdropClick}>
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ import { useAppStore, MediaData } from '../../store';
|
|||||||
import { showToast } from '../Toast';
|
import { showToast } from '../Toast';
|
||||||
import './LinkedMediaPanel.css';
|
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 {
|
function getMediaDisplayName(media: MediaData): string {
|
||||||
if (media.caption) {
|
if (media.title) {
|
||||||
return media.caption.length > 60
|
return media.title.length > 60
|
||||||
? media.caption.substring(0, 60) + '...'
|
? media.title.substring(0, 60) + '...'
|
||||||
: media.caption;
|
: media.title;
|
||||||
}
|
}
|
||||||
return media.originalName;
|
return media.originalName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ export const SettingsView: React.FC = () => {
|
|||||||
<SettingRow
|
<SettingRow
|
||||||
id="project-language"
|
id="project-language"
|
||||||
label="Main 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
|
<select
|
||||||
id="project-language"
|
id="project-language"
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import { groupPostsByStatus } from '../../utils';
|
|||||||
import type { ChatConversation, ImportDefinitionData } from '../../types/electron';
|
import type { ChatConversation, ImportDefinitionData } from '../../types/electron';
|
||||||
import './Sidebar.css';
|
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 {
|
function getMediaDisplayName(media: MediaData): string {
|
||||||
if (media.caption) {
|
if (media.title) {
|
||||||
return media.caption.length > 60
|
return media.title.length > 60
|
||||||
? media.caption.substring(0, 60) + '...'
|
? media.title.substring(0, 60) + '...'
|
||||||
: media.caption;
|
: media.title;
|
||||||
}
|
}
|
||||||
return media.originalName;
|
return media.originalName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export interface MediaData {
|
|||||||
size: number;
|
size: number;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
|
title?: string;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
createdAt: 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;
|
size: number;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
|
title?: string;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -101,6 +102,7 @@ export interface MediaFilter {
|
|||||||
export interface MediaSearchResult {
|
export interface MediaSearchResult {
|
||||||
id: string;
|
id: string;
|
||||||
originalName: string;
|
originalName: string;
|
||||||
|
title?: string;
|
||||||
mimeType: string;
|
mimeType: string;
|
||||||
createdAt: 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 }>;
|
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
|
// 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
|
// Event listeners for streaming/progress
|
||||||
onStreamDelta: (callback: (data: ChatStreamDelta) => void) => () => void;
|
onStreamDelta: (callback: (data: ChatStreamDelta) => void) => () => void;
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const insertedPosts: Array<{
|
|||||||
const insertedMedia: Array<{
|
const insertedMedia: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
linkedPostIds: string[];
|
linkedPostIds: string[];
|
||||||
caption?: string;
|
title?: string;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
const createdTags: string[] = [];
|
const createdTags: string[] = [];
|
||||||
@@ -167,7 +167,7 @@ const mockMediaEngine = {
|
|||||||
id: `media-${Math.random().toString(36).substr(2, 9)}`,
|
id: `media-${Math.random().toString(36).substr(2, 9)}`,
|
||||||
filename: path.basename(sourcePath),
|
filename: path.basename(sourcePath),
|
||||||
originalName: metadata?.originalName || path.basename(sourcePath),
|
originalName: metadata?.originalName || path.basename(sourcePath),
|
||||||
caption: metadata?.caption,
|
title: metadata?.title,
|
||||||
linkedPostIds: metadata?.linkedPostIds || [],
|
linkedPostIds: metadata?.linkedPostIds || [],
|
||||||
};
|
};
|
||||||
insertedMedia.push(result);
|
insertedMedia.push(result);
|
||||||
@@ -1044,9 +1044,9 @@ describe('ImportExecutionEngine E2E Tests', () => {
|
|||||||
|
|
||||||
expect(result.media.imported).toBe(1);
|
expect(result.media.imported).toBe(1);
|
||||||
|
|
||||||
// Should be imported with caption from WXR title
|
// Should be imported with title from WXR title
|
||||||
expect(insertedMedia.length).toBe(1);
|
expect(insertedMedia.length).toBe(1);
|
||||||
expect(insertedMedia[0].caption).toBe('standalone-logo');
|
expect(insertedMedia[0].title).toBe('standalone-logo');
|
||||||
|
|
||||||
// No linked posts (standalone)
|
// No linked posts (standalone)
|
||||||
expect(insertedMedia[0].linkedPostIds.length).toBe(0);
|
expect(insertedMedia[0].linkedPostIds.length).toBe(0);
|
||||||
|
|||||||
@@ -1016,7 +1016,7 @@ describe('ImportExecutionEngine', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set caption from WXR title', async () => {
|
it('should set title from WXR title', async () => {
|
||||||
const wxrMedia = createMockWxrMedia({ title: 'Beautiful Sunset' });
|
const wxrMedia = createMockWxrMedia({ title: 'Beautiful Sunset' });
|
||||||
const report = createMockAnalysisReport({
|
const report = createMockAnalysisReport({
|
||||||
media: {
|
media: {
|
||||||
@@ -1035,7 +1035,7 @@ describe('ImportExecutionEngine', () => {
|
|||||||
expect(mockMediaEngine.importMedia).toHaveBeenCalledWith(
|
expect(mockMediaEngine.importMedia).toHaveBeenCalledWith(
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
caption: 'Beautiful Sunset',
|
title: 'Beautiful Sunset',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -535,11 +535,19 @@ describe('MediaEngine', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Alt Text and Caption', () => {
|
describe('Title, Alt Text and Caption', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockFiles.set('/source/image.jpg', Buffer.from('image-data'));
|
mockFiles.set('/source/image.jpg', Buffer.from('image-data'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should store title for display in lists and search', async () => {
|
||||||
|
const media = await mediaEngine.importMedia('/source/image.jpg', {
|
||||||
|
title: 'Mountain Sunrise Photo',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(media.title).toBe('Mountain Sunrise Photo');
|
||||||
|
});
|
||||||
|
|
||||||
it('should store alt text for accessibility', async () => {
|
it('should store alt text for accessibility', async () => {
|
||||||
const media = await mediaEngine.importMedia('/source/image.jpg', {
|
const media = await mediaEngine.importMedia('/source/image.jpg', {
|
||||||
alt: 'A scenic mountain view',
|
alt: 'A scenic mountain view',
|
||||||
@@ -556,9 +564,10 @@ describe('MediaEngine', () => {
|
|||||||
expect(media.caption).toBe('Photo taken at Mt. Rainier, 2024');
|
expect(media.caption).toBe('Photo taken at Mt. Rainier, 2024');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle media without alt or caption', async () => {
|
it('should handle media without title, alt or caption', async () => {
|
||||||
const media = await mediaEngine.importMedia('/source/image.jpg');
|
const media = await mediaEngine.importMedia('/source/image.jpg');
|
||||||
|
|
||||||
|
expect(media.title).toBeUndefined();
|
||||||
expect(media.alt).toBeUndefined();
|
expect(media.alt).toBeUndefined();
|
||||||
expect(media.caption).toBeUndefined();
|
expect(media.caption).toBeUndefined();
|
||||||
});
|
});
|
||||||
@@ -847,10 +856,36 @@ linkedPostIds: ["post-a", "post-b", "post-c"]`;
|
|||||||
return chain;
|
return chain;
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await mediaEngine.updateMedia('non-existent-id', { caption: 'New caption' });
|
const result = await mediaEngine.updateMedia('non-existent-id', { title: 'New title' });
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should update media title', async () => {
|
||||||
|
vi.mocked(mockLocalDb.select).mockImplementation(() => {
|
||||||
|
const chain = createSelectChain();
|
||||||
|
chain.where = vi.fn().mockReturnValue({
|
||||||
|
...chain,
|
||||||
|
get: vi.fn().mockResolvedValue({
|
||||||
|
id: 'media-id',
|
||||||
|
projectId: 'default',
|
||||||
|
originalName: 'test.jpg',
|
||||||
|
mimeType: 'image/jpeg',
|
||||||
|
size: 1024,
|
||||||
|
filePath: '/mock/media/test.jpg',
|
||||||
|
title: 'Old title',
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return chain;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await mediaEngine.updateMedia('media-id', { title: 'New title' });
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.title).toBe('New title');
|
||||||
|
});
|
||||||
|
|
||||||
it('should update media caption', async () => {
|
it('should update media caption', async () => {
|
||||||
vi.mocked(mockLocalDb.select).mockImplementation(() => {
|
vi.mocked(mockLocalDb.select).mockImplementation(() => {
|
||||||
const chain = createSelectChain();
|
const chain = createSelectChain();
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export function createMockMedia(overrides?: Partial<MediaData>): MediaData {
|
|||||||
size: 1024 * 100, // 100KB
|
size: 1024 * 100, // 100KB
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 600,
|
height: 600,
|
||||||
|
title: 'Test Image Title',
|
||||||
alt: 'Test image',
|
alt: 'Test image',
|
||||||
caption: 'A test image caption',
|
caption: 'A test image caption',
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
|
|||||||
Reference in New Issue
Block a user