create picture page #3
No files matched your search
@@ -2,6 +2,7 @@ export interface Request{
|
|||||||
id: number;
|
id: number;
|
||||||
text: string;
|
text: string;
|
||||||
files: File[];
|
files: File[];
|
||||||
|
ratio: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Response{
|
export interface Response{
|
||||||
|
|||||||
@@ -0,0 +1,540 @@
|
|||||||
|
.page {
|
||||||
|
display: grid;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: var(--space-6) var(--space-10) var(--space-10);
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 52px;
|
||||||
|
padding-bottom: var(--space-4);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backButton,
|
||||||
|
.removeFileButton {
|
||||||
|
display: inline-grid;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
place-items: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--color-text);
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transition:
|
||||||
|
color var(--duration-fast) var(--ease-standard),
|
||||||
|
border-color var(--duration-fast) var(--ease-standard),
|
||||||
|
background-color var(--duration-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backButton {
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backButton:hover,
|
||||||
|
.removeFileButton:hover {
|
||||||
|
color: var(--color-black);
|
||||||
|
background: var(--color-primary);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backButton:focus-visible,
|
||||||
|
.removeFileButton:focus-visible,
|
||||||
|
.uploadButton:focus-within,
|
||||||
|
.segmentedControl button:focus-visible,
|
||||||
|
.styleChoices button:focus-visible,
|
||||||
|
.generateButton:focus-visible {
|
||||||
|
outline: 3px solid var(--color-primary-soft);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 150px;
|
||||||
|
max-height: 38px;
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: left center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode,
|
||||||
|
.eyebrow,
|
||||||
|
.labelRow,
|
||||||
|
.previewHeader,
|
||||||
|
.previewFooter {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.2;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode,
|
||||||
|
.eyebrow {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(300px, 0.86fr) minmax(380px, 1.14fr);
|
||||||
|
gap: var(--space-6);
|
||||||
|
width: min(100%, 1120px);
|
||||||
|
margin: auto;
|
||||||
|
padding-block: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls,
|
||||||
|
.previewPanel {
|
||||||
|
min-width: 0;
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-6);
|
||||||
|
padding: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(30px, 4vw, 44px);
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field legend {
|
||||||
|
padding: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelRow {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-3);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelRow span:first-child {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 132px;
|
||||||
|
padding: var(--space-4);
|
||||||
|
resize: vertical;
|
||||||
|
color: var(--color-text);
|
||||||
|
font: inherit;
|
||||||
|
line-height: 1.45;
|
||||||
|
background: rgba(3, 6, 16, 0.42);
|
||||||
|
border: 1px solid rgba(216, 227, 251, 0.16);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
outline: 0;
|
||||||
|
transition: border-color var(--duration-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field textarea::placeholder {
|
||||||
|
color: var(--color-gray-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field textarea:focus {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadButton,
|
||||||
|
.fileSelected {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 48px;
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
background: rgba(3, 6, 16, 0.42);
|
||||||
|
border: 1px dashed rgba(216, 227, 251, 0.22);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadButton {
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
color var(--duration-fast) var(--ease-standard),
|
||||||
|
border-color var(--duration-fast) var(--ease-standard),
|
||||||
|
background-color var(--duration-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadButton:hover {
|
||||||
|
color: var(--color-black);
|
||||||
|
background: var(--color-primary);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadButton input {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
clip-path: inset(50%);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileSelected {
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-3);
|
||||||
|
border-style: solid;
|
||||||
|
border-color: rgba(143, 84, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileName {
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 14px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.removeFileButton {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmentedControl,
|
||||||
|
.styleChoices {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmentedControl,
|
||||||
|
.styleChoices {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmentedControl button,
|
||||||
|
.styleChoices button {
|
||||||
|
min-width: 0;
|
||||||
|
padding: var(--space-3);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
background: rgba(3, 6, 16, 0.42);
|
||||||
|
border: 1px solid rgba(216, 227, 251, 0.16);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transition:
|
||||||
|
color var(--duration-fast) var(--ease-standard),
|
||||||
|
border-color var(--duration-fast) var(--ease-standard),
|
||||||
|
background-color var(--duration-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmentedControl button:hover,
|
||||||
|
.styleChoices button:hover {
|
||||||
|
color: var(--color-text);
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmentedControl .selected,
|
||||||
|
.styleChoices .styleSelected {
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-primary-soft);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.styleChoices button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.synthesis {
|
||||||
|
background: var(--color-primary);
|
||||||
|
box-shadow: 10px 0 0 var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.neon {
|
||||||
|
background: var(--color-accent);
|
||||||
|
box-shadow: 10px 0 0 var(--color-accent-warm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cinema {
|
||||||
|
background: var(--color-accent-gold);
|
||||||
|
box-shadow: 10px 0 0 var(--color-accent-warm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
padding: var(--space-3);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-rose-200);
|
||||||
|
background: rgba(190, 18, 60, 0.16);
|
||||||
|
border: 1px solid rgba(254, 205, 211, 0.36);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.generateButton {
|
||||||
|
min-height: 52px;
|
||||||
|
padding: var(--space-3) var(--space-5);
|
||||||
|
margin-top: auto;
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 900;
|
||||||
|
color: var(--color-black);
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--color-primary);
|
||||||
|
border: 1px solid var(--color-primary);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transition:
|
||||||
|
background-color var(--duration-fast) var(--ease-standard),
|
||||||
|
border-color var(--duration-fast) var(--ease-standard),
|
||||||
|
transform var(--duration-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.generateButton:hover:not(:disabled) {
|
||||||
|
background: var(--color-primary-hover);
|
||||||
|
border-color: var(--color-primary-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.generateButton:disabled {
|
||||||
|
cursor: wait;
|
||||||
|
opacity: 0.66;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewPanel {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewHeader,
|
||||||
|
.previewFooter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 56px;
|
||||||
|
padding-inline: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewHeader {
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewFooter {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusDot {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
background: var(--color-gray-500);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusDot[data-active="true"] {
|
||||||
|
background: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 5px var(--color-primary-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewArea {
|
||||||
|
display: grid;
|
||||||
|
min-height: 460px;
|
||||||
|
padding: var(--space-6);
|
||||||
|
overflow: auto;
|
||||||
|
place-items: center;
|
||||||
|
background:
|
||||||
|
linear-gradient(rgba(53, 216, 255, 0.04) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(53, 216, 255, 0.04) 1px, transparent 1px),
|
||||||
|
rgba(3, 6, 16, 0.3);
|
||||||
|
background-size: 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewCanvas {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
width: min(100%, 560px);
|
||||||
|
overflow: hidden;
|
||||||
|
place-items: center;
|
||||||
|
background: #0a1430;
|
||||||
|
border: 1px solid rgba(53, 216, 255, 0.34);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.square {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portrait {
|
||||||
|
width: min(80%, 430px);
|
||||||
|
aspect-ratio: 4 / 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wide {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referenceImage {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyState {
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyMark {
|
||||||
|
position: relative;
|
||||||
|
width: 74px;
|
||||||
|
height: 74px;
|
||||||
|
border: 2px solid var(--color-primary);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transform: rotate(8deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyMark::before,
|
||||||
|
.emptyMark::after {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyMark::before {
|
||||||
|
inset: 12px;
|
||||||
|
border: 2px solid var(--color-accent);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyMark::after {
|
||||||
|
right: -12px;
|
||||||
|
bottom: -12px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: var(--color-accent-warm);
|
||||||
|
border: 2px solid var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanLine {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 auto auto 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--color-primary);
|
||||||
|
box-shadow: 0 0 18px var(--color-primary);
|
||||||
|
animation: scan 1.8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scan {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateY(300px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.page {
|
||||||
|
padding: var(--space-4) var(--space-6) var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
padding-block: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
padding: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewArea {
|
||||||
|
min-height: 360px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.page {
|
||||||
|
padding-inline: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 118px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
gap: var(--space-5);
|
||||||
|
padding: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.styleChoices button {
|
||||||
|
padding-inline: var(--space-2);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewHeader,
|
||||||
|
.previewFooter {
|
||||||
|
padding-inline: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewArea {
|
||||||
|
min-height: 300px;
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,208 @@
|
|||||||
|
import { useEffect, useRef, useState, type ChangeEvent, type FormEvent } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import logo from "../../assets/common/HITGENERATOR-Logo.png";
|
||||||
|
import { useAppDispatch, useAppSelector } from "../../store/hooks";
|
||||||
|
import { clearRequestError, request } from "../../store/slices/slice";
|
||||||
|
import styles from "./Picture.module.css";
|
||||||
|
|
||||||
|
type AspectRatio = "1:1" | "4:5" | "16:9";
|
||||||
|
|
||||||
|
const aspectRatios: AspectRatio[] = ["1:1", "4:5", "16:9"];
|
||||||
|
|
||||||
export default function Picture() {
|
export default function Picture() {
|
||||||
return (
|
const navigate = useNavigate();
|
||||||
<main>
|
const dispatch = useAppDispatch();
|
||||||
<h1>Picture</h1>
|
const { loading, error: requestError } = useAppSelector((state) => state.request);
|
||||||
</main>
|
|
||||||
);
|
const [prompt, setPrompt] = useState("");
|
||||||
|
const [reference, setReference] = useState<File[]>([]);
|
||||||
|
const [aspectRatio, setAspectRatio] = useState<AspectRatio>("1:1");
|
||||||
|
const [formError, setFormError] = useState<string | null>(null);
|
||||||
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(clearRequestError());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
function handleReferenceChange(event: ChangeEvent<HTMLInputElement>) {
|
||||||
|
const nextFile = event.currentTarget.files?.[0] ?? null;
|
||||||
|
|
||||||
|
if (nextFile && !nextFile.type.startsWith("image/")) {
|
||||||
|
setFormError("Выберите файл изображения.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormError(null);
|
||||||
|
//updateReference(nextFile);
|
||||||
|
setIsSubmitted(false);
|
||||||
|
setReference(nextFile ? [...reference, nextFile] : []);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeReference(index: number) {
|
||||||
|
//updateReference(null);
|
||||||
|
setIsSubmitted(false);
|
||||||
|
setReference(reference.filter((_, i) => i !== index));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const text = prompt.trim();
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
setFormError("Добавьте описание картинки.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormError(null);
|
||||||
|
setIsSubmitted(false);
|
||||||
|
|
||||||
|
await dispatch(
|
||||||
|
request({
|
||||||
|
id: Date.now(),
|
||||||
|
text,
|
||||||
|
files: reference ? reference : [],
|
||||||
|
ratio: aspectRatio,
|
||||||
|
}),
|
||||||
|
).unwrap();
|
||||||
|
setIsSubmitted(true);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewClassName = [
|
||||||
|
styles.previewCanvas,
|
||||||
|
aspectRatio === "1:1"
|
||||||
|
? styles.square
|
||||||
|
: aspectRatio === "4:5"
|
||||||
|
? styles.portrait
|
||||||
|
: styles.wide,
|
||||||
|
].join(" ");
|
||||||
|
const status = loading ? "Генерируем" : isSubmitted ? "Запрос принят" : "Ожидание";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.page}>
|
||||||
|
<header className={styles.toolbar}>
|
||||||
|
<div className={styles.brand}>
|
||||||
|
<button
|
||||||
|
className={styles.backButton}
|
||||||
|
type="button"
|
||||||
|
aria-label="На главную"
|
||||||
|
title="На главную"
|
||||||
|
onClick={() => navigate("/")}
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">←</span>
|
||||||
|
</button>
|
||||||
|
<img className={styles.logo} src={logo} alt="ShitGenerator" />
|
||||||
|
</div>
|
||||||
|
<span className={styles.mode}>PICTURE</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className={styles.workspace}>
|
||||||
|
<form className={styles.controls} onSubmit={handleSubmit}>
|
||||||
|
<div className={styles.heading}>
|
||||||
|
<p className={styles.eyebrow}>IMAGE LAB</p>
|
||||||
|
<h1>Генерация картинки</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className={styles.field}>
|
||||||
|
<span className={styles.labelRow}>
|
||||||
|
<span>Описание</span>
|
||||||
|
<span>{prompt.length}/800</span>
|
||||||
|
</span>
|
||||||
|
<textarea
|
||||||
|
value={prompt}
|
||||||
|
maxLength={800}
|
||||||
|
placeholder="Опишите изображение"
|
||||||
|
onChange={(event) => {
|
||||||
|
setFormError(null);
|
||||||
|
setPrompt(event.target.value);
|
||||||
|
setIsSubmitted(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<section className={styles.field} aria-labelledby="reference-label">
|
||||||
|
<span className={styles.labelRow} id="reference-label">
|
||||||
|
<span>Референс</span>
|
||||||
|
<span>Необязательно</span>
|
||||||
|
</span>
|
||||||
|
{reference.length > 0 && (
|
||||||
|
reference.map((file, index) => (
|
||||||
|
<div className={styles.fileSelected} key={index}>
|
||||||
|
<span className={styles.fileName}>{file.name}</span>
|
||||||
|
<button
|
||||||
|
className={styles.removeFileButton}
|
||||||
|
type="button"
|
||||||
|
aria-label="Удалить референс"
|
||||||
|
title="Удалить референс"
|
||||||
|
onClick={() => removeReference(index)}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
<label className={styles.uploadButton}>
|
||||||
|
<input type="file" accept="image/*" onChange={handleReferenceChange} />
|
||||||
|
<span>Выбрать файл</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<fieldset className={styles.field}>
|
||||||
|
<span className={styles.labelRow}>Формат</span>
|
||||||
|
<div className={styles.segmentedControl}>
|
||||||
|
{aspectRatios.map((ratio) => (
|
||||||
|
<button
|
||||||
|
className={ratio === aspectRatio ? styles.selected : undefined}
|
||||||
|
type="button"
|
||||||
|
key={ratio}
|
||||||
|
aria-pressed={ratio === aspectRatio}
|
||||||
|
onClick={() => {
|
||||||
|
setAspectRatio(ratio);
|
||||||
|
setIsSubmitted(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ratio}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
{(formError || requestError) && (
|
||||||
|
<p className={styles.error} role="alert">
|
||||||
|
{formError || requestError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button className={styles.generateButton} type="submit" disabled={loading}>
|
||||||
|
{loading ? "Генерируем..." : "Сгенерировать"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<section className={styles.previewPanel} aria-label="Область результата">
|
||||||
|
<div className={styles.previewHeader}>
|
||||||
|
<span>{status}</span>
|
||||||
|
<span className={styles.statusDot} data-active={loading || isSubmitted} aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.previewArea}>
|
||||||
|
<div className={previewClassName}>
|
||||||
|
<div className={styles.emptyState}>
|
||||||
|
<span className={styles.emptyMark} aria-hidden="true" />
|
||||||
|
<span>{loading ? "Генерируем" : isSubmitted ? "Запрос принят" : "Ожидаемое изображение"}</span>
|
||||||
|
</div>
|
||||||
|
{loading && <span className={styles.scanLine} aria-hidden="true" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer className={styles.previewFooter}>
|
||||||
|
<span>{aspectRatio}</span>
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
:root {
|
:root {
|
||||||
color-scheme: light;
|
color-scheme: dark;
|
||||||
|
|
||||||
--font-family-base:
|
--font-family-base:
|
||||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||||
|
|||||||
Reference in new issue
Block a user