separate text extraction and translation
This commit is contained in:
@@ -414,7 +414,7 @@ export default function ImageLightbox({ url, name, onClose, onPrev, onNext, item
|
||||
;(e.currentTarget as HTMLElement).style.color = 'var(--text-secondary)'
|
||||
}}
|
||||
>
|
||||
{retranslating ? '⟳ Translating…' : '🌐 Re-translate'}
|
||||
{retranslating ? '⟳ Translating…' : translatedText ? '🌐 Re-translate' : '🌐 Translate'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -453,6 +453,18 @@ export default function MixedView({ libraryId, initialPath }: Props) {
|
||||
}
|
||||
}
|
||||
}}
|
||||
onTranslate={async (e) => {
|
||||
const itemKey = itemKeyFor(e)
|
||||
const res = await fetch('/api/ai-tagging/translate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ itemKey }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}))
|
||||
throw new Error((data as { error?: string }).error ?? 'Translation failed')
|
||||
}
|
||||
}}
|
||||
onDelete={(e) => {
|
||||
const rel = filtersActive ? e.name : (currentPath ? `${currentPath}/${e.name}` : e.name)
|
||||
fetch(`/api/browse?libraryId=${encodeURIComponent(libraryId)}&path=${encodeURIComponent(rel)}`, { method: 'DELETE' })
|
||||
@@ -582,7 +594,7 @@ export default function MixedView({ libraryId, initialPath }: Props) {
|
||||
)
|
||||
}
|
||||
|
||||
function EntryTile({ entry, onOpen, onTag, onDelete, onRename, onAiTag, onExtractText, onDescribe }: { entry: FileEntry; onOpen: (e: FileEntry) => void; onTag: (e: FileEntry) => void; onDelete?: (e: FileEntry) => void; onRename?: (e: FileEntry, newName: string) => Promise<boolean>; onAiTag?: (e: FileEntry) => Promise<void>; onExtractText?: (e: FileEntry) => Promise<void>; onDescribe?: (e: FileEntry) => Promise<void> }) {
|
||||
function EntryTile({ entry, onOpen, onTag, onDelete, onRename, onAiTag, onExtractText, onDescribe, onTranslate }: { entry: FileEntry; onOpen: (e: FileEntry) => void; onTag: (e: FileEntry) => void; onDelete?: (e: FileEntry) => void; onRename?: (e: FileEntry, newName: string) => Promise<boolean>; onAiTag?: (e: FileEntry) => Promise<void>; onExtractText?: (e: FileEntry) => Promise<void>; onDescribe?: (e: FileEntry) => Promise<void>; onTranslate?: (e: FileEntry) => Promise<void> }) {
|
||||
type ImgState = 'loading' | 'loaded' | 'error'
|
||||
const [imgState, setImgState] = useState<ImgState>(
|
||||
entry.thumbnailUrl ? 'loading' : 'error'
|
||||
@@ -601,6 +613,8 @@ function EntryTile({ entry, onOpen, onTag, onDelete, onRename, onAiTag, onExtrac
|
||||
const [textExtractError, setTextExtractError] = useState<string | null>(null)
|
||||
const [describing, setDescribing] = useState(false)
|
||||
const [describeError, setDescribeError] = useState<string | null>(null)
|
||||
const [translating, setTranslating] = useState(false)
|
||||
const [translateError, setTranslateError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!menuOpen) return
|
||||
@@ -830,6 +844,26 @@ function EntryTile({ entry, onOpen, onTag, onDelete, onRename, onAiTag, onExtrac
|
||||
🔍 Extract Text for Folder
|
||||
</button>
|
||||
)}
|
||||
{onTranslate && entry.mediaType === 'image' && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setMenuOpen(false)
|
||||
setTranslating(true)
|
||||
setTranslateError(null)
|
||||
onTranslate(entry)
|
||||
.catch((err) => setTranslateError(err instanceof Error ? err.message : 'Translation failed'))
|
||||
.finally(() => setTranslating(false))
|
||||
}}
|
||||
disabled={translating}
|
||||
className="flex items-center gap-2 w-full px-4 py-2 text-sm text-left transition-colors disabled:opacity-50"
|
||||
style={{ color: 'var(--text-primary)' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--border)')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'transparent')}
|
||||
>
|
||||
🌐 Translate
|
||||
</button>
|
||||
)}
|
||||
{onRename && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
@@ -929,6 +963,28 @@ function EntryTile({ entry, onOpen, onTag, onDelete, onRename, onAiTag, onExtrac
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Translation status overlay */}
|
||||
{(translating || translateError) && (
|
||||
<div
|
||||
className="absolute inset-x-0 bottom-0 z-10 px-2 py-1.5 text-xs"
|
||||
style={{ backgroundColor: translateError ? 'rgba(127,29,29,0.9)' : 'rgba(0,0,0,0.75)' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span style={{ color: translateError ? '#fca5a5' : 'var(--text-secondary)' }}>
|
||||
{translateError ?? 'Translating…'}
|
||||
</span>
|
||||
{translateError && (
|
||||
<button
|
||||
onClick={() => setTranslateError(null)}
|
||||
className="ml-2 underline text-xs"
|
||||
style={{ color: '#fca5a5' }}
|
||||
>
|
||||
dismiss
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete confirmation overlay */}
|
||||
{confirming && (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user