From 76737cfc9e462165cb256601acd883116959a5fa Mon Sep 17 00:00:00 2001 From: Gregor Adams <1148334+pixelass@users.noreply.github.com> Date: Wed, 21 Feb 2024 12:04:46 +0100 Subject: [PATCH] test: setup playwright (#48) ## Summary This PR establishes the foundation for end-to-end (E2E) testing in our project by integrating Playwright. It's a crucial step towards building a tested and stable application, focusing on simplifying our setup to kickstart the testing process effectively. ## Changes - **Integrated Playwright:** Sets up Playwright for E2E testing, enabling us to begin automating tests for our application. - **Codebase Cleanup:** Removed unused files and dependencies to facilitate a smoother build process and ensure the testing environment is focused only on active features. - **Preparation for Feature Reintegration:** This cleanup paves the way for a structured reintroduction of features, ensuring each is thoroughly tested before becoming part of the main codebase again. ## Rationale Following a significant refactor and restructuring of our source code, this move is aimed at ensuring the new structure not only supports development mode but is also testable and buildable. By starting with a lean codebase, we aim to methodically reintroduce features, ensuring each is compatible with our new structure and passes E2E tests. ## Next Steps With the testing framework in place, we will gradually reintroduce features, testing each thoroughly to maintain the integrity and stability of our application. ### Issues Closed - closes #22 --- package-lock.json | 60 ++ package.json | 7 +- playwright-report/index.html | 69 ++ playwright.config.ts | 10 + playwright/index.test.ts | 33 + src/client/organisms/delete-confirm/index.tsx | 68 -- src/client/organisms/folder-drop/index.tsx | 52 -- src/client/organisms/folder-field/index.tsx | 55 -- .../live-painting/drawing-canvas.tsx | 196 ------ .../organisms/live-painting/output-canvas.tsx | 57 -- .../live-painting/update-properties.tsx | 128 ---- src/client/organisms/modals/add-dataset.tsx | 167 ----- .../organisms/modals/caption/batch-edit.tsx | 177 ------ src/client/organisms/modals/caption/index.tsx | 568 ----------------- src/client/organisms/model-card/index.tsx | 309 --------- src/client/organisms/password-field/index.tsx | 49 -- src/client/organisms/text-field/index.tsx | 28 - .../organisms/zoomable-image-stage/index.tsx | 152 ----- src/client/pages/[locale]/dataset.tsx | 548 ---------------- src/client/pages/[locale]/feedback.tsx | 115 ---- src/client/pages/[locale]/home.tsx | 247 -------- src/client/pages/[locale]/inventory.tsx | 58 -- src/client/pages/[locale]/livepainting.tsx | 66 -- src/client/pages/[locale]/marketplace.tsx | 595 ------------------ src/client/pages/[locale]/settings.tsx | 155 ----- src/client/pages/[locale]/training.tsx | 58 -- 26 files changed, 177 insertions(+), 3850 deletions(-) create mode 100644 playwright-report/index.html create mode 100644 playwright.config.ts create mode 100644 playwright/index.test.ts delete mode 100644 src/client/organisms/delete-confirm/index.tsx delete mode 100644 src/client/organisms/folder-drop/index.tsx delete mode 100644 src/client/organisms/folder-field/index.tsx delete mode 100644 src/client/organisms/live-painting/drawing-canvas.tsx delete mode 100644 src/client/organisms/live-painting/output-canvas.tsx delete mode 100644 src/client/organisms/live-painting/update-properties.tsx delete mode 100644 src/client/organisms/modals/add-dataset.tsx delete mode 100644 src/client/organisms/modals/caption/batch-edit.tsx delete mode 100644 src/client/organisms/modals/caption/index.tsx delete mode 100644 src/client/organisms/model-card/index.tsx delete mode 100644 src/client/organisms/password-field/index.tsx delete mode 100644 src/client/organisms/text-field/index.tsx delete mode 100644 src/client/organisms/zoomable-image-stage/index.tsx delete mode 100644 src/client/pages/[locale]/dataset.tsx delete mode 100644 src/client/pages/[locale]/feedback.tsx delete mode 100644 src/client/pages/[locale]/home.tsx delete mode 100644 src/client/pages/[locale]/inventory.tsx delete mode 100644 src/client/pages/[locale]/livepainting.tsx delete mode 100644 src/client/pages/[locale]/marketplace.tsx delete mode 100644 src/client/pages/[locale]/training.tsx diff --git a/package-lock.json b/package-lock.json index 827700b78..b84776b8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "@mui/joy": "5.0.0-beta.24", "@mui/material": "5.15.6", "@next/mdx": "14.1.0", + "@playwright/test": "^1.41.2", "@semantic-release/changelog": "^6.0.3", "@semantic-release/exec": "^6.0.3", "@semantic-release/git": "^10.0.1", @@ -5085,6 +5086,21 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@playwright/test": { + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz", + "integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==", + "dev": true, + "dependencies": { + "playwright": "1.41.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -20938,6 +20954,50 @@ "node": ">=4" } }, + "node_modules/playwright": { + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz", + "integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==", + "dev": true, + "dependencies": { + "playwright-core": "1.41.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz", + "integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", diff --git a/package.json b/package.json index 31dbca1e0..f427bbcd0 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build": "nextron build", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", "dev": "nextron", - "eslint": "eslint \"{renderer,main}/{**/*,*}{.,.config.,.test.,.e2e.}{js,mjs,ts,tsx}\"", + "eslint": "eslint \"src/{**/*,*}.{ts,tsx}\"", "postinstall": "electron-builder install-app-deps", "ncu": "npx npm-check-updates@latest -u", "ncu:check": "npx npm-check-updates@latest", @@ -21,7 +21,9 @@ "toc": "npx markdown-toc README.md -i", "tsc:noEmit": "tsc --noEmit", "caption:test": "ts-node-esm main/captions/misc.ts", - "test:unit": "jest --runInBand --config jest.config.unit.mjs --verbose" + "test:unit": "jest --runInBand --config jest.config.unit.mjs --verbose", + "pretest:e2e": "nextron build --no-pack", + "test:e2e": "npx playwright test" }, "dependencies": { "electron-context-menu": "3.6.1", @@ -43,6 +45,7 @@ "@mui/joy": "5.0.0-beta.24", "@mui/material": "5.15.6", "@next/mdx": "14.1.0", + "@playwright/test": "^1.41.2", "@semantic-release/changelog": "^6.0.3", "@semantic-release/exec": "^6.0.3", "@semantic-release/git": "^10.0.1", diff --git a/playwright-report/index.html b/playwright-report/index.html new file mode 100644 index 000000000..76188316c --- /dev/null +++ b/playwright-report/index.html @@ -0,0 +1,69 @@ + + + + + + + + + Playwright Test Report + + + + +
+ + + + \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..f8805f1b4 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,10 @@ +import type { PlaywrightTestConfig } from "@playwright/test"; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: "./playwright", +}; + +export default config; diff --git a/playwright/index.test.ts b/playwright/index.test.ts new file mode 100644 index 000000000..b8e9357e6 --- /dev/null +++ b/playwright/index.test.ts @@ -0,0 +1,33 @@ +import type { ElectronApplication, Page } from "@playwright/test"; +import { test, expect } from "@playwright/test"; +import { _electron as electron } from "playwright"; + +let electronApp: ElectronApplication; +let page: Page; + +test.beforeAll(async () => { + // Use package.main + electronApp = await electron.launch({ args: ["."] }); + const isPackaged = await electronApp.evaluate(async ({ app }) => app.isPackaged); + + expect(isPackaged).toBe(false); +}); + +test.afterAll(async () => { + await electronApp.close(); +}); + +test("Renders the first page", async () => { + page = await electronApp.firstWindow(); + const title = await page.title(); + expect(title).toBe("Blibla"); +}); + +test("Allows switching the language", async () => { + page = await electronApp.firstWindow(); + + await expect(page.getByTestId("language-selector-list")).toBeVisible(); + await expect(page.getByText("Deutsch")).toBeVisible(); + await page.getByText("Deutsch").click(); + await expect(page.getByText("Sprache")).toBeVisible(); +}); diff --git a/src/client/organisms/delete-confirm/index.tsx b/src/client/organisms/delete-confirm/index.tsx deleted file mode 100644 index f9e02bbea..000000000 --- a/src/client/organisms/delete-confirm/index.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import CancelIcon from "@mui/icons-material/Cancel"; -import DeleteForeverIcon from "@mui/icons-material/DeleteForever"; -import Button from "@mui/joy/Button"; -import IconButton from "@mui/joy/IconButton"; -import Sheet from "@mui/joy/Sheet"; -import { useAtom } from "jotai"; -import { useTranslation } from "next-i18next"; -import { useState } from "react"; - -import { datasetsAtom } from "@/ions/atoms"; - -export function DeleteConfirm({ projectId }: { projectId: string }) { - const [confirm, setConfirm] = useState(false); - const [, setDatasets] = useAtom(datasetsAtom); - const { t } = useTranslation(["common"]); - return confirm ? ( - - - - - ) : ( - { - setConfirm(true); - }} - > - - - ); -} diff --git a/src/client/organisms/folder-drop/index.tsx b/src/client/organisms/folder-drop/index.tsx deleted file mode 100644 index d849617cd..000000000 --- a/src/client/organisms/folder-drop/index.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import Box from "@mui/joy/Box"; -import type { ReactNode } from "react"; -import { useState } from "react"; - -export function FolderDrop({ - children, - onDrop, -}: { - children?: ReactNode; - onDrop?(path: string): void; -}) { - const [dragCounter, setDragCounter] = useState(0); - - const isDragOver = dragCounter > 0; - - return ( - { - event.preventDefault(); - }} - onDragEnter={event => { - event.preventDefault(); - setDragCounter(previousState => previousState + 1); - }} - onDragLeave={event => { - event.preventDefault(); - setDragCounter(previousState => previousState - 1); - }} - onDrop={event => { - event.preventDefault(); - setDragCounter(0); - const folder = event.dataTransfer.items[0]; - if (folder?.kind === "file" && folder.webkitGetAsEntry()?.isDirectory && onDrop) { - const file = folder.getAsFile(); - if (file?.path) { - onDrop(file.path); - } - } - }} - > - {children} - - ); -} diff --git a/src/client/organisms/folder-field/index.tsx b/src/client/organisms/folder-field/index.tsx deleted file mode 100644 index 2bc380560..000000000 --- a/src/client/organisms/folder-field/index.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import FolderIcon from "@mui/icons-material/Folder"; -import FormControl from "@mui/joy/FormControl"; -import FormHelperText from "@mui/joy/FormHelperText"; -import FormLabel from "@mui/joy/FormLabel"; -import IconButton from "@mui/joy/IconButton"; -import Input from "@mui/joy/Input"; -import type { InputProps } from "@mui/joy/Input"; -import { useTranslation } from "next-i18next"; -import type { ReactNode } from "react"; -import type { Except } from "type-fest"; - -export function FolderField({ - helpText, - error, - label, - onSelect, - ...properties -}: Except & { - helpText?: ReactNode; - label?: ReactNode; - error?: string; - onSelect?(value: string): Promise | void; -}) { - const { t } = useTranslation(["common"]); - return ( - - {label && {label}} - { - try { - const directory_ = await window.ipc.getDirectory(); - if (directory_ && onSelect) { - onSelect(directory_); - } - } catch (error) { - console.log(error); - } - }} - > - - - } - /> - {(error || helpText) && {error || helpText}} - - ); -} diff --git a/src/client/organisms/live-painting/drawing-canvas.tsx b/src/client/organisms/live-painting/drawing-canvas.tsx deleted file mode 100644 index 44eb119f7..000000000 --- a/src/client/organisms/live-painting/drawing-canvas.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import Brush from "@mui/icons-material/Brush"; -import Clear from "@mui/icons-material/Clear"; -import IconButton from "@mui/joy/IconButton"; -import Input from "@mui/joy/Input"; -import Stack from "@mui/joy/Stack"; -import { Box } from "@mui/material"; -import type { MouseEvent } from "react"; -import { useRef, useEffect, useState } from "react"; - -interface DrawingCanvasProperties { - width?: number; - height?: number; -} - -export function DrawingCanvas({ width = 512, height = 512 }: DrawingCanvasProperties) { - const canvasReference = useRef(null); - const [isDrawing, setIsDrawing] = useState(false); - const [strokeColor, setStrokeColor] = useState("#00ff00"); - const [brushSize, setBrushSize] = useState(30); - const [backgroundColor] = useState("#ffffff"); - const brushReference = useRef(null); - - function startDrawing({ nativeEvent }: MouseEvent) { - const context = canvasReference.current?.getContext("2d"); - if (context) { - const { offsetX, offsetY } = nativeEvent; - context.strokeStyle = strokeColor; - context.lineWidth = brushSize; - context.beginPath(); - context.moveTo(offsetX, offsetY); - setIsDrawing(true); - } - } - - function finishDrawing() { - const context = canvasReference.current?.getContext("2d"); - if (context) { - context.closePath(); - setIsDrawing(false); - } - } - - function draw({ nativeEvent }: MouseEvent) { - const { offsetX, offsetY } = nativeEvent; - if (brushReference.current) { - brushReference.current.style.transform = `translate3d(calc(${offsetX}px - 50%), calc(${offsetY}px - 50%), 0)`; - } - - if (!isDrawing) { - return; - } - - const context = canvasReference.current?.getContext("2d"); - if (context) { - context.strokeStyle = strokeColor; - context.lineWidth = brushSize; - context.lineTo(offsetX, offsetY); - context.stroke(); - } - } - - function clearCanvas() { - if (canvasReference.current) { - const canvas = canvasReference.current; - const context = canvas?.getContext("2d"); - - if (context) { - context.clearRect(0, 0, canvas.width, canvas.height); - context.fillStyle = backgroundColor; - context.fillRect(0, 0, canvas.width, canvas.height); - } - } - } - - useEffect(() => { - // Keep this function local to this side effect - // it simplifies the usage and reduces the number of required hooks - let animationFrameId: number; - async function captureAndSendCanvasData() { - const drawingCanvas = canvasReference.current; - - if (drawingCanvas) { - const data = drawingCanvas.toDataURL(); - window.ipc.send("live-painting:input", data); - } - - animationFrameId = requestAnimationFrame(captureAndSendCanvasData); - } - - if (canvasReference.current) { - const dpr = window.devicePixelRatio || 1; - const canvas = canvasReference.current; - - canvas.width = width * dpr; - canvas.height = height * dpr; - - const context = canvas.getContext("2d"); - if (context) { - context.scale(dpr, dpr); - } - - animationFrameId = requestAnimationFrame(captureAndSendCanvasData); - } - - return () => { - cancelAnimationFrame(animationFrameId); - }; - }, [height, width]); - - // Only handle adjustments here - // Never paint in this side effect - useEffect(() => { - if (brushReference.current) { - brushReference.current.style.width = `${brushSize}px`; - brushReference.current.style.height = `${brushSize}px`; - } - - if (canvasReference.current) { - const canvas = canvasReference.current; - - const context = canvas.getContext("2d"); - if (context) { - context.fillStyle = backgroundColor; - context.lineCap = "round"; - context.lineJoin = "round"; // This makes the corner round - context.strokeStyle = strokeColor; - context.lineWidth = brushSize; - } - } - }, [backgroundColor, brushSize, strokeColor]); - - return ( - - - { - setStrokeColor(event.target.value); - }} - /> - - } - slotProps={{ input: { min: 0 } }} - onChange={event => { - setBrushSize(Number.parseInt(event.target.value, 10)); - }} - /> - - - - - - - - - - - - ); -} diff --git a/src/client/organisms/live-painting/output-canvas.tsx b/src/client/organisms/live-painting/output-canvas.tsx deleted file mode 100644 index f307e2e27..000000000 --- a/src/client/organisms/live-painting/output-canvas.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { useRef, useEffect } from "react"; - -interface OutputCanvasProperties { - width?: number; - height?: number; -} - -export function OutputCanvas({ width = 512, height = 512 }: OutputCanvasProperties) { - const canvasReference = useRef(null); - - useEffect(() => { - if (canvasReference.current) { - const dpr = window.devicePixelRatio || 1; - const canvas = canvasReference.current; - - canvas.width = width * dpr; - canvas.height = height * dpr; - canvas.style.width = `${width}px`; - canvas.style.height = `${height}px`; - } - }, [height, width]); - - useEffect(() => { - const image = new Image(); - - if (!canvasReference.current) { - return; - } - - const canvas = canvasReference.current; - const context = canvas.getContext("2d"); - - function handleLoad() { - context!.drawImage(image, 0, 0, canvas.width, canvas.height); - } - - image.addEventListener("load", handleLoad); - - function handleImageGenerated(base64Image: string) { - if (!base64Image.trim() || image.src === base64Image) { - return; - } - - console.log("tick"); - image.src = `${base64Image}`; - } - - const unsubscribe = window.ipc.on("image-generated", handleImageGenerated); - - return () => { - unsubscribe(); - image.removeEventListener("load", handleLoad); - }; - }, []); - - return ; -} diff --git a/src/client/organisms/live-painting/update-properties.tsx b/src/client/organisms/live-painting/update-properties.tsx deleted file mode 100644 index 37a46d267..000000000 --- a/src/client/organisms/live-painting/update-properties.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import Recycling from "@mui/icons-material/Recycling"; -import FormControl from "@mui/joy/FormControl"; -import FormLabel from "@mui/joy/FormLabel"; -import IconButton from "@mui/joy/IconButton"; -import Input from "@mui/joy/Input"; -import Stack from "@mui/joy/Stack"; -import type { ChangeEvent } from "react"; -import { useEffect, useState } from "react"; - -export function UpdateProperties() { - const [prompt, setPrompt] = useState(""); - const [seed, setSeed] = useState("0"); - const [size, setSize] = useState({ width: 512, height: 512 }); - const [strength, setStrength] = useState("1"); - const [guidance, setGuidance] = useState("0"); - - // Handle input changes - function handlePromptChange(event: ChangeEvent) { - setPrompt(event.target.value); - } - - function handleSeedChange(event: ChangeEvent) { - setSeed(event.target.value); - } - - function handleSizeChange(dimension: "height" | "width", event: ChangeEvent) { - setSize(previousSize => ({ - ...previousSize, - [dimension]: Number.parseInt(event.target.value, 10), - })); - } - - function handleStrengthChange(event: ChangeEvent) { - setStrength(event.target.value); - } - - function handleGuidanceChange(event: ChangeEvent) { - setGuidance(event.target.value); - } - - function randomSeed() { - setSeed(Math.floor(Math.random() * 100_000_000 + 1).toString()); - } - - // Use effect to send updates - useEffect(() => { - const properties = { - prompt, - seed: seed === "" ? 0 : Number.parseInt(seed, 10), - size, - strength: Number.parseFloat(strength), - guidance_scale: Number.parseFloat(guidance), - }; - window.ipc.send("live-painting:update-properties", properties); - }, [prompt, seed, size, strength, guidance]); - - return ( - - - Prompt - - - - - Strength - { - handleStrengthChange(event); - }} - /> - - - {/* - Guidance - { - handleGuidanceChange(event); - }} - /> - */} - - - Seed - - - - } - onChange={handleSeedChange} - /> - - - Width - { - handleSizeChange("width", event); - }} - /> - - - Height - { - handleSizeChange("height", event); - }} - /> - - - ); -} diff --git a/src/client/organisms/modals/add-dataset.tsx b/src/client/organisms/modals/add-dataset.tsx deleted file mode 100644 index c072d8940..000000000 --- a/src/client/organisms/modals/add-dataset.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; -import FolderIcon from "@mui/icons-material/Folder"; -import RefreshIcon from "@mui/icons-material/Refresh"; -import Button from "@mui/joy/Button"; -import CircularProgress from "@mui/joy/CircularProgress"; -import FormControl from "@mui/joy/FormControl"; -import FormHelperText from "@mui/joy/FormHelperText"; -import FormLabel from "@mui/joy/FormLabel"; -import IconButton from "@mui/joy/IconButton"; -import Input from "@mui/joy/Input"; -import Modal from "@mui/joy/Modal"; -import type { ModalProps } from "@mui/joy/Modal"; -import ModalClose from "@mui/joy/ModalClose"; -import ModalDialog from "@mui/joy/ModalDialog"; -import Stack from "@mui/joy/Stack"; -import Typography from "@mui/joy/Typography"; -import { useAtom } from "jotai"; -import { useTranslation } from "next-i18next"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import type { Except } from "type-fest"; - -import { directoryAtom, datasetsAtom } from "@/ions/atoms"; -import { adjectives, colors, generateRandomName, nouns } from "@/ions/utils/get-random-name"; - -function AddDatasetForm({ onClose }: { onClose: ModalProps["onClose"] }) { - const [, setDatasets] = useAtom(datasetsAtom); - const [directory, setDirectory] = useAtom(directoryAtom); - - const [loading, setLoading] = useState(false); - const [, setError] = useState(null); - - const { t } = useTranslation(["common"]); - - const { register, formState, handleSubmit, setValue, clearErrors } = useForm({ - defaultValues: { - name: generateRandomName([adjectives, colors, nouns], { - humanize: true, - separator: " ", - }), - directory, - }, - }); - - useEffect( - () => () => { - setDirectory(""); - }, - [setDirectory] - ); - - useEffect(() => { - clearErrors("directory"); - }, [clearErrors]); - - return ( -
{ - setLoading(true); - try { - await window.ipc.createDataset(data.directory, data.name); - await window.ipc.getDatasets().then(datasets_ => { - setDatasets(datasets_); - }); - setDirectory(""); - if (onClose) { - onClose({}, "closeClick"); - } - } catch (error) { - console.log(error); - setError(error as Error); - } finally { - setLoading(false); - } - })} - > - - - {t("common:pages.datasets.newDataset")} - - - {t("common:name")} - { - setValue( - "name", - generateRandomName([adjectives, colors, nouns], { - humanize: true, - separator: " ", - }) - ); - }} - > - - - } - {...register("name", { required: true })} - /> - {formState.errors.name && ( - {t(`common:form.errors.name`)} - )} - - - {t("common:directory")} - { - try { - const directory_ = await window.ipc.getDirectory(); - if (directory_) { - setValue("directory", directory_); - } - } catch (error) { - console.log(error); - setError(error as Error); - } - }} - > - - - } - {...register("directory", { required: true })} - /> - {formState.errors.directory && ( - {t(`common:form.errors.directory`)} - )} - - - - -
- ); -} - -export function AddDatasetModal({ onClose, ...properties }: Except) { - const { t } = useTranslation(["common"]); - - return ( - - - - - - - ); -} diff --git a/src/client/organisms/modals/caption/batch-edit.tsx b/src/client/organisms/modals/caption/batch-edit.tsx deleted file mode 100644 index 0974aaa3f..000000000 --- a/src/client/organisms/modals/caption/batch-edit.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import FindReplaceIcon from "@mui/icons-material/FindReplace"; -import Box from "@mui/joy/Box"; -import Button from "@mui/joy/Button"; -import DialogActions from "@mui/joy/DialogActions"; -import FormControl from "@mui/joy/FormControl"; -import FormLabel from "@mui/joy/FormLabel"; -import IconButton from "@mui/joy/IconButton"; -import Input from "@mui/joy/Input"; -import Modal from "@mui/joy/Modal"; -import ModalClose from "@mui/joy/ModalClose"; -import ModalDialog from "@mui/joy/ModalDialog"; -import Stack from "@mui/joy/Stack"; -import SvgIcon from "@mui/joy/SvgIcon"; -import Tooltip from "@mui/joy/Tooltip"; -import Typography from "@mui/joy/Typography"; -import { useAtom } from "jotai/index"; -import { useTranslation } from "next-i18next"; -import React, { useId, useState } from "react"; -import { useForm } from "react-hook-form"; - -import { editCaptionScopeAtom, imagesAtom } from "@/ions/atoms"; -import { EditCaptionScope } from "@/organisms/modals/caption/index"; - -export function RegexIcon() { - return ( - - - - ); -} - -export function TextSearchIcon() { - return ( - - - - ); -} - -export function SuffixIcon() { - return ( - - - - ); -} - -export function PrefixIcon() { - return ( - - - - ); -} - -export function BatchEditModal({ - open, - onClose, -}: { - onClose(): void | Promise; - open: boolean; -}) { - const FindReplaceId = useId(); - const { t } = useTranslation(["common"]); - const [regex, setRegex] = useState(false); - const [images, setImages] = useAtom(imagesAtom); - const [editCaptionScope] = useAtom(editCaptionScopeAtom); - const { register, reset, handleSubmit } = useForm({ - defaultValues: { - find: "", - replace: "", - prefix: "", - suffix: "", - }, - }); - - return ( - - - - - {t("common:batchEdit")}: - { - const newData = images.map(image => { - if (editCaptionScope === "empty" && image.caption) { - return image; - } - - if (editCaptionScope === "selected" && !image.selected) { - return image; - } - - if (data.find) { - const pattern = regex ? new RegExp(data.find, "ig") : data.find; - image.caption = image.caption.replace(pattern, data.replace); - } - - image.caption = data.prefix + image.caption + data.suffix; - return image; - }); - setImages(newData); - window.ipc.batchEditCaption(newData); - reset(); - onClose(); - })} - > - - - - {t("common:searchAndReplace")} - - - } - endDecorator={ - - { - setRegex(previousState => !previousState); - }} - > - - - - } - /> - } - /> - - - - {t("common:prefix")} - } /> - - - {t("common:suffix")} - } /> - - - - - - - - - ); -} diff --git a/src/client/organisms/modals/caption/index.tsx b/src/client/organisms/modals/caption/index.tsx deleted file mode 100644 index 517536e73..000000000 --- a/src/client/organisms/modals/caption/index.tsx +++ /dev/null @@ -1,568 +0,0 @@ -import CloudDownloadIcon from "@mui/icons-material/CloudDownload"; -import CheckAllIcon from "@mui/icons-material/DoneAll"; -import ErrorIcon from "@mui/icons-material/Error"; -import RuleIcon from "@mui/icons-material/Rule"; -import StyleIcon from "@mui/icons-material/Style"; -import VisibilityIcon from "@mui/icons-material/Visibility"; -import WarningIcon from "@mui/icons-material/Warning"; -import Alert from "@mui/joy/Alert"; -import Box from "@mui/joy/Box"; -import Button from "@mui/joy/Button"; -import FormControl from "@mui/joy/FormControl"; -import FormLabel from "@mui/joy/FormLabel"; -import Link from "@mui/joy/Link"; -import Modal from "@mui/joy/Modal"; -import ModalClose from "@mui/joy/ModalClose"; -import ModalDialog from "@mui/joy/ModalDialog"; -import Option from "@mui/joy/Option"; -import Select from "@mui/joy/Select"; -import Slider from "@mui/joy/Slider"; -import Stack from "@mui/joy/Stack"; -import { styled } from "@mui/joy/styles"; -import SvgIcon from "@mui/joy/SvgIcon"; -import Tab from "@mui/joy/Tab"; -import TabList from "@mui/joy/TabList"; -import TabPanel from "@mui/joy/TabPanel"; -import Tabs from "@mui/joy/Tabs"; -import Textarea from "@mui/joy/Textarea"; -import ToggleButtonGroup from "@mui/joy/ToggleButtonGroup"; -import Typography from "@mui/joy/Typography"; -import { useAtom } from "jotai/index"; -import dynamic from "next/dynamic"; -import { Trans, useTranslation } from "next-i18next"; -import React, { useEffect, useMemo, useState } from "react"; -import useSWR from "swr"; - -import { - CAPTIONS, - DOWNLOADS, - GPT_VISION_OPTIONS, - OPENAI_API_KEY, -} from "../../../../main/helpers/constants"; - -import { captioningErrorAtom, imagesAtom, editCaptionScopeAtom } from "@/ions/atoms"; -import { PasswordField } from "@/organisms/password-field"; - -export const CodeMirror = dynamic( - () => import("react-codemirror2").then(module_ => module_.Controlled), - { ssr: false } -); -export const StyledEditor = styled(CodeMirror)({ - height: "100%", - ">.CodeMirror": { - height: "100%", - }, -}); - -export const defaultGptOptions = { - batchSize: 4, - parallel: true, - guidelines: `Please caption these images, separate groups by comma, ensure logical groups: "black torn wide pants, red stained sweater" instead of "black, torn, wide pants and red, stained sweater"`, - exampleResponse: `[ - "a photo of a young man, red hair, blue torn overalls with brass buttons, orange t-shirt with holes, white background", - "a watercolor painting of an elderly woman, grey hair, yellow and blue floral print sundress with puffy sleeves, pink high heels, looking at a castle in the distance" -]`, -}; - -export function EmptyCaptionIcon() { - return ( - - - - ); -} - -export function useFilteredImages() { - const [filterScope] = useAtom(editCaptionScopeAtom); - const [images] = useAtom(imagesAtom); - return useMemo(() => { - switch (filterScope) { - case "empty": { - return images.filter(image => !image.caption); - } - - case "selected": { - return images.filter(image => image.selected); - } - - case "all": { - return images; - } - - default: { - return []; - } - } - }, [filterScope, images]); -} - -export function EditCaptionScope() { - const { t } = useTranslation(["common"]); - const [value, setValue] = useAtom(editCaptionScopeAtom); - - return ( - - {t("common:scopeForEditing")} - { - if (newValue) { - setValue(newValue); - } - }} - > - - - - - - ); -} - -export function GPTVCaptionModal({ - onClose, - onStart, -}: { - onClose(): void | Promise; - onStart(): void | Promise; - onDone(): void | Promise; -}) { - const [openAiApiKey, setOpenAiApiKey] = useState(""); - const [gptVisionOptions, setGptVisionOptions] = useState(defaultGptOptions); - const [confirmGpt, setConfirmGpt] = useState(false); - const { t } = useTranslation(["common"]); - const [, setCaptioningError] = useAtom(captioningErrorAtom); - const { data: openApiKeyData } = useSWR(OPENAI_API_KEY); - const { data: gptVisionData } = useSWR(GPT_VISION_OPTIONS); - const filteredImages = useFilteredImages(); - - useEffect(() => { - if (openApiKeyData) { - setOpenAiApiKey(openApiKeyData); - } - }, [openApiKeyData]); - - useEffect(() => { - if (gptVisionData) { - setGptVisionOptions( - gptVisionData as { - batchSize: number; - guidelines: string; - exampleResponse: string; - parallel: boolean; - } - ); - } - }, [gptVisionData]); - - return ( - - - - {confirmGpt && ( - - }> - - - ), - }} - /> - - - {!openAiApiKey && ( - }> - - {t("common:pages.dataset.enterKeyToUseGPTVision")}{" "} - - {t("common:getApiKey")} - - - - )} - - - )} - - { - setOpenAiApiKey(event.target.value); - }} - onBlur={event => { - window.ipc.fetch(OPENAI_API_KEY, { - method: "POST", - data: event.target.value, - }); - }} - /> - - - - - - {t("common:batch")} - { - setGptVisionOptions(previousState => ({ - ...previousState, - batchSize: value as number, - })); - }} - onChangeCommitted={(_event, value) => { - window.ipc.fetch(GPT_VISION_OPTIONS, { - method: "POST", - data: { - ...gptVisionOptions, - batchSize: value, - }, - }); - }} - /> - - {t("common:guideline")} - - { - setGptVisionOptions({ - ...gptVisionOptions, - guidelines: value, - }); - window.ipc.fetch(GPT_VISION_OPTIONS, { - method: "POST", - data: { - ...gptVisionOptions, - guidelines: value, - }, - }); - }} - /> - - {t("common:exampleResponse")} - - { - setGptVisionOptions({ - ...gptVisionOptions, - exampleResponse: value, - }); - window.ipc.fetch(GPT_VISION_OPTIONS, { - method: "POST", - data: { - ...gptVisionOptions, - exampleResponse: value, - }, - }); - }} - /> - - - - ); -} - -export function WD14CaptionModal({ - onClose, - onStart, -}: { - onClose(): void | Promise; - onStart(): void | Promise; - onDone(): void | Promise; -}) { - const [options, setOptions] = useState({ batchSize: 10, model: "", exclude: "" }); - const { t } = useTranslation(["common"]); - const [, setCaptioningError] = useAtom(captioningErrorAtom); - const { data: loadingModel } = useSWR("SmilingWolf/wd-v1-4-convnextv2-tagger-v2/model"); - const { data: loadingCSV } = useSWR("SmilingWolf/wd-v1-4-convnextv2-tagger-v2/selected_tags"); - const { data: checkpointsData } = useSWR(CAPTIONS, () => window.ipc.getModels("captions")); - const isInstalled = Boolean(checkpointsData?.length); - const model = - "https://huggingface.co/SmilingWolf/wd-v1-4-convnextv2-tagger-v2/resolve/main/model.onnx"; - const csv = - "https://huggingface.co/SmilingWolf/wd-v1-4-convnextv2-tagger-v2/resolve/main/selected_tags.csv"; - - useEffect(() => { - if (checkpointsData) { - setOptions(previousState => ({ ...previousState, model: checkpointsData[0] })); - } - }, [checkpointsData]); - - const filteredImages = useFilteredImages(); - - return ( - - {!isInstalled && ( - } - endDecorator={ - - } - sx={{ - ".MuiAlert-endDecorator": { - alignSelf: "flex-start", - mt: -0.5, - mr: -0.5, - }, - }} - > - {t("common:pages.dataset.oneTimeDownloadNote")} - - )} - - - - - {t("common:model")} - - - - {t("common:excludedTags")} -