jbilcke-hf HF staff commited on
Commit
2f9a87c
β€’
1 Parent(s): 1c1e6e9

the number of pages can now be controlled by the

Browse files
.env CHANGED
@@ -14,7 +14,7 @@ RENDERING_ENGINE="INFERENCE_API"
14
  LLM_ENGINE="INFERENCE_API"
15
 
16
  # set this to control the number of pages
17
- MAX_NB_PAGES=1
18
 
19
  # Set to "true" to create artificial delays and smooth out traffic
20
  NEXT_PUBLIC_ENABLE_RATE_LIMITER="false"
 
14
  LLM_ENGINE="INFERENCE_API"
15
 
16
  # set this to control the number of pages
17
+ MAX_NB_PAGES=2
18
 
19
  # Set to "true" to create artificial delays and smooth out traffic
20
  NEXT_PUBLIC_ENABLE_RATE_LIMITER="false"
package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "@jbilcke/comic-factory",
3
- "version": "0.0.0",
4
  "private": true,
5
  "scripts": {
6
  "dev": "next dev",
 
1
  {
2
  "name": "@jbilcke/comic-factory",
3
+ "version": "1.1.0",
4
  "private": true,
5
  "scripts": {
6
  "dev": "next dev",
src/app/interface/about/index.tsx CHANGED
@@ -1,8 +1,14 @@
 
 
1
  import { Button } from "@/components/ui/button"
2
  import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
3
- import { useState } from "react"
4
  import { Login } from "../login"
5
 
 
 
 
 
6
  export function About() {
7
  const [isOpen, setOpen] = useState(false)
8
 
@@ -10,34 +16,34 @@ export function About() {
10
  <Dialog open={isOpen} onOpenChange={setOpen}>
11
  <DialogTrigger asChild>
12
  <Button variant="outline">
13
- <span className="hidden md:inline">AI-Comic-Factory 1.0</span>
14
- <span className="inline md:hidden">Version 1.0</span>
15
  </Button>
16
  </DialogTrigger>
17
- <DialogContent className="sm:max-w-[425px] md:max-w-[600px]">
18
  <DialogHeader>
19
- <DialogTitle>AI Comic Factory 1.0</DialogTitle>
20
  <DialogDescription className="w-full text-center text-2xl font-bold text-stone-700">
21
- AI Comic Factory 1.0 (March 2024 Update)
22
  </DialogDescription>
23
  </DialogHeader>
24
  <div className="grid gap-4 py-4 text-stone-700 text-sm md:text-base xl:text-lg">
25
  <p className="">
26
- The AI Comic Factory generates stories using AI in a few clicks.
27
  </p>
28
  <p>
29
- App is free for Hugging Face users πŸ‘‰ <Login />
30
- </p>
31
- <p className="pt-2 pb-2">
32
- Are you an artist? Learn <a className="text-stone-600 underline" href="https://huggingface.co/spaces/jbilcke-hf/ai-comic-factory/discussions/402#654ab848fa25dfb780aa19fb" target="_blank">how to use your own art style</a>
33
- </p>
34
- <p>
35
- πŸ‘‰ Default AI model used for stories is <a className="text-stone-600 underline" href="https://huggingface.co/HuggingFaceH4/zephyr-7b-beta" target="_blank">Zephyr-7b-beta</a>
36
- </p>
37
- <p>
38
- πŸ‘‰ Default AI model used for drawing is <a className="text-stone-600 underline" href="https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0" target="_blank">SDXL</a> by Stability AI
39
- </p>
40
- <p className="pt-2 pb-2">
41
  This is an open-source project, see the <a className="text-stone-600 underline" href="https://huggingface.co/spaces/jbilcke-hf/ai-comic-factory/blob/main/README.md" target="_blank">README</a> for more info.
42
  </p>
43
  </div>
 
1
+ import { useState } from "react"
2
+
3
  import { Button } from "@/components/ui/button"
4
  import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
5
+
6
  import { Login } from "../login"
7
 
8
+ const APP_NAME = `AI Comic Factory`
9
+ const APP_VERSION = `1.1`
10
+ const APP_RELEASE_DATE = `March 2024`
11
+
12
  export function About() {
13
  const [isOpen, setOpen] = useState(false)
14
 
 
16
  <Dialog open={isOpen} onOpenChange={setOpen}>
17
  <DialogTrigger asChild>
18
  <Button variant="outline">
19
+ <span className="hidden md:inline">{APP_NAME.replaceAll(" ", "-")} {APP_VERSION}</span>
20
+ <span className="inline md:hidden">Version {APP_VERSION}</span>
21
  </Button>
22
  </DialogTrigger>
23
+ <DialogContent className="w-full sm:max-w-[500px] md:max-w-[600px] overflow-y-scroll h-[100vh] sm:h-[550px]">
24
  <DialogHeader>
25
+ <DialogTitle>{APP_NAME} {APP_VERSION}</DialogTitle>
26
  <DialogDescription className="w-full text-center text-2xl font-bold text-stone-700">
27
+ {APP_NAME} {APP_VERSION} ({APP_RELEASE_DATE})
28
  </DialogDescription>
29
  </DialogHeader>
30
  <div className="grid gap-4 py-4 text-stone-700 text-sm md:text-base xl:text-lg">
31
  <p className="">
32
+ The {APP_NAME} generates stories using AI in a few clicks.
33
  </p>
34
  <p>
35
+ The app is free for Hugging Face users πŸ‘‰ <Login />
36
+ </p>
37
+ <p className="pt-2 pb-2">
38
+ Are you an artist? Learn <a className="text-stone-600 underline" href="https://huggingface.co/spaces/jbilcke-hf/ai-comic-factory/discussions/402#654ab848fa25dfb780aa19fb" target="_blank">how to use your own art style</a>
39
+ </p>
40
+ <p>
41
+ πŸ‘‰ Default AI model used for stories is <a className="text-stone-600 underline" href="https://huggingface.co/HuggingFaceH4/zephyr-7b-beta" target="_blank">Zephyr-7b-beta</a>
42
+ </p>
43
+ <p>
44
+ πŸ‘‰ Default AI model used for drawing is <a className="text-stone-600 underline" href="https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0" target="_blank">SDXL</a> by Stability AI
45
+ </p>
46
+ <p className="pt-2 pb-2">
47
  This is an open-source project, see the <a className="text-stone-600 underline" href="https://huggingface.co/spaces/jbilcke-hf/ai-comic-factory/blob/main/README.md" target="_blank">README</a> for more info.
48
  </p>
49
  </div>
src/app/interface/bottom-bar/bottom-bar.tsx CHANGED
@@ -14,13 +14,16 @@ import { localStorageKeys } from "../settings-dialog/localStorageKeys"
14
  import { defaultSettings } from "../settings-dialog/defaultSettings"
15
 
16
  function BottomBar() {
17
- const download = useStore(state => state.download)
 
 
 
 
18
  const isGeneratingStory = useStore(state => state.isGeneratingStory)
19
  const prompt = useStore(state => state.prompt)
20
  const panelGenerationStatus = useStore(state => state.panelGenerationStatus)
21
- const page = useStore(state => state.page)
22
  const preset = useStore(state => state.preset)
23
- const pageToImage = useStore(state => state.pageToImage)
24
 
25
  const allStatus = Object.values(panelGenerationStatus)
26
  const remainingImages = allStatus.reduce((acc, s) => (acc + (s ? 1 : 0)), 0)
 
14
  import { defaultSettings } from "../settings-dialog/defaultSettings"
15
 
16
  function BottomBar() {
17
+ // deprecated, as HTML-to-bitmap didn't work that well for us
18
+ // const page = useStore(state => state.page)
19
+ // const download = useStore(state => state.download)
20
+ // const pageToImage = useStore(state => state.pageToImage)
21
+
22
  const isGeneratingStory = useStore(state => state.isGeneratingStory)
23
  const prompt = useStore(state => state.prompt)
24
  const panelGenerationStatus = useStore(state => state.panelGenerationStatus)
25
+
26
  const preset = useStore(state => state.preset)
 
27
 
28
  const allStatus = Object.values(panelGenerationStatus)
29
  const remainingImages = allStatus.reduce((acc, s) => (acc + (s ? 1 : 0)), 0)
src/app/interface/page/index.tsx CHANGED
@@ -6,11 +6,13 @@ import { allLayoutAspectRatios, allLayouts } from "@/app/layouts"
6
  import { useStore } from "@/app/store"
7
  import { cn } from "@/lib/utils"
8
 
9
- export function Page({ page }: { page: number}) {
10
  const zoomLevel = useStore(state => state.zoomLevel)
11
  const layouts = useStore(state => state.layouts)
12
 
13
- const layout = layouts[page]
 
 
14
 
15
  const LayoutElement = (allLayouts as any)[layout]
16
  const aspectRatio = ((allLayoutAspectRatios as any)[layout] as string) || "aspect-[250/297]"
@@ -28,19 +30,16 @@ export function Page({ page }: { page: number}) {
28
  // Layout4: currentNbPanelsPerPage
29
  }
30
 
31
- const currentNbPanels = ((allLayoutsNbPanels as any)[layout] as number) || currentNbPanelsPerPage
 
 
32
 
 
 
 
 
 
33
  /*
34
- const [canLoad, setCanLoad] = useState(false)
35
- useEffect(() => {
36
- if (prompt?.length) {
37
- setCanLoad(false)
38
- setTimeout(() => {
39
- setCanLoad(true)
40
- }, page * 4000)
41
- }
42
- }, [prompt])
43
- */
44
 
45
  const setPage = useStore(state => state.setPage)
46
  const pageRef = useRef<HTMLDivElement>(null)
@@ -50,18 +49,13 @@ export function Page({ page }: { page: number}) {
50
  if (!element) { return }
51
  setPage(element)
52
  }, [pageRef.current])
53
-
54
- /*
55
- console.log("PAGE DEBUG:", {
56
- currentNbPages,
57
- maxNbPages,
58
- "currentNbPages < maxNbPages": currentNbPages < maxNbPages,
59
- })
60
  */
61
-
 
62
  return (
63
  <div
64
- ref={pageRef}
 
65
  className={cn(
66
  `w-full`,
67
  `print:w-screen`,
@@ -86,14 +80,16 @@ export function Page({ page }: { page: number}) {
86
  // marginLeft: `${zoomLevel > 100 ? `100`}`
87
  }}
88
  >
89
- <LayoutElement page={page} nbPanels={currentNbPanels} />
90
  </div>
91
  {currentNbPages > 1 &&
92
  <p className="w-full text-center pt-4 font-sans text-2xs font-semibold text-stone-600">
93
- Page {page + 1}
94
  {/*
95
- alternative style:
96
- Page {page + 1} / {nbPages}
 
 
97
  */}
98
  </p>}
99
  </div>
 
6
  import { useStore } from "@/app/store"
7
  import { cn } from "@/lib/utils"
8
 
9
+ export function Page({ page }: { page: number }) {
10
  const zoomLevel = useStore(state => state.zoomLevel)
11
  const layouts = useStore(state => state.layouts)
12
 
13
+ // attention: here we use a fallback to layouts[0]
14
+ // if no predetermined layout exists for this page number
15
+ const layout = layouts[page] || layouts[0]
16
 
17
  const LayoutElement = (allLayouts as any)[layout]
18
  const aspectRatio = ((allLayoutAspectRatios as any)[layout] as string) || "aspect-[250/297]"
 
30
  // Layout4: currentNbPanelsPerPage
31
  }
32
 
33
+ // it's a bit confusing and too rigid we can't change the layouts for each panel,
34
+ // I should refactor this
35
+ const panelsPerPage = ((allLayoutsNbPanels as any)[layout] as number) || currentNbPanelsPerPage
36
 
37
+
38
+ // I think we should deprecate this part
39
+ // this was used to keep track of the page HTML element,
40
+ // for use with a HTML-to-bitmap library
41
+ // but the CSS layout wasn't followed properly and it depended on the zoom level
42
  /*
 
 
 
 
 
 
 
 
 
 
43
 
44
  const setPage = useStore(state => state.setPage)
45
  const pageRef = useRef<HTMLDivElement>(null)
 
49
  if (!element) { return }
50
  setPage(element)
51
  }, [pageRef.current])
 
 
 
 
 
 
 
52
  */
53
+
54
+
55
  return (
56
  <div
57
+ // deprecated
58
+ // ref={pageRef}
59
  className={cn(
60
  `w-full`,
61
  `print:w-screen`,
 
80
  // marginLeft: `${zoomLevel > 100 ? `100`}`
81
  }}
82
  >
83
+ <LayoutElement page={page} nbPanels={panelsPerPage} />
84
  </div>
85
  {currentNbPages > 1 &&
86
  <p className="w-full text-center pt-4 font-sans text-2xs font-semibold text-stone-600">
87
+ {page + 1}/{maxNbPages}
88
  {/*
89
+ alternative styles:
90
+ Page {page + 1}
91
+ Page {page + 1} / {maxNbPages}
92
+ {page + 1} / {maxNbPages}
93
  */}
94
  </p>}
95
  </div>
src/app/interface/panel/index.tsx CHANGED
@@ -44,11 +44,12 @@ export function Panel({
44
  // index of the panel in the whole app
45
  const panelIndex = page * nbPanels + panel
46
 
47
- // console.log("debug:", { page, nbPanels, panel })
48
  // the panel Id must be unique across all pages
49
  const panelId = `${panelIndex}`
50
 
51
- // console.log("panelId: " + panelId)
 
52
 
53
  const [mouseOver, setMouseOver] = useState(false)
54
  const ref = useRef<HTMLImageElement>(null)
@@ -94,6 +95,18 @@ export function Panel({
94
 
95
  let delay = enableRateLimiter ? (1000 + (500 * panelIndex)) : 1000
96
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  // Let's be gentle with Replicate or else they will believe they are under attack
98
  if (renderingModelVendor === "REPLICATE") {
99
  delay += 8000
@@ -117,6 +130,7 @@ export function Panel({
117
  nbFrames: number
118
  revision: number
119
  }) => {
 
120
  if (!prompt?.length) { return }
121
 
122
  // important: update the status, and clear the scene
@@ -133,7 +147,7 @@ export function Panel({
133
  // atrocious and very, very, very, very, very, very, very ugly hack for the Inference API
134
  // as apparently "use_cache: false" doesn't work, or doesn't do what we want it to do
135
  let cacheInvalidationHack = ""
136
- const nbMaxRevisions = 10
137
  for (let i = 0; i < revision && revision < nbMaxRevisions; i++) {
138
  const j = Math.random()
139
  cacheInvalidationHack += j < 0.3 ? "_" : j < 0.6 ? "," : "-"
 
44
  // index of the panel in the whole app
45
  const panelIndex = page * nbPanels + panel
46
 
47
+
48
  // the panel Id must be unique across all pages
49
  const panelId = `${panelIndex}`
50
 
51
+ // console.log(`panel/index.tsx: <Panel panelId=${panelId}> rendered again!`)
52
+
53
 
54
  const [mouseOver, setMouseOver] = useState(false)
55
  const ref = useRef<HTMLImageElement>(null)
 
95
 
96
  let delay = enableRateLimiter ? (1000 + (500 * panelIndex)) : 1000
97
 
98
+ /*
99
+ console.log("panel/index.tsx: DEBUG: " + JSON.stringify({
100
+ page,
101
+ nbPanels,
102
+ panel,
103
+ panelIndex,
104
+ panelId,
105
+ revision,
106
+ renderedScenes: Object.keys(renderedScenes),
107
+ }, null, 2))
108
+ */
109
+
110
  // Let's be gentle with Replicate or else they will believe they are under attack
111
  if (renderingModelVendor === "REPLICATE") {
112
  delay += 8000
 
130
  nbFrames: number
131
  revision: number
132
  }) => {
133
+ console.log(`panel/index.tsx: startImageGeneration(${JSON.stringify({ prompt, width, height, nbFrames, revision }, null, 2)})`)
134
  if (!prompt?.length) { return }
135
 
136
  // important: update the status, and clear the scene
 
147
  // atrocious and very, very, very, very, very, very, very ugly hack for the Inference API
148
  // as apparently "use_cache: false" doesn't work, or doesn't do what we want it to do
149
  let cacheInvalidationHack = ""
150
+ const nbMaxRevisions = 20
151
  for (let i = 0; i < revision && revision < nbMaxRevisions; i++) {
152
  const j = Math.random()
153
  cacheInvalidationHack += j < 0.3 ? "_" : j < 0.6 ? "," : "-"
src/app/interface/settings-dialog/index.tsx CHANGED
@@ -87,35 +87,35 @@ export function SettingsDialog() {
87
  <DialogTrigger asChild>
88
  <Button className="space-x-1 md:space-x-2">
89
  <div>
90
- <span className="hidden md:inline">Custom models</span>
91
  </div>
92
  </Button>
93
  </DialogTrigger>
94
- <DialogContent className="w-full sm:max-w-[500px] md:max-w-[700px] overflow-y-auto h-max-[100vh] md:h-max-[80vh]">
95
  <DialogHeader>
96
  <DialogDescription className="w-full text-center text-lg font-bold text-stone-800">
97
- Custom Models
98
  </DialogDescription>
99
  </DialogHeader>
100
- {
101
- // isConfigReady && <Field>
102
- // <Label>Maximum number of pages: {userDefinedMaxNumberOfPages}</Label>
103
- // <Slider
104
- // min={1}
105
- // max={maxNbPages}
106
- // step={1}
107
- // onValueChange={(value: any) => {
108
- // let numericValue = Number(value[0])
109
- // numericValue = !isNaN(value[0]) && isFinite(value[0]) ? numericValue : 0
110
- // numericValue = Math.min(maxNbPages, Math.max(1, numericValue))
111
- // setUserDefinedMaxNumberOfPages(numericValue)
112
- // }}
113
- // defaultValue={[userDefinedMaxNumberOfPages]}
114
- // value={[userDefinedMaxNumberOfPages]}
115
- // />
116
- // </Field>
117
  }
118
- <div className="grid gap-4 py-1 space-y-1 text-stone-800">
119
  <Field>
120
  <Label>Image rendering provider:</Label>
121
  <p className="pt-2 pb-3 text-base italic text-zinc-600">
@@ -301,6 +301,8 @@ export function SettingsDialog() {
301
  </p>
302
  </div>
303
 
 
 
304
  <DialogFooter>
305
  <Button type="submit" onClick={() => setOpen(false)}>Close</Button>
306
  </DialogFooter>
 
87
  <DialogTrigger asChild>
88
  <Button className="space-x-1 md:space-x-2">
89
  <div>
90
+ <span className="hidden md:inline">Settings</span>
91
  </div>
92
  </Button>
93
  </DialogTrigger>
94
+ <DialogContent className="w-full sm:max-w-[500px] md:max-w-[700px]">
95
  <DialogHeader>
96
  <DialogDescription className="w-full text-center text-lg font-bold text-stone-800">
97
+ Settings
98
  </DialogDescription>
99
  </DialogHeader>
100
+ <div className="overflow-y-scroll h-[75vh] md:h-[70vh]">
101
+ {isConfigReady && <Field>
102
+ <Label>(new!) Control the number of pages: {userDefinedMaxNumberOfPages}</Label>
103
+ <Slider
104
+ min={1}
105
+ max={maxNbPages}
106
+ step={1}
107
+ onValueChange={(value: any) => {
108
+ let numericValue = Number(value[0])
109
+ numericValue = !isNaN(value[0]) && isFinite(value[0]) ? numericValue : 0
110
+ numericValue = Math.min(maxNbPages, Math.max(1, numericValue))
111
+ setUserDefinedMaxNumberOfPages(numericValue)
112
+ }}
113
+ defaultValue={[userDefinedMaxNumberOfPages]}
114
+ value={[userDefinedMaxNumberOfPages]}
115
+ />
116
+ </Field>
117
  }
118
+ <div className="grid gap-4 pt-8 pb-1 space-y-1 text-stone-800">
119
  <Field>
120
  <Label>Image rendering provider:</Label>
121
  <p className="pt-2 pb-3 text-base italic text-zinc-600">
 
301
  </p>
302
  </div>
303
 
304
+ </div>
305
+
306
  <DialogFooter>
307
  <Button type="submit" onClick={() => setOpen(false)}>Close</Button>
308
  </DialogFooter>
src/app/interface/sign-up-cta/sign-up-cta.tsx CHANGED
@@ -7,7 +7,7 @@ function SignUpCTA() {
7
  return (
8
  <div className={cn(
9
  `print:hidden`,
10
- `fixed flex flex-col items-center bottom-8 top-28 right-2 md:top-17 md:right-6 z-10`,
11
  )}>
12
  <div className="font-bold text-sm pb-2 text-stone-600 bg-stone-50 dark:text-stone-600 dark:bg-stone-50 p-1 rounded-sm">
13
  anonymous users can generate 1 comic.<br/> <span
 
7
  return (
8
  <div className={cn(
9
  `print:hidden`,
10
+ `fixed flex flex-col items-center bottom-24 top-28 right-2 md:top-17 md:right-6 z-10`,
11
  )}>
12
  <div className="font-bold text-sm pb-2 text-stone-600 bg-stone-50 dark:text-stone-600 dark:bg-stone-50 p-1 rounded-sm">
13
  anonymous users can generate 1 comic.<br/> <span
src/app/interface/top-menu/index.tsx CHANGED
@@ -51,6 +51,9 @@ export function TopMenu() {
51
  const setShowCaptions = useStore(state => state.setShowCaptions)
52
  const showCaptions = useStore(state => state.showCaptions)
53
 
 
 
 
54
  const generate = useStore(state => state.generate)
55
 
56
  const isGeneratingStory = useStore(state => state.isGeneratingStory)
@@ -102,7 +105,7 @@ export function TopMenu() {
102
  setShowAuthWall(true)
103
  return
104
  }
105
-
106
  const promptChanged = draftPrompt.trim() !== prompt.trim()
107
  const presetChanged = draftPreset !== preset.id
108
  const layoutChanged = draftLayout !== layout
 
51
  const setShowCaptions = useStore(state => state.setShowCaptions)
52
  const showCaptions = useStore(state => state.showCaptions)
53
 
54
+ const currentNbPages = useStore(state => state.currentNbPages)
55
+ const setCurrentNbPages = useStore(state => state.setCurrentNbPages)
56
+
57
  const generate = useStore(state => state.generate)
58
 
59
  const isGeneratingStory = useStore(state => state.isGeneratingStory)
 
105
  setShowAuthWall(true)
106
  return
107
  }
108
+
109
  const promptChanged = draftPrompt.trim() !== prompt.trim()
110
  const presetChanged = draftPreset !== preset.id
111
  const layoutChanged = draftLayout !== layout
src/app/main.tsx CHANGED
@@ -1,11 +1,14 @@
1
  "use client"
2
 
3
- import { Suspense, useEffect, useState, useTransition } from "react"
 
4
 
5
  import { cn } from "@/lib/utils"
6
  import { fonts } from "@/lib/fonts"
7
  import { GeneratedPanel } from "@/types"
8
  import { joinWords } from "@/lib/joinWords"
 
 
9
 
10
  import { TopMenu } from "./interface/top-menu"
11
  import { useStore } from "./store"
@@ -13,12 +16,10 @@ import { Zoom } from "./interface/zoom"
13
  import { BottomBar } from "./interface/bottom-bar"
14
  import { Page } from "./interface/page"
15
  import { getStoryContinuation } from "./queries/getStoryContinuation"
16
- import { useDynamicConfig } from "@/lib/useDynamicConfig"
17
- import { useLocalStorage } from "usehooks-ts"
18
  import { localStorageKeys } from "./interface/settings-dialog/localStorageKeys"
19
  import { defaultSettings } from "./interface/settings-dialog/defaultSettings"
20
- import { Button } from "@/components/ui/button"
21
  import { SignUpCTA } from "./interface/sign-up-cta"
 
22
 
23
  export default function Main() {
24
  const [_isPending, startTransition] = useTransition()
@@ -31,10 +32,9 @@ export default function Main() {
31
  const preset = useStore(s => s.preset)
32
  const prompt = useStore(s => s.prompt)
33
 
34
- const currentNbPanelsPerPage = useStore(s => s.currentNbPanelsPerPage)
35
- const maxNbPanelsPerPage = useStore(s => s.maxNbPanelsPerPage)
36
  const currentNbPages = useStore(s => s.currentNbPages)
37
  const maxNbPages = useStore(s => s.maxNbPages)
 
38
  const currentNbPanels = useStore(s => s.currentNbPanels)
39
  const maxNbPanels = useStore(s => s.maxNbPanels)
40
 
@@ -42,10 +42,14 @@ export default function Main() {
42
  const setMaxNbPanelsPerPage = useStore(s => s.setMaxNbPanelsPerPage)
43
  const setCurrentNbPages = useStore(s => s.setCurrentNbPages)
44
  const setMaxNbPages = useStore(s => s.setMaxNbPages)
45
- const setCurrentNbPanels = useStore(s => s.setCurrentNbPanels)
46
- const setMaxNbPanels = useStore(s => s.setMaxNbPanels)
47
 
 
48
  const setPanels = useStore(s => s.setPanels)
 
 
 
 
 
49
  const setCaptions = useStore(s => s.setCaptions)
50
 
51
  const zoomLevel = useStore(s => s.zoomLevel)
@@ -57,6 +61,35 @@ export default function Main() {
57
  defaultSettings.userDefinedMaxNumberOfPages
58
  )
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  useEffect(() => {
61
  if (maxNbPages !== userDefinedMaxNumberOfPages) {
62
  setMaxNbPages(userDefinedMaxNumberOfPages)
@@ -64,6 +97,14 @@ export default function Main() {
64
  }, [maxNbPages, userDefinedMaxNumberOfPages])
65
 
66
 
 
 
 
 
 
 
 
 
67
  useEffect(() => {
68
  if (isConfigReady) {
69
 
@@ -76,15 +117,30 @@ export default function Main() {
76
 
77
  // react to prompt changes
78
  useEffect(() => {
 
79
  if (!prompt) { return }
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  startTransition(async () => {
82
  setWaitABitMore(false)
83
  setGeneratingStory(true)
84
 
85
- // I don't think we are going to need a rate limiter on the LLM part anymore
86
- const enableRateLimiter = false // `${process.env.NEXT_PUBLIC_ENABLE_RATE_LIMITER}` === "true"
87
-
88
  const [stylePrompt, userStoryPrompt] = prompt.split("||").map(x => x.trim())
89
 
90
  // we have to limit the size of the prompt, otherwise the rest of the style won't be followed
@@ -95,25 +151,30 @@ export default function Main() {
95
  }
96
 
97
  // new experimental prompt: let's drop the user prompt, and only use the style
98
- const lightPanelPromptPrefix = joinWords(preset.imagePrompt(limitedStylePrompt))
99
 
100
  // this prompt will be used if the LLM generation failed
101
- const degradedPanelPromptPrefix = joinWords([
102
  ...preset.imagePrompt(limitedStylePrompt),
103
 
104
  // we re-inject the story, then
105
  userStoryPrompt
106
  ])
107
 
108
- let existingPanels: GeneratedPanel[] = []
109
- const newPanelsPrompts: string[] = []
110
- const newCaptions: string[] = []
111
-
112
  // we always generate panels 2 by 2
113
  const nbPanelsToGenerate = 2
114
 
 
 
 
 
 
 
 
 
 
115
  for (
116
- let currentPanel = 0;
117
  currentPanel < currentNbPanels;
118
  currentPanel += nbPanelsToGenerate
119
  ) {
@@ -124,48 +185,55 @@ export default function Main() {
124
  userStoryPrompt,
125
  nbPanelsToGenerate,
126
  maxNbPanels,
127
- existingPanels,
 
 
 
128
  })
129
- console.log("LLM generated some new panels:", candidatePanels)
130
 
131
- existingPanels.push(...candidatePanels)
 
132
 
133
- console.log(`Converting the ${nbPanelsToGenerate} new panels into image prompts..`)
134
 
135
  const startAt = currentPanel
136
  const endAt = currentPanel + nbPanelsToGenerate
137
  for (let p = startAt; p < endAt; p++) {
138
- newCaptions.push(existingPanels[p]?.caption.trim() || "...")
139
  const newPanel = joinWords([
140
 
141
  // what we do here is that ideally we give full control to the LLM for prompting,
142
  // unless there was a catastrophic failure, in that case we preserve the original prompt
143
- existingPanels[p]?.instructions
144
  ? lightPanelPromptPrefix
145
  : degradedPanelPromptPrefix,
146
 
147
- existingPanels[p]?.instructions
148
  ])
149
- newPanelsPrompts.push(newPanel)
150
 
151
- console.log(`Image prompt for panel ${p} => "${newPanel}"`)
152
  }
153
 
154
  // update the frontend
155
  // console.log("updating the frontend..")
156
- setCaptions(newCaptions)
157
- setPanels(newPanelsPrompts)
158
 
159
  setGeneratingStory(false)
160
  } catch (err) {
161
- console.log("failed to generate the story, aborting here")
162
  setGeneratingStory(false)
163
  break
164
  }
165
  if (currentPanel > (currentNbPanels / 2)) {
166
- console.log("good, we are half way there, hold tight!")
167
  // setWaitABitMore(true)
168
  }
 
 
 
169
  }
170
 
171
  /*
@@ -176,7 +244,13 @@ export default function Main() {
176
  */
177
 
178
  })
179
- }, [prompt, preset?.label, currentNbPanels, maxNbPanels]) // important: we need to react to preset changes too
 
 
 
 
 
 
180
 
181
  return (
182
  <Suspense>
@@ -205,11 +279,13 @@ export default function Main() {
205
  {Array(currentNbPages).fill(0).map((_, i) => <Page key={i} page={i} />)}
206
  </div>
207
  {
208
- // currentNbPages < maxNbPages &&
209
- // <div className="flex flex-col space-y-2 pt-2 pb-6 text-gray-600 dark:text-gray-600">
210
- // <div>Happy with your story?</div>
211
- // <div>You can <Button>Add page {currentNbPages + 1} πŸ‘€</Button></div>
212
- // </div>
 
 
213
  }
214
  </div>
215
  </div>
 
1
  "use client"
2
 
3
+ import { Suspense, useEffect, useRef, useState, useTransition } from "react"
4
+ import { useLocalStorage } from "usehooks-ts"
5
 
6
  import { cn } from "@/lib/utils"
7
  import { fonts } from "@/lib/fonts"
8
  import { GeneratedPanel } from "@/types"
9
  import { joinWords } from "@/lib/joinWords"
10
+ import { useDynamicConfig } from "@/lib/useDynamicConfig"
11
+ import { Button } from "@/components/ui/button"
12
 
13
  import { TopMenu } from "./interface/top-menu"
14
  import { useStore } from "./store"
 
16
  import { BottomBar } from "./interface/bottom-bar"
17
  import { Page } from "./interface/page"
18
  import { getStoryContinuation } from "./queries/getStoryContinuation"
 
 
19
  import { localStorageKeys } from "./interface/settings-dialog/localStorageKeys"
20
  import { defaultSettings } from "./interface/settings-dialog/defaultSettings"
 
21
  import { SignUpCTA } from "./interface/sign-up-cta"
22
+ import { sleep } from "@/lib/sleep"
23
 
24
  export default function Main() {
25
  const [_isPending, startTransition] = useTransition()
 
32
  const preset = useStore(s => s.preset)
33
  const prompt = useStore(s => s.prompt)
34
 
 
 
35
  const currentNbPages = useStore(s => s.currentNbPages)
36
  const maxNbPages = useStore(s => s.maxNbPages)
37
+ const previousNbPanels = useStore(s => s.previousNbPanels)
38
  const currentNbPanels = useStore(s => s.currentNbPanels)
39
  const maxNbPanels = useStore(s => s.maxNbPanels)
40
 
 
42
  const setMaxNbPanelsPerPage = useStore(s => s.setMaxNbPanelsPerPage)
43
  const setCurrentNbPages = useStore(s => s.setCurrentNbPages)
44
  const setMaxNbPages = useStore(s => s.setMaxNbPages)
 
 
45
 
46
+ const panels = useStore(s => s.panels)
47
  const setPanels = useStore(s => s.setPanels)
48
+
49
+ // do we need those?
50
+ const renderedScenes = useStore(s => s.renderedScenes)
51
+ const captions = useStore(s => s.captions)
52
+
53
  const setCaptions = useStore(s => s.setCaptions)
54
 
55
  const zoomLevel = useStore(s => s.zoomLevel)
 
61
  defaultSettings.userDefinedMaxNumberOfPages
62
  )
63
 
64
+ const numberOfPanels = Object.keys(panels).length
65
+ const panelGenerationStatus = useStore(state => state.panelGenerationStatus)
66
+ const allStatus = Object.values(panelGenerationStatus)
67
+ const numberOfPendingGenerations = allStatus.reduce((acc, s) => (acc + (s ? 1 : 0)), 0)
68
+
69
+ const hasAtLeastOnePage = numberOfPanels > 0
70
+
71
+ const hasNoPendingGeneration =
72
+ numberOfPendingGenerations === 0
73
+
74
+ const hasStillMorePagesToGenerate =
75
+ currentNbPages < maxNbPages
76
+
77
+ const showNextPageButton =
78
+ hasAtLeastOnePage &&
79
+ hasNoPendingGeneration &&
80
+ hasStillMorePagesToGenerate
81
+
82
+ /*
83
+ console.log("<Main>: " + JSON.stringify({
84
+ currentNbPages,
85
+ hasAtLeastOnePage,
86
+ numberOfPendingGenerations,
87
+ hasNoPendingGeneration,
88
+ hasStillMorePagesToGenerate,
89
+ showNextPageButton
90
+ }, null, 2))
91
+ */
92
+
93
  useEffect(() => {
94
  if (maxNbPages !== userDefinedMaxNumberOfPages) {
95
  setMaxNbPages(userDefinedMaxNumberOfPages)
 
97
  }, [maxNbPages, userDefinedMaxNumberOfPages])
98
 
99
 
100
+ const ref = useRef({
101
+ existingPanels: [] as GeneratedPanel[],
102
+ newPanelsPrompts: [] as string[],
103
+ newCaptions: [] as string[],
104
+ prompt: "",
105
+ preset: "",
106
+ })
107
+
108
  useEffect(() => {
109
  if (isConfigReady) {
110
 
 
117
 
118
  // react to prompt changes
119
  useEffect(() => {
120
+ // console.log(`main.tsx: asked to re-generate!!`)
121
  if (!prompt) { return }
122
 
123
+ // if the prompt or preset changed, we clear the cache
124
+ // this part is important, otherwise when trying to change the prompt
125
+ // we wouldn't still have remnants of the previous comic
126
+ // in the data sent to the LLM (also the page cursor would be wrong)
127
+ if (
128
+ prompt !== ref.current.prompt ||
129
+ preset?.label !== ref.current.preset) {
130
+ // console.log("overwriting ref.current!")
131
+ ref.current = {
132
+ existingPanels: [],
133
+ newPanelsPrompts: [],
134
+ newCaptions: [],
135
+ prompt,
136
+ preset: preset?.label || "",
137
+ }
138
+ }
139
+
140
  startTransition(async () => {
141
  setWaitABitMore(false)
142
  setGeneratingStory(true)
143
 
 
 
 
144
  const [stylePrompt, userStoryPrompt] = prompt.split("||").map(x => x.trim())
145
 
146
  // we have to limit the size of the prompt, otherwise the rest of the style won't be followed
 
151
  }
152
 
153
  // new experimental prompt: let's drop the user prompt, and only use the style
154
+ const lightPanelPromptPrefix: string = joinWords(preset.imagePrompt(limitedStylePrompt))
155
 
156
  // this prompt will be used if the LLM generation failed
157
+ const degradedPanelPromptPrefix: string = joinWords([
158
  ...preset.imagePrompt(limitedStylePrompt),
159
 
160
  // we re-inject the story, then
161
  userStoryPrompt
162
  ])
163
 
 
 
 
 
164
  // we always generate panels 2 by 2
165
  const nbPanelsToGenerate = 2
166
 
167
+ /*
168
+ console.log("going to call getStoryContinuation based on: " + JSON.stringify({
169
+ previousNbPanels,
170
+ currentNbPanels,
171
+ nbPanelsToGenerate,
172
+ "ref.current:": ref.current,
173
+ }, null, 2))
174
+ */
175
+
176
  for (
177
+ let currentPanel = previousNbPanels;
178
  currentPanel < currentNbPanels;
179
  currentPanel += nbPanelsToGenerate
180
  ) {
 
185
  userStoryPrompt,
186
  nbPanelsToGenerate,
187
  maxNbPanels,
188
+
189
+ // existing panels are critical here: this is how we can
190
+ // continue over an existing story
191
+ existingPanels: ref.current.existingPanels,
192
  })
193
+ // console.log("LLM generated some new panels:", candidatePanels)
194
 
195
+ ref.current.existingPanels.push(...candidatePanels)
196
+ // console.log("ref.current.existingPanels.push(...candidatePanels) successful, now we have ref.current.existingPanels = ", ref.current.existingPanels)
197
 
198
+ // console.log(`main.tsx: converting the ${nbPanelsToGenerate} new panels into image prompts..`)
199
 
200
  const startAt = currentPanel
201
  const endAt = currentPanel + nbPanelsToGenerate
202
  for (let p = startAt; p < endAt; p++) {
203
+ ref.current.newCaptions.push(ref.current.existingPanels[p]?.caption.trim() || "...")
204
  const newPanel = joinWords([
205
 
206
  // what we do here is that ideally we give full control to the LLM for prompting,
207
  // unless there was a catastrophic failure, in that case we preserve the original prompt
208
+ ref.current.existingPanels[p]?.instructions
209
  ? lightPanelPromptPrefix
210
  : degradedPanelPromptPrefix,
211
 
212
+ ref.current.existingPanels[p]?.instructions || ""
213
  ])
214
+ ref.current.newPanelsPrompts.push(newPanel)
215
 
216
+ console.log(`main.tsx: image prompt for panel ${p} => "${newPanel}"`)
217
  }
218
 
219
  // update the frontend
220
  // console.log("updating the frontend..")
221
+ setCaptions(ref.current.newCaptions)
222
+ setPanels(ref.current.newPanelsPrompts)
223
 
224
  setGeneratingStory(false)
225
  } catch (err) {
226
+ console.log("main.tsx: LLM generation failed:", err)
227
  setGeneratingStory(false)
228
  break
229
  }
230
  if (currentPanel > (currentNbPanels / 2)) {
231
+ console.log("main.tsx: we are halfway there, hold tight!")
232
  // setWaitABitMore(true)
233
  }
234
+
235
+ // we could sleep here if we want to
236
+ // await sleep(1000)
237
  }
238
 
239
  /*
 
244
  */
245
 
246
  })
247
+ }, [
248
+ prompt,
249
+ preset?.label,
250
+ previousNbPanels,
251
+ currentNbPanels,
252
+ maxNbPanels
253
+ ]) // important: we need to react to preset changes too
254
 
255
  return (
256
  <Suspense>
 
279
  {Array(currentNbPages).fill(0).map((_, i) => <Page key={i} page={i} />)}
280
  </div>
281
  {
282
+ showNextPageButton &&
283
+ <div className="flex flex-col space-y-2 pt-2 pb-6 text-gray-600 dark:text-gray-600">
284
+ <div>Happy with your story?</div>
285
+ <div>You can <Button onClick={() => {
286
+ setCurrentNbPages(currentNbPages + 1)
287
+ }}>Add page {currentNbPages + 1} πŸ‘€</Button></div>
288
+ </div>
289
  }
290
  </div>
291
  </div>
src/app/queries/getStoryContinuation.ts CHANGED
@@ -2,20 +2,21 @@ import { Preset } from "../engine/presets"
2
  import { GeneratedPanel } from "@/types"
3
  import { predictNextPanels } from "./predictNextPanels"
4
  import { joinWords } from "@/lib/joinWords"
 
5
 
6
  export const getStoryContinuation = async ({
7
  preset,
8
  stylePrompt = "",
9
  userStoryPrompt = "",
10
- nbPanelsToGenerate = 2,
11
- maxNbPanels = 4,
12
  existingPanels = [],
13
  }: {
14
  preset: Preset;
15
  stylePrompt?: string;
16
  userStoryPrompt?: string;
17
- nbPanelsToGenerate?: number;
18
- maxNbPanels?: number;
19
  existingPanels?: GeneratedPanel[];
20
  }): Promise<GeneratedPanel[]> => {
21
 
@@ -63,6 +64,7 @@ export const getStoryContinuation = async ({
63
  caption: "(Sorry, LLM generation failed: using degraded mode)"
64
  })
65
  }
 
66
  // console.error(err)
67
  } finally {
68
  return panels
 
2
  import { GeneratedPanel } from "@/types"
3
  import { predictNextPanels } from "./predictNextPanels"
4
  import { joinWords } from "@/lib/joinWords"
5
+ import { sleep } from "@/lib/sleep"
6
 
7
  export const getStoryContinuation = async ({
8
  preset,
9
  stylePrompt = "",
10
  userStoryPrompt = "",
11
+ nbPanelsToGenerate,
12
+ maxNbPanels,
13
  existingPanels = [],
14
  }: {
15
  preset: Preset;
16
  stylePrompt?: string;
17
  userStoryPrompt?: string;
18
+ nbPanelsToGenerate: number;
19
+ maxNbPanels: number;
20
  existingPanels?: GeneratedPanel[];
21
  }): Promise<GeneratedPanel[]> => {
22
 
 
64
  caption: "(Sorry, LLM generation failed: using degraded mode)"
65
  })
66
  }
67
+ await sleep(2000)
68
  // console.error(err)
69
  } finally {
70
  return panels
src/app/queries/predictNextPanels.ts CHANGED
@@ -11,14 +11,14 @@ import { sleep } from "@/lib/sleep"
11
  export const predictNextPanels = async ({
12
  preset,
13
  prompt = "",
14
- nbPanelsToGenerate = 2,
15
- maxNbPanels = 4,
16
  existingPanels = [],
17
  }: {
18
  preset: Preset;
19
  prompt: string;
20
- nbPanelsToGenerate?: number;
21
- maxNbPanels?: number;
22
  existingPanels: GeneratedPanel[];
23
  }): Promise<GeneratedPanel[]> => {
24
  // console.log("predictNextPanels: ", { prompt, nbPanelsToGenerate })
 
11
  export const predictNextPanels = async ({
12
  preset,
13
  prompt = "",
14
+ nbPanelsToGenerate,
15
+ maxNbPanels,
16
  existingPanels = [],
17
  }: {
18
  preset: Preset;
19
  prompt: string;
20
+ nbPanelsToGenerate: number;
21
+ maxNbPanels: number;
22
  existingPanels: GeneratedPanel[];
23
  }): Promise<GeneratedPanel[]> => {
24
  // console.log("predictNextPanels: ", { prompt, nbPanelsToGenerate })
src/app/store/index.ts CHANGED
@@ -16,6 +16,7 @@ export const useStore = create<{
16
  maxNbPanelsPerPage: number
17
  currentNbPages: number
18
  maxNbPages: number
 
19
  currentNbPanels: number
20
  maxNbPanels: number
21
  panels: string[]
@@ -36,6 +37,7 @@ export const useStore = create<{
36
  setMaxNbPanelsPerPage: (maxNbPanelsPerPage: number) => void
37
  setCurrentNbPages: (currentNbPages: number) => void
38
  setMaxNbPages: (maxNbPages: number) => void
 
39
  setCurrentNbPanels: (currentNbPanels: number) => void
40
  setMaxNbPanels: (maxNbPanels: number) => void
41
 
@@ -53,12 +55,19 @@ export const useStore = create<{
53
  setCaptions: (captions: string[]) => void
54
  setPanelCaption: (newCaption: string, index: number) => void
55
  setZoomLevel: (zoomLevel: number) => void
56
- setPage: (page: HTMLDivElement) => void
57
  setGeneratingStory: (isGeneratingStory: boolean) => void
58
  setGeneratingImages: (panelId: string, value: boolean) => void
59
  setGeneratingText: (isGeneratingText: boolean) => void
60
- pageToImage: () => Promise<string>
61
- download: () => Promise<void>
 
 
 
 
 
 
 
62
  generate: (prompt: string, presetName: PresetName, layoutName: LayoutName) => void
63
  }>((set, get) => ({
64
  prompt: "",
@@ -69,6 +78,7 @@ export const useStore = create<{
69
  maxNbPanelsPerPage: 4,
70
  currentNbPages: 1,
71
  maxNbPages: 1,
 
72
  currentNbPanels: 4,
73
  maxNbPanels: 4,
74
 
@@ -77,16 +87,22 @@ export const useStore = create<{
77
  upscaleQueue: {} as Record<string, RenderedScene>,
78
  renderedScenes: {} as Record<string, RenderedScene>,
79
  showCaptions: false,
 
 
80
  layout: defaultLayout,
81
- layouts: [defaultLayout, defaultLayout],
 
 
82
  zoomLevel: 60,
 
 
83
  page: undefined as unknown as HTMLDivElement,
 
84
  isGeneratingStory: false,
85
  panelGenerationStatus: {},
86
  isGeneratingText: false,
87
  atLeastOnePanelIsBusy: false,
88
 
89
-
90
  setCurrentNbPanelsPerPage: (currentNbPanelsPerPage: number) => {
91
  const { currentNbPages } = get()
92
  set({
@@ -102,10 +118,41 @@ export const useStore = create<{
102
  })
103
  },
104
  setCurrentNbPages: (currentNbPages: number) => {
105
- const { currentNbPanelsPerPage } = get()
106
- set({
 
 
 
 
 
 
 
107
  currentNbPages,
108
- currentNbPanels: currentNbPanelsPerPage * currentNbPages
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  })
110
  },
111
  setMaxNbPages: (maxNbPages: number) => {
@@ -115,8 +162,40 @@ export const useStore = create<{
115
  maxNbPanels: maxNbPanelsPerPage * maxNbPages,
116
  })
117
  },
 
 
 
 
 
118
  setCurrentNbPanels: (currentNbPanels: number) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  set({
 
 
 
 
 
 
 
 
120
  currentNbPanels,
121
  })
122
  },
@@ -200,35 +279,37 @@ export const useStore = create<{
200
  })
201
  },
202
  setLayout: (layoutName: LayoutName) => {
203
-
204
- const { currentNbPages } = get()
205
-
206
- const layout = layoutName === "random"
207
- ? getRandomLayoutName()
208
- : layoutName
209
 
210
  const layouts: LayoutName[] = []
211
- for (let i = 0; i < currentNbPages; i++) {
212
  layouts.push(
213
  layoutName === "random"
214
  ? getRandomLayoutName()
215
- : layoutName
216
- )
217
-
218
- // TODO: update the number of total panels here!
219
  }
220
 
221
  set({
222
- layout,
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  layouts,
 
224
  })
225
  },
226
  setLayouts: (layouts: LayoutName[]) => set({ layouts }),
227
  setZoomLevel: (zoomLevel: number) => set({ zoomLevel }),
228
- setPage: (page: HTMLDivElement) => {
229
- if (!page) { return }
230
- set({ page })
231
- },
232
  setGeneratingStory: (isGeneratingStory: boolean) => set({ isGeneratingStory }),
233
  setGeneratingImages: (panelId: string, value: boolean) => {
234
  const panelGenerationStatus: Record<string, boolean> = {
@@ -244,6 +325,16 @@ export const useStore = create<{
244
  })
245
  },
246
  setGeneratingText: (isGeneratingText: boolean) => set({ isGeneratingText }),
 
 
 
 
 
 
 
 
 
 
247
  pageToImage: async () => {
248
  const { page } = get()
249
  if (!page) { return "" }
@@ -271,33 +362,37 @@ export const useStore = create<{
271
  window.open(data)
272
  }
273
  },
 
274
  generate: (prompt: string, presetName: PresetName, layoutName: LayoutName) => {
275
-
276
- const { currentNbPages } = get()
277
-
278
- const layout = layoutName === "random"
279
- ? getRandomLayoutName()
280
- : layoutName
281
 
282
  const layouts: LayoutName[] = []
283
- for (let i = 0; i < currentNbPages; i++) {
284
  layouts.push(
285
  layoutName === "random"
286
  ? getRandomLayoutName()
287
- : layoutName
288
- )
289
-
290
- // TODO: update the number of total panels here!
291
  }
292
 
293
  set({
294
- prompt,
 
 
 
295
  panels: [],
296
  captions: [],
 
 
 
 
 
 
 
 
297
  preset: presetName === "random"
298
  ? getRandomPreset()
299
  : getPreset(presetName),
300
- layout,
301
  layouts,
302
  })
303
  }
 
16
  maxNbPanelsPerPage: number
17
  currentNbPages: number
18
  maxNbPages: number
19
+ previousNbPanels: number
20
  currentNbPanels: number
21
  maxNbPanels: number
22
  panels: string[]
 
37
  setMaxNbPanelsPerPage: (maxNbPanelsPerPage: number) => void
38
  setCurrentNbPages: (currentNbPages: number) => void
39
  setMaxNbPages: (maxNbPages: number) => void
40
+ setPreviousNbPanels: (previousNbPanels: number) => void
41
  setCurrentNbPanels: (currentNbPanels: number) => void
42
  setMaxNbPanels: (maxNbPanels: number) => void
43
 
 
55
  setCaptions: (captions: string[]) => void
56
  setPanelCaption: (newCaption: string, index: number) => void
57
  setZoomLevel: (zoomLevel: number) => void
58
+
59
  setGeneratingStory: (isGeneratingStory: boolean) => void
60
  setGeneratingImages: (panelId: string, value: boolean) => void
61
  setGeneratingText: (isGeneratingText: boolean) => void
62
+
63
+ // I think we should deprecate those three functions
64
+ // this was used to keep track of the page HTML element,
65
+ // for use with a HTML-to-bitmap library
66
+ // but the CSS layout wasn't followed properly and it depended on the zoom level
67
+ // pageToImage: () => Promise<string>
68
+ // download: () => Promise<void>
69
+ // setPage: (page: HTMLDivElement) => void
70
+
71
  generate: (prompt: string, presetName: PresetName, layoutName: LayoutName) => void
72
  }>((set, get) => ({
73
  prompt: "",
 
78
  maxNbPanelsPerPage: 4,
79
  currentNbPages: 1,
80
  maxNbPages: 1,
81
+ previousNbPanels: 0,
82
  currentNbPanels: 4,
83
  maxNbPanels: 4,
84
 
 
87
  upscaleQueue: {} as Record<string, RenderedScene>,
88
  renderedScenes: {} as Record<string, RenderedScene>,
89
  showCaptions: false,
90
+
91
+ // deprecated?
92
  layout: defaultLayout,
93
+
94
+ layouts: [defaultLayout, defaultLayout, defaultLayout, defaultLayout],
95
+
96
  zoomLevel: 60,
97
+
98
+ // deprecated?
99
  page: undefined as unknown as HTMLDivElement,
100
+
101
  isGeneratingStory: false,
102
  panelGenerationStatus: {},
103
  isGeneratingText: false,
104
  atLeastOnePanelIsBusy: false,
105
 
 
106
  setCurrentNbPanelsPerPage: (currentNbPanelsPerPage: number) => {
107
  const { currentNbPages } = get()
108
  set({
 
118
  })
119
  },
120
  setCurrentNbPages: (currentNbPages: number) => {
121
+ const state = get()
122
+
123
+ const newCurrentNumberOfPages = Math.min(state.maxNbPages, currentNbPages)
124
+
125
+ const newCurrentNbPanels = state.currentNbPanelsPerPage * newCurrentNumberOfPages
126
+
127
+ /*
128
+ console.log(`setCurrentNbPages(${currentNbPages}): ${JSON.stringify({
129
+ "state.maxNbPages": state.maxNbPages,
130
  currentNbPages,
131
+ newCurrentNumberOfPages,
132
+ "state.currentNbPanelsPerPage": state.currentNbPanelsPerPage,
133
+ newCurrentNbPanels,
134
+ "state.currentNbPanels": state.currentNbPanels,
135
+ "state.previousNbPanels": state.previousNbPanels,
136
+ previousNbPanels:
137
+ newCurrentNbPanels > state.currentNbPanels ? state.currentNbPanels :
138
+ newCurrentNbPanels < state.currentNbPanels ? 0 :
139
+ state.previousNbPanels,
140
+
141
+ }, null, 2)}`)
142
+ */
143
+
144
+ set({
145
+ // we keep the previous number of panels for convenience
146
+ // so if we are adding a new panel,
147
+ // state.currentNbPanels gets copied to state.previousNbPanels
148
+ previousNbPanels:
149
+ newCurrentNbPanels > state.currentNbPanels ? state.currentNbPanels :
150
+ newCurrentNbPanels < state.currentNbPanels ? 0 :
151
+ state.previousNbPanels,
152
+
153
+ currentNbPanels: newCurrentNbPanels,
154
+ currentNbPages: newCurrentNumberOfPages,
155
+
156
  })
157
  },
158
  setMaxNbPages: (maxNbPages: number) => {
 
162
  maxNbPanels: maxNbPanelsPerPage * maxNbPages,
163
  })
164
  },
165
+ setPreviousNbPanels: (previousNbPanels: number) => {
166
+ set({
167
+ previousNbPanels
168
+ })
169
+ },
170
  setCurrentNbPanels: (currentNbPanels: number) => {
171
+ const state = get()
172
+
173
+
174
+ /*
175
+ console.log(`setCurrentNbPanels(${currentNbPanels}): ${JSON.stringify({
176
+ "state.maxNbPages": state.maxNbPages,
177
+ "state.currentNbPages": state.currentNbPages,
178
+ currentNbPanels,
179
+ "state.currentNbPanelsPerPage": state.currentNbPanelsPerPage,
180
+ "state.currentNbPanels": state.currentNbPanels,
181
+ "state.previousNbPanels": state.previousNbPanels,
182
+ previousNbPanels:
183
+ currentNbPanels > state.currentNbPanels ? state.currentNbPanels :
184
+ currentNbPanels < state.currentNbPanels ? 0 :
185
+ state.previousNbPanels,
186
+
187
+ }, null, 2)}`)
188
+ */
189
+
190
  set({
191
+ // we keep the previous number of panels for convenience
192
+ // so if we are adding a new panel,
193
+ // state.currentNbPanels gets copied to state.previousNbPanels
194
+ previousNbPanels:
195
+ currentNbPanels > state.currentNbPanels ? state.currentNbPanels :
196
+ currentNbPanels < state.currentNbPanels ? 0 :
197
+ state.previousNbPanels,
198
+
199
  currentNbPanels,
200
  })
201
  },
 
279
  })
280
  },
281
  setLayout: (layoutName: LayoutName) => {
282
+ const { maxNbPages, currentNbPanelsPerPage } = get()
 
 
 
 
 
283
 
284
  const layouts: LayoutName[] = []
285
+ for (let i = 0; i < maxNbPages; i++) {
286
  layouts.push(
287
  layoutName === "random"
288
  ? getRandomLayoutName()
289
+ : layoutName)
 
 
 
290
  }
291
 
292
  set({
293
+ // changing the layout isn't a free pass to generate tons of panels at once,
294
+ // so we reset pretty much everything
295
+ previousNbPanels: 0,
296
+ currentNbPages: 1,
297
+ currentNbPanels: currentNbPanelsPerPage,
298
+ panels: [],
299
+ captions: [],
300
+ upscaleQueue: {},
301
+ renderedScenes: {},
302
+ isGeneratingStory: false,
303
+ panelGenerationStatus: {},
304
+ isGeneratingText: false,
305
+ atLeastOnePanelIsBusy: false,
306
+
307
  layouts,
308
+ layout: layouts[0],
309
  })
310
  },
311
  setLayouts: (layouts: LayoutName[]) => set({ layouts }),
312
  setZoomLevel: (zoomLevel: number) => set({ zoomLevel }),
 
 
 
 
313
  setGeneratingStory: (isGeneratingStory: boolean) => set({ isGeneratingStory }),
314
  setGeneratingImages: (panelId: string, value: boolean) => {
315
  const panelGenerationStatus: Record<string, boolean> = {
 
325
  })
326
  },
327
  setGeneratingText: (isGeneratingText: boolean) => set({ isGeneratingText }),
328
+
329
+ // I think we should deprecate those three functions
330
+ // this was used to keep track of the page HTML element,
331
+ // for use with a HTML-to-bitmap library
332
+ // but the CSS layout wasn't followed properly and it depended on the zoom level
333
+ /*
334
+ setPage: (page: HTMLDivElement) => {
335
+ if (!page) { return }
336
+ set({ page })
337
+ },
338
  pageToImage: async () => {
339
  const { page } = get()
340
  if (!page) { return "" }
 
362
  window.open(data)
363
  }
364
  },
365
+ */
366
  generate: (prompt: string, presetName: PresetName, layoutName: LayoutName) => {
367
+ const { maxNbPages, currentNbPanelsPerPage } = get()
 
 
 
 
 
368
 
369
  const layouts: LayoutName[] = []
370
+ for (let i = 0; i < maxNbPages; i++) {
371
  layouts.push(
372
  layoutName === "random"
373
  ? getRandomLayoutName()
374
+ : layoutName)
 
 
 
375
  }
376
 
377
  set({
378
+ // we reset pretty much everything
379
+ previousNbPanels: 0,
380
+ currentNbPages: 1,
381
+ currentNbPanels: currentNbPanelsPerPage,
382
  panels: [],
383
  captions: [],
384
+ upscaleQueue: {},
385
+ renderedScenes: {},
386
+ isGeneratingStory: false,
387
+ panelGenerationStatus: {},
388
+ isGeneratingText: false,
389
+ atLeastOnePanelIsBusy: false,
390
+
391
+ prompt,
392
  preset: presetName === "random"
393
  ? getRandomPreset()
394
  : getPreset(presetName),
395
+ layout: layouts[0],
396
  layouts,
397
  })
398
  }