增加生成进度功能

master
落雨楓 2 years ago
parent 41cb391fcd
commit 91417f0408

@ -0,0 +1 @@
NOVELAI_BACKEND_URL=https://imgen.hyperz.top

@ -1,3 +1,3 @@
defaults.url=https://sentry.io/ defaults.url=https://sentry.io/
defaults.org=anlatan defaults.org=naifu
defaults.project=novelai defaults.project=naifu

@ -19,7 +19,7 @@ import {
import { GlobalUserContext } from '../../globals/globals' import { GlobalUserContext } from '../../globals/globals'
import { DataOrigin, StoryMode } from '../../data/story/story' import { DataOrigin, StoryMode } from '../../data/story/story'
import { StoryContent, StoryMetadata } from '../../data/story/storycontainer' import { StoryContent, StoryMetadata } from '../../data/story/storycontainer'
import { ConversationContainer, ConversationMain, LoadingBar } from '../../styles/components/conversation' import { ConversationContainer, ConversationMain } from '../../styles/components/conversation'
import { buildEphemeralContext } from '../../data/ai/ephemeralcontext' import { buildEphemeralContext } from '../../data/ai/ephemeralcontext'
import Book from '../../assets/images/book-open.svg' import Book from '../../assets/images/book-open.svg'
import { Dark } from '../../styles/themes/dark' import { Dark } from '../../styles/themes/dark'
@ -46,6 +46,7 @@ import { activeNodes, SchemaNode, SchemaNodes } from './schema'
import { ConversationControls } from './conversationcontrols' import { ConversationControls } from './conversationcontrols'
import ConversationEditor, { getEditorText } from './conversationeditor' import ConversationEditor, { getEditorText } from './conversationeditor'
import ConversationTitle from './conversationtitle' import ConversationTitle from './conversationtitle'
import { LoadingBar } from '../loading'
const Emphasis = styled.span` const Emphasis = styled.span`
color: ${(props) => props.theme.colors.textHeadings}; color: ${(props) => props.theme.colors.textHeadings};

@ -21,7 +21,6 @@ import { GenerateError, GenerateErrorType, RequestWrapper, useGenerate } from '.
import { useLogout } from '../../hooks/useLogout' import { useLogout } from '../../hooks/useLogout'
import { EventHandler } from '../../data/event/eventhandling' import { EventHandler } from '../../data/event/eventhandling'
import { LogProbs } from '../../data/request/remoterequest' import { LogProbs } from '../../data/request/remoterequest'
import { LoadingBar } from '../../styles/components/conversation'
import CommentDisplay from '../comment' import CommentDisplay from '../comment'
import { getUserSetting, UserSettings } from '../../data/user/settings' import { getUserSetting, UserSettings } from '../../data/user/settings'
import { trimBrokenUnicode } from '../../data/ai/processresponse' import { trimBrokenUnicode } from '../../data/ai/processresponse'
@ -38,6 +37,7 @@ import { EditorToolbox, EditorToolboxHandle } from './toolbox'
import { createEditorEvent, EditorEvent, EditorEventType } from './events' import { createEditorEvent, EditorEvent, EditorEventType } from './events'
import { EditorMenu, EditorMenuHandle } from './menu' import { EditorMenu, EditorMenuHandle } from './menu'
import { schema } from './schema' import { schema } from './schema'
import { LoadingBar } from '../loading'
interface EditorContainerProps { interface EditorContainerProps {
story: StoryContent story: StoryContent

@ -1,4 +1,10 @@
import { HTMLProps } from 'react'
import { useRecoilValue } from 'recoil'
import styled from 'styled-components' import styled from 'styled-components'
import { UserSettings } from '../data/user/settings'
import { SessionValue, SiteTheme } from '../globals/state'
import { Gradient } from '../styles/animations'
import { LoadingBarInnerAnimate, LoadingBarInner } from '../styles/components/conversation'
import { LoginBackground } from '../styles/components/login' import { LoginBackground } from '../styles/components/login'
import Spinner from './spinner' import Spinner from './spinner'
@ -14,3 +20,27 @@ export default function Loading(): JSX.Element {
</LoginBackground> </LoginBackground>
) )
} }
export const LoadingBar: React.FC<{visible: boolean; progress: number, className?: string} & HTMLProps<any>> = ({
visible,
progress,
style = {},
...props
}) => {
return (
<div
style={{
height: '1px',
position: 'relative',
...style
}}
{...props}
>
{progress === 0 ? (
<LoadingBarInnerAnimate visible={visible} />
) : (
<LoadingBarInner visible={visible} style={{ width: progress + '%' }} />
)}
</div>
)
}

@ -66,7 +66,6 @@ import Tooltip from '../../tooltip'
import { getDropdownStyle, getDropdownTheme, Select } from '../../controls/select' import { getDropdownStyle, getDropdownTheme, Select } from '../../controls/select'
import { getGenerationRequest } from '../../../data/request/request' import { getGenerationRequest } from '../../../data/request/request'
import { trimResponse } from '../../../data/ai/processresponse' import { trimResponse } from '../../../data/ai/processresponse'
import { LoadingBar } from '../../../styles/components/conversation'
import Modal, { ModalType } from '../../modals/modal' import Modal, { ModalType } from '../../modals/modal'
import { TutorialStates } from '../../tutorial' import { TutorialStates } from '../../tutorial'
import { getModelEncoderType } from '../../../tokenizer/encoder' import { getModelEncoderType } from '../../../tokenizer/encoder'
@ -86,6 +85,7 @@ import {
import { EditorCardHint } from '../../../styles/ui/editorcard' import { EditorCardHint } from '../../../styles/ui/editorcard'
import { SettingsPages } from '../../settings/constants' import { SettingsPages } from '../../settings/constants'
import { getUserSetting } from '../../../data/user/settings' import { getUserSetting } from '../../../data/user/settings'
import { LoadingBar } from '../../loading'
const placeholders = new Map<string, string>([ const placeholders = new Map<string, string>([
['general', 'an ancient ruin, a punk band, The Vindicator, Morl (insect), Nia (diety, forge)...'], ['general', 'an ancient ruin, a punk band, The Vindicator, Morl (insect), Nia (diety, forge)...'],

@ -1,5 +1,5 @@
import { Buffer } from 'buffer' import { Buffer } from 'buffer'
import { SSE } from '../../lib/sse.js'; import { SSE } from '../../lib/sse';
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { import {
BackendURLSubscriptions, BackendURLSubscriptions,
@ -18,6 +18,9 @@ import {
BackendURLUserData, BackendURLUserData,
BackendURLVerifyDeleteAccount, BackendURLVerifyDeleteAccount,
BackendURLGenerateImage, BackendURLGenerateImage,
BackendURLGetTaskInfo,
BackendURLStartGenerateImage,
BackendURLGetGenerateImageOutput,
} from '../../globals/constants' } from '../../globals/constants'
import { logError, logWarning } from '../../util/browser' import { logError, logWarning } from '../../util/browser'
import { fetchWithTimeout } from '../../util/general' import { fetchWithTimeout } from '../../util/general'
@ -50,7 +53,7 @@ import {
prepareStopSequences, prepareStopSequences,
} from '../../util/generationrequest' } from '../../util/generationrequest'
import { getAvailiableModels, modelsCompatible } from '../../util/models' import { getAvailiableModels, modelsCompatible } from '../../util/models'
import { isAdventureModeStory, formatErrorResponse } from '../../util/util' import { isAdventureModeStory, formatErrorResponse, sleep } from '../../util/util'
import { DarkOld } from '../../styles/themes/darkold' import { DarkOld } from '../../styles/themes/darkold'
import { themeEquivalent } from '../../styles/themes/theme' import { themeEquivalent } from '../../styles/themes/theme'
import { Dark } from '../../styles/themes/dark' import { Dark } from '../../styles/themes/dark'
@ -227,9 +230,9 @@ export class RemoteGenerationRequest implements IGenerationRequest {
headers: this.user.noAccount headers: this.user.noAccount
? { 'Content-Type': 'application/json' } ? { 'Content-Type': 'application/json' }
: { : {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: 'Bearer ' + this.user.auth_token, Authorization: 'Bearer ' + this.user.auth_token,
}, },
method: 'POST', method: 'POST',
} }
// Copy params // Copy params
@ -1078,6 +1081,32 @@ export class RemoteDeleteAccountRequest {
} }
} }
export class RemoteGetTaskInfoRequest {
taskId: string
constructor(taskId: string) {
this.taskId = taskId
}
async request(): Promise<void> {
const request: RequestInit = {
mode: 'cors',
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify({
task_id: this.taskId,
}),
}
const bind = await fetchWithTimeout(BackendURLGetTaskInfo, request)
if (!bind.ok) {
logError(bind, false)
throw await formatErrorResponse(bind, false)
}
}
}
export class RemoteImageGenerationRequest { export class RemoteImageGenerationRequest {
user: User user: User
input: string input: string
@ -1099,11 +1128,12 @@ export class RemoteImageGenerationRequest {
this.parameters = parameters this.parameters = parameters
} }
async requestStream( async requestStream(
onProgress: (progress: number, queuePos: number) => void,
onImage: (img: Buffer, id: string) => void, onImage: (img: Buffer, id: string) => void,
onError: (err: { status: number; message: string }) => void, onError: (err: { status: number; message: string }) => void,
onClose: () => void onClose: () => void
): Promise<void> { ): Promise<void> {
const request: RequestInit = { const requestStartGeneration: RequestInit = {
mode: 'cors', mode: 'cors',
cache: 'no-store', cache: 'no-store',
headers: { headers: {
@ -1123,6 +1153,87 @@ export class RemoteImageGenerationRequest {
}*/), }*/),
} }
const bind = await fetchWithTimeout(BackendURLStartGenerateImage, requestStartGeneration)
if (!bind.ok) {
logError(bind, false)
try {
let errorData = await bind.json()
onError({
status: bind.status ?? 500,
message: errorData.error,
})
} 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`,
})
}
return
}
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 bind = await fetchWithTimeout(BackendURLGetTaskInfo, requestGetTask)
if (!bind.ok) {
logError(bind, false)
try {
let errorData = await bind.json()
onError({
status: bind.status ?? 500,
message: errorData.error,
})
} 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`,
})
}
return
}
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)
}
} else if (res.status === "queued") {
onProgress(0, res.position)
}
let reqEndTime = new Date().getTime();
if (reqEndTime - reqStartTime < minDelay) {
await sleep(minDelay - (reqEndTime - reqStartTime))
}
}
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
source.close() source.close()
onError({ onError({
@ -1132,7 +1243,28 @@ export class RemoteImageGenerationRequest {
}) })
}, 3600 * 1000) }, 3600 * 1000)
const source = new SSE(BackendURLGenerateImage, { headers: request.headers, payload: request.body }) const requestGetGenerationOutput: 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
}
/*{
input: this.input,
model: this.model,
parameters: this.parameters,
}*/),
}
const source = new SSE(BackendURLGetGenerateImageOutput, {
headers: requestGetGenerationOutput.headers,
payload: requestGetGenerationOutput.body
})
source.addEventListener('newImage', (message: any) => { source.addEventListener('newImage', (message: any) => {
clearTimeout(timeout) clearTimeout(timeout)
onImage(Buffer.from(message.data, 'base64'), message.id) onImage(Buffer.from(message.data, 'base64'), message.id)

@ -197,6 +197,7 @@ export interface IImageGenerationRequest {
model: ImageGenerationModels model: ImageGenerationModels
parameters: StableDiffusionParameters | DalleMiniParameters parameters: StableDiffusionParameters | DalleMiniParameters
requestStream( requestStream(
onProgress: (progress: number, queuePos: number) => void,
onImage: (image: Buffer, id: string) => Promise<void>, onImage: (image: Buffer, id: string) => Promise<void>,
onError: (err: { status: number; message: string }) => void, onError: (err: { status: number; message: string }) => void,
onClose: () => void onClose: () => void

@ -45,6 +45,11 @@ export const BackendURLGenerate = BackendURL + '/ai/generate'
export const BackendURLGenerateStream = BackendURL + '/generate-stream' export const BackendURLGenerateStream = BackendURL + '/generate-stream'
export const BackendURLGenerateImage = BackendURL + '/generate-stream' export const BackendURLGenerateImage = BackendURL + '/generate-stream'
export const BackendURLGenerateImagePrice = BackendURL + '/ai/generate-image/request-price' export const BackendURLGenerateImagePrice = BackendURL + '/ai/generate-image/request-price'
export const BackendURLStartGenerateStream = BackendURL + '/start-generate-stream'
export const BackendURLStartGenerateImage = BackendURL + '/start-generate-stream'
export const BackendURLGetGenerateStreamOutput = BackendURL + '/get-generate-stream-output'
export const BackendURLGetGenerateImageOutput = BackendURL + '/get-generate-stream-output'
export const BackendURLGetTaskInfo = BackendURL + '/task-info'
export const BackendURLRecoveryInitiation = BackendURL + '/user/recovery/request' export const BackendURLRecoveryInitiation = BackendURL + '/user/recovery/request'
export const BackendURLRecoverySubmit = BackendURL + '/user/recovery/recover' export const BackendURLRecoverySubmit = BackendURL + '/user/recovery/recover'
export const BackendURLPrefixSubmit = BackendURL + '/ai/module/train' export const BackendURLPrefixSubmit = BackendURL + '/ai/module/train'

@ -0,0 +1,9 @@
import { useEffect, useRef } from 'react'
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<typeof value>()
useEffect(() => {
ref.current = value
})
return ref.current
}

@ -0,0 +1,224 @@
/**
* Copyright (C) 2016 Maxime Petazzoni <maxime.petazzoni@bulix.org>.
* All rights reserved.
*/
export type SSEOptions = Partial<{
headers: Record<string, any>;
payload: any;
method: string;
withCredentials: boolean;
}>;
export type SSECallback = (e: any) => any;
export class SSE {
public INITIALIZING = -1;
public CONNECTING = 0;
public OPEN = 1;
public CLOSED = 2;
public url: string;
public headers: Record<string, any>;
public payload: any;
public method: string;
public withCredentials: boolean;
public FIELD_SEPARATOR = ':';
public listeners: Record<string, SSECallback[]> = {};
public xhr?: XMLHttpRequest;
public readyState: number = this.INITIALIZING;
public progress: number = 0;
public chunk: string = '';
constructor(url: string, options: SSEOptions = {}) {
this.url = url;
this.headers = options.headers || {};
this.payload = options.payload !== undefined ? options.payload : '';
this.method = options.method || (this.payload && 'POST' || 'GET');
this.withCredentials = !!options.withCredentials;
}
public addEventListener(type: string, listener: SSECallback) {
if (this.listeners[type] === undefined) {
this.listeners[type] = [];
}
if (this.listeners[type].indexOf(listener) === -1) {
this.listeners[type].push(listener);
}
}
public removeEventListener(type: string, listener: SSECallback) {
if (this.listeners[type] === undefined) {
return;
}
var filtered: SSECallback[] = [];
this.listeners[type].forEach(function (element) {
if (element !== listener) {
filtered.push(element);
}
});
if (filtered.length === 0) {
delete this.listeners[type];
} else {
this.listeners[type] = filtered;
}
}
public dispatchEvent(e: any | null) {
if (!e) {
return true;
}
e.source = this;
var onHandler = 'on' + e.type;
if (this.hasOwnProperty(onHandler)) {
(this as any)[onHandler].call(this, e);
if (e.defaultPrevented) {
return false;
}
}
if (this.listeners[e.type]) {
return this.listeners[e.type].every(function (callback) {
callback(e);
return !e.defaultPrevented;
});
}
return true;
}
private _setReadyState(state: number) {
var event: any = new CustomEvent('readystatechange');
event.readyState = state;
this.readyState = state;
this.dispatchEvent(event);
};
private _onStreamFailure(e: ProgressEvent<XMLHttpRequestEventTarget>) {
var event: any = new CustomEvent('error');
event.data = (e.currentTarget as any)?.response;
this.dispatchEvent(event);
this.close();
}
private _onStreamAbort(e: ProgressEvent<XMLHttpRequestEventTarget>) {
this.dispatchEvent(new CustomEvent('abort'));
this.close();
}
private _onStreamProgress(e: ProgressEvent<XMLHttpRequestEventTarget>) {
if (!this.xhr) {
return;
}
if (this.xhr.status !== 200) {
this._onStreamFailure(e);
return;
}
if (this.readyState == this.CONNECTING) {
this.dispatchEvent(new CustomEvent('open'));
this._setReadyState(this.OPEN);
}
var data = this.xhr.responseText.substring(this.progress);
this.progress += data.length;
data.split(/(\r\n|\r|\n){2}/g).forEach((part) => {
if (part.trim().length === 0) {
this.dispatchEvent(this._parseEventChunk(this.chunk.trim()));
this.chunk = '';
} else {
this.chunk += part;
}
});
}
private _onStreamLoaded(e: ProgressEvent<XMLHttpRequestEventTarget>) {
this._onStreamProgress(e);
// Parse the last chunk.
this.dispatchEvent(this._parseEventChunk(this.chunk));
this.chunk = '';
};
/**
* Parse a received SSE event chunk into a constructed event object.
*/
private _parseEventChunk(chunk: string | null) {
if (!chunk || chunk.length === 0) {
return null;
}
var e = { 'id': null, 'retry': null, 'data': '', 'event': 'message' };
chunk.split(/\n|\r\n|\r/).forEach((line) => {
line = line.trimEnd();
var index = line.indexOf(this.FIELD_SEPARATOR);
if (index <= 0) {
// Line was either empty, or started with a separator and is a comment.
// Either way, ignore.
return;
}
var field = line.substring(0, index);
if (!(field in e)) {
return;
}
var value = line.substring(index + 1).trimLeft();
if (field === 'data') {
e[field] += value;
} else {
(e as any)[field] = value;
}
});
var event: any = new CustomEvent(e.event);
event.data = e.data;
event.id = e.id;
return event;
}
private _checkStreamClosed () {
if (!this.xhr) {
return;
}
if (this.xhr.readyState === XMLHttpRequest.DONE) {
this._setReadyState(this.CLOSED);
}
}
public stream() {
this._setReadyState(this.CONNECTING);
this.xhr = new XMLHttpRequest();
this.xhr.addEventListener('progress', this._onStreamProgress.bind(this));
this.xhr.addEventListener('load', this._onStreamLoaded.bind(this));
this.xhr.addEventListener('readystatechange', this._checkStreamClosed.bind(this));
this.xhr.addEventListener('error', this._onStreamFailure.bind(this));
this.xhr.addEventListener('abort', this._onStreamAbort.bind(this));
this.xhr.open(this.method, this.url);
for (var header in this.headers) {
this.xhr.setRequestHeader(header, this.headers[header]);
}
this.xhr.withCredentials = this.withCredentials;
this.xhr.send(this.payload);
};
public close() {
if (this.readyState === this.CLOSED) {
return;
}
this.xhr?.abort();
this.xhr = undefined;
this._setReadyState(this.CLOSED);
};
}

@ -1,6 +1,7 @@
import Head from 'next/head' import Head from 'next/head'
import React, { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import React, { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { cssTransition, toast, ToastContainer } from 'react-toastify' import { cssTransition, toast, ToastContainer } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css';
import { useRecoilState, useRecoilValue } from 'recoil' import { useRecoilState, useRecoilValue } from 'recoil'
import styled from 'styled-components' import styled from 'styled-components'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
@ -18,7 +19,6 @@ import {
StableDiffusionSampler, StableDiffusionSampler,
} from '../data/request/request' } from '../data/request/request'
import { LightColorButton, SubtleButton } from '../styles/ui/button' import { LightColorButton, SubtleButton } from '../styles/ui/button'
import { LoadingBar } from '../styles/components/conversation'
import { import {
ArrowDownIcon, ArrowDownIcon,
ArrowLeftIcon, ArrowLeftIcon,
@ -53,7 +53,7 @@ import {
copyPngToClipboard, copyPngToClipboard,
copyToClipboard, copyToClipboard,
} from '../components/sidebars/infobar/items/storyexporter' } from '../components/sidebars/infobar/items/storyexporter'
import { LoadingSpinner } from '../components/loading' import { LoadingBar, LoadingSpinner } from '../components/loading'
import { CloseButton } from '../components/modals/common' import { CloseButton } from '../components/modals/common'
import useRememberedValue from '../hooks/useRememberedValue' import useRememberedValue from '../hooks/useRememberedValue'
import Modal, { ModalType } from '../components/modals/modal' import Modal, { ModalType } from '../components/modals/modal'
@ -455,6 +455,7 @@ function ImageGenContent(): JSX.Element {
}, [images.length]) }, [images.length])
const [generating, setGenerating] = useState(false) const [generating, setGenerating] = useState(false)
const [progress, setProgress] = useState(0) const [progress, setProgress] = useState(0)
const [queuePos, setQueuePos] = useState<number>(0)
const [selectedImage, setSelectedImage] = useState(0) const [selectedImage, setSelectedImage] = useState(0)
const [session, setSession] = useRecoilState(Session) const [session, setSession] = useRecoilState(Session)
@ -465,6 +466,30 @@ function ImageGenContent(): JSX.Element {
const [promptLines, setPromptLines] = useState([1] as number[]) const [promptLines, setPromptLines] = useState([1] as number[])
const [negPrompt, setNegPrompt] = useRememberedValue('imagegen-negativeprompt', '') const [negPrompt, setNegPrompt] = useRememberedValue('imagegen-negativeprompt', '')
// Queue remaining toast
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
toast.dismiss(queueToastId.current)
queueToastId.current = null
} else if (queuePos > 0) {
if (queueToastId.current) { // update toast
toast.update(queueToastId.current, {
render: msg
})
} else { // show toast
queueToastId.current = toast.loading(msg, {
autoClose: false,
draggable: false,
closeButton: false,
toastId: 'queueToast',
position: toast.POSITION.TOP_CENTER
})
}
}
}, [queuePos])
const didMessageNoAccount = useRef(false) const didMessageNoAccount = useRef(false)
useEffect(() => { useEffect(() => {
if (session.noAccount && !didMessageNoAccount.current) { if (session.noAccount && !didMessageNoAccount.current) {
@ -839,12 +864,12 @@ function ImageGenContent(): JSX.Element {
const chars = [...'🕛🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚'] const chars = [...'🕛🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚']
let i = 0 let i = 0
const update = () => { const update = () => {
document.title = chars[i] + ' ' + title.current document.title = chars[i] + ' ' + title.current
i = (i + 1) % chars.length i = (i + 1) % chars.length
} }
clearInterval(spinnerRef.current) clearInterval(spinnerRef.current)
if (generating) { if (generating) {
spinnerRef.current = setInterval(update, 25) as unknown as number spinnerRef.current = setInterval(update, 100) as unknown as number
} else { } else {
document.title = visible.current ? title.current : '✅ ' + title.current document.title = visible.current ? title.current : '✅ ' + title.current
} }
@ -984,6 +1009,10 @@ function ImageGenContent(): JSX.Element {
setProgress(0) setProgress(0)
const newImages: ImageInfo[] = [] const newImages: ImageInfo[] = []
request.requestStream( request.requestStream(
(progress: number, queuePos: number) => {
setProgress(progress)
setQueuePos(queuePos)
},
async (buffer: Buffer, id: string) => { async (buffer: Buffer, id: string) => {
const numId = Number.parseInt(id, 0) - 1 const numId = Number.parseInt(id, 0) - 1
const url = URL.createObjectURL(new Blob([buffer], { type: 'image/jpeg' })) const url = URL.createObjectURL(new Blob([buffer], { type: 'image/jpeg' }))
@ -1075,6 +1104,10 @@ function ImageGenContent(): JSX.Element {
setProgress(0) setProgress(0)
const newImages: ImageInfo[] = [] const newImages: ImageInfo[] = []
request.requestStream( request.requestStream(
(progress: number, queuePos: number) => {
setProgress(progress)
setQueuePos(queuePos)
},
async (buffer: Buffer, id: string) => { async (buffer: Buffer, id: string) => {
const numId = Number.parseInt(id, 0) - 1 const numId = Number.parseInt(id, 0) - 1
const url = URL.createObjectURL(new Blob([buffer], { type: 'image/jpeg' })) const url = URL.createObjectURL(new Blob([buffer], { type: 'image/jpeg' }))
@ -4845,6 +4878,31 @@ function Canvas(props: {
const [generating, setGenerating] = useState(false) const [generating, setGenerating] = useState(false)
const [progress, setProgress] = useState<number>(0) const [progress, setProgress] = useState<number>(0)
const [queuePos, setQueuePos] = useState<number>(0)
// Queue remaining toast
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
toast.dismiss(queueToastId.current)
queueToastId.current = null
} else if (queuePos > 0) {
if (queueToastId.current) { // update toast
toast.update(queueToastId.current, {
render: msg
})
} else { // show toast
queueToastId.current = toast.loading(msg, {
autoClose: false,
draggable: false,
closeButton: false,
toastId: 'queueToast',
position: toast.POSITION.TOP_CENTER
})
}
}
}, [queuePos])
return ( return (
<div <div

@ -1,3 +1,4 @@
import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { StoryMode } from '../../data/story/story' import { StoryMode } from '../../data/story/story'
import { Gradient } from '../animations' import { Gradient } from '../animations'
@ -49,27 +50,33 @@ export const ConversationMain = styled.div<{
: ''} : ''}
` `
export const LoadingBar = styled.div<{ visible: boolean; progress: number }>` export const LoadingBarInnerAnimate = styled.div<{ visible: boolean }>`
height: 1px; height: 1px;
position: relative; position: absolute;
top: 0;
&::after { width: 100%;
content: ''; background-image: linear-gradient(
height: 1px; 90deg,
position: absolute; ${(props) => (props.theme.colors.textHeadings)} 0%,
top: 0; ${(props) => (props.theme.colors.bg2)} 40%,
width: 100%; ${(props) => (props.theme.colors.bg3)} 60%,
background-image: linear-gradient( ${(props) => (props.theme.colors.textHeadings)} 100%
90deg, );
${(props) => props.theme.colors.textHeadings} 0%, background-size: 200%;
${(props) => props.theme.colors.bg2} 40%, background-repeat: repeat-x;
${(props) => props.theme.colors.bg3} 60%, animation: ${Gradient} 2s linear infinite;
${(props) => props.theme.colors.textHeadings} 100% opacity: ${(props) => (props.visible ? '1' : '0')};
); transition: opacity 0.2s ease-in-out;
background-size: 200%;
background-repeat: repeat-x;
animation: ${Gradient} 2s linear infinite;
opacity: ${(props) => (props.visible ? '1' : '0')};
transition: opacity 0.2s ease-in-out;
}
` `
export const LoadingBarInner = styled.div<{ visible: boolean }>`
height: 1px;
position: absolute;
top: 0;
background-color: ${(props) => (props.theme.colors.textHeadings)};
background-size: 200%;
background-repeat: repeat-x;
animation: ${Gradient} 2s linear infinite;
opacity: ${(props) => (props.visible ? '1' : '0')};
transition: width 250ms ease-in-out;
`

@ -8,7 +8,7 @@
], ],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": false, "strict": true,
"alwaysStrict": false, "alwaysStrict": false,
"strictPropertyInitialization": false, "strictPropertyInitialization": false,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,

Loading…
Cancel
Save