diff --git a/.github/workflows/set-pr-title.yml b/.github/workflows/set-pr-title.yml new file mode 100644 index 000000000..6e9b5b05a --- /dev/null +++ b/.github/workflows/set-pr-title.yml @@ -0,0 +1,30 @@ +name: Set PR Title to First Commit Message + +on: + pull_request: + types: [opened, synchronize] + +jobs: + set-pr-title: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + # Fetches the entire commit history so that we can access the first commit + fetch-depth: 0 + + - name: Get first commit message + run: | + # Get the SHA of the first commit in the PR + FIRST_COMMIT_SHA=$(git log --reverse --format="%H" "${{ github.event.pull_request.base.sha }}..HEAD" | head -n 1) + # Get the message of the first commit in the PR + FIRST_COMMIT_MESSAGE=$(git log -n 1 --format=%B "$FIRST_COMMIT_SHA") + echo "FIRST_COMMIT_MESSAGE=$FIRST_COMMIT_MESSAGE" >> $GITHUB_ENV + + - name: Update PR title + run: gh pr edit "$PR_NUMBER" --title "$FIRST_COMMIT_MESSAGE" + env: + FIRST_COMMIT_MESSAGE: ${{ env.FIRST_COMMIT_MESSAGE }} + GITHUB_TOKEN: ${{ secrets.PIXELASS_PAT_BLIBLA }} + PR_NUMBER: ${{ github.event.pull_request.number }} diff --git a/package-lock.json b/package-lock.json index 3814c899a..f002fbf02 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", @@ -5086,6 +5087,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", @@ -20952,6 +20968,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 1aa0e9bce..0b74e92de 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.ts --verbose" + "test:unit": "jest --runInBand --config jest.config.ts --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")} -