Compare commits

...

6 Commits

@ -1,3 +1,5 @@
NOVLEAI_ENVIRONMENT=debug
NOVELAI_BACKEND_URL=http://localhost:6969
NOVELAI_MOCK_ENV=false
NOVELAI_RECAPTCHA_KEY=
NEXT_PUBLIC_IS_ISEKAI=false

1
.gitignore vendored

@ -29,6 +29,7 @@ yarn-error.log*
.env.development.local
.env.test.local
.env.production.local
.env.production
# vercel
.vercel

@ -1,3 +0,0 @@
{
"*.{js,jsx,ts,tsx}": "eslint --fix"
}

@ -1,3 +0,0 @@
{
"pre-commit": "npx lint-staged"
}

@ -0,0 +1,60 @@
// server.js
const express = require('express')
const next = require('next')
const { createProxyMiddleware } = require('http-proxy-middleware');
const devProxy = {
'/generate-stream': {
target: 'http://localhost:6969',
changeOrigin: true
},
'/start-generate-stream': {
target: 'http://localhost:6969',
changeOrigin: true
},
'/get-generate-stream-output': {
target: 'http://localhost:6969',
},
'/predict-tags': {
target: 'http://localhost:6969',
changeOrigin: true
},
'/task-info': {
target: 'http://localhost:6969',
changeOrigin: true,
ws: true
},
}
const port = parseInt(process.env.PORT, 10) || 3000
const dev = process.env.NODE_ENV !== 'production'
const app = next({
dev
})
const handle = app.getRequestHandler()
app.prepare()
.then(() => {
const server = express()
if (dev && devProxy) {
Object.keys(devProxy).forEach(function(context) {
server.use(context, createProxyMiddleware(devProxy[context]))
})
}
server.all('*', (req, res) => {
handle(req, res)
})
server.listen(port, err => {
if (err) {
throw err
}
console.log(`> Ready on http://localhost:${port}`)
})
})
.catch(err => {
console.log('An error occurred, unable to start the server')
console.log(err)
})

@ -52,6 +52,7 @@ const sharedConfig = {
NEXT_PUBLIC_PADDLE_GIFTKEY_TABLET_ID: process.env.NOVELAI_PADDLE_GIFTKEY_TABLET_ID,
NEXT_PUBLIC_PADDLE_GIFTKEY_SCROLL_ID: process.env.NOVELAI_PADDLE_GIFTKEY_SCROLL_ID,
NEXT_PUBLIC_PADDLE_GIFTKEY_OPUS_ID: process.env.NOVELAI_PADDLE_GIFTKEY_OPUS_ID,
NEXT_PUBLIC_IS_ISEKAI: process.env.NOVELAI_IS_ISEKAI,
},
productionBrowserSourceMaps: true,
generateBuildId: () => commithash.slice(0, 7) + '-' + process.env.NOVELAI_ENVIRONMENT,

@ -15,7 +15,7 @@
"scripts": {
"start": "concurrently -m 1 npm:start:clean npm:start:start",
"start:clean": "rimraf .next",
"start:start": "cross-env NODE_ENV=development NODE_OPTIONS='--max-old-space-size=16384' next dev",
"start:start": "cross-env NODE_ENV=development NODE_OPTIONS='--max-old-space-size=16384' node dev-server.js",
"build": "concurrently -m 1 npm:build:clean npm:build:build npm:build:export npm:build:postexport",
"build:clean": "rimraf -o build .next",
"build:build": "cross-env NODE_ENV=production NODE_OPTIONS='--max-old-space-size=8192' next build",
@ -39,11 +39,13 @@
"compromise": "^14.5.0",
"dayjs": "^1.11.5",
"diff-match-patch": "^1.0.5",
"express": "^4.18.2",
"fflate": "^0.7.4",
"file-saver": "^2.0.5",
"framer-motion": "^7.5.1",
"html-entities": "^2.3.3",
"html-to-image": "^1.10.8",
"http-proxy-middleware": "^2.0.6",
"idb": "^7.1.0",
"js-cookie": "^3.0.1",
"jszip": "^3.10.1",

4896
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

@ -54,7 +54,7 @@ function formatRequestError(error: string): string {
`${error}`.toLowerCase() === 'timeout' ||
`${error}`.toLowerCase().includes('timeout fetching')
) {
return 'Error: Timeout - Unable to reach NovelAI servers. Please wait for a moment and try again'
return 'Error: Timeout - Unable to reach Naifu servers. Please wait for a moment and try again'
}
if (`${error}`.toLowerCase().includes('worker timeout')) {
return `${error} - Please try again, or if this issue persists, \

@ -744,7 +744,7 @@ const help_tips = [
[
'Privacy',
'Encryption',
'AES-256 is used to encrypt your Stories before being sent to the NovelAI servers. No one can access them without your encryption key.',
'AES-256 is used to encrypt your Stories before being sent to the Naifu servers. No one can access them without your encryption key.',
],
*/
[
@ -969,7 +969,7 @@ const help_tips = [
[
'Settings',
'Default Storage Location',
'Choose to keep your Stories on your device, or upload them to the NovelAI servers. Local stories rely on your browser cache, which can be cleared on occasion. All Stories on the NovelAI servers are specially encrypted so only you can view them.',
'Choose to keep your Stories on your device, or upload them to the Naifu servers. Local stories rely on your browser cache, which can be cleared on occasion. All Stories on the Naifu servers are specially encrypted so only you can view them.',
],
[
'Settings',

@ -510,7 +510,7 @@ export class RemoteGenerationRequest implements IGenerationRequest {
onError({
status: 408,
message:
'Error: Timeout - Unable to reach NovelAI servers. Please wait for a moment and try again',
'Error: Timeout - Unable to reach Naifu servers. Please wait for a moment and try again',
})
}, 3600 * 1000)
@ -1133,6 +1133,8 @@ export class RemoteImageGenerationRequest {
onError: (err: { status: number; message: string }) => void,
onClose: () => void
): Promise<void> {
const MAX_RETRY_TIMES = 3;
const requestStartGeneration: RequestInit = {
mode: 'cors',
cache: 'no-store',
@ -1159,15 +1161,15 @@ export class RemoteImageGenerationRequest {
try {
let errorData = await bind.json()
onError({
status: bind.status ?? 500,
message: errorData.error,
})
} catch(err) {
} catch (err) {
onError({
status: bind.status ?? 500,
message: `Error: HTTP ${bind.status} - Unable to reach NovelAI servers. Please wait for a moment and try again`,
message: `Error: HTTP ${bind.status} - Unable to reach Naifu servers. Please wait for a moment and try again`,
})
}
return
@ -1175,71 +1177,156 @@ export class RemoteImageGenerationRequest {
let res = await bind.json()
const taskId = res.task_id
const minDelay = 2000;
while (true) {
let reqStartTime = new Date().getTime();
const requestGetTask: RequestInit = {
mode: 'cors',
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + this.user.auth_token,
},
method: 'POST',
body: JSON.stringify({
task_id: taskId
}),
const handleTaskInfoUpdate = (taskInfo: any): boolean => {
let progress = 0;
if (taskInfo.status === "finished") {
onProgress(100, 0)
return true
} else if (taskInfo.status === "error") {
throw new Error("Remote error")
} else if (taskInfo.status === "running") {
if (typeof taskInfo.current_step === "number" && typeof taskInfo.total_steps === "number" && taskInfo.total_steps > 0) {
progress = Math.min(Math.round(taskInfo.current_step / taskInfo.total_steps * 100), 100)
onProgress(progress, 0)
}
} else if (taskInfo.status === "queued") {
onProgress(0, taskInfo.position)
}
return false
}
const bind = await fetchWithTimeout(BackendURLGetTaskInfo, requestGetTask)
if (!bind.ok) {
logError(bind, false)
if ('WebSocket' in window) {
try {
await new Promise<void>((resolve, reject) => {
const wsUrl = new URL(BackendURLGetTaskInfo, location.href.replace(/^http/, 'ws'))
wsUrl.search = '?task_id=' + encodeURIComponent(taskId)
const wsConnect = () => {
let ws: WebSocket | undefined = undefined;
let willClose = false;
let hasError = false;
ws = new WebSocket(wsUrl.href)
let heartbeatTimer: NodeJS.Timer | undefined = setInterval(() => {
if (ws.readyState === ws.OPEN) {
ws.send('ping')
}
}, 15000)
ws.addEventListener('error', () => {
hasError = true
// reconnect on error
sleep(2000).then(() => {
wsConnect()
})
})
ws.addEventListener('close', () => {
if (heartbeatTimer) {
clearInterval(heartbeatTimer)
heartbeatTimer = undefined
}
if (!willClose && !hasError) {
// reconnect on abnormal disconnect
sleep(2000).then(() => {
wsConnect()
})
}
})
ws.addEventListener('message', (event) => {
if (!event.data) return
try {
let data = JSON.parse(event.data)
if (data.error && data.error === 'ERR::TASK_NOT_FOUND') {
onError({ status: 500, message: 'Task not found, maybe lost.' })
willClose = true
ws.close()
resolve()
return
}
if (handleTaskInfoUpdate(data)) {
willClose = true
ws.close()
resolve()
return
}
} catch(err) {
}
})
}
wsConnect()
})
} catch(err) {
if (err.message === "Remote error") {
onError({ status: 500, message: 'Internal server error occurred. Please try again later' })
return
} else {
throw err;
}
}
} else {
const minDelay = 2000;
while (true) {
let reqStartTime = new Date().getTime();
const requestGetTask: RequestInit = {
mode: 'cors',
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + this.user.auth_token,
},
method: 'POST',
body: JSON.stringify({
task_id: taskId
}),
}
let res: any = {};
try {
let errorData = await bind.json()
onError({
status: bind.status ?? 500,
message: errorData.error,
})
return
} catch(err) {
const bind = await fetchWithTimeout(BackendURLGetTaskInfo, requestGetTask)
if (!bind.ok) {
logError(bind, false)
let errorData = await bind.json()
if (errorData.code === 'ERR::TASK_NOT_FOUND') {
onError({ status: 500, message: 'Task not found, maybe lost.' })
} else {
onError({
status: bind.status ?? 500,
message: errorData.error,
})
}
return
}
res = await bind.json()
} catch (err) {
// don't stop generate when http error
}
}
let res = await bind.json()
// console.log('task info', res)
let progress = 0;
if (res.status === "finished") {
onProgress(100, 0)
break;
} else if (res.status === "error") {
break;
} else if (res.status === "running") {
if (typeof res.current_step === "number" && typeof res.total_steps === "number" && res.total_steps > 0) {
progress = Math.min(Math.round(res.current_step / res.total_steps * 100), 100)
onProgress(progress, 0)
// console.log('task info', res)
try {
if (handleTaskInfoUpdate(res)) {
break
}
} catch(err) {
if (err.message === "Remote error") {
onError({ status: 500, message: 'Internal server error occurred. Please try again later' })
return
} else {
throw err;
}
}
} else if (res.status === "queued") {
onProgress(0, res.position)
}
let reqEndTime = new Date().getTime();
if (reqEndTime - reqStartTime < minDelay) {
await sleep(minDelay - (reqEndTime - reqStartTime))
let reqEndTime = new Date().getTime();
if (reqEndTime - reqStartTime < minDelay) {
await sleep(minDelay - (reqEndTime - reqStartTime))
}
}
}
const timeout = setTimeout(() => {
source.close()
onError({
status: 408,
message:
'Error: Timeout - Unable to reach NovelAI servers. Please wait for a moment and try again',
})
}, 30 * 1000)
const requestGetGenerationOutput: RequestInit = {
mode: 'cors',
cache: 'no-store',
@ -1258,28 +1345,50 @@ export class RemoteImageGenerationRequest {
}*/),
}
const source = new SSE(BackendURLGetGenerateImageOutput, {
headers: requestGetGenerationOutput.headers,
payload: requestGetGenerationOutput.body
})
source.addEventListener('newImage', (message: any) => {
clearTimeout(timeout)
onImage(Buffer.from(message.data, 'base64'), message.id)
})
source.addEventListener('error', (err: any) => {
clearTimeout(timeout)
source.close()
onError({
status: err.detail.statusCode ?? 'unknown status',
message: err.detail.message || err.detail.error,
let retryTimes = 0;
const requestOutput = () => {
const timeout = setTimeout(() => {
source.close()
onError({
status: 408,
message:
'Error: Timeout - Unable to reach Naifu servers. Please wait for a moment and try again',
})
}, 30 * 1000)
const source = new SSE(BackendURLGetGenerateImageOutput, {
headers: requestGetGenerationOutput.headers,
payload: requestGetGenerationOutput.body
})
logWarning(err, true, 'streaming error')
})
source.addEventListener('readystatechange', (e: any) => {
if (source.readyState === 2) {
onClose()
}
})
source.stream()
source.addEventListener('newImage', (message: any) => {
clearTimeout(timeout)
onImage(Buffer.from(message.data, 'base64'), message.id)
})
source.addEventListener('error', async (err: any) => {
clearTimeout(timeout)
source.close()
logWarning(err, true, 'streaming error')
if (retryTimes < MAX_RETRY_TIMES) { // Should retry
retryTimes ++
await sleep(2000)
requestOutput()
} else {
onError({
status: err.detail.statusCode ?? 'unknown status',
message: err.detail.message || err.detail.error,
})
}
})
source.addEventListener('readystatechange', (e: any) => {
if (source.readyState === 2) {
onClose()
}
})
source.stream()
}
requestOutput()
}
}

@ -1,3 +1,5 @@
export const IsIsekai = process.env.NEXT_PUBLIC_IS_ISEKAI === 'true'
export const Environment = process.env.NEXT_PUBLIC_ENVIRONMENT || 'debug'
export const CommitHash = process.env.NEXT_PUBLIC_COMMITHASH || 'unknown'

@ -153,12 +153,11 @@ function App(props: AppPropsWithErrAndLayout): JSX.Element {
<meta
name="description"
// eslint-disable-next-line max-len
content="GPT-powered AI Storyteller. Driven by AI, construct unique stories, thrilling tales, seductive romances, or just fool around. Anything goes!"
content="Naifu AI Image generator!"
/>
<meta
name="keywords"
content="ai, adventure, writing, novelai, novel ai, anlatan, ai dungeon, aidungeon, openai, \
nai, games, computer, videogames, text, textadventure, novel, kurumuz, latitude"
content="ai, ai image"
/>
<meta httpEquiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta httpEquiv="Pragma" content="no-cache" />

@ -1,6 +1,7 @@
import Document, { DocumentContext, DocumentInitialProps, Html, Head, Main, NextScript } from 'next/document'
import { Fragment } from 'react'
import { ServerStyleSheet } from 'styled-components'
import { IsIsekai } from '../globals/constants'
export default class CustomDocument extends Document {
static async getInitialProps(ctx: DocumentContext): Promise<DocumentInitialProps> {
@ -35,10 +36,18 @@ export default class CustomDocument extends Document {
<link rel="preconnect" href="https://fonts.gstatic.com" />
<meta name="referrer" content="no-referrer" />
{/* Remove resources which not exists
<link rel="icon" href="/icons/novelai-round.png" />
<link rel="apple-touch-icon" href="/icons/novelai-square.png" />
<link rel="mask-icon" href="/icons/pen-tip-light.svg" color="#ffffff" />
<link rel="manifest" href="/manifest.json" />
*/}
{IsIsekai && (
<>
<script id="LA_COLLECT" src="//sdk.51.la/js-sdk-pro.min.js"></script>
<script dangerouslySetInnerHTML={{ __html: 'LA.init({id: "JqlW3LkE9dgxYnN1",ck: "JqlW3LkE9dgxYnN1"})' }}></script>
</>
)}
</Head>
<body>
<Main />

@ -1,14 +1,14 @@
import Head from 'next/head'
import React, { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { cssTransition, toast, ToastContainer } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css';
import 'react-toastify/dist/ReactToastify.css'
import { useRecoilState, useRecoilValue } from 'recoil'
import styled from 'styled-components'
import { useRouter } from 'next/router'
import { AnimatePresence, motion } from 'framer-motion'
import { HexAlphaColorPicker, HexColorInput } from 'react-colorful'
import { MdFileUpload, MdHelpOutline } from 'react-icons/md'
import { FaQuestion } from 'react-icons/fa'
import { FaCoffee, FaQuestion } from 'react-icons/fa'
import TextareaAutosize from 'react-textarea-autosize'
import JSZip from 'jszip'
import { saveAs } from 'file-saver'
@ -64,9 +64,7 @@ import Checkbox from '../components/controls/checkbox'
import { getStorage } from '../data/storage/storage'
import { getUserSetting } from '../data/user/settings'
import { useReload } from '../hooks/useReload'
import {
BackendURLTagSearch,
} from '../globals/constants'
import { BackendURLTagSearch, IsIsekai } from '../globals/constants'
import Tooltip from '../components/tooltip'
import { WorkerInterface } from '../tokenizer/interface'
import { EncoderType } from '../tokenizer/encoder'
@ -98,7 +96,7 @@ export const ToastArea = styled.div`
--toastify-color-progress-light: ${(props) => props.theme.colors.textHeadings};
--toastify-text-color-light: ${(props) => props.theme.colors.textMain};
--toastify-font-family: ${(props) => props.theme.fonts.default};
`;
`
const MOBILE_WIDTH = 900
@ -144,7 +142,7 @@ const parsePrompt = (prompt: string[]): string => {
}
const maxSamplesForSize = (width: number, height: number, max?: number): number => {
let limit = 100;
const limit = 100
if (max) return Math.min(limit, max)
return limit
}
@ -462,29 +460,52 @@ function ImageGenContent(): JSX.Element {
const [params, actualSetParams] = useState<any>(
getModelDefaultParams(ImageGenerationModels.stableDiffusion)
)
const [initialized, setInitialized] = useState(false)
const [prompt, setPrompt] = useState([''] as string[])
const [promptLines, setPromptLines] = useState([1] as number[])
const [savedPrompt, setSavedPrompt] = useRememberedValue('imagegen-prompt', [''] as string[])
const [negPrompt, setNegPrompt] = useRememberedValue('imagegen-negativeprompt', '')
const [notiEnabled, setNotiEnabled] = useRememberedValue<boolean>('imagegen-enable-notification', false)
// Sync prompt and savedPrompt
useEffect(() => {
if (!initialized) {
setInitialized(true)
return // Ignore default value
}
if (prompt != savedPrompt) {
setSavedPrompt(prompt)
}
}, [prompt])
useEffect(() => {
if (savedPrompt != prompt) {
setPrompt(savedPrompt)
}
}, [savedPrompt])
// Queue remaining toast
const queueToastId = React.useRef<any>(null);
const queueToastId = React.useRef<any>(null)
useEffect(() => {
const msg = `Task is lining up, ${queuePos} task${queuePos > 1 ? 's' : ''} remaining ahead`;
if (queuePos === 0 && queueToastId.current) { // hide toast
const msg = `Task is lining up, ${queuePos} task${queuePos > 1 ? 's' : ''} remaining ahead`
if (queuePos === 0 && queueToastId.current) {
// hide toast
toast.dismiss(queueToastId.current)
queueToastId.current = null
} else if (queuePos > 0) {
if (queueToastId.current) { // update toast
if (queueToastId.current) {
// update toast
toast.update(queueToastId.current, {
render: msg
render: msg,
})
} else { // show toast
} else {
// show toast
queueToastId.current = toast.loading(msg, {
autoClose: false,
draggable: false,
closeButton: false,
toastId: 'queueToast',
position: toast.POSITION.TOP_CENTER
position: toast.POSITION.TOP_CENTER,
})
}
}
@ -540,8 +561,7 @@ function ImageGenContent(): JSX.Element {
const [purchaseModalOpen, setPurchaseModalOpen] = useState(false)
useEffect(() => {
}, [
useEffect(() => { }, [
params,
selectedModel,
session,
@ -654,9 +674,7 @@ function ImageGenContent(): JSX.Element {
fetch(
BackendURLTagSearch +
//`?model=${encodeURIComponent(selectedModel.toString())}&prompt=${encodeURIComponent(
`?prompt=${encodeURIComponent(
prompt.trim()
)}`,
`?prompt=${encodeURIComponent(prompt.trim())}`,
{
mode: 'cors',
cache: 'default',
@ -675,9 +693,12 @@ function ImageGenContent(): JSX.Element {
})
.catch((error) => {
logError(error)
toast.error(`Error: ${error.message} - Unable to reach NovelAI servers. Please wait for a moment and try again`, {
toastId: 'promptSuggesionError'
})
toast.error(
`Error: ${error.message} - Unable to reach Naifu servers. Please wait for a moment and try again`,
{
toastId: 'promptSuggesionError',
}
)
})
.finally(() => {
setSearchingTags(false)
@ -764,7 +785,11 @@ function ImageGenContent(): JSX.Element {
if (!prompt[lastFocusedPrompt].slice(cursorPosition)?.trim()) {
newPrompt += ', '
}
setPrompt([...prompt.slice(0, lastFocusedPrompt), newPrompt, ...prompt.slice(lastFocusedPrompt + 1)])
setPrompt([
...prompt.slice(0, lastFocusedPrompt),
newPrompt,
...prompt.slice(lastFocusedPrompt + 1),
])
if (promptid !== undefined) {
setTimeout(() => {
const input = document.querySelector(`#prompt-input-${promptid}`) as HTMLInputElement
@ -1034,7 +1059,7 @@ function ImageGenContent(): JSX.Element {
},
(error: any) => {
setGenerating(false)
toast.error(error.message);
toast.error(error.message)
},
() => {
setGenerating(false)
@ -1049,6 +1074,17 @@ function ImageGenContent(): JSX.Element {
setImages([newImages, ...images])
}
lastGenerationParams = paramString
if (notiEnabled) {
const notification = new Notification('Naifu', {
body: 'Image generation completed',
icon: newImages[0].url,
image: newImages[0].url,
})
notification.addEventListener('click', () => {
window.focus()
})
}
}
}
)
@ -1136,6 +1172,17 @@ function ImageGenContent(): JSX.Element {
setImages([newImages, ...images])
setRerollImageInfo(newImages[0])
lastGenerationParams = paramString
if (notiEnabled) {
const notification = new Notification('Naifu', {
body: 'Image generation completed',
icon: newImages[0].url,
image: newImages[0].url,
})
notification.addEventListener('click', () => {
window.focus()
})
}
}
resolve({ images: newImages, seeds: masks.map((m) => m.seed) })
}
@ -1381,9 +1428,7 @@ function ImageGenContent(): JSX.Element {
</InputLabel>
<EnhanceButton
disabled={
generating ||
prompt.some((p) => !p) ||
!validateParameters(params, selectedModel)
generating || prompt.some((p) => !p) || !validateParameters(params, selectedModel)
}
onClick={() => {
setEnhanceBoxVisible(false)
@ -1743,10 +1788,7 @@ function ImageGenContent(): JSX.Element {
<PenIcon style={{ width: 16, height: 16 }} />
</OverlayButton>
</Tooltip>
<Tooltip
tooltip={`Generate Variations`}
delay={0}
>
<Tooltip tooltip={`Generate Variations`} delay={0}>
<OverlayButton
disabled={generating}
style={{
@ -1845,7 +1887,7 @@ function ImageGenContent(): JSX.Element {
}, 0)
const dupeNames: any = {}
const loadingToastId = toast.loading(`Downloading ${imageCount} images...`, {
autoClose: false
autoClose: false,
})
images.forEach((image, i) => {
image.forEach((img, j) => {
@ -1962,18 +2004,43 @@ function ImageGenContent(): JSX.Element {
<HistoryIcon />
</OpenHistoryButton>
<FlexSpaceFull />
<ImportImageLink
href="//docs.novelai.net"
target="_blank"
<div
style={{
fontSize: '0.875rem',
opacity: 0.8,
width: 'max-content',
padding: '0 0 0 10px',
display: 'flex',
gap: '10px',
alignItems: 'center',
justifyContent: 'space-between'
}}
>
<FaQuestion />
</ImportImageLink>
{IsIsekai && (
<ImportImageLink
href="https://www.isekai.cn"
target="_blank"
style={{
fontSize: '0.875rem',
opacity: 0.8,
width: 'max-content',
padding: '0 0 0 10px',
}}
>
<FaCoffee />
&nbsp;
</ImportImageLink>
)}
<ImportImageLink
href="//docs.novelai.net"
target="_blank"
style={{
fontSize: '0.875rem',
opacity: 0.8,
width: 'max-content',
padding: '0 0 0 10px',
}}
>
<FaQuestion />
</ImportImageLink>
</div>
</HideMobileInline>
</MainTopperInnerRight>
</MainTopper>
@ -2596,6 +2663,8 @@ function ImageGenContent(): JSX.Element {
params={params}
setParams={setParams}
model={selectedModel}
notiEnabled={notiEnabled}
setNotiEnabled={setNotiEnabled}
initImage={initImage}
setInitImage={setInitImage}
negPrompt={negPrompt}
@ -2733,13 +2802,7 @@ function ImageGenContent(): JSX.Element {
}
}}
>
{!downloadingImages ? (
'Download ZIP'
) : (
<span>
Downloading...
</span>
)}
{!downloadingImages ? 'Download ZIP' : <span>Downloading...</span>}
</SubtleButton>
</HistoryBar>
</Sidebar>
@ -3234,6 +3297,8 @@ function GenerationOptions(props: {
params: any
setParams: React.Dispatch<React.SetStateAction<any>>
children?: JSX.Element | JSX.Element[]
notiEnabled: boolean
setNotiEnabled: (b: boolean) => void
initImage: Buffer | undefined
setInitImage: React.Dispatch<React.SetStateAction<Buffer | undefined>>
negPrompt: string
@ -3243,6 +3308,31 @@ function GenerationOptions(props: {
}): JSX.Element {
const [selectedResolution, setSelectedResolution] = useState(0)
const siteTheme = useRecoilValue(SiteTheme)
const [notiEnabled, setLocalNotiEnabled] = useState(false)
useEffect(() => {
setLocalNotiEnabled(props.notiEnabled)
}, [props.notiEnabled])
const setNotiEnabled = useCallback((b: boolean) => {
if (typeof window !== 'undefined') {
if (window.Notification.permission === 'denied') {
toast.warn("You've disabled Notification in browser settings.")
} else {
if (window.Notification.permission !== 'granted' && b) {
window.Notification.requestPermission().then((res) => {
if (res === 'granted') {
props.setNotiEnabled(true)
} else {
props.setNotiEnabled(false)
}
})
} else {
props.setNotiEnabled(b)
}
}
}
}, [])
useEffect(() => {
if (props.params.width && props.params.height) {
@ -3267,6 +3357,21 @@ function GenerationOptions(props: {
const stableDiffusionSettings = (
<>
<BorderBox>
<FlexRow>
<Title>Notification</Title>
</FlexRow>
<Checkbox
label={'Enable Notification'}
checkedText={'Notification will be displayed when image generation is completed.'}
uncheckedText={'Notification will be displayed when image generation is completed.'}
value={notiEnabled}
setValue={setNotiEnabled}
alternate
/>
</BorderBox>
<FlexColSpacer min={20} max={20} />
<BorderBox>
<FlexRow>
<Title>Image Resolution</Title>
@ -3611,7 +3716,6 @@ function GenerationOptions(props: {
maxRows={6}
placeholder="Anything in here is added to the preset selected above."
value={props.negPrompt ?? ''}
warn={props.negPromptTokens > SD_TOKEN_LIMIT}
onChange={(e) => {
props.setNegPrompt((e.target.value || '').replace(/(\n|\r)/g, ''))
}}
@ -3850,7 +3954,7 @@ const GenerateButton = styled.button`
overflow: hidden;
cursor: pointer;
font-weight: 700;
flex: .1 0 auto;
flex: 0.1 0 auto;
border: 1px solid ${(props) => props.theme.colors.textHeadings};
&:disabled {
color: ${(props) => props.theme.colors.textHeadings};
@ -4885,24 +4989,27 @@ function Canvas(props: {
const [queuePos, setQueuePos] = useState(0)
// Queue remaining toast
const queueToastId = React.useRef<any>(null);
const queueToastId = React.useRef<any>(null)
useEffect(() => {
const msg = `Task is lining up, ${queuePos} task${queuePos > 1 ? 's' : ''} remaining ahead`;
if (queuePos === 0 && queueToastId.current) { // hide toast
const msg = `Task is lining up, ${queuePos} task${queuePos > 1 ? 's' : ''} remaining ahead`
if (queuePos === 0 && queueToastId.current) {
// hide toast
toast.dismiss(queueToastId.current)
queueToastId.current = null
} else if (queuePos > 0) {
if (queueToastId.current) { // update toast
if (queueToastId.current) {
// update toast
toast.update(queueToastId.current, {
render: msg
render: msg,
})
} else { // show toast
} else {
// show toast
queueToastId.current = toast.loading(msg, {
autoClose: false,
draggable: false,
closeButton: false,
toastId: 'queueToast',
position: toast.POSITION.TOP_CENTER
position: toast.POSITION.TOP_CENTER,
})
}
}

@ -78,5 +78,5 @@ export const LoadingBarInner = styled.div<{ visible: boolean }>`
background-repeat: repeat-x;
animation: ${Gradient} 2s linear infinite;
opacity: ${(props) => (props.visible ? '1' : '0')};
transition: width 250ms ease-in-out;
transition: width 250ms linear;
`
Loading…
Cancel
Save