diff --git a/backend/app/populate_db_models.py b/backend/app/populate_db_models.py index b68c8d5..dbef183 100644 --- a/backend/app/populate_db_models.py +++ b/backend/app/populate_db_models.py @@ -137,7 +137,8 @@ async def populate_prompts(): async def populate_db_models(): + await populate_prompts() await populate_models() await populate_styles() await populate_categories() - await populate_prompts() + diff --git a/backend/app/settings.py b/backend/app/settings.py index 469367e..0b3adc3 100644 --- a/backend/app/settings.py +++ b/backend/app/settings.py @@ -14,6 +14,7 @@ DATABASE_HOST = os.getenv('DATABASE_HOST') DATABASE_PORT = os.getenv('DATABASE_PORT') DATABASE_PASSWORD = os.getenv('DATABASE_PASSWORD') +PROMPT_KIND = os.getenv('PROMPT_KIND') TORTOISE_ORM = { "connections": { diff --git a/backend/app/things_to_draw.py b/backend/app/things_to_draw.py index 24e5c21..893e718 100644 --- a/backend/app/things_to_draw.py +++ b/backend/app/things_to_draw.py @@ -8,5 +8,55 @@ "A monster shaped like a pillow with soft, fluffy fur and eyes that glow a gentle blue, lounging in a child's bedroom.", "A spaceship piloted by anthropomorphic cats, navigating an asteroid field with ease while sipping alien beverages.", "A steampunk dragon with intricate metallic wings and a furnace-like chest, breathing fire mixed with sparks and gears.", - "A mythical creature with the body of a lion, the wings of an eagle, and the tail of a serpent, perched on a rock in a mystical landscape." + "A mythical creature with the body of a lion, the wings of an eagle, and the tail of a serpent, perched on a rock in a mystical landscape.", + "Sunset over a mountain", + "City skyline at night", + "Forest in autumn", + "Beach with palm trees", + "Desert landscape", + "Snow-covered village", + "Underwater coral reef", + "Space with planets and stars", + "Medieval castle", + "Futuristic city", + "Tropical rainforest", + "Farm with a barn and animals", + "Old European street", + "Japanese garden", + "Safari with wild animals", + "Volcano erupting", + "Waterfall in a jungle", + "Lighthouse on a cliff", + "Winter wonderland", + "Ancient ruins", + "Carnival with rides", + "Fairytale forest", + "Garden with flowers", + "Zoo with various animals", + "Island with a lighthouse", + "Ski resort", + "Cave interior", + "Aquarium with marine life", + "Space station", + "Pirate ship on the sea", + "Western desert town", + "Gothic cathedral", + "Vineyard in Tuscany", + "Zen temple", + "Hot air balloon festival", + "Grand Canyon", + "River with boats", + "Fantasy kingdom", + "Medieval market", + "Amusement park", + "Haunted house", + "Steampunk city", + "Mushroom forest", + "Busy airport", + "Rocket launch site", + "Circus tent", + "Ice cave", + "Surfing beach", + "Train station", + "Watermill in the countryside" ] diff --git a/backend/generate-images.py b/backend/generate-images.py index dd62f86..9c6f5a7 100644 --- a/backend/generate-images.py +++ b/backend/generate-images.py @@ -5,6 +5,7 @@ from app.db import init as init_db from app.exceptions import ImageGenerationException from app.models import Style, Prompt, StylePromptImage +from app.settings import PROMPT_KIND image_generator = AiHordeImageGenerator() @@ -49,7 +50,7 @@ async def get_missing_images() -> list[QueueItem]: from itertools import product styles = await Style.all() - prompts = await Prompt.all() + prompts = await Prompt.filter(kind=PROMPT_KIND).all() style_prompt_ids = set((spi.style_id, spi.prompt_id) for spi in await StylePromptImage.all()) missing = [QueueItem(style, prompt) for style, prompt in product(styles, prompts) if (style.id, prompt.id) not in style_prompt_ids] diff --git a/ui/package-lock.json b/ui/package-lock.json index b61e347..a2e488e 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -24,6 +24,7 @@ "react-dom": "^18.3.1", "react-query": "^3.39.3", "react-scripts": "5.0.1", + "react-select": "^5.8.0", "typescript": "^4.9.5", "web-vitals": "^2.1.4" } @@ -3989,6 +3990,31 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.4.tgz", + "integrity": "sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.4" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.7.tgz", + "integrity": "sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.4" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz", + "integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==", + "license": "MIT" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -5961,6 +5987,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -8943,6 +8978,16 @@ "utila": "~0.4" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -14985,6 +15030,12 @@ "node": ">= 4.0.0" } }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -17887,6 +17938,27 @@ } } }, + "node_modules/react-select": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.0.tgz", + "integrity": "sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.1.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -17910,6 +17982,22 @@ } } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -20357,6 +20445,20 @@ } } }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sidecar": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", diff --git a/ui/package.json b/ui/package.json index c1a6eca..99726f0 100644 --- a/ui/package.json +++ b/ui/package.json @@ -19,6 +19,7 @@ "react-dom": "^18.3.1", "react-query": "^3.39.3", "react-scripts": "5.0.1", + "react-select": "^5.8.0", "typescript": "^4.9.5", "web-vitals": "^2.1.4" }, diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 0394861..3af663c 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,26 +1,49 @@ import React, {useEffect, useState} from 'react'; import axios from 'axios'; -import {Box, ChakraProvider, Heading, Select, Spinner, VStack} from '@chakra-ui/react'; +import {Alert, AlertIcon, Box, ChakraProvider, Heading, Spinner, VStack} from '@chakra-ui/react'; +import {PromptSelector} from "./components/PromptSelector"; import ImageComparison from './components/ImageComparison'; import CategorySelector from "./components/CategorySelector"; -import {Category, Prompt, Style} from "./components/SimpleTypes"; import LargeImage from "./components/LargeImage"; import StyleSelector from "./components/StyleSelector"; +export const API_URL_CATEGORIES = "/categories"; +export const API_URL_STYLES = "/styles"; +export const API_URL_PROMPTS = "/prompts"; + +// Custom hook for data fetching +const useFetchData = (url: string) => { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + axios.get(url) + .then(response => setData(response.data)) + .catch(error => { + console.error(`Error when fetching data from ${url}`, error); + setError("Unable to load data."); + }) + .finally(() => setLoading(false)); + }, [url]); + + return {data, loading, error}; +}; + const App: React.FC = () => { - const [styles, setStyles] = useState([]); - const [categories, setCategories] = useState([]); const [selectedCategory, setSelectedCategory] = useState(null); - const [prompts, setPrompts] = useState([]); const [selectedStyle, setSelectedStyle] = useState(null); - const [selectedPrompt, setSelectedPrompt] = useState(null); + const [selectedPrompts, setSelectedPrompts] = useState([]); const [shownPrompt, setShownPrompt] = useState(null); const [shownStyle, setShownStyle] = useState(null); - const [loadingCategories, setLoadingCategories] = useState(true); - const [loadingStyles, setLoadingStyles] = useState(true); - const [loadingPrompts, setLoadingPrompts] = useState(true); const [isLargeImageModalOpen, setLargeImageModalOpen] = useState(false); + const {data: categories, loading: loadingCategories, error: errorCategories} = useFetchData(API_URL_CATEGORIES); + const {data: prompts, loading: loadingPrompts, error: errorPrompts} = useFetchData(API_URL_PROMPTS); + + const stylesUrl = selectedCategory !== null ? `${API_URL_STYLES}?category_id=${selectedCategory}` : API_URL_STYLES; + const {data: styles, loading: loadingStyles, error: errorStyles} = useFetchData(stylesUrl); + const handleClick = (prompt_id: number, style_id: number) => { setShownPrompt(prompt_id); setShownStyle(style_id); @@ -33,59 +56,55 @@ const App: React.FC = () => { setShownStyle(null); }; - useEffect(() => { - // Fetch categories - axios.get('/categories') - .then(response => { - setCategories(response.data); - setLoadingCategories(false); - }) - .catch(error => { - console.error("There was an error fetching categories!", error); - setLoadingCategories(false); - }); - }, []); - - useEffect(() => { - let url = '/styles'; - if (selectedCategory !== null) { - url = `/styles?category_id=${selectedCategory}`; + const promptOptions = prompts.map(prompt => ({ + value: prompt.id.toString(), + label: prompt.text + })); + + const handleSelectChange = (selectedOptions: any) => { + if (!selectedOptions) { + setSelectedPrompts([]); + } else { + const selectedIds = selectedOptions.map((option: { value: string }) => parseInt(option.value)); + setSelectedPrompts(selectedIds); } + }; - console.log('loading styles from url:', url) - - // Fetch styles - axios.get(url) - .then(response => { - setStyles(response.data); - setLoadingStyles(false); - }) - .catch(error => { - console.error("There was an error fetching styles!", error); - setLoadingStyles(false); - }); + const shownPrompts = prompts.filter(prompt => selectedPrompts.includes(prompt.id)); + const selectedPromptValues = promptOptions.filter(option => selectedPrompts.includes(parseInt(option.value))); - }, [selectedCategory]); - - useEffect(() => { - // Fetch prompts - axios.get('/prompts') - .then(response => { - setPrompts(response.data); - setLoadingPrompts(false); - }) - .catch(error => { - console.error("There was an error fetching prompts!", error); - setLoadingPrompts(false); - }); - }, []); + if (loadingCategories || loadingStyles || loadingPrompts) { + return ( + + + + + + ); + } return ( - - AI Horde Style Comparator - + AI Horde Style Comparator + {errorCategories && ( + + + {errorCategories} + + )} + {errorStyles && ( + + + {errorStyles} + + )} + {errorPrompts && ( + + + {errorPrompts} + + )} {shownPrompt && shownStyle && ( { )} - {loadingCategories || loadingStyles || loadingPrompts ? ( - - ) : ( - <> - - - - - - - - - - - - - )} - + + + + + + + + + - ) - ; + ); }; -export default App; \ No newline at end of file +export default App; diff --git a/ui/src/components/PromptSelector.tsx b/ui/src/components/PromptSelector.tsx new file mode 100644 index 0000000..f63c783 --- /dev/null +++ b/ui/src/components/PromptSelector.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import {Box} from "@chakra-ui/react"; +import Select from "react-select"; +import {Option} from "./SimpleTypes"; + + +interface Props { + promptOptions: Option[]; + handleSelectChange: (values: any) => void; +} + +export const PromptSelector: React.FC = ({ + promptOptions, + handleSelectChange + }) => { + return ( +