feat(editor): AI modal preview/apply, translate lang picker, fix p-tag injection, explain modal - no UTF-8 corruption
This commit is contained in:
@@ -114,6 +114,9 @@ export function ContextualAIChat({
|
||||
// Action state
|
||||
const [actionLoading, setActionLoading] = useState<string | null>(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 (
|
||||
<div key={action.id} className="flex flex-col gap-1.5">
|
||||
<button
|
||||
onClick={() => setShowLangPicker(v => !v)}
|
||||
disabled={!!actionLoading}
|
||||
className="w-full flex items-center gap-3 rounded-xl border border-border/60 bg-card px-4 py-3 text-sm font-medium text-foreground hover:bg-muted hover:border-primary/40 transition-all text-left disabled:opacity-60"
|
||||
>
|
||||
{loading ? <Loader2 className="h-4 w-4 text-primary animate-spin shrink-0" /> : <Icon className="h-4 w-4 text-primary shrink-0" />}
|
||||
<span>{t(action.i18nKey)}</span>
|
||||
{translateTarget && <span className="ml-auto text-xs text-primary/70 font-normal">{translateTarget}</span>}
|
||||
</button>
|
||||
{showLangPicker && (
|
||||
<div className="flex flex-col gap-2 px-3 py-3 rounded-xl border border-border/40 bg-muted/30">
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{['Francais','English','Espanol','Deutsch','Arabe','Portugais','Italiano','Chinois','Japonais'].map(l => (
|
||||
<button
|
||||
key={l}
|
||||
className={`text-xs px-2.5 py-1 rounded-lg border transition-colors ${translateTarget === l ? 'bg-primary text-primary-foreground border-primary' : 'bg-card border-border hover:bg-accent'}`}
|
||||
onClick={() => setTranslateTarget(l)}
|
||||
>{l}</button>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
className="text-xs px-3 py-1.5 rounded-lg border border-border bg-card outline-none focus:border-primary w-full"
|
||||
placeholder={t('ai.action.customLang') || 'Autre langue...'}
|
||||
value={customLangInput}
|
||||
onChange={e => setCustomLangInput(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter' && customLangInput.trim()) { setTranslateTarget(customLangInput.trim()); setCustomLangInput('') } }}
|
||||
/>
|
||||
<button
|
||||
disabled={!!actionLoading || !translateTarget}
|
||||
onClick={() => handleAction(action, translateTarget)}
|
||||
className="flex items-center justify-center gap-2 rounded-lg bg-primary text-primary-foreground px-3 py-1.5 text-xs font-medium disabled:opacity-50 hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
{loading ? <Loader2 className="h-3 w-3 animate-spin" /> : <Languages className="h-3 w-3" />}
|
||||
{t('ai.action.translate')} {translateTarget}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<button
|
||||
key={action.id}
|
||||
|
||||
Reference in New Issue
Block a user