feat: added field "title" and switched to it to free up caption for its normal use

This commit is contained in:
2026-02-15 09:09:48 +01:00
parent 4f71ac25bc
commit b5795867a8
20 changed files with 886 additions and 42 deletions

View File

@@ -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

View File

@@ -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}>

View File

@@ -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;
}

View File

@@ -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"

View File

@@ -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;
}

View File

@@ -54,6 +54,7 @@ export interface MediaData {
size: number;
width?: number;
height?: number;
title?: string;
alt?: string;
caption?: string;
createdAt: string;

View File

@@ -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;