feat: ai suggestions with modal dialog
This commit is contained in:
@@ -0,0 +1,275 @@
|
|||||||
|
.ai-suggestions-modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestions-modal {
|
||||||
|
background: var(--color-bg-secondary, #1e1e1e);
|
||||||
|
border: 1px solid var(--color-border, #3c3c3c);
|
||||||
|
border-radius: 8px;
|
||||||
|
min-width: 500px;
|
||||||
|
max-width: 650px;
|
||||||
|
max-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestions-modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid var(--color-border, #3c3c3c);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestions-modal-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestions-modal-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--color-text-muted, #888);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestions-modal-close:hover {
|
||||||
|
background: var(--color-bg-tertiary, #2a2a2a);
|
||||||
|
color: var(--color-text, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestions-modal-body {
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading state */
|
||||||
|
.ai-suggestions-loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestions-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 3px solid var(--color-border, #3c3c3c);
|
||||||
|
border-top-color: var(--color-accent, #0078d4);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: ai-spinner-spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ai-spinner-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestions-loading p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-muted, #888);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error state */
|
||||||
|
.ai-suggestions-error {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
background: rgba(255, 100, 100, 0.1);
|
||||||
|
border: 1px solid rgba(255, 100, 100, 0.3);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--color-error, #f88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestions-error .error-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty state */
|
||||||
|
.ai-suggestions-empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
color: var(--color-text-muted, #888);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Suggestions intro */
|
||||||
|
.ai-suggestions-intro {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
color: var(--color-text-muted, #888);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Suggestion list */
|
||||||
|
.ai-suggestions-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestion-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--color-bg-tertiary, #2a2a2a);
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--color-border, #3c3c3c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom checkbox */
|
||||||
|
.ai-suggestion-checkbox {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-top: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestion-checkbox input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestion-checkbox .checkmark {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: var(--color-bg-secondary, #1e1e1e);
|
||||||
|
border: 2px solid var(--color-border, #4c4c4c);
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestion-checkbox:hover .checkmark {
|
||||||
|
border-color: var(--color-accent, #0078d4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestion-checkbox input:checked ~ .checkmark {
|
||||||
|
background: var(--color-accent, #0078d4);
|
||||||
|
border-color: var(--color-accent, #0078d4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestion-checkbox input:checked ~ .checkmark::after {
|
||||||
|
content: '✓';
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Suggestion content */
|
||||||
|
.ai-suggestion-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestion-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text, #fff);
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestion-has-value {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--color-text-muted, #888);
|
||||||
|
background: var(--color-bg-secondary, #1e1e1e);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestion-value {
|
||||||
|
color: var(--color-text, #fff);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestion-current {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-muted, #888);
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid var(--color-border, #3c3c3c);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestion-current em {
|
||||||
|
font-style: normal;
|
||||||
|
color: var(--color-text-secondary, #ccc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.ai-suggestions-modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid var(--color-border, #3c3c3c);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestions-modal-footer button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestions-modal-footer .button-cancel {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--color-border, #4c4c4c);
|
||||||
|
color: var(--color-text, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestions-modal-footer .button-cancel:hover:not(:disabled) {
|
||||||
|
background: var(--color-bg-tertiary, #2a2a2a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestions-modal-footer .button-cancel:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestions-modal-footer .button-apply {
|
||||||
|
background: var(--color-accent, #0078d4);
|
||||||
|
border: 1px solid var(--color-accent, #0078d4);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestions-modal-footer .button-apply:hover:not(:disabled) {
|
||||||
|
background: var(--color-accent-hover, #106ebe);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestions-modal-footer .button-apply:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import './AISuggestionsModal.css';
|
||||||
|
|
||||||
|
export interface AISuggestions {
|
||||||
|
title?: string;
|
||||||
|
alt?: string;
|
||||||
|
caption?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CurrentValues {
|
||||||
|
title: string;
|
||||||
|
alt: string;
|
||||||
|
caption: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AISuggestionsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
suggestions: AISuggestions | null;
|
||||||
|
currentValues: CurrentValues;
|
||||||
|
error?: string;
|
||||||
|
onConfirm: (values: Partial<AISuggestions>) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AISuggestionsModal: React.FC<AISuggestionsModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
isLoading,
|
||||||
|
suggestions,
|
||||||
|
currentValues,
|
||||||
|
error,
|
||||||
|
onConfirm,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
// Checkbox state - initialized based on whether current values are empty
|
||||||
|
const [useTitle, setUseTitle] = useState(false);
|
||||||
|
const [useAlt, setUseAlt] = useState(false);
|
||||||
|
const [useCaption, setUseCaption] = useState(false);
|
||||||
|
|
||||||
|
// Update checkbox state when suggestions arrive, based on whether current fields are empty
|
||||||
|
useEffect(() => {
|
||||||
|
if (suggestions) {
|
||||||
|
setUseTitle(suggestions.title ? !currentValues.title : false);
|
||||||
|
setUseAlt(suggestions.alt ? !currentValues.alt : false);
|
||||||
|
setUseCaption(suggestions.caption ? !currentValues.caption : false);
|
||||||
|
}
|
||||||
|
}, [suggestions, currentValues]);
|
||||||
|
|
||||||
|
const handleBackdropClick = useCallback((e: React.MouseEvent) => {
|
||||||
|
if (e.target === e.currentTarget && !isLoading) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}, [isLoading, onClose]);
|
||||||
|
|
||||||
|
const handleConfirm = useCallback(() => {
|
||||||
|
const valuesToApply: Partial<AISuggestions> = {};
|
||||||
|
if (useTitle && suggestions?.title) valuesToApply.title = suggestions.title;
|
||||||
|
if (useAlt && suggestions?.alt) valuesToApply.alt = suggestions.alt;
|
||||||
|
if (useCaption && suggestions?.caption) valuesToApply.caption = suggestions.caption;
|
||||||
|
onConfirm(valuesToApply);
|
||||||
|
}, [useTitle, useAlt, useCaption, suggestions, onConfirm]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const hasAnySuggestion = suggestions && (suggestions.title || suggestions.alt || suggestions.caption);
|
||||||
|
const hasAnySelected = useTitle || useAlt || useCaption;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ai-suggestions-modal-backdrop" onClick={handleBackdropClick}>
|
||||||
|
<div className="ai-suggestions-modal">
|
||||||
|
<div className="ai-suggestions-modal-header">
|
||||||
|
<h2>AI Image Analysis</h2>
|
||||||
|
{!isLoading && (
|
||||||
|
<button className="ai-suggestions-modal-close" onClick={onClose} title="Close">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ai-suggestions-modal-body">
|
||||||
|
{isLoading && (
|
||||||
|
<div className="ai-suggestions-loading">
|
||||||
|
<div className="ai-suggestions-spinner"></div>
|
||||||
|
<p>Analyzing image...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && !isLoading && (
|
||||||
|
<div className="ai-suggestions-error">
|
||||||
|
<span className="error-icon">⚠️</span>
|
||||||
|
<span>{error}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && !error && hasAnySuggestion && (
|
||||||
|
<div className="ai-suggestions-list">
|
||||||
|
<p className="ai-suggestions-intro">
|
||||||
|
Select which AI-generated values to apply. Existing values are preserved by default.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{suggestions?.title && (
|
||||||
|
<div className="ai-suggestion-item">
|
||||||
|
<label className="ai-suggestion-checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={useTitle}
|
||||||
|
onChange={(e) => setUseTitle(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span className="checkmark"></span>
|
||||||
|
</label>
|
||||||
|
<div className="ai-suggestion-content">
|
||||||
|
<div className="ai-suggestion-label">
|
||||||
|
Title
|
||||||
|
{currentValues.title && (
|
||||||
|
<span className="ai-suggestion-has-value" title="This field already has a value">
|
||||||
|
(has existing value)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="ai-suggestion-value">{suggestions.title}</div>
|
||||||
|
{currentValues.title && (
|
||||||
|
<div className="ai-suggestion-current">
|
||||||
|
Current: <em>{currentValues.title}</em>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{suggestions?.alt && (
|
||||||
|
<div className="ai-suggestion-item">
|
||||||
|
<label className="ai-suggestion-checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={useAlt}
|
||||||
|
onChange={(e) => setUseAlt(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span className="checkmark"></span>
|
||||||
|
</label>
|
||||||
|
<div className="ai-suggestion-content">
|
||||||
|
<div className="ai-suggestion-label">
|
||||||
|
Alt Text
|
||||||
|
{currentValues.alt && (
|
||||||
|
<span className="ai-suggestion-has-value" title="This field already has a value">
|
||||||
|
(has existing value)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="ai-suggestion-value">{suggestions.alt}</div>
|
||||||
|
{currentValues.alt && (
|
||||||
|
<div className="ai-suggestion-current">
|
||||||
|
Current: <em>{currentValues.alt}</em>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{suggestions?.caption && (
|
||||||
|
<div className="ai-suggestion-item">
|
||||||
|
<label className="ai-suggestion-checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={useCaption}
|
||||||
|
onChange={(e) => setUseCaption(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span className="checkmark"></span>
|
||||||
|
</label>
|
||||||
|
<div className="ai-suggestion-content">
|
||||||
|
<div className="ai-suggestion-label">
|
||||||
|
Caption
|
||||||
|
{currentValues.caption && (
|
||||||
|
<span className="ai-suggestion-has-value" title="This field already has a value">
|
||||||
|
(has existing value)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="ai-suggestion-value">{suggestions.caption}</div>
|
||||||
|
{currentValues.caption && (
|
||||||
|
<div className="ai-suggestion-current">
|
||||||
|
Current: <em>{currentValues.caption}</em>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && !error && !hasAnySuggestion && suggestions && (
|
||||||
|
<div className="ai-suggestions-empty">
|
||||||
|
No suggestions were generated for this image.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ai-suggestions-modal-footer">
|
||||||
|
{isLoading ? (
|
||||||
|
<button className="button-cancel" disabled>
|
||||||
|
Please wait...
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button className="button-cancel" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
{hasAnySuggestion && (
|
||||||
|
<button
|
||||||
|
className="button-apply"
|
||||||
|
onClick={handleConfirm}
|
||||||
|
disabled={!hasAnySelected}
|
||||||
|
>
|
||||||
|
Apply Selected
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -16,6 +16,7 @@ import { ImportAnalysisView } from '../ImportAnalysisView';
|
|||||||
import { AutoSaveManager } from '../../utils';
|
import { AutoSaveManager } from '../../utils';
|
||||||
import { parseMacros, getMacro } from '../../macros/registry';
|
import { parseMacros, getMacro } from '../../macros/registry';
|
||||||
import { InsertModal } from '../InsertModal';
|
import { InsertModal } from '../InsertModal';
|
||||||
|
import { AISuggestionsModal, AISuggestions } from '../AISuggestionsModal/AISuggestionsModal';
|
||||||
import './Editor.css';
|
import './Editor.css';
|
||||||
|
|
||||||
/** Get display name for media: prefer title over originalName */
|
/** Get display name for media: prefer title over originalName */
|
||||||
@@ -1443,10 +1444,15 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
|||||||
|
|
||||||
// Quick action menu state
|
// Quick action menu state
|
||||||
const [showQuickActions, setShowQuickActions] = useState(false);
|
const [showQuickActions, setShowQuickActions] = useState(false);
|
||||||
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
|
||||||
const [projectLanguage, setProjectLanguage] = useState('en');
|
const [projectLanguage, setProjectLanguage] = useState('en');
|
||||||
const quickActionsRef = useRef<HTMLDivElement>(null);
|
const quickActionsRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// AI suggestions modal state
|
||||||
|
const [showAISuggestionsModal, setShowAISuggestionsModal] = useState(false);
|
||||||
|
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
||||||
|
const [aiSuggestions, setAISuggestions] = useState<AISuggestions | null>(null);
|
||||||
|
const [aiError, setAIError] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
// Load project language setting
|
// Load project language setting
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.electronAPI?.meta.getProjectMetadata().then(metadata => {
|
window.electronAPI?.meta.getProjectMetadata().then(metadata => {
|
||||||
@@ -1474,33 +1480,49 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
|||||||
if (!item || isAnalyzing) return;
|
if (!item || isAnalyzing) return;
|
||||||
|
|
||||||
setShowQuickActions(false);
|
setShowQuickActions(false);
|
||||||
|
setShowAISuggestionsModal(true);
|
||||||
setIsAnalyzing(true);
|
setIsAnalyzing(true);
|
||||||
|
setAISuggestions(null);
|
||||||
|
setAIError(undefined);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
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);
|
setAISuggestions({
|
||||||
if (result.alt) setAlt(result.alt);
|
title: result.title,
|
||||||
if (result.caption) setCaption(result.caption);
|
alt: result.alt,
|
||||||
showToast.success('AI analysis complete');
|
caption: result.caption,
|
||||||
} else {
|
|
||||||
showErrorModal({
|
|
||||||
title: 'AI Analysis Failed',
|
|
||||||
message: result?.error || 'Failed to analyze image',
|
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
setAIError(result?.error || 'Failed to analyze image');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to analyze image:', error);
|
console.error('Failed to analyze image:', error);
|
||||||
showErrorModal({
|
setAIError((error as Error).message || 'Failed to analyze image');
|
||||||
title: 'AI Analysis Error',
|
|
||||||
message: (error as Error).message || 'Failed to analyze image',
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsAnalyzing(false);
|
setIsAnalyzing(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle applying AI suggestions
|
||||||
|
const handleApplyAISuggestions = (values: Partial<AISuggestions>) => {
|
||||||
|
if (values.title) setTitle(values.title);
|
||||||
|
if (values.alt) setAlt(values.alt);
|
||||||
|
if (values.caption) setCaption(values.caption);
|
||||||
|
setShowAISuggestionsModal(false);
|
||||||
|
if (Object.keys(values).length > 0) {
|
||||||
|
showToast.success('AI suggestions applied');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close AI suggestions modal
|
||||||
|
const handleCloseAISuggestionsModal = () => {
|
||||||
|
setShowAISuggestionsModal(false);
|
||||||
|
setAISuggestions(null);
|
||||||
|
setAIError(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
// Load linked posts for this media and fetch their titles
|
// Load linked posts for this media and fetch their titles
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadLinkedPosts = async () => {
|
const loadLinkedPosts = async () => {
|
||||||
@@ -1875,6 +1897,17 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* AI Suggestions Modal */}
|
||||||
|
<AISuggestionsModal
|
||||||
|
isOpen={showAISuggestionsModal}
|
||||||
|
isLoading={isAnalyzing}
|
||||||
|
suggestions={aiSuggestions}
|
||||||
|
currentValues={{ title, alt, caption }}
|
||||||
|
error={aiError}
|
||||||
|
onConfirm={handleApplyAISuggestions}
|
||||||
|
onClose={handleCloseAISuggestionsModal}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export { PostLinks } from './PostLinks';
|
|||||||
export { LinkedMediaPanel } from './LinkedMediaPanel';
|
export { LinkedMediaPanel } from './LinkedMediaPanel';
|
||||||
export { ErrorModal, type ErrorDetails } from './ErrorModal';
|
export { ErrorModal, type ErrorDetails } from './ErrorModal';
|
||||||
export { ConfirmDeleteModal, type ConfirmDeleteDetails, type DeleteReference } from './ConfirmDeleteModal';
|
export { ConfirmDeleteModal, type ConfirmDeleteDetails, type DeleteReference } from './ConfirmDeleteModal';
|
||||||
|
export { AISuggestionsModal, type AISuggestions, type CurrentValues } from './AISuggestionsModal/AISuggestionsModal';
|
||||||
export { ChatPanel } from './ChatPanel';
|
export { ChatPanel } from './ChatPanel';
|
||||||
export { ImportAnalysisView } from './ImportAnalysisView';
|
export { ImportAnalysisView } from './ImportAnalysisView';
|
||||||
export { InsertModal } from './InsertModal';
|
export { InsertModal } from './InsertModal';
|
||||||
|
|||||||
Reference in New Issue
Block a user