@ -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 { Fa Question } from 'react-icons/fa'
import { Fa Coffee, Fa Question } 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 ,
} )
}
}