Skip to content

Commit

Permalink
refactor: use Map for generation state
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaucau committed Apr 15, 2024
1 parent 866a740 commit 5b9aded
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 118 deletions.
141 changes: 91 additions & 50 deletions src/components/BulkGenerateModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,75 +8,116 @@ import {
ToggleControl,
} from "@wordpress/components";
import { __, _n, _x, sprintf } from "@wordpress/i18n";
import { decodeEntities } from "@wordpress/html-entities";

import type { AttachmentWithGenerationStatus } from "../types";
import type { AltGenerationMap } from "../types";
import BulkGenerationStatusTable from "./BulkGenerationStatusTable";
import generateAltText from "../utils/generateAltText";
import sleep from "../utils/sleep";
import useAttachments from "../hooks/useAttachments";

export default function BulkGenerateModal({
attachment_ids,
attachmentIds,
onClose,
}: BulkGenerateModalProps) {
const [overwriteExisting, setOverwriteExisting] = useState(false);
const [customPrompt, setCustomPrompt] = useState("");
const [isGenerating, setIsGenerating] = useState(false);
const [attachments, setAttachments] = useState<
AttachmentWithGenerationStatus[]
>([]);

const { attachmentData, hasAttachmentDataResolved } =
useAttachments(attachment_ids);
const { attachments, hasResolved } = useAttachments(attachmentIds);
const [altGenerationMap, setAltGenerationMap] = useState<AltGenerationMap>(
new Map(attachmentIds.map((id) => [id, { status: "", alt: "" }])),
);

useEffect(() => {
if (!hasAttachmentDataResolved) return;
if (!attachments.length) return;

setAltGenerationMap((prevMap) => {
const newMap = new Map(prevMap);

attachments.forEach((attachment) => {
const details = newMap.get(attachment.id);
if (!details) {
console.error(
"Generation details not found for attachment",
attachment,
);
return;
}

setAttachments(
attachmentData.map((attachment) => ({
...attachment,
generation_status: "waiting",
})),
);
}, [attachmentData, hasAttachmentDataResolved]);
details.alt = attachment.alt_text;
details.title = decodeEntities(attachment.title.rendered);
details.source_url = attachment.source_url;

const thumbnail =
//@ts-ignore - missing WP types
attachment.media_details.sizes?.thumbnail ??
attachment.media_details.sizes?.[0];

if (thumbnail)
details.thumbnail = {
width: thumbnail.width,
height: thumbnail.height,
source_url: thumbnail.source_url,
};

newMap.set(attachment.id, details);
});

return newMap;
});
}, [attachments, hasResolved]);

const handleStart = async () => {
setIsGenerating(true);

for (let attachment of attachments) {
if (!overwriteExisting && attachment.alt_text.length) {
setAttachments((prev) =>
prev.map((prevAttachment) =>
prevAttachment.id === attachment.id
? { ...prevAttachment, generation_status: "skipped" }
: prevAttachment,
),
);
continue;
}

generateAltText(attachment.id, true)
.then((res) => {
setAttachments((prev) => {
const newAttachments = [...prev];
const index = newAttachments.findIndex(
(prevAttachment) => prevAttachment.id === attachment.id,
);
newAttachments[index] = {
...newAttachments[index],
alt_text: res,
generation_status: "done",
};
return newAttachments;
});
setAltGenerationMap(
(prevMap) =>
new Map(
Array.from(prevMap, ([id, details]) => {
details.status = "queued";

if (!overwriteExisting && details.alt.length) {
details.status = "skipped";
}

return [id, details];
}),
),
);

for (const [id, details] of altGenerationMap) {
if (details.status !== "queued") continue;

setAltGenerationMap(
(prevMap) =>
new Map(prevMap.set(id, { ...details, status: "generating" })),
);

generateAltText(id, true)
.then((alt) => {
setAltGenerationMap(
(prevMap) =>
new Map(prevMap.set(id, { ...details, alt, status: "done" })),
);
})
.catch((error) => {
attachment.generation_status = "error";
setAltGenerationMap(
(prevMap) =>
new Map(
prevMap.set(id, {
...details,
status: "error",
message: error.message,
}),
),
);
console.error(error);
});

// Wait for 1 second before processing the next image to avoid too many requests at once
// TODO: Use rate limiting info and implement a better solution
// TODO:
// Use rate limiting info and implement a better solution
// https://platform.openai.com/docs/guides/rate-limits/rate-limits-in-headers
await sleep(1000);
}

Expand Down Expand Up @@ -131,8 +172,8 @@ export default function BulkGenerateModal({
/>

<BulkGenerationStatusTable
attachments={attachments}
loading={hasAttachmentDataResolved}
loading={hasResolved}
generationMap={altGenerationMap}
/>

<Flex>
Expand All @@ -141,10 +182,10 @@ export default function BulkGenerateModal({
_n(
"%d image selected",
"%d images selected",
attachment_ids.length,
attachmentIds.length,
"alt-text-generator-gpt-vision",
),
attachment_ids.length,
attachmentIds.length,
)}
</p>

Expand All @@ -156,7 +197,7 @@ export default function BulkGenerateModal({

<Button
variant="primary"
disabled={isGenerating || !hasAttachmentDataResolved}
disabled={isGenerating || !hasResolved}
isBusy={isGenerating}
onClick={handleStart}
>
Expand All @@ -170,6 +211,6 @@ export default function BulkGenerateModal({
}

export type BulkGenerateModalProps = {
attachment_ids: number[];
attachmentIds: number[];
onClose: () => void;
};
27 changes: 11 additions & 16 deletions src/components/BulkGenerationStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { _x } from "@wordpress/i18n";
import { _x, sprintf } from "@wordpress/i18n";
import { Flex, Spinner } from "@wordpress/components";
import type { AttachmentWithGenerationStatus } from "../types";
import type { AltGenerationDetails } from "../types";

export default function BulkGenerationStatus({
status,
details,
}: BulkGenerationStatusProps) {
const { status, message } = details;

switch (status) {
case "waiting":
return _x(
"Waiting",
"Generation status",
"alt-text-generator-gpt-vision",
);
case "queued":
return _x("Queued", "Generation status", "alt-text-generator-gpt-vision");
case "generating":
Expand All @@ -35,16 +31,15 @@ export default function BulkGenerationStatus({
"alt-text-generator-gpt-vision",
);
case "error":
return _x("Error", "Generation status", "alt-text-generator-gpt-vision");
default:
return _x(
"Unknown",
"Generation status",
"alt-text-generator-gpt-vision",
return sprintf(
_x("Error: %s", "Generation status", "alt-text-generator-gpt-vision"),
message,
);
default:
return "";
}
}

export type BulkGenerationStatusProps = {
status: AttachmentWithGenerationStatus["generation_status"];
details: AltGenerationDetails;
};
66 changes: 29 additions & 37 deletions src/components/BulkGenerationStatusTable.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Flex, Spinner } from "@wordpress/components";
import { __ } from "@wordpress/i18n";
import { decodeEntities } from "@wordpress/html-entities";
import type { AttachmentWithGenerationStatus } from "../types";
import type { AltGenerationMap } from "../types";
import BulkGenerationStatus from "./BulkGenerationStatus";

export default function BulkGenerationStatusTable({
attachments,
loading,
generationMap,
}: BulkGenerationStatusTableProps) {
return (
<table className="wp-list-table fixed widefat striped">
Expand All @@ -26,46 +25,39 @@ export default function BulkGenerationStatusTable({
</td>
</tr>
) : (
attachments.map((attachment) => {
const size =
// @ts-ignore - wrong `sizes` type
attachment.media_details.sizes?.thumbnail ??
attachment.media_details.sizes?.[0];

return (
<tr key={attachment.id}>
<td>
<a href={attachment.source_url} target="_blank">
<Flex align="start" justify="start">
{size && (
<img
src={size.source_url}
height={size.height}
width={size.width}
alt={attachment.alt_text}
loading="lazy"
decoding="async"
style={{ maxWidth: "60px", height: "auto" }}
/>
)}
{decodeEntities(attachment.title.rendered)}
</Flex>
</a>
</td>
<td>{attachment.alt_text}</td>
<td>
<BulkGenerationStatus status={attachment.generation_status} />
</td>
</tr>
);
})
Array.from(generationMap, ([id, details]) => (
<tr key={id}>
<td>
<a href={details.source_url} target="_blank">
<Flex align="start" justify="start">
{details.thumbnail && (
<img
src={details.thumbnail.source_url}
height={details.thumbnail.height}
width={details.thumbnail.width}
alt={details.alt}
loading="lazy"
decoding="async"
style={{ maxWidth: "60px", height: "auto" }}
/>
)}
{details.title}
</Flex>
</a>
</td>
<td>{details.alt}</td>
<td>
<BulkGenerationStatus details={details} />
</td>
</tr>
))
)}
</tbody>
</table>
);
}

export interface BulkGenerationStatusTableProps {
attachments: AttachmentWithGenerationStatus[];
generationMap: AltGenerationMap;
loading: boolean;
}
12 changes: 6 additions & 6 deletions src/hooks/useAttachments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { useSelect } from "@wordpress/data";
import { type Attachment, store as coreStore } from "@wordpress/core-data";

export default function useAttachments(ids: number[]) {
const { attachmentData, hasAttachmentDataResolved } = useSelect(
const { attachments, hasResolved } = useSelect(
(select) => {
if (!ids.length) {
return {
attachmentData: [],
hasAttachmentDataResolved: true,
attachments: [],
hasResolved: true,
};
}

Expand All @@ -18,12 +18,12 @@ export default function useAttachments(ids: number[]) {
] as const;

return {
attachmentData:
attachments:
select(coreStore).getEntityRecords<Attachment<"view">>(
...selectorArgs,
) ?? [],
// @ts-ignore - missing hasFinishedResolution types
hasAttachmentDataResolved: select(coreStore).hasFinishedResolution(
hasResolved: select(coreStore).hasFinishedResolution(
"getEntityRecords",
selectorArgs,
),
Expand All @@ -32,5 +32,5 @@ export default function useAttachments(ids: number[]) {
[ids],
);

return { attachmentData, hasAttachmentDataResolved };
return { attachments, hasResolved };
}
2 changes: 1 addition & 1 deletion src/media/media-upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const App = () => {

return isModalOpen ? (
<BulkGenerateModal
attachment_ids={selectedMediaIds}
attachmentIds={selectedMediaIds}
onClose={() => setIsModalOpen(false)}
/>
) : null;
Expand Down
Loading

0 comments on commit 5b9aded

Please sign in to comment.