import { useCallback, useEffect, useRef, useState, type ChangeEvent } from 'react'; import type { Artist } from '../types'; import { api, debugImageProxyUrl, portraitUrl, type DebugImageSearchResult, type DebugImageSearchResultItem, type FixArtistPortraitResult, } from '../api/client'; import DebugSearchResultsModal from './DebugSearchResultsModal'; import '../components/PaintingDetail.css'; import './ArtistBio.css'; interface Props { artist: Artist & { movement_name?: string }; debugMode?: boolean; debugShowMore?: boolean; portraitRevision?: number; onBack: () => void; onEnterGallery: () => void; onArtistPortraitFixed?: ( artistId: number, fixResult: FixArtistPortraitResult ) => void | Promise; onArtistCheckupFlagsUpdated?: ( artistId: number, flags: { checked: boolean; fixed: boolean } ) => void | Promise; } export default function ArtistBio({ artist, debugMode = false, debugShowMore = false, portraitRevision = 0, onBack, onEnterGallery, onArtistPortraitFixed, onArtistCheckupFlagsUpdated, }: Props) { const [debugSearch, setDebugSearch] = useState(null); const [debugLoading, setDebugLoading] = useState(false); const [debugError, setDebugError] = useState(null); const [fixing, setFixing] = useState(false); const [markingChecked, setMarkingChecked] = useState(false); const [moreOpen, setMoreOpen] = useState(false); const [moreLoading, setMoreLoading] = useState(false); const [moreError, setMoreError] = useState(null); const [moreResults, setMoreResults] = useState> | null>(null); const [applyingUrl, setApplyingUrl] = useState(null); const [clearing, setClearing] = useState(false); const [uploading, setUploading] = useState(false); const uploadInputRef = useRef(null); const portraitCleared = !artist.portrait_path && !!artist.checkup_fixed; const showPortrait = !!artist.portrait_path || !artist.checkup_fixed; const portraitSrc = portraitUrl(artist.portrait_path, portraitRevision || undefined); const lifespan = artist.birth_year && artist.death_year ? `${artist.birth_year} – ${artist.death_year}` : artist.birth_year ? `b. ${artist.birth_year}` : ''; useEffect(() => { if (!debugMode) { setDebugSearch(null); setDebugError(null); setMoreOpen(false); return; } let cancelled = false; setDebugLoading(true); setDebugError(null); setDebugSearch(null); api.getArtistDebugPortraitSearch(artist.id) .then((result) => { if (!cancelled) setDebugSearch(result); }) .catch(() => { if (!cancelled) setDebugError('Portrait image search failed.'); }) .finally(() => { if (!cancelled) setDebugLoading(false); }); return () => { cancelled = true; }; }, [debugMode, artist.id, artist.name]); const applyPortraitUpdate = async (fixResult: FixArtistPortraitResult) => { if (onArtistPortraitFixed) { await onArtistPortraitFixed(artist.id, fixResult); } }; const applyFixFromSearch = async ( imageUrl: string, context: { searchUrl: string; source: string; thumbUrl?: string } ) => { const fixResult = await api.fixArtistPortrait(artist.id, imageUrl, context); await applyPortraitUpdate(fixResult); setDebugSearch((prev) => prev ? { ...prev, imageUrl, thumbUrl: context.thumbUrl ?? prev.thumbUrl, source: context.source } : prev ); }; const handleFixPortrait = async () => { if (!debugSearch?.imageUrl || fixing) return; setFixing(true); setDebugError(null); try { await applyFixFromSearch(debugSearch.imageUrl, { searchUrl: debugSearch.searchUrl, source: debugSearch.source, thumbUrl: debugSearch.thumbUrl, }); } catch (err) { setDebugError(err instanceof Error ? err.message : 'Could not replace portrait.'); } finally { setFixing(false); } }; const handleOpenMore = useCallback(async () => { setMoreOpen(true); setMoreLoading(true); setMoreError(null); setMoreResults(null); try { const results = await api.getArtistDebugPortraitSearchMore(artist.id); setMoreResults(results); } catch { setMoreError('Could not load search results.'); } finally { setMoreLoading(false); } }, [artist.id]); useEffect(() => { if (!debugMode || !debugShowMore) return; void handleOpenMore(); }, [debugMode, debugShowMore, artist.id, handleOpenMore]); const handleSelectMoreResult = async (item: DebugImageSearchResultItem) => { if (fixing || applyingUrl) return; setApplyingUrl(item.imageUrl); setMoreError(null); setDebugError(null); try { const searchUrl = moreResults?.searchUrl ?? debugSearch?.searchUrl ?? ''; await applyFixFromSearch(item.imageUrl, { searchUrl, source: item.source, thumbUrl: item.thumbUrl, }); setMoreOpen(false); } catch (err) { setMoreError(err instanceof Error ? err.message : 'Could not replace portrait.'); } finally { setApplyingUrl(null); } }; const handleMarkChecked = async () => { if (artist.checkup_checked || markingChecked) return; setMarkingChecked(true); setDebugError(null); try { const updated = await api.updateArtistCheckupFlags(artist.id, { checked: true }); await onArtistCheckupFlagsUpdated?.(artist.id, updated); } catch (err) { setDebugError(err instanceof Error ? err.message : 'Could not mark as checked.'); } finally { setMarkingChecked(false); } }; const handleClearPortrait = async () => { if (clearing || fixing || uploading) return; setClearing(true); setDebugError(null); try { const result = await api.clearArtistPortrait(artist.id); await applyPortraitUpdate(result); } catch (err) { setDebugError(err instanceof Error ? err.message : 'Could not clear portrait.'); } finally { setClearing(false); } }; const handleUploadClick = () => { uploadInputRef.current?.click(); }; const handleUploadFile = async (e: ChangeEvent) => { const file = e.target.files?.[0]; e.target.value = ''; if (!file || uploading || fixing || clearing) return; setUploading(true); setDebugError(null); try { const result = await api.uploadArtistPortrait(artist.id, file); await applyPortraitUpdate(result); } catch (err) { setDebugError(err instanceof Error ? err.message : 'Could not upload portrait.'); } finally { setUploading(false); } }; return (

{artist.name}

{showPortrait ? ( {artist.name} { (e.target as HTMLImageElement).src = '/placeholder-portrait.svg'; }} /> ) : null}
{lifespan && {lifespan}} {artist.movement_name && ( {artist.movement_name} )}
{artist.bio_short && (

{artist.bio_short}

)} {artist.bio_full && (
{artist.bio_full.split('\n').map((para, i) => (

{para}

))}
)} {!artist.bio_full && !artist.bio_short && (

Biographical information not yet available.

)} {artist.wikipedia_title && (

Information sourced from Wikipedia article: {artist.wikipedia_title}

)}
{debugMode && ( )} setMoreOpen(false)} onSelect={handleSelectMoreResult} />
); }