feat: AI Quickaction to generate caption and alt text for images
This commit is contained in:
@@ -950,3 +950,80 @@
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* Quick Actions Dropdown */
|
||||
.quick-actions-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.quick-actions-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.quick-actions-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.quick-actions-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 4px;
|
||||
min-width: 280px;
|
||||
background: var(--vscode-dropdown-background, #3c3c3c);
|
||||
border: 1px solid var(--vscode-dropdown-border, #454545);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quick-action-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--vscode-dropdown-foreground, #ccc);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.quick-action-item:hover:not(:disabled) {
|
||||
background: var(--vscode-list-hoverBackground, #2a2d2e);
|
||||
}
|
||||
|
||||
.quick-action-item:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.quick-action-icon {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.quick-action-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.quick-action-text strong {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.quick-action-text small {
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@@ -1223,6 +1223,65 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
const [linkedPosts, setLinkedPosts] = useState<{ postId: string; sortOrder: number }[]>([]);
|
||||
const [showPostPicker, setShowPostPicker] = useState(false);
|
||||
const [postSearchQuery, setPostSearchQuery] = useState('');
|
||||
|
||||
// Quick action menu state
|
||||
const [showQuickActions, setShowQuickActions] = useState(false);
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
||||
const [projectLanguage, setProjectLanguage] = useState('en');
|
||||
const quickActionsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Load project language setting
|
||||
useEffect(() => {
|
||||
window.electronAPI?.meta.getProjectMetadata().then(metadata => {
|
||||
if (metadata?.mainLanguage) {
|
||||
setProjectLanguage(metadata.mainLanguage);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Close quick actions menu when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (quickActionsRef.current && !quickActionsRef.current.contains(event.target as Node)) {
|
||||
setShowQuickActions(false);
|
||||
}
|
||||
};
|
||||
if (showQuickActions) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
}, [showQuickActions]);
|
||||
|
||||
// Handle AI image analysis for alt text and caption
|
||||
const handleAIAnalysis = async () => {
|
||||
if (!item || isAnalyzing) return;
|
||||
|
||||
setShowQuickActions(false);
|
||||
setIsAnalyzing(true);
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI?.chat.analyzeMediaImage(item.id, projectLanguage);
|
||||
|
||||
if (result?.success) {
|
||||
if (result.alt) setAlt(result.alt);
|
||||
if (result.caption) setCaption(result.caption);
|
||||
showToast.success('AI analysis complete');
|
||||
} else {
|
||||
showErrorModal({
|
||||
title: 'AI Analysis Failed',
|
||||
message: result?.error || 'Failed to analyze image',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to analyze image:', error);
|
||||
showErrorModal({
|
||||
title: 'AI Analysis Error',
|
||||
message: (error as Error).message || 'Failed to analyze image',
|
||||
});
|
||||
} finally {
|
||||
setIsAnalyzing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Load linked posts for this media
|
||||
useEffect(() => {
|
||||
@@ -1381,6 +1440,34 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="editor-actions">
|
||||
{/* Quick Actions Dropdown */}
|
||||
{item.mimeType.startsWith('image/') && (
|
||||
<div className="quick-actions-wrapper" ref={quickActionsRef}>
|
||||
<button
|
||||
className="secondary quick-actions-btn"
|
||||
onClick={() => setShowQuickActions(!showQuickActions)}
|
||||
disabled={isAnalyzing}
|
||||
title="Quick Actions"
|
||||
>
|
||||
{isAnalyzing ? '⏳ Analyzing...' : '⚡ Quick Actions'}
|
||||
</button>
|
||||
{showQuickActions && (
|
||||
<div className="quick-actions-menu">
|
||||
<button
|
||||
className="quick-action-item"
|
||||
onClick={handleAIAnalysis}
|
||||
disabled={isAnalyzing}
|
||||
>
|
||||
<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>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<button onClick={handleSave}>Save</button>
|
||||
<button onClick={handleDelete} className="secondary danger">Delete</button>
|
||||
</div>
|
||||
|
||||
@@ -110,6 +110,7 @@ export const SettingsView: React.FC = () => {
|
||||
const [projectDescription, setProjectDescription] = useState('');
|
||||
const [projectDataPath, setProjectDataPath] = useState('');
|
||||
const [defaultProjectPath, setDefaultProjectPath] = useState('');
|
||||
const [projectMainLanguage, setProjectMainLanguage] = useState('en');
|
||||
|
||||
// Post categories management
|
||||
const [postCategories, setPostCategories] = useState<string[]>(DEFAULT_POST_CATEGORIES);
|
||||
@@ -143,6 +144,13 @@ export const SettingsView: React.FC = () => {
|
||||
window.electronAPI?.app.getDefaultProjectPath(activeProject.id).then(path => {
|
||||
setDefaultProjectPath(path);
|
||||
});
|
||||
|
||||
// Load project metadata (includes mainLanguage)
|
||||
window.electronAPI?.meta.getProjectMetadata().then(metadata => {
|
||||
if (metadata?.mainLanguage) {
|
||||
setProjectMainLanguage(metadata.mainLanguage);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [activeProject]);
|
||||
|
||||
@@ -299,12 +307,13 @@ export const SettingsView: React.FC = () => {
|
||||
setActiveProject(updated as any);
|
||||
useAppStore.getState().updateProject(activeProject.id, updated as any);
|
||||
|
||||
// Also update project.json to keep dataPath in sync
|
||||
// Also update project.json to keep dataPath and mainLanguage in sync
|
||||
await window.electronAPI?.meta.updateProjectMetadata({
|
||||
name: projectName.trim() || activeProject.name,
|
||||
description: projectDescription.trim(),
|
||||
dataPath: projectDataPath.trim() || undefined,
|
||||
} as any);
|
||||
mainLanguage: projectMainLanguage,
|
||||
});
|
||||
}
|
||||
showToast.success('Project settings saved');
|
||||
} catch (error) {
|
||||
@@ -325,7 +334,7 @@ export const SettingsView: React.FC = () => {
|
||||
};
|
||||
|
||||
// Keywords for each section for search filtering
|
||||
const projectKeywords = ['project', 'name', 'description', 'blog', 'site', 'path', 'folder', 'location', 'data'];
|
||||
const projectKeywords = ['project', 'name', 'description', 'blog', 'site', 'path', 'folder', 'location', 'data', 'language'];
|
||||
const editorKeywords = ['editor', 'mode', 'wysiwyg', 'markdown', 'preview', 'visual'];
|
||||
const contentKeywords = ['content', 'categories', 'post', 'article', 'picture', 'aside', 'page'];
|
||||
const aiKeywords = ['ai', 'assistant', 'chat', 'model', 'prompt', 'system', 'api', 'key', 'claude', 'gpt', 'opencode'];
|
||||
@@ -392,6 +401,39 @@ export const SettingsView: React.FC = () => {
|
||||
</div>
|
||||
</SettingRow>
|
||||
|
||||
<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."
|
||||
>
|
||||
<select
|
||||
id="project-language"
|
||||
value={projectMainLanguage}
|
||||
onChange={(e) => setProjectMainLanguage(e.target.value)}
|
||||
>
|
||||
<option value="en">English</option>
|
||||
<option value="de">German (Deutsch)</option>
|
||||
<option value="es">Spanish (Español)</option>
|
||||
<option value="fr">French (Français)</option>
|
||||
<option value="it">Italian (Italiano)</option>
|
||||
<option value="pt">Portuguese (Português)</option>
|
||||
<option value="nl">Dutch (Nederlands)</option>
|
||||
<option value="pl">Polish (Polski)</option>
|
||||
<option value="ru">Russian (Русский)</option>
|
||||
<option value="ja">Japanese (日本語)</option>
|
||||
<option value="zh">Chinese (中文)</option>
|
||||
<option value="ko">Korean (한국어)</option>
|
||||
<option value="ar">Arabic (العربية)</option>
|
||||
<option value="hi">Hindi (हिन्दी)</option>
|
||||
<option value="tr">Turkish (Türkçe)</option>
|
||||
<option value="sv">Swedish (Svenska)</option>
|
||||
<option value="da">Danish (Dansk)</option>
|
||||
<option value="no">Norwegian (Norsk)</option>
|
||||
<option value="fi">Finnish (Suomi)</option>
|
||||
<option value="cs">Czech (Čeština)</option>
|
||||
</select>
|
||||
</SettingRow>
|
||||
|
||||
<div className="setting-actions">
|
||||
<button className="primary" onClick={handleSaveProject}>
|
||||
Save Project Settings
|
||||
|
||||
6
src/renderer/types/electron.d.ts
vendored
6
src/renderer/types/electron.d.ts
vendored
@@ -15,6 +15,7 @@ export interface ProjectMetadata {
|
||||
name: string;
|
||||
description?: string;
|
||||
dataPath?: string;
|
||||
mainLanguage?: string;
|
||||
}
|
||||
|
||||
export interface ProjectData {
|
||||
@@ -377,7 +378,7 @@ export interface ElectronAPI {
|
||||
syncOnStartup: () => Promise<{ tags: string[]; categories: string[]; projectMetadata: ProjectMetadata | null }>;
|
||||
getProjectMetadata: () => Promise<ProjectMetadata | null>;
|
||||
setProjectMetadata: (metadata: { name: string; description?: string }) => Promise<ProjectMetadata | null>;
|
||||
updateProjectMetadata: (updates: { name?: string; description?: string; dataPath?: string }) => Promise<ProjectMetadata | null>;
|
||||
updateProjectMetadata: (updates: { name?: string; description?: string; dataPath?: string; mainLanguage?: string }) => Promise<ProjectMetadata | null>;
|
||||
};
|
||||
tags: {
|
||||
getAll: () => Promise<TagData[]>;
|
||||
@@ -436,6 +437,9 @@ export interface ElectronAPI {
|
||||
// Taxonomy Analysis
|
||||
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 }>;
|
||||
|
||||
// Event listeners for streaming/progress
|
||||
onStreamDelta: (callback: (data: ChatStreamDelta) => void) => () => void;
|
||||
onToolCall: (callback: (data: ChatToolCall) => void) => () => void;
|
||||
|
||||
Reference in New Issue
Block a user