diff --git a/createNewWallpaper_image.ts b/createNewWallpaper_image.ts new file mode 100644 index 000000000..216f9cb63 --- /dev/null +++ b/createNewWallpaper_image.ts @@ -0,0 +1,243 @@ +import spaceTrim from 'spacetrim'; +import { Vector } from 'xyzt'; +import { + COLORSTATS_DEFAULT_COMPUTE_IN_FRONTEND, + WALLPAPER_IMAGE_ASPECT_RATIO_ALLOWED_RANGE, + WALLPAPER_IMAGE_MAX_ALLOWED_SIZE, +} from '../../../../config'; +import { WebgptTaskProgress } from '../../../components/TaskInProgress/task/WebgptTaskProgress'; +import { UploadWallpaperResponse } from '../../../pages/api/upload-image'; +import { aspectRatioRangeExplain } from '../../../utils/aspect-ratio/aspectRatioRangeExplain'; +import { downscaleWithAspectRatio } from '../../../utils/aspect-ratio/downscaleWithAspectRatio'; +import { isInAspectRatioRange } from '../../../utils/aspect-ratio/isInAspectRatioRange'; +import { createImageInWorker } from '../../../utils/image/createImageInWorker'; +import { measureImageBlob } from '../../../utils/image/measureImageBlob'; +import { resizeImageBlob } from '../../../utils/image/resizeImageBlob'; +import { IImageColorStats } from '../../../utils/image/utils/IImageColorStats'; +import { string_image_prompt, string_url_image, uuid } from '../../../utils/typeAliases'; +import { imageGeneratorDialogue } from '../../dialogues/image-generator/imageGeneratorDialogue'; + +interface CreateNewWallpaperImageRequest { + /** + * Author of the wallpaper + * Note: It must be valid client ID and same as identity of the user + */ + readonly author: uuid; + + /** + * Image of the wallpaper + */ + readonly wallpaperImage?: Blob; + + /** + * Same image as wallpaperImage + * + * Note: This is used to not reupload the same image if it is already uploaded on our CDN + */ + readonly wallpaperUrl?: string_url_image; + + /** + * Text prompt which was used to generate the wallpaper image + */ + readonly wallpaperPrompt?: string_image_prompt; +} + +interface CreateNewWallpaperImageResult { + /** + * URL of the wallpaper in our CDN + */ + readonly wallpaperUrl: string_url_image; + + /** + * Original size of the wallpaper + */ + readonly originalSize: Vector; + + /** + * Color statistics of the wallpaper + */ + readonly colorStats: IImageColorStats; +} + +/** + * Process text part for createNewWallpaper + * + * @private Use ONLY in createNewWallpaper + */ +export async function createNewWallpaper_image( + request: CreateNewWallpaperImageRequest, + onProgress: (taskProgress: WebgptTaskProgress) => void, +): Promise { + let { author, wallpaperImage, wallpaperUrl, wallpaperPrompt } = request; + const computeColorstats = COLORSTATS_DEFAULT_COMPUTE_IN_FRONTEND; + + if ((!wallpaperImage && !wallpaperPrompt) || (wallpaperImage && wallpaperPrompt)) { + throw new Error('One of wallpaperImage or wallpaperPrompt must be provided BUT not both'); + // <- TODO: [👮‍♂️] Maybe constrain this logic into CreateNewWallpaperImageRequest + // <- TODO: ShouldNeverHappenError + } + + //=========================================================================== + //-------[ Image generate: ]--- + if (!wallpaperImage) { + await onProgress({ + name: 'image-generate', + title: 'Generating image', + isDone: false, + }); + + if (wallpaperPrompt === undefined) { + throw new Error('wallpaperPrompt is undefined'); + // <- TODO: ShouldNeverHappenError + } + + const { pickedImage: imagePromptResult } = await imageGeneratorDialogue({ + message: 'Pick the wallpaper image for your website', + defaultImagePrompt: wallpaperPrompt!, + }); + + await onProgress({ + name: 'image-generate', + isDone: true, + }); + + await onProgress({ + name: 'image-generate-download', + title: 'Downloading image', + isDone: false, + }); + + // TODO: [🧠] Is there some way to save normalized prompt to the database along the wallpaper + // > wallpaperPrompt = imagePromptResult.normalizedPrompt.content; + + wallpaperUrl = imagePromptResult.imageSrc; + wallpaperImage = await fetch(wallpaperUrl).then((response) => response.blob()); + + await onProgress({ + name: 'image-generate-download', + isDone: true, + }); + } + + //-------[ / Image generate ]--- + + //=========================================================================== + //-------[ Image analysis and check: ]--- + + await onProgress({ + name: 'image-check', + title: 'Checking image', + isDone: false, + }); + + if (!wallpaperImage) { + throw new Error('wallpaperImage is undefined'); + // <- TODO: ShouldNeverHappenError + } + + /* + Note: This is not needed because it is already checked by the measureImageBlob etc... Implement only if we want nicer error message + if (!wallpaper.type.startsWith('image/')) { + // TODO: [🈵] If 4XX error, show also the message from json body + throw new Error(`File is not an image`); + } + */ + + const originalSize = await measureImageBlob(wallpaperImage); + let naturalSize = originalSize.clone(); + + // Note: Checking first fatal problems then warnings and fixable problems (like too large image fixable by automatic resize) + + if (!isInAspectRatioRange(WALLPAPER_IMAGE_ASPECT_RATIO_ALLOWED_RANGE, originalSize)) { + throw new Error( + spaceTrim( + (block) => ` + Image has aspect ratio that is not allowed: + + ${block(aspectRatioRangeExplain(WALLPAPER_IMAGE_ASPECT_RATIO_ALLOWED_RANGE, originalSize))} + `, + ), + ); + } + + if (originalSize.x > WALLPAPER_IMAGE_MAX_ALLOWED_SIZE.x || originalSize.y > WALLPAPER_IMAGE_MAX_ALLOWED_SIZE.y) { + naturalSize = downscaleWithAspectRatio(originalSize, WALLPAPER_IMAGE_MAX_ALLOWED_SIZE); + } + + await onProgress({ + name: 'image-check', + isDone: true, + }); + + //-------[ / Image analysis and check ]--- + //=========================================================================== + //-------[ Image resize: ]--- + await onProgress({ + name: 'image-resize', + title: 'Resizing image', + isDone: false, + }); + + let wallpaperForUpload: Blob; + if (!wallpaperUrl) { + wallpaperForUpload = await resizeImageBlob(wallpaperImage, naturalSize); + } + const wallpaperForColorAnalysis = await resizeImageBlob( + wallpaperImage, + downscaleWithAspectRatio(naturalSize, computeColorstats.preferredSize), + ); + + await onProgress({ + name: 'image-resize', + isDone: true, + }); + //-------[ / Image resize ]--- + //=========================================================================== + //-------[ Color analysis: ]--- + + const colorStatsPromise = /* not await */ createImageInWorker(wallpaperForColorAnalysis).then( + (imageForColorAnalysis) => + computeColorstats( + imageForColorAnalysis, + onProgress /* <- Note: computeColorstats will show its own tasks */, + ), + ); + //-------[ / Color analysis ]--- + //=========================================================================== + //-------[ Upload image: ]--- + if (!wallpaperUrl) { + await onProgress({ + name: 'upload-wallpaper-image', + title: 'Uploading image', + isDone: false, + // TODO: Make it more granular + }); + const formData = new FormData(); + formData.append('wallpaper', wallpaperForUpload!); + + const response = await fetch('/api/upload-image', { + method: 'POST', + body: formData, + }); + + if (response.ok === false) { + throw new Error(`Upload wallpaper failed with status ${response.status}`); + } + + const uploadWallpaperResponse = (await response.json()) as UploadWallpaperResponse; + wallpaperUrl = uploadWallpaperResponse.wallpaperUrl; + await onProgress({ + name: 'upload-wallpaper-image', + isDone: true, + }); + console.info({ wallpaperUrl }); + } + //-------[ /Upload image ]--- + //=========================================================================== + + return { wallpaperUrl, originalSize, colorStats: await colorStatsPromise }; +} + +/** + * TODO: [🧠][♒] Watermark image + */ diff --git a/src/components/TaskInProgress/TasksInProgress.tsx b/src/components/TaskInProgress/TasksInProgress.tsx index 21a3f7ad7..c0517b2e9 100644 --- a/src/components/TaskInProgress/TasksInProgress.tsx +++ b/src/components/TaskInProgress/TasksInProgress.tsx @@ -110,7 +110,8 @@ export function TasksInProgress(props: TaskInProgressProps) { console.info({ taskProgress }); }} > - {taskProgress.title} + {taskProgress.title} + ))} diff --git a/src/components/TaskInProgress/task/WebgptTaskProgress.ts b/src/components/TaskInProgress/task/WebgptTaskProgress.ts index cbbd9e56c..cfcd5c1e6 100644 --- a/src/components/TaskInProgress/task/WebgptTaskProgress.ts +++ b/src/components/TaskInProgress/task/WebgptTaskProgress.ts @@ -1,16 +1,16 @@ -import { string_name, title } from '../../../utils/typeAliases'; +import { string_markdown_text, string_name } from '../../../utils/typeAliases'; export type WebgptTaskProgress = PendingWebgptTaskProgress | DoneWebgptTaskProgress; export interface PendingWebgptTaskProgress { readonly name: string_name; - readonly title: title; + readonly title: string_markdown_text; readonly isDone: false; } export interface DoneWebgptTaskProgress { readonly name: string_name; - readonly title?: title; + readonly title?: string_markdown_text /* <- TODO> && Exclude */; readonly isDone: true; } diff --git a/src/components/TaskInProgress/task/mock/_tasks.tsx b/src/components/TaskInProgress/task/mock/_tasks.tsx index d40e50bc2..7a3020e0a 100644 --- a/src/components/TaskInProgress/task/mock/_tasks.tsx +++ b/src/components/TaskInProgress/task/mock/_tasks.tsx @@ -27,11 +27,7 @@ export const MOCKED_TASKS_PROGRESS_QUEUE: Array = [ }, { name: 'text-analysis', - title: ( - <> - Analyzing newsletter text (2) - - ), + title: `Analyzing *newsletter* text (2)`, isDone: true, }, { diff --git a/src/components/TaskInProgress/task/mock/mockedMultitaskWithPrompts.tsx b/src/components/TaskInProgress/task/mock/mockedMultitaskWithPrompts.tsx index 5ddfa50cf..4530789f6 100644 --- a/src/components/TaskInProgress/task/mock/mockedMultitaskWithPrompts.tsx +++ b/src/components/TaskInProgress/task/mock/mockedMultitaskWithPrompts.tsx @@ -39,7 +39,7 @@ export async function mockedMultitaskWithPrompts( await onProgress({ name: `mocked-task-${i}`, - title: <>{title}, + title, isDone: false, }); @@ -58,11 +58,7 @@ export async function mockedMultitaskWithPrompts( await onProgress({ name: `mocked-task-${i}`, - title: ( - <> - {title} ({response.answer}) - - ), + title: `${title} *(${response.answer})*`, isDone: true, }); } diff --git a/src/components/Translate/Translate.tsx b/src/components/Translate/Translate.tsx index 8687139a5..69a59702d 100644 --- a/src/components/Translate/Translate.tsx +++ b/src/components/Translate/Translate.tsx @@ -1,5 +1,5 @@ -import { ReactNode } from 'react'; import { useLocale } from '../../utils/hooks/useLocale'; +import { string_markdown_text, string_translate_language } from '../../utils/typeAliases'; /** * A component that renders its children only if the locale matches the router locale @@ -9,13 +9,18 @@ import { useLocale } from '../../utils/hooks/useLocale'; */ interface TranslateProps { /** - * @@@ + * Language !!! */ - locale: string; + locale: string_translate_language; - children: ReactNode; + /** + * Content to translate + */ + children: string_markdown_text; } +// Use only one at once + /** * @@@ */ diff --git a/src/components/WebsiteTablo/WebsiteTablo.tsx b/src/components/WebsiteTablo/WebsiteTablo.tsx index 7d601be8f..a5c0c569c 100644 --- a/src/components/WebsiteTablo/WebsiteTablo.tsx +++ b/src/components/WebsiteTablo/WebsiteTablo.tsx @@ -51,11 +51,7 @@ export function WebsiteTablo() { tasksProgress={[ { name: 'publishing', - title: ( - <> - Publishing {domain} - - ), + title: `Publishing **${domain}**`, isDone: false, }, /*