diff --git a/memento-note/app/globals.css b/memento-note/app/globals.css index d2911e5..2fba9fd 100644 --- a/memento-note/app/globals.css +++ b/memento-note/app/globals.css @@ -1139,6 +1139,61 @@ html.font-system * { .notion-ai-subitem:hover { background: var(--accent); } +.notion-ai-lang-picker { + border-top: 1px solid var(--border); + margin-top: 4px; + display: flex; + flex-wrap: wrap; + gap: 3px; + padding: 6px; +} +.notion-ai-lang-item { + font-size: 0.72rem; + padding: 3px 8px; + border-radius: 4px; + background: var(--muted); + border: 1px solid var(--border); + cursor: pointer; + color: var(--foreground); + transition: background 0.1s; +} +.notion-ai-lang-item:hover { background: var(--accent); } +.notion-ai-result-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.45); + z-index: 99998; + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(2px); +} +.notion-ai-result-modal { + background: var(--popover); + border: 1px solid var(--border); + border-radius: 14px; + box-shadow: 0 20px 60px rgba(0,0,0,0.25); + padding: 20px; + width: min(520px, 90vw); + max-height: 80vh; + overflow-y: auto; +} +.dark .notion-ai-result-modal { box-shadow: 0 20px 60px rgba(0,0,0,0.6); } +.notion-ai-result-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; +} +.notion-ai-result-section { margin-bottom: 12px; } +.notion-ai-result-label { + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--muted-foreground); +} + /* Inline input inside bubble menu (link editor) */ .notion-inline-input { diff --git a/memento-note/components/contextual-ai-chat.tsx b/memento-note/components/contextual-ai-chat.tsx index 14b380d..1c08432 100644 --- a/memento-note/components/contextual-ai-chat.tsx +++ b/memento-note/components/contextual-ai-chat.tsx @@ -114,6 +114,9 @@ export function ContextualAIChat({ // Action state const [actionLoading, setActionLoading] = useState(null) const [actionPreview, setActionPreview] = useState<{ label: string; text: string } | null>(null) + const [showLangPicker, setShowLangPicker] = useState(false) + const [translateTarget, setTranslateTarget] = useState('') + const [customLangInput, setCustomLangInput] = useState('') // Resource tab state const [resourceUrl, setResourceUrl] = useState('') @@ -170,7 +173,7 @@ export function ContextualAIChat({ } // ── Action execution ──────────────────────────────────────────────────────── - const handleAction = async (action: ActionDef) => { + const handleAction = async (action: ActionDef, targetLang?: string) => { // Image-specific action if (action.isImageAction) { if (!noteImages || noteImages.length === 0) { @@ -216,7 +219,7 @@ export function ContextualAIChat({ const res = await fetch(action.apiPath, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(action.body(noteContent, undefined, language)), + body: JSON.stringify(action.body(noteContent, undefined, targetLang || language)), }) const data = await res.json() if (!res.ok) { @@ -716,6 +719,49 @@ export function ContextualAIChat({ ACTION_IDS.filter(a => !a.isImageAction).map(action => { const Icon = action.icon const loading = actionLoading === action.id + if (action.id === 'translate') { + return ( +
+ + {showLangPicker && ( +
+
+ {['Francais','English','Espanol','Deutsch','Arabe','Portugais','Italiano','Chinois','Japonais'].map(l => ( + + ))} +
+ setCustomLangInput(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter' && customLangInput.trim()) { setTranslateTarget(customLangInput.trim()); setCustomLangInput('') } }} + /> + +
+ )} +
+ ) + } return ( - + + {translateOpen && ( +
+ {AI_LANGS.map(l => ( + + ))} +
+ setCustomLang(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter' && customLang.trim()) handleAI('translate', customLang.trim()) }} + /> +
+
+ )} )} + {aiModal && ( +
setAiModal(null)}> +
e.stopPropagation()}> +
+ + {aiModal.type === 'explain' ? t('ai.action.explain') : (t('ai.result.preview') || 'Apercu IA')} + + +
+ {aiModal.type === 'preview' && ( +
+ {t('ai.result.original') || 'Original'} +

{aiModal.origText}

+
+ )} +
+ {aiModal.type === 'preview' && {t('ai.result.suggestion') || 'Suggestion'}} +
+
+
+ + {aiModal.type === 'preview' && ( + + )} +
+
+
+ )}
) }