PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / Agent / hooks / useImageAcquisition.js

useImageAcquisition.js in Extendify 3.2.1, at src/Agent/hooks/useImageAcquisition.js

187 lines 4.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { generateImage } from '@shared/api/DataApi';
2 import { downloadImage } from '@shared/api/wp';
3 import {
4 DEFAULT_IMAGE_SEARCH,
5 resolveImageSearch,
6 } from '@shared/lib/image-search-defaults';
7 import { fetchImages } from '@shared/lib/unsplash';
8 import { useImageGenerationStore } from '@shared/state/generate-images';
9 import { useUnsplashCacheStore } from '@shared/state/unsplash-cache';
10 import { useCallback, useRef, useState } from '@wordpress/element';
11 import { __ } from '@wordpress/i18n';
12 import useSWRImmutable from 'swr/immutable';
13
14 const SIZES = [
15 ['1024x1024', 1],
16 ['1536x1024', 3 / 2],
17 ['1024x1536', 2 / 3],
18 ['1792x1024', 16 / 9],
19 ['1024x1792', 9 / 16],
20 ];
21
22 // The model renders one of five ratios; the wrong one gets cropped by
23 // object-cover, and 2k costs seconds a slot this size can't show.
24 export const targetFor = (el) => {
25 const { width, height } = el?.getBoundingClientRect?.() ?? {};
26 if (!width || !height) return {};
27 const ratio = width / height;
28 const [size] = SIZES.reduce((best, entry) =>
29 Math.abs(entry[1] - ratio) < Math.abs(best[1] - ratio) ? entry : best,
30 );
31 return { size, quality: width > 1024 ? 'medium' : 'low' };
32 };
33
34 // onUrlReady is awaited so surfaces can preload/preview before an url turns ready.
35 export const useImageAcquisition = ({ source, onUrlReady } = {}) => {
36 const [states, setStates] = useState({});
37 const { imageCredits, updateImageCredits, subtractOneCredit } =
38 useImageGenerationStore();
39 const statesRef = useRef(states);
40 statesRef.current = states;
41 const onUrlReadyRef = useRef(onUrlReady);
42 onUrlReadyRef.current = onUrlReady;
43
44 const setItemState = useCallback(
45 (key, state) => setStates((states) => ({ ...states, [key]: state })),
46 [],
47 );
48
49 const generate = useCallback(
50 async (key, prompt, target = {}) => {
51 if (
52 Number(useImageGenerationStore.getState().imageCredits.remaining) === 0
53 ) {
54 setItemState(key, {
55 status: 'error',
56 message: __(
57 "You've run out of daily image credits.",
58 'extendify-local',
59 ),
60 });
61 return;
62 }
63 setItemState(key, { status: 'generating' });
64 subtractOneCredit();
65 try {
66 const {
67 imageCredits: credits,
68 images,
69 id,
70 } = await generateImage({
71 prompt,
72 source,
73 quality: 'low',
74 ...target,
75 });
76 updateImageCredits(credits);
77 const url = images?.[0]?.url;
78 if (!url) throw new Error(__('No image returned', 'extendify-local'));
79 await onUrlReadyRef.current?.(key, url);
80 setItemState(key, {
81 status: 'ready',
82 url,
83 alt: images[0].alt ?? prompt,
84 requestId: id ?? null,
85 });
86 } catch (error) {
87 if (error?.imageCredits) updateImageCredits(error.imageCredits);
88 setItemState(key, {
89 status: 'error',
90 message:
91 error?.message ||
92 __('An unknown error occurred.', 'extendify-local'),
93 });
94 }
95 },
96 [setItemState, subtractOneCredit, updateImageCredits, source],
97 );
98
99 const attachImage = useCallback(
100 (key, image) => setItemState(key, { status: 'ready', image }),
101 [setItemState],
102 );
103
104 const pickUnsplash = useCallback(
105 async (key, photo) => {
106 const url = photo?.urls?.regular;
107 if (!url) return;
108 try {
109 await onUrlReadyRef.current?.(key, url);
110 } catch (error) {
111 setItemState(key, {
112 status: 'error',
113 message:
114 error?.message ||
115 __('An unknown error occurred.', 'extendify-local'),
116 });
117 return;
118 }
119 setItemState(key, {
120 status: 'ready',
121 url,
122 photoId: photo.id,
123 requestId: photo.requestMetadata?.id ?? null,
124 });
125 },
126 [setItemState],
127 );
128
129 const markAwaiting = useCallback(
130 (key) => setItemState(key, { status: 'awaiting' }),
131 [setItemState],
132 );
133
134 // Media picks are already attachments; generated/Unsplash urls are off-site until imported.
135 const resolveImage = useCallback(async (key, { disclose = false } = {}) => {
136 const state = statesRef.current[key];
137 if (state?.image) return state.image;
138 if (!state?.url) return null;
139 if (state.photoId) {
140 return await downloadImage(
141 state.requestId,
142 state.url,
143 'unsplash',
144 state.photoId,
145 );
146 }
147 return await downloadImage(
148 state.requestId,
149 state.url,
150 'ai-generated',
151 null,
152 {
153 alt: state.alt,
154 disclose,
155 },
156 );
157 }, []);
158
159 return {
160 states,
161 imageCredits,
162 generate,
163 attachImage,
164 pickUnsplash,
165 markAwaiting,
166 resolveImage,
167 };
168 };
169
170 const searchImages = async (search, source) => {
171 const cache = useUnsplashCacheStore.getState();
172 if (
173 search === DEFAULT_IMAGE_SEARCH &&
174 !cache.isEmpty() &&
175 !cache.hasExpired()
176 ) {
177 return cache.images;
178 }
179 return await fetchImages(resolveImageSearch(search), source);
180 };
181
182 export const useUnsplashSearch = (search, source = null) => {
183 const key = search || DEFAULT_IMAGE_SEARCH;
184 const { data, error } = useSWRImmutable(key, () => searchImages(key, source));
185 return { data, error, loading: !data && !error };
186 };
187