Commit
•
1c1e6e9
1
Parent(s):
f6f68f6
slight UI redesign
Browse files- .env +1 -1
- README.md +4 -4
- src/app/globals.css +4 -5
- src/app/interface/about/index.tsx +16 -16
- src/app/interface/page/index.tsx +19 -10
- src/app/interface/settings-dialog/defaultSettings.ts +1 -0
- src/app/interface/settings-dialog/getSettings.ts +3 -1
- src/app/interface/settings-dialog/index.tsx +75 -42
- src/app/interface/settings-dialog/label.tsx +1 -1
- src/app/interface/settings-dialog/localStorageKeys.ts +1 -0
- src/app/interface/sign-up-cta/index.tsx +8 -0
- src/app/interface/sign-up-cta/sign-up-cta.tsx +22 -0
- src/app/interface/top-menu/index.tsx +22 -12
- src/app/interface/zoom/index.tsx +2 -2
- src/app/layout.tsx +2 -4
- src/app/main.tsx +49 -12
- src/app/queries/getDynamicConfig.ts +2 -2
- src/app/queries/getStoryContinuation.ts +3 -3
- src/app/queries/predictNextPanels.ts +4 -4
- src/app/store/index.ts +60 -25
- src/components/ui/dialog.tsx +1 -1
- src/components/ui/select.tsx +3 -3
- src/lib/usePageOrientation.ts +38 -0
- src/types.ts +1 -0
- tailwind.config.js +10 -0
.env
CHANGED
@@ -82,7 +82,7 @@ LLM_OPENAI_API_MODEL="gpt-4"
|
|
82 |
LLM_HF_INFERENCE_ENDPOINT_URL=""
|
83 |
|
84 |
# If you decided to use a Hugging Face Inference API model for the LLM engine
|
85 |
-
# LLM_HF_INFERENCE_API_MODEL="
|
86 |
LLM_HF_INFERENCE_API_MODEL="HuggingFaceH4/zephyr-7b-beta"
|
87 |
|
88 |
# ----------- COMMUNITY SHARING (OPTIONAL) -----------
|
|
|
82 |
LLM_HF_INFERENCE_ENDPOINT_URL=""
|
83 |
|
84 |
# If you decided to use a Hugging Face Inference API model for the LLM engine
|
85 |
+
# LLM_HF_INFERENCE_API_MODEL="HuggingFaceH4/zephyr-7b-beta"
|
86 |
LLM_HF_INFERENCE_API_MODEL="HuggingFaceH4/zephyr-7b-beta"
|
87 |
|
88 |
# ----------- COMMUNITY SHARING (OPTIONAL) -----------
|
README.md
CHANGED
@@ -70,13 +70,13 @@ To customise a variable locally, you should create a `.env.local`
|
|
70 |
|
71 |
## The LLM API (Large Language Model)
|
72 |
|
73 |
-
Currently the AI Comic Factory uses [
|
74 |
|
75 |
You have three options:
|
76 |
|
77 |
### Option 1: Use an Inference API model
|
78 |
|
79 |
-
This is a new option added recently, where you can use one of the models from the Hugging Face Hub. By default we suggest to use
|
80 |
|
81 |
To activate it, create a `.env.local` configuration file:
|
82 |
|
@@ -85,10 +85,10 @@ LLM_ENGINE="INFERENCE_API"
|
|
85 |
|
86 |
HF_API_TOKEN="Your Hugging Face token"
|
87 |
|
88 |
-
#
|
89 |
# note: You should use a model able to generate JSON responses,
|
90 |
# so it is storngly suggested to use at least the 34b model
|
91 |
-
HF_INFERENCE_API_MODEL="
|
92 |
```
|
93 |
|
94 |
### Option 2: Use an Inference Endpoint URL
|
|
|
70 |
|
71 |
## The LLM API (Large Language Model)
|
72 |
|
73 |
+
Currently the AI Comic Factory uses [zephyr-7b-beta](https://huggingface.co/HuggingFaceH4/zephyr-7b-beta) through an [Inference Endpoint](https://huggingface.co/docs/inference-endpoints/index).
|
74 |
|
75 |
You have three options:
|
76 |
|
77 |
### Option 1: Use an Inference API model
|
78 |
|
79 |
+
This is a new option added recently, where you can use one of the models from the Hugging Face Hub. By default we suggest to use [zephyr-7b-beta](https://huggingface.co/HuggingFaceH4/zephyr-7b-beta) as it will provide better results than the 7b model.
|
80 |
|
81 |
To activate it, create a `.env.local` configuration file:
|
82 |
|
|
|
85 |
|
86 |
HF_API_TOKEN="Your Hugging Face token"
|
87 |
|
88 |
+
# "HuggingFaceH4/zephyr-7b-beta" is used by default, but you can change this
|
89 |
# note: You should use a model able to generate JSON responses,
|
90 |
# so it is storngly suggested to use at least the 34b model
|
91 |
+
HF_INFERENCE_API_MODEL="HuggingFaceH4/zephyr-7b-beta"
|
92 |
```
|
93 |
|
94 |
### Option 2: Use an Inference Endpoint URL
|
src/app/globals.css
CHANGED
@@ -28,13 +28,12 @@ body {
|
|
28 |
|
29 |
/* this is the trick to bypass the style={{}} attribute when printing */
|
30 |
@media print {
|
31 |
-
.comic-page, .comic-page[style] {
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
}
|
36 |
|
37 |
-
|
38 |
.render-to-image .comic-panel {
|
39 |
height: auto !important;
|
40 |
/* max-width: fit-content !important; */
|
|
|
28 |
|
29 |
/* this is the trick to bypass the style={{}} attribute when printing */
|
30 |
@media print {
|
31 |
+
.comic-page, .comic-page[style] {
|
32 |
+
width: 100vw !important;
|
33 |
+
page-break-before: always;
|
34 |
+
}
|
35 |
}
|
36 |
|
|
|
37 |
.render-to-image .comic-panel {
|
38 |
height: auto !important;
|
39 |
/* max-width: fit-content !important; */
|
src/app/interface/about/index.tsx
CHANGED
@@ -10,35 +10,35 @@ export function About() {
|
|
10 |
<Dialog open={isOpen} onOpenChange={setOpen}>
|
11 |
<DialogTrigger asChild>
|
12 |
<Button variant="outline">
|
13 |
-
<span className="hidden md:inline">
|
14 |
-
<span className="inline md:hidden">
|
15 |
</Button>
|
16 |
</DialogTrigger>
|
17 |
<DialogContent className="sm:max-w-[425px] md:max-w-[600px]">
|
18 |
<DialogHeader>
|
19 |
-
<DialogTitle>
|
20 |
-
<DialogDescription className="w-full text-center text-
|
21 |
-
|
22 |
</DialogDescription>
|
23 |
</DialogHeader>
|
24 |
-
<div className="grid gap-4 py-4 text-stone-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
<p>
|
29 |
-
|
30 |
</p>
|
31 |
-
<p>
|
32 |
-
|
33 |
</p>
|
34 |
<p>
|
35 |
-
👉
|
36 |
</p>
|
37 |
<p>
|
38 |
-
👉
|
39 |
</p>
|
40 |
-
<p>
|
41 |
-
|
42 |
</p>
|
43 |
</div>
|
44 |
<DialogFooter>
|
|
|
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>
|
44 |
<DialogFooter>
|
src/app/interface/page/index.tsx
CHANGED
@@ -15,19 +15,20 @@ export function Page({ page }: { page: number}) {
|
|
15 |
const LayoutElement = (allLayouts as any)[layout]
|
16 |
const aspectRatio = ((allLayoutAspectRatios as any)[layout] as string) || "aspect-[250/297]"
|
17 |
|
18 |
-
const
|
19 |
-
const
|
|
|
20 |
|
21 |
// in the future, different layouts might have different numbers of panels
|
22 |
const allLayoutsNbPanels = {
|
23 |
-
Layout0:
|
24 |
-
Layout1:
|
25 |
-
Layout2:
|
26 |
-
Layout3:
|
27 |
-
// Layout4:
|
28 |
}
|
29 |
|
30 |
-
const
|
31 |
|
32 |
/*
|
33 |
const [canLoad, setCanLoad] = useState(false)
|
@@ -50,6 +51,14 @@ export function Page({ page }: { page: number}) {
|
|
50 |
setPage(element)
|
51 |
}, [pageRef.current])
|
52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
53 |
return (
|
54 |
<div
|
55 |
ref={pageRef}
|
@@ -77,9 +86,9 @@ export function Page({ page }: { page: number}) {
|
|
77 |
// marginLeft: `${zoomLevel > 100 ? `100`}`
|
78 |
}}
|
79 |
>
|
80 |
-
<LayoutElement page={page} nbPanels={
|
81 |
</div>
|
82 |
-
{
|
83 |
<p className="w-full text-center pt-4 font-sans text-2xs font-semibold text-stone-600">
|
84 |
Page {page + 1}
|
85 |
{/*
|
|
|
15 |
const LayoutElement = (allLayouts as any)[layout]
|
16 |
const aspectRatio = ((allLayoutAspectRatios as any)[layout] as string) || "aspect-[250/297]"
|
17 |
|
18 |
+
const currentNbPages = useStore(s => s.currentNbPages)
|
19 |
+
const maxNbPages = useStore(s => s.maxNbPages)
|
20 |
+
const currentNbPanelsPerPage = useStore(s => s.currentNbPanelsPerPage)
|
21 |
|
22 |
// in the future, different layouts might have different numbers of panels
|
23 |
const allLayoutsNbPanels = {
|
24 |
+
Layout0: currentNbPanelsPerPage,
|
25 |
+
Layout1: currentNbPanelsPerPage,
|
26 |
+
Layout2: currentNbPanelsPerPage,
|
27 |
+
Layout3: currentNbPanelsPerPage,
|
28 |
+
// Layout4: currentNbPanelsPerPage
|
29 |
}
|
30 |
|
31 |
+
const currentNbPanels = ((allLayoutsNbPanels as any)[layout] as number) || currentNbPanelsPerPage
|
32 |
|
33 |
/*
|
34 |
const [canLoad, setCanLoad] = useState(false)
|
|
|
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}
|
|
|
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 |
{/*
|
src/app/interface/settings-dialog/defaultSettings.ts
CHANGED
@@ -18,4 +18,5 @@ export const defaultSettings: Settings = {
|
|
18 |
groqApiKey: "",
|
19 |
groqApiLanguageModel: "mixtral-8x7b-32768",
|
20 |
hasGeneratedAtLeastOnce: false,
|
|
|
21 |
}
|
|
|
18 |
groqApiKey: "",
|
19 |
groqApiLanguageModel: "mixtral-8x7b-32768",
|
20 |
hasGeneratedAtLeastOnce: false,
|
21 |
+
userDefinedMaxNumberOfPages: 1,
|
22 |
}
|
src/app/interface/settings-dialog/getSettings.ts
CHANGED
@@ -4,6 +4,7 @@ import { getValidString } from "@/lib/getValidString"
|
|
4 |
import { localStorageKeys } from "./localStorageKeys"
|
5 |
import { defaultSettings } from "./defaultSettings"
|
6 |
import { getValidBoolean } from "@/lib/getValidBoolean"
|
|
|
7 |
|
8 |
export function getSettings(): Settings {
|
9 |
try {
|
@@ -24,7 +25,8 @@ export function getSettings(): Settings {
|
|
24 |
openaiApiLanguageModel: getValidString(localStorage?.getItem?.(localStorageKeys.openaiApiLanguageModel), defaultSettings.openaiApiLanguageModel),
|
25 |
groqApiKey: getValidString(localStorage?.getItem?.(localStorageKeys.groqApiKey), defaultSettings.groqApiKey),
|
26 |
groqApiLanguageModel: getValidString(localStorage?.getItem?.(localStorageKeys.groqApiLanguageModel), defaultSettings.groqApiLanguageModel),
|
27 |
-
hasGeneratedAtLeastOnce: getValidBoolean(localStorage?.getItem?.(localStorageKeys.hasGeneratedAtLeastOnce), defaultSettings.hasGeneratedAtLeastOnce),
|
|
|
28 |
}
|
29 |
} catch (err) {
|
30 |
return {
|
|
|
4 |
import { localStorageKeys } from "./localStorageKeys"
|
5 |
import { defaultSettings } from "./defaultSettings"
|
6 |
import { getValidBoolean } from "@/lib/getValidBoolean"
|
7 |
+
import { getValidNumber } from "@/lib/getValidNumber"
|
8 |
|
9 |
export function getSettings(): Settings {
|
10 |
try {
|
|
|
25 |
openaiApiLanguageModel: getValidString(localStorage?.getItem?.(localStorageKeys.openaiApiLanguageModel), defaultSettings.openaiApiLanguageModel),
|
26 |
groqApiKey: getValidString(localStorage?.getItem?.(localStorageKeys.groqApiKey), defaultSettings.groqApiKey),
|
27 |
groqApiLanguageModel: getValidString(localStorage?.getItem?.(localStorageKeys.groqApiLanguageModel), defaultSettings.groqApiLanguageModel),
|
28 |
+
hasGeneratedAtLeastOnce: getValidBoolean(localStorage?.getItem?.(localStorageKeys.hasGeneratedAtLeastOnce), defaultSettings.hasGeneratedAtLeastOnce),
|
29 |
+
userDefinedMaxNumberOfPages: getValidNumber(localStorage?.getItem?.(localStorageKeys.userDefinedMaxNumberOfPages), 1, Number.MAX_SAFE_INTEGER, defaultSettings.userDefinedMaxNumberOfPages),
|
30 |
}
|
31 |
} catch (err) {
|
32 |
return {
|
src/app/interface/settings-dialog/index.tsx
CHANGED
@@ -1,3 +1,5 @@
|
|
|
|
|
|
1 |
import { useState } from "react"
|
2 |
import { useLocalStorage } from 'usehooks-ts'
|
3 |
|
@@ -18,8 +20,10 @@ import { Label } from "./label"
|
|
18 |
import { Field } from "./field"
|
19 |
import { localStorageKeys } from "./localStorageKeys"
|
20 |
import { defaultSettings } from "./defaultSettings"
|
21 |
-
|
22 |
-
import {
|
|
|
|
|
23 |
|
24 |
export function SettingsDialog() {
|
25 |
const [isOpen, setOpen] = useState(false)
|
@@ -71,6 +75,12 @@ export function SettingsDialog() {
|
|
71 |
localStorageKeys.openaiApiModel,
|
72 |
defaultSettings.openaiApiModel
|
73 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
74 |
|
75 |
return (
|
76 |
<Dialog open={isOpen} onOpenChange={setOpen}>
|
@@ -87,15 +97,32 @@ export function SettingsDialog() {
|
|
87 |
Custom Models
|
88 |
</DialogDescription>
|
89 |
</DialogHeader>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
90 |
<div className="grid gap-4 py-1 space-y-1 text-stone-800">
|
91 |
-
<p className="text-sm text-zinc-700">
|
92 |
-
Note: most vendors have a warm-up delay when using a custom or rarely used model. Do not hesitate to try again after 5 minutes if that happens.
|
93 |
-
</p>
|
94 |
-
<p className="text-sm text-zinc-700">
|
95 |
-
Security note: we do not save your API credentials on our server but inside your web browser, using the local storage.
|
96 |
-
</p>
|
97 |
<Field>
|
98 |
<Label>Image rendering provider:</Label>
|
|
|
|
|
|
|
|
|
|
|
99 |
<Select
|
100 |
onValueChange={(value: string) => {
|
101 |
setRenderingModelVendor(value as RenderingModelVendor)
|
@@ -115,36 +142,37 @@ export function SettingsDialog() {
|
|
115 |
|
116 |
|
117 |
{
|
118 |
-
renderingModelVendor === "SERVER" && <>
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
</>
|
|
|
142 |
|
143 |
{renderingModelVendor === "HUGGINGFACE" && <>
|
144 |
<Field>
|
145 |
<Label>Hugging Face API Token (<a className="text-stone-600 underline" href="https://huggingface.co/subscribe/pro" target="_blank">PRO account</a> recommended for higher rate limit):</Label>
|
146 |
<Input
|
147 |
-
className=
|
148 |
type="password"
|
149 |
placeholder="Enter your private api token"
|
150 |
onChange={(x) => {
|
@@ -156,7 +184,7 @@ export function SettingsDialog() {
|
|
156 |
<Field>
|
157 |
<Label>Inference API model (custom SDXL or SDXL LoRA):</Label>
|
158 |
<Input
|
159 |
-
className=
|
160 |
placeholder="Name of the Inference API model"
|
161 |
onChange={(x) => {
|
162 |
setHuggingfaceInferenceApiModel(x.target.value)
|
@@ -167,7 +195,7 @@ export function SettingsDialog() {
|
|
167 |
<Field>
|
168 |
<Label>The file type supported by the model (jpg, webp..):</Label>
|
169 |
<Input
|
170 |
-
className=
|
171 |
placeholder="Inference API file type"
|
172 |
onChange={(x) => {
|
173 |
setHuggingfaceInferenceApiFileType(x.target.value)
|
@@ -181,7 +209,7 @@ export function SettingsDialog() {
|
|
181 |
<Field>
|
182 |
<Label>LoRA model trigger (optional):</Label>
|
183 |
<Input
|
184 |
-
className=
|
185 |
placeholder="Trigger keyword (if you use a LoRA)"
|
186 |
onChange={(x) => {
|
187 |
setHuggingfaceInferenceApiModelTrigger(x.target.value)
|
@@ -195,7 +223,7 @@ export function SettingsDialog() {
|
|
195 |
<Field>
|
196 |
<Label>OpenAI API Token (you will be billed based on OpenAI pricing):</Label>
|
197 |
<Input
|
198 |
-
className=
|
199 |
type="password"
|
200 |
placeholder="Enter your private api token"
|
201 |
onChange={(x) => {
|
@@ -207,7 +235,7 @@ export function SettingsDialog() {
|
|
207 |
<Field>
|
208 |
<Label>OpenAI image model:</Label>
|
209 |
<Input
|
210 |
-
className=
|
211 |
placeholder="OpenAI image model"
|
212 |
onChange={(x) => {
|
213 |
setOpenaiApiModel(x.target.value)
|
@@ -221,7 +249,7 @@ export function SettingsDialog() {
|
|
221 |
<Field>
|
222 |
<Label>Replicate API Token (you will be billed based on Replicate pricing):</Label>
|
223 |
<Input
|
224 |
-
className=
|
225 |
type="password"
|
226 |
placeholder="Enter your private api token"
|
227 |
onChange={(x) => {
|
@@ -233,7 +261,7 @@ export function SettingsDialog() {
|
|
233 |
<Field>
|
234 |
<Label>Replicate model name:</Label>
|
235 |
<Input
|
236 |
-
className=
|
237 |
placeholder="Name of the Replicate model"
|
238 |
onChange={(x) => {
|
239 |
setReplicateApiModel(x.target.value)
|
@@ -244,7 +272,7 @@ export function SettingsDialog() {
|
|
244 |
<Field>
|
245 |
<Label>Model version:</Label>
|
246 |
<Input
|
247 |
-
className=
|
248 |
placeholder="Version of the Replicate model"
|
249 |
onChange={(x) => {
|
250 |
setReplicateApiModelVersion(x.target.value)
|
@@ -258,7 +286,7 @@ export function SettingsDialog() {
|
|
258 |
<Field>
|
259 |
<Label>LoRA model trigger (optional):</Label>
|
260 |
<Input
|
261 |
-
className=
|
262 |
placeholder={'Eg. "In the style of TOK" etc'}
|
263 |
onChange={(x) => {
|
264 |
setReplicateApiModelTrigger(x.target.value)
|
@@ -267,7 +295,12 @@ export function SettingsDialog() {
|
|
267 |
/>
|
268 |
</Field>
|
269 |
</>}
|
|
|
|
|
|
|
|
|
270 |
</div>
|
|
|
271 |
<DialogFooter>
|
272 |
<Button type="submit" onClick={() => setOpen(false)}>Close</Button>
|
273 |
</DialogFooter>
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
import { useState } from "react"
|
4 |
import { useLocalStorage } from 'usehooks-ts'
|
5 |
|
|
|
20 |
import { Field } from "./field"
|
21 |
import { localStorageKeys } from "./localStorageKeys"
|
22 |
import { defaultSettings } from "./defaultSettings"
|
23 |
+
|
24 |
+
import { useDynamicConfig } from "@/lib/useDynamicConfig"
|
25 |
+
import { Slider } from "@/components/ui/slider"
|
26 |
+
import { fonts } from "@/lib/fonts"
|
27 |
|
28 |
export function SettingsDialog() {
|
29 |
const [isOpen, setOpen] = useState(false)
|
|
|
75 |
localStorageKeys.openaiApiModel,
|
76 |
defaultSettings.openaiApiModel
|
77 |
)
|
78 |
+
const [userDefinedMaxNumberOfPages, setUserDefinedMaxNumberOfPages] = useLocalStorage<number>(
|
79 |
+
localStorageKeys.userDefinedMaxNumberOfPages,
|
80 |
+
defaultSettings.userDefinedMaxNumberOfPages
|
81 |
+
)
|
82 |
+
|
83 |
+
const { config: { maxNbPages }, isConfigReady } = useDynamicConfig()
|
84 |
|
85 |
return (
|
86 |
<Dialog open={isOpen} onOpenChange={setOpen}>
|
|
|
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">
|
122 |
+
ℹ️ Some API vendors have a delay for rarely used models.<br/>
|
123 |
+
👉 In case of trouble, try again after 5-10 minutes.
|
124 |
+
</p>
|
125 |
+
|
126 |
<Select
|
127 |
onValueChange={(value: string) => {
|
128 |
setRenderingModelVendor(value as RenderingModelVendor)
|
|
|
142 |
|
143 |
|
144 |
{
|
145 |
+
// renderingModelVendor === "SERVER" && <>
|
146 |
+
// <Field>
|
147 |
+
// <Label>Quality over performance ratio (beta, deprecated):</Label>
|
148 |
+
// <div className="flex flex-row space-x-2 text-zinc-500">
|
149 |
+
// <Switch
|
150 |
+
// // checked={renderingUseTurbo}
|
151 |
+
// // onCheckedChange={setRenderingUseTurbo}
|
152 |
+
// checked={false}
|
153 |
+
// disabled
|
154 |
+
// className="opacity-30 pointer-events-none"
|
155 |
+
// />
|
156 |
+
// {/*
|
157 |
+
// <span
|
158 |
+
// onClick={() => setRenderingUseTurbo(!renderingUseTurbo)}
|
159 |
+
// className={cn("cursor-pointer", { "text-zinc-800": renderingUseTurbo })}>
|
160 |
+
// Use a faster, but lower quality model (you are warned!)
|
161 |
+
// </span>
|
162 |
+
// */}
|
163 |
+
// <span className="text-zinc-500 italic">
|
164 |
+
// Following feedbacks from users (low rendering quality on comics) the fast renderer has been disabled.
|
165 |
+
// </span>
|
166 |
+
// </div>
|
167 |
+
// </Field>
|
168 |
+
// </>
|
169 |
+
}
|
170 |
|
171 |
{renderingModelVendor === "HUGGINGFACE" && <>
|
172 |
<Field>
|
173 |
<Label>Hugging Face API Token (<a className="text-stone-600 underline" href="https://huggingface.co/subscribe/pro" target="_blank">PRO account</a> recommended for higher rate limit):</Label>
|
174 |
<Input
|
175 |
+
className={fonts.actionman.className}
|
176 |
type="password"
|
177 |
placeholder="Enter your private api token"
|
178 |
onChange={(x) => {
|
|
|
184 |
<Field>
|
185 |
<Label>Inference API model (custom SDXL or SDXL LoRA):</Label>
|
186 |
<Input
|
187 |
+
className={fonts.actionman.className}
|
188 |
placeholder="Name of the Inference API model"
|
189 |
onChange={(x) => {
|
190 |
setHuggingfaceInferenceApiModel(x.target.value)
|
|
|
195 |
<Field>
|
196 |
<Label>The file type supported by the model (jpg, webp..):</Label>
|
197 |
<Input
|
198 |
+
className={fonts.actionman.className}
|
199 |
placeholder="Inference API file type"
|
200 |
onChange={(x) => {
|
201 |
setHuggingfaceInferenceApiFileType(x.target.value)
|
|
|
209 |
<Field>
|
210 |
<Label>LoRA model trigger (optional):</Label>
|
211 |
<Input
|
212 |
+
className={fonts.actionman.className}
|
213 |
placeholder="Trigger keyword (if you use a LoRA)"
|
214 |
onChange={(x) => {
|
215 |
setHuggingfaceInferenceApiModelTrigger(x.target.value)
|
|
|
223 |
<Field>
|
224 |
<Label>OpenAI API Token (you will be billed based on OpenAI pricing):</Label>
|
225 |
<Input
|
226 |
+
className={fonts.actionman.className}
|
227 |
type="password"
|
228 |
placeholder="Enter your private api token"
|
229 |
onChange={(x) => {
|
|
|
235 |
<Field>
|
236 |
<Label>OpenAI image model:</Label>
|
237 |
<Input
|
238 |
+
className={fonts.actionman.className}
|
239 |
placeholder="OpenAI image model"
|
240 |
onChange={(x) => {
|
241 |
setOpenaiApiModel(x.target.value)
|
|
|
249 |
<Field>
|
250 |
<Label>Replicate API Token (you will be billed based on Replicate pricing):</Label>
|
251 |
<Input
|
252 |
+
className={fonts.actionman.className}
|
253 |
type="password"
|
254 |
placeholder="Enter your private api token"
|
255 |
onChange={(x) => {
|
|
|
261 |
<Field>
|
262 |
<Label>Replicate model name:</Label>
|
263 |
<Input
|
264 |
+
className={fonts.actionman.className}
|
265 |
placeholder="Name of the Replicate model"
|
266 |
onChange={(x) => {
|
267 |
setReplicateApiModel(x.target.value)
|
|
|
272 |
<Field>
|
273 |
<Label>Model version:</Label>
|
274 |
<Input
|
275 |
+
className={fonts.actionman.className}
|
276 |
placeholder="Version of the Replicate model"
|
277 |
onChange={(x) => {
|
278 |
setReplicateApiModelVersion(x.target.value)
|
|
|
286 |
<Field>
|
287 |
<Label>LoRA model trigger (optional):</Label>
|
288 |
<Input
|
289 |
+
className={fonts.actionman.className}
|
290 |
placeholder={'Eg. "In the style of TOK" etc'}
|
291 |
onChange={(x) => {
|
292 |
setReplicateApiModelTrigger(x.target.value)
|
|
|
295 |
/>
|
296 |
</Field>
|
297 |
</>}
|
298 |
+
|
299 |
+
<p className="text-sm text-zinc-700 italic">
|
300 |
+
🔒 Settings such as API keys are stored inside your browser and aren't kept on our servers.
|
301 |
+
</p>
|
302 |
</div>
|
303 |
+
|
304 |
<DialogFooter>
|
305 |
<Button type="submit" onClick={() => setOpen(false)}>Close</Button>
|
306 |
</DialogFooter>
|
src/app/interface/settings-dialog/label.tsx
CHANGED
@@ -2,6 +2,6 @@ import { ReactNode } from "react"
|
|
2 |
|
3 |
export function Label({ children }: { children: ReactNode }) {
|
4 |
return (
|
5 |
-
<label className="text-
|
6 |
)
|
7 |
}
|
|
|
2 |
|
3 |
export function Label({ children }: { children: ReactNode }) {
|
4 |
return (
|
5 |
+
<label className="text-xl font-semibold text-zinc-700">{children}</label>
|
6 |
)
|
7 |
}
|
src/app/interface/settings-dialog/localStorageKeys.ts
CHANGED
@@ -18,4 +18,5 @@ export const localStorageKeys: Record<keyof Settings, string> = {
|
|
18 |
groqApiKey: "CONF_AUTH_GROQ_API_KEY",
|
19 |
groqApiLanguageModel: "CONF_AUTH_GROQ_API_LANGUAGE_MODEL",
|
20 |
hasGeneratedAtLeastOnce: "CONF_HAS_GENERATED_AT_LEAST_ONCE",
|
|
|
21 |
}
|
|
|
18 |
groqApiKey: "CONF_AUTH_GROQ_API_KEY",
|
19 |
groqApiLanguageModel: "CONF_AUTH_GROQ_API_LANGUAGE_MODEL",
|
20 |
hasGeneratedAtLeastOnce: "CONF_HAS_GENERATED_AT_LEAST_ONCE",
|
21 |
+
userDefinedMaxNumberOfPages: "CONF_USER_DEFINED_MAX_NUMBER_OF_PAGES"
|
22 |
}
|
src/app/interface/sign-up-cta/index.tsx
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import dynamic from "next/dynamic";
|
4 |
+
|
5 |
+
export const SignUpCTA = dynamic(() => import("./sign-up-cta"), {
|
6 |
+
// Make sure we turn SSR off
|
7 |
+
ssr: false,
|
8 |
+
});
|
src/app/interface/sign-up-cta/sign-up-cta.tsx
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useOAuth } from "@/lib/useOAuth"
|
2 |
+
import { cn } from "@/lib/utils"
|
3 |
+
|
4 |
+
function SignUpCTA() {
|
5 |
+
const { login, isLoggedIn } = useOAuth({ debug: false })
|
6 |
+
if (isLoggedIn) { return null }
|
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
|
14 |
+
onClick={login}
|
15 |
+
className="underline underline-offset-2 cursor-pointer text-sky-800 dark:text-sky-800 hover:text-sky-700 hover:dark:text-sky-700"
|
16 |
+
>Sign-up to Hugging Face</span> to make more!
|
17 |
+
</div>
|
18 |
+
</div>
|
19 |
+
)
|
20 |
+
}
|
21 |
+
|
22 |
+
export default SignUpCTA
|
src/app/interface/top-menu/index.tsx
CHANGED
@@ -126,13 +126,14 @@ export function TopMenu() {
|
|
126 |
`backdrop-blur-xl`,
|
127 |
`transition-all duration-200 ease-in-out`,
|
128 |
`px-2 py-2 border-b-1 border-gray-50 dark:border-gray-50`,
|
129 |
-
|
|
|
130 |
`space-y-2 md:space-y-0 md:space-x-3 lg:space-x-6`
|
131 |
)}>
|
132 |
<div className="flex flex-row space-x-2 md:space-x-3 w-full md:w-auto">
|
133 |
<div className={cn(
|
134 |
`transition-all duration-200 ease-in-out`,
|
135 |
-
`flex flex-row items-center justify-start space-x-3
|
136 |
`flex-grow`
|
137 |
)}>
|
138 |
|
@@ -143,7 +144,7 @@ export function TopMenu() {
|
|
143 |
onValueChange={(value) => { setDraftPreset(value as PresetName) }}
|
144 |
disabled={isBusy}
|
145 |
>
|
146 |
-
<SelectTrigger className="flex-grow">
|
147 |
<SelectValue className="text-2xs md:text-sm" placeholder="Style" />
|
148 |
</SelectTrigger>
|
149 |
<SelectContent>
|
@@ -155,7 +156,7 @@ export function TopMenu() {
|
|
155 |
</div>
|
156 |
<div className={cn(
|
157 |
`transition-all duration-200 ease-in-out`,
|
158 |
-
`flex flex-row items-center justify-start space-x-3
|
159 |
`w-40`
|
160 |
)}>
|
161 |
|
@@ -166,13 +167,13 @@ export function TopMenu() {
|
|
166 |
onValueChange={(value) => { setDraftLayout(value as LayoutName) }}
|
167 |
disabled={isBusy}
|
168 |
>
|
169 |
-
<SelectTrigger className="flex-grow">
|
170 |
<SelectValue className="text-2xs md:text-sm" placeholder="Layout" />
|
171 |
</SelectTrigger>
|
172 |
<SelectContent>
|
173 |
{nonRandomLayouts.map(key =>
|
174 |
<SelectItem key={key} value={key} className="w-full">
|
175 |
-
<div className="space-x-6 flex flex-row items-center justify-between
|
176 |
<div className="flex">{
|
177 |
(allLayoutLabels as any)[key]
|
178 |
}</div>
|
@@ -197,7 +198,7 @@ export function TopMenu() {
|
|
197 |
checked={showCaptions}
|
198 |
onCheckedChange={setShowCaptions}
|
199 |
/>
|
200 |
-
<Label>
|
201 |
<span className="hidden md:inline">Caption</span>
|
202 |
<span className="inline md:hidden">Cap.</span>
|
203 |
</Label>
|
@@ -205,7 +206,7 @@ export function TopMenu() {
|
|
205 |
{/*
|
206 |
<div className={cn(
|
207 |
`transition-all duration-200 ease-in-out`,
|
208 |
-
`flex flex-row items-center space-x-3
|
209 |
)}>
|
210 |
<Label className="flex text-2xs md:text-sm md:w-24">Font:</Label>
|
211 |
<Select
|
@@ -232,13 +233,17 @@ export function TopMenu() {
|
|
232 |
</div>
|
233 |
<div className={cn(
|
234 |
`transition-all duration-200 ease-in-out`,
|
235 |
-
`flex flex-grow flex-col space-y-2 md:space-y-0 md:flex-row items-center md:space-x-3
|
236 |
)}>
|
237 |
<div className="flex flex-row flex-grow w-full">
|
238 |
<div className="flex flex-row flex-grow w-full">
|
239 |
<Input
|
240 |
placeholder="1. Story (eg. detective dog)"
|
241 |
-
className=
|
|
|
|
|
|
|
|
|
242 |
// disabled={atLeastOnePanelIsBusy}
|
243 |
onChange={(e) => {
|
244 |
setDraftPromptB(e.target.value)
|
@@ -252,7 +257,11 @@ export function TopMenu() {
|
|
252 |
/>
|
253 |
<Input
|
254 |
placeholder="2. Style (eg 'rain, shiba')"
|
255 |
-
className=
|
|
|
|
|
|
|
|
|
256 |
// disabled={atLeastOnePanelIsBusy}
|
257 |
onChange={(e) => {
|
258 |
setDraftPromptA(e.target.value)
|
@@ -269,6 +278,7 @@ export function TopMenu() {
|
|
269 |
className={cn(
|
270 |
`rounded-l-none cursor-pointer`,
|
271 |
`transition-all duration-200 ease-in-out`,
|
|
|
272 |
`bg-[rgb(59,134,247)] hover:bg-[rgb(69,144,255)] disabled:bg-[rgb(59,134,247)]`
|
273 |
)}
|
274 |
onClick={() => {
|
@@ -287,7 +297,7 @@ export function TopMenu() {
|
|
287 |
are confused about why they can't activate it
|
288 |
<div className={cn(
|
289 |
`transition-all duration-200 ease-in-out`,
|
290 |
-
`hidden md:flex flex-row items-center space-x-3
|
291 |
)}>
|
292 |
<Label className="flex text-2xs md:text-sm w-24">Font:</Label>
|
293 |
<Select
|
|
|
126 |
`backdrop-blur-xl`,
|
127 |
`transition-all duration-200 ease-in-out`,
|
128 |
`px-2 py-2 border-b-1 border-gray-50 dark:border-gray-50`,
|
129 |
+
//`bg-[#2d435c] dark:bg-[#2d435c] text-gray-50 dark:text-gray-50`,
|
130 |
+
`bg-gradient-to-r from-[#102c4c] to-[#1a426f] dark:bg-gradient-to-r dark:from-[#102c4c] dark:to-[#1a426f]`,
|
131 |
`space-y-2 md:space-y-0 md:space-x-3 lg:space-x-6`
|
132 |
)}>
|
133 |
<div className="flex flex-row space-x-2 md:space-x-3 w-full md:w-auto">
|
134 |
<div className={cn(
|
135 |
`transition-all duration-200 ease-in-out`,
|
136 |
+
`flex flex-row items-center justify-start space-x-3`,
|
137 |
`flex-grow`
|
138 |
)}>
|
139 |
|
|
|
144 |
onValueChange={(value) => { setDraftPreset(value as PresetName) }}
|
145 |
disabled={isBusy}
|
146 |
>
|
147 |
+
<SelectTrigger className="flex-grow bg-gray-100 text-gray-700 dark:bg-gray-100 dark:text-gray-700">
|
148 |
<SelectValue className="text-2xs md:text-sm" placeholder="Style" />
|
149 |
</SelectTrigger>
|
150 |
<SelectContent>
|
|
|
156 |
</div>
|
157 |
<div className={cn(
|
158 |
`transition-all duration-200 ease-in-out`,
|
159 |
+
`flex flex-row items-center justify-start space-x-3`,
|
160 |
`w-40`
|
161 |
)}>
|
162 |
|
|
|
167 |
onValueChange={(value) => { setDraftLayout(value as LayoutName) }}
|
168 |
disabled={isBusy}
|
169 |
>
|
170 |
+
<SelectTrigger className="flex-grow bg-gray-100 text-gray-700 dark:bg-gray-100 dark:text-gray-700">
|
171 |
<SelectValue className="text-2xs md:text-sm" placeholder="Layout" />
|
172 |
</SelectTrigger>
|
173 |
<SelectContent>
|
174 |
{nonRandomLayouts.map(key =>
|
175 |
<SelectItem key={key} value={key} className="w-full">
|
176 |
+
<div className="space-x-6 flex flex-row items-center justify-between">
|
177 |
<div className="flex">{
|
178 |
(allLayoutLabels as any)[key]
|
179 |
}</div>
|
|
|
198 |
checked={showCaptions}
|
199 |
onCheckedChange={setShowCaptions}
|
200 |
/>
|
201 |
+
<Label className="text-gray-200 dark:text-gray-200">
|
202 |
<span className="hidden md:inline">Caption</span>
|
203 |
<span className="inline md:hidden">Cap.</span>
|
204 |
</Label>
|
|
|
206 |
{/*
|
207 |
<div className={cn(
|
208 |
`transition-all duration-200 ease-in-out`,
|
209 |
+
`flex flex-row items-center space-x-3 w-1/2 md:w-auto md:hidden`
|
210 |
)}>
|
211 |
<Label className="flex text-2xs md:text-sm md:w-24">Font:</Label>
|
212 |
<Select
|
|
|
233 |
</div>
|
234 |
<div className={cn(
|
235 |
`transition-all duration-200 ease-in-out`,
|
236 |
+
`flex flex-grow flex-col space-y-2 md:space-y-0 md:flex-row items-center md:space-x-3 w-full md:w-auto`
|
237 |
)}>
|
238 |
<div className="flex flex-row flex-grow w-full">
|
239 |
<div className="flex flex-row flex-grow w-full">
|
240 |
<Input
|
241 |
placeholder="1. Story (eg. detective dog)"
|
242 |
+
className={cn(
|
243 |
+
`w-1/2 rounded-r-none`,
|
244 |
+
`bg-gray-100 text-gray-700 dark:bg-gray-100 dark:text-gray-700`,
|
245 |
+
`border-r-stone-100`
|
246 |
+
)}
|
247 |
// disabled={atLeastOnePanelIsBusy}
|
248 |
onChange={(e) => {
|
249 |
setDraftPromptB(e.target.value)
|
|
|
257 |
/>
|
258 |
<Input
|
259 |
placeholder="2. Style (eg 'rain, shiba')"
|
260 |
+
className={cn(
|
261 |
+
`w-1/2`,
|
262 |
+
`bg-gray-100 text-gray-700 dark:bg-gray-100 dark:text-gray-700`,
|
263 |
+
`border-l-gray-300 rounded-l-none rounded-r-none`
|
264 |
+
)}
|
265 |
// disabled={atLeastOnePanelIsBusy}
|
266 |
onChange={(e) => {
|
267 |
setDraftPromptA(e.target.value)
|
|
|
278 |
className={cn(
|
279 |
`rounded-l-none cursor-pointer`,
|
280 |
`transition-all duration-200 ease-in-out`,
|
281 |
+
`text-xl`,
|
282 |
`bg-[rgb(59,134,247)] hover:bg-[rgb(69,144,255)] disabled:bg-[rgb(59,134,247)]`
|
283 |
)}
|
284 |
onClick={() => {
|
|
|
297 |
are confused about why they can't activate it
|
298 |
<div className={cn(
|
299 |
`transition-all duration-200 ease-in-out`,
|
300 |
+
`hidden md:flex flex-row items-center space-x-3 w-full md:w-auto`
|
301 |
)}>
|
302 |
<Label className="flex text-2xs md:text-sm w-24">Font:</Label>
|
303 |
<Select
|
src/app/interface/zoom/index.tsx
CHANGED
@@ -11,11 +11,11 @@ export function Zoom() {
|
|
11 |
<div className={cn(
|
12 |
`print:hidden`,
|
13 |
// `fixed flex items-center justify-center bottom-8 top-32 right-8 z-10 h-screen`,
|
14 |
-
`fixed flex flex-col items-center bottom-8 top-
|
15 |
`animation-all duration-300 ease-in-out`,
|
16 |
isGeneratingStory ? `scale-0 opacity-0` : ``,
|
17 |
)}>
|
18 |
-
<div className="font-
|
19 |
Zoom
|
20 |
</div>
|
21 |
<div className="w-2">
|
|
|
11 |
<div className={cn(
|
12 |
`print:hidden`,
|
13 |
// `fixed flex items-center justify-center bottom-8 top-32 right-8 z-10 h-screen`,
|
14 |
+
`fixed flex flex-col items-center bottom-8 top-40 right-2 md:top-28 md:right-6 z-10`,
|
15 |
`animation-all duration-300 ease-in-out`,
|
16 |
isGeneratingStory ? `scale-0 opacity-0` : ``,
|
17 |
)}>
|
18 |
+
<div className="font-bold text-xs pb-2 text-stone-600 bg-stone-50 dark:text-stone-600 dark:bg-stone-50 p-1 rounded-sm">
|
19 |
Zoom
|
20 |
</div>
|
21 |
<div className="w-2">
|
src/app/layout.tsx
CHANGED
@@ -1,8 +1,6 @@
|
|
|
|
1 |
import './globals.css'
|
2 |
import type { Metadata } from 'next'
|
3 |
-
import { Inter } from 'next/font/google'
|
4 |
-
|
5 |
-
const inter = Inter({ subsets: ['latin'] })
|
6 |
|
7 |
export const metadata: Metadata = {
|
8 |
title: 'AI Comic Factory: generate your own comics! Powered by Hugging Face 🤗',
|
@@ -16,7 +14,7 @@ export default function RootLayout({
|
|
16 |
}) {
|
17 |
return (
|
18 |
<html lang="en">
|
19 |
-
<body className={
|
20 |
{children}
|
21 |
</body>
|
22 |
</html>
|
|
|
1 |
+
import { fonts } from '@/lib/fonts'
|
2 |
import './globals.css'
|
3 |
import type { Metadata } from 'next'
|
|
|
|
|
|
|
4 |
|
5 |
export const metadata: Metadata = {
|
6 |
title: 'AI Comic Factory: generate your own comics! Powered by Hugging Face 🤗',
|
|
|
14 |
}) {
|
15 |
return (
|
16 |
<html lang="en">
|
17 |
+
<body className={fonts.actionman.className}>
|
18 |
{children}
|
19 |
</body>
|
20 |
</html>
|
src/app/main.tsx
CHANGED
@@ -14,6 +14,11 @@ 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 |
|
18 |
export default function Main() {
|
19 |
const [_isPending, startTransition] = useTransition()
|
@@ -26,11 +31,19 @@ export default function Main() {
|
|
26 |
const preset = useStore(s => s.preset)
|
27 |
const prompt = useStore(s => s.prompt)
|
28 |
|
29 |
-
const
|
30 |
-
const
|
31 |
-
const
|
32 |
-
const
|
33 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
|
35 |
const setPanels = useStore(s => s.setPanels)
|
36 |
const setCaptions = useStore(s => s.setCaptions)
|
@@ -39,12 +52,28 @@ export default function Main() {
|
|
39 |
|
40 |
const [waitABitMore, setWaitABitMore] = useState(false)
|
41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
useEffect(() => {
|
43 |
if (isConfigReady) {
|
44 |
-
|
45 |
-
|
|
|
|
|
|
|
46 |
}
|
47 |
}, [JSON.stringify(config), isConfigReady])
|
|
|
48 |
// react to prompt changes
|
49 |
useEffect(() => {
|
50 |
if (!prompt) { return }
|
@@ -85,7 +114,7 @@ export default function Main() {
|
|
85 |
|
86 |
for (
|
87 |
let currentPanel = 0;
|
88 |
-
currentPanel <
|
89 |
currentPanel += nbPanelsToGenerate
|
90 |
) {
|
91 |
try {
|
@@ -94,7 +123,7 @@ export default function Main() {
|
|
94 |
stylePrompt,
|
95 |
userStoryPrompt,
|
96 |
nbPanelsToGenerate,
|
97 |
-
|
98 |
existingPanels,
|
99 |
})
|
100 |
console.log("LLM generated some new panels:", candidatePanels)
|
@@ -133,7 +162,7 @@ export default function Main() {
|
|
133 |
setGeneratingStory(false)
|
134 |
break
|
135 |
}
|
136 |
-
if (currentPanel > (
|
137 |
console.log("good, we are half way there, hold tight!")
|
138 |
// setWaitABitMore(true)
|
139 |
}
|
@@ -147,7 +176,7 @@ export default function Main() {
|
|
147 |
*/
|
148 |
|
149 |
})
|
150 |
-
}, [prompt, preset?.label,
|
151 |
|
152 |
return (
|
153 |
<Suspense>
|
@@ -173,10 +202,18 @@ export default function Main() {
|
|
173 |
style={{
|
174 |
width: `${zoomLevel}%`
|
175 |
}}>
|
176 |
-
{Array(
|
177 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
178 |
</div>
|
179 |
</div>
|
|
|
180 |
<Zoom />
|
181 |
<BottomBar />
|
182 |
<div className={cn(
|
|
|
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 |
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 |
+
|
41 |
+
const setCurrentNbPanelsPerPage = useStore(s => s.setCurrentNbPanelsPerPage)
|
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)
|
|
|
52 |
|
53 |
const [waitABitMore, setWaitABitMore] = useState(false)
|
54 |
|
55 |
+
const [userDefinedMaxNumberOfPages, setUserDefinedMaxNumberOfPages] = useLocalStorage<number>(
|
56 |
+
localStorageKeys.userDefinedMaxNumberOfPages,
|
57 |
+
defaultSettings.userDefinedMaxNumberOfPages
|
58 |
+
)
|
59 |
+
|
60 |
+
useEffect(() => {
|
61 |
+
if (maxNbPages !== userDefinedMaxNumberOfPages) {
|
62 |
+
setMaxNbPages(userDefinedMaxNumberOfPages)
|
63 |
+
}
|
64 |
+
}, [maxNbPages, userDefinedMaxNumberOfPages])
|
65 |
+
|
66 |
+
|
67 |
useEffect(() => {
|
68 |
if (isConfigReady) {
|
69 |
+
|
70 |
+
// note: this has very low impact at the moment as we are always using the value 4
|
71 |
+
// however I would like to progressively evolve the code to make it dynamic
|
72 |
+
setCurrentNbPanelsPerPage(config.nbPanelsPerPage)
|
73 |
+
setMaxNbPanelsPerPage(config.nbPanelsPerPage)
|
74 |
}
|
75 |
}, [JSON.stringify(config), isConfigReady])
|
76 |
+
|
77 |
// react to prompt changes
|
78 |
useEffect(() => {
|
79 |
if (!prompt) { return }
|
|
|
114 |
|
115 |
for (
|
116 |
let currentPanel = 0;
|
117 |
+
currentPanel < currentNbPanels;
|
118 |
currentPanel += nbPanelsToGenerate
|
119 |
) {
|
120 |
try {
|
|
|
123 |
stylePrompt,
|
124 |
userStoryPrompt,
|
125 |
nbPanelsToGenerate,
|
126 |
+
maxNbPanels,
|
127 |
existingPanels,
|
128 |
})
|
129 |
console.log("LLM generated some new panels:", candidatePanels)
|
|
|
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 |
}
|
|
|
176 |
*/
|
177 |
|
178 |
})
|
179 |
+
}, [prompt, preset?.label, currentNbPanels, maxNbPanels]) // important: we need to react to preset changes too
|
180 |
|
181 |
return (
|
182 |
<Suspense>
|
|
|
202 |
style={{
|
203 |
width: `${zoomLevel}%`
|
204 |
}}>
|
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>
|
216 |
+
<SignUpCTA />
|
217 |
<Zoom />
|
218 |
<BottomBar />
|
219 |
<div className={cn(
|
src/app/queries/getDynamicConfig.ts
CHANGED
@@ -6,8 +6,8 @@ import { getValidString } from "@/lib/getValidString"
|
|
6 |
import { DynamicConfig } from "@/types"
|
7 |
|
8 |
export async function getDynamicConfig(): Promise<DynamicConfig> {
|
9 |
-
const maxNbPages = getValidNumber(process.env.MAX_NB_PAGES, 1,
|
10 |
-
const nbPanelsPerPage = 4
|
11 |
const nbTotalPanelsToGenerate = maxNbPages * nbPanelsPerPage
|
12 |
|
13 |
const config = {
|
|
|
6 |
import { DynamicConfig } from "@/types"
|
7 |
|
8 |
export async function getDynamicConfig(): Promise<DynamicConfig> {
|
9 |
+
const maxNbPages = getValidNumber(process.env.MAX_NB_PAGES, 1, Number.MAX_SAFE_INTEGER, 1)
|
10 |
+
const nbPanelsPerPage = 4 // for now this is static
|
11 |
const nbTotalPanelsToGenerate = maxNbPages * nbPanelsPerPage
|
12 |
|
13 |
const config = {
|
src/app/queries/getStoryContinuation.ts
CHANGED
@@ -8,14 +8,14 @@ export const getStoryContinuation = async ({
|
|
8 |
stylePrompt = "",
|
9 |
userStoryPrompt = "",
|
10 |
nbPanelsToGenerate = 2,
|
11 |
-
|
12 |
existingPanels = [],
|
13 |
}: {
|
14 |
preset: Preset;
|
15 |
stylePrompt?: string;
|
16 |
userStoryPrompt?: string;
|
17 |
nbPanelsToGenerate?: number;
|
18 |
-
|
19 |
existingPanels?: GeneratedPanel[];
|
20 |
}): Promise<GeneratedPanel[]> => {
|
21 |
|
@@ -31,7 +31,7 @@ export const getStoryContinuation = async ({
|
|
31 |
preset,
|
32 |
prompt,
|
33 |
nbPanelsToGenerate,
|
34 |
-
|
35 |
existingPanels,
|
36 |
})
|
37 |
|
|
|
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 |
|
|
|
31 |
preset,
|
32 |
prompt,
|
33 |
nbPanelsToGenerate,
|
34 |
+
maxNbPanels,
|
35 |
existingPanels,
|
36 |
})
|
37 |
|
src/app/queries/predictNextPanels.ts
CHANGED
@@ -12,13 +12,13 @@ export const predictNextPanels = async ({
|
|
12 |
preset,
|
13 |
prompt = "",
|
14 |
nbPanelsToGenerate = 2,
|
15 |
-
|
16 |
existingPanels = [],
|
17 |
}: {
|
18 |
preset: Preset;
|
19 |
prompt: string;
|
20 |
nbPanelsToGenerate?: number;
|
21 |
-
|
22 |
existingPanels: GeneratedPanel[];
|
23 |
}): Promise<GeneratedPanel[]> => {
|
24 |
// console.log("predictNextPanels: ", { prompt, nbPanelsToGenerate })
|
@@ -35,7 +35,7 @@ export const predictNextPanels = async ({
|
|
35 |
const firstNextOrLast =
|
36 |
existingPanels.length === 0
|
37 |
? "first"
|
38 |
-
: (
|
39 |
? "last"
|
40 |
: "next"
|
41 |
|
@@ -44,7 +44,7 @@ export const predictNextPanels = async ({
|
|
44 |
role: "system",
|
45 |
content: [
|
46 |
`You are a writer specialized in ${preset.llmPrompt}`,
|
47 |
-
`Please write detailed drawing instructions and short (2-3 sentences long) speech captions for the ${firstNextOrLast} ${nbPanelsToGenerate} panels (out of ${
|
48 |
`Give your response as a VALID JSON array like this: \`Array<{ panel: number; instructions: string; caption: string; }>\`.`,
|
49 |
// `Give your response as Markdown bullet points.`,
|
50 |
`Be brief in the instructions and narrative captions of those ${nbPanelsToGenerate} panels, don't add your own comments. The captions must be captivating, smart, entertaining. Be straight to the point, and never reply things like "Sure, I can.." etc. Reply using valid JSON!! Important: Write valid JSON!`
|
|
|
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 })
|
|
|
35 |
const firstNextOrLast =
|
36 |
existingPanels.length === 0
|
37 |
? "first"
|
38 |
+
: (maxNbPanels - existingPanels.length) === maxNbPanels
|
39 |
? "last"
|
40 |
: "next"
|
41 |
|
|
|
44 |
role: "system",
|
45 |
content: [
|
46 |
`You are a writer specialized in ${preset.llmPrompt}`,
|
47 |
+
`Please write detailed drawing instructions and short (2-3 sentences long) speech captions for the ${firstNextOrLast} ${nbPanelsToGenerate} panels (out of ${maxNbPanels} in total) of a new story, but keep it open-ended (it will be continued and expanded later). Please make sure each of those ${nbPanelsToGenerate} panels include info about character gender, age, origin, clothes, colors, location, lights, etc. Only generate those ${nbPanelsToGenerate} panels, but take into account the fact the panels are part of a longer story (${maxNbPanels} panels long).`,
|
48 |
`Give your response as a VALID JSON array like this: \`Array<{ panel: number; instructions: string; caption: string; }>\`.`,
|
49 |
// `Give your response as Markdown bullet points.`,
|
50 |
`Be brief in the instructions and narrative captions of those ${nbPanelsToGenerate} panels, don't add your own comments. The captions must be captivating, smart, entertaining. Be straight to the point, and never reply things like "Sure, I can.." etc. Reply using valid JSON!! Important: Write valid JSON!`
|
src/app/store/index.ts
CHANGED
@@ -12,9 +12,12 @@ export const useStore = create<{
|
|
12 |
prompt: string
|
13 |
font: FontName
|
14 |
preset: Preset
|
15 |
-
|
16 |
-
|
17 |
-
|
|
|
|
|
|
|
18 |
panels: string[]
|
19 |
captions: string[]
|
20 |
upscaleQueue: Record<string, RenderedScene>
|
@@ -28,9 +31,14 @@ export const useStore = create<{
|
|
28 |
panelGenerationStatus: Record<number, boolean>
|
29 |
isGeneratingText: boolean
|
30 |
atLeastOnePanelIsBusy: boolean
|
31 |
-
|
32 |
-
|
33 |
-
|
|
|
|
|
|
|
|
|
|
|
34 |
setRendered: (panelId: string, renderedScene: RenderedScene) => void
|
35 |
addToUpscaleQueue: (panelId: string, renderedScene: RenderedScene) => void
|
36 |
removeFromUpscaleQueue: (panelId: string) => void
|
@@ -56,9 +64,14 @@ export const useStore = create<{
|
|
56 |
prompt: "",
|
57 |
font: "actionman",
|
58 |
preset: getPreset(defaultPreset),
|
59 |
-
|
60 |
-
|
61 |
-
|
|
|
|
|
|
|
|
|
|
|
62 |
panels: [],
|
63 |
captions: [],
|
64 |
upscaleQueue: {} as Record<string, RenderedScene>,
|
@@ -72,25 +85,47 @@ export const useStore = create<{
|
|
72 |
panelGenerationStatus: {},
|
73 |
isGeneratingText: false,
|
74 |
atLeastOnePanelIsBusy: false,
|
75 |
-
|
76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
77 |
set({
|
78 |
-
|
79 |
-
|
80 |
})
|
81 |
},
|
82 |
-
|
83 |
-
const { nbPanelsPerPage } = get()
|
84 |
set({
|
85 |
-
|
86 |
-
nbTotalPanels: nbPanelsPerPage * nbPages,
|
87 |
})
|
88 |
},
|
89 |
-
|
90 |
set({
|
91 |
-
|
92 |
})
|
93 |
},
|
|
|
94 |
setRendered: (panelId: string, renderedScene: RenderedScene) => {
|
95 |
const { renderedScenes } = get()
|
96 |
set({
|
@@ -166,14 +201,14 @@ export const useStore = create<{
|
|
166 |
},
|
167 |
setLayout: (layoutName: LayoutName) => {
|
168 |
|
169 |
-
const {
|
170 |
|
171 |
const layout = layoutName === "random"
|
172 |
? getRandomLayoutName()
|
173 |
: layoutName
|
174 |
|
175 |
const layouts: LayoutName[] = []
|
176 |
-
for (let i = 0; i <
|
177 |
layouts.push(
|
178 |
layoutName === "random"
|
179 |
? getRandomLayoutName()
|
@@ -215,9 +250,9 @@ export const useStore = create<{
|
|
215 |
|
216 |
|
217 |
const canvas = await html2canvas(page)
|
218 |
-
console.log("canvas:", canvas)
|
219 |
|
220 |
-
const data = canvas.toDataURL('image/jpeg', 0.
|
221 |
return data
|
222 |
},
|
223 |
download: async () => {
|
@@ -238,14 +273,14 @@ export const useStore = create<{
|
|
238 |
},
|
239 |
generate: (prompt: string, presetName: PresetName, layoutName: LayoutName) => {
|
240 |
|
241 |
-
const {
|
242 |
|
243 |
const layout = layoutName === "random"
|
244 |
? getRandomLayoutName()
|
245 |
: layoutName
|
246 |
|
247 |
const layouts: LayoutName[] = []
|
248 |
-
for (let i = 0; i <
|
249 |
layouts.push(
|
250 |
layoutName === "random"
|
251 |
? getRandomLayoutName()
|
|
|
12 |
prompt: string
|
13 |
font: FontName
|
14 |
preset: Preset
|
15 |
+
currentNbPanelsPerPage: number
|
16 |
+
maxNbPanelsPerPage: number
|
17 |
+
currentNbPages: number
|
18 |
+
maxNbPages: number
|
19 |
+
currentNbPanels: number
|
20 |
+
maxNbPanels: number
|
21 |
panels: string[]
|
22 |
captions: string[]
|
23 |
upscaleQueue: Record<string, RenderedScene>
|
|
|
31 |
panelGenerationStatus: Record<number, boolean>
|
32 |
isGeneratingText: boolean
|
33 |
atLeastOnePanelIsBusy: boolean
|
34 |
+
|
35 |
+
setCurrentNbPanelsPerPage: (currentNbPanelsPerPage: number) => void
|
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 |
+
|
42 |
setRendered: (panelId: string, renderedScene: RenderedScene) => void
|
43 |
addToUpscaleQueue: (panelId: string, renderedScene: RenderedScene) => void
|
44 |
removeFromUpscaleQueue: (panelId: string) => void
|
|
|
64 |
prompt: "",
|
65 |
font: "actionman",
|
66 |
preset: getPreset(defaultPreset),
|
67 |
+
|
68 |
+
currentNbPanelsPerPage: 4,
|
69 |
+
maxNbPanelsPerPage: 4,
|
70 |
+
currentNbPages: 1,
|
71 |
+
maxNbPages: 1,
|
72 |
+
currentNbPanels: 4,
|
73 |
+
maxNbPanels: 4,
|
74 |
+
|
75 |
panels: [],
|
76 |
captions: [],
|
77 |
upscaleQueue: {} as Record<string, RenderedScene>,
|
|
|
85 |
panelGenerationStatus: {},
|
86 |
isGeneratingText: false,
|
87 |
atLeastOnePanelIsBusy: false,
|
88 |
+
|
89 |
+
|
90 |
+
setCurrentNbPanelsPerPage: (currentNbPanelsPerPage: number) => {
|
91 |
+
const { currentNbPages } = get()
|
92 |
+
set({
|
93 |
+
currentNbPanelsPerPage,
|
94 |
+
currentNbPanels: currentNbPanelsPerPage * currentNbPages
|
95 |
+
})
|
96 |
+
},
|
97 |
+
setMaxNbPanelsPerPage: (maxNbPanelsPerPage: number) => {
|
98 |
+
const { maxNbPages } = get()
|
99 |
+
set({
|
100 |
+
maxNbPanelsPerPage,
|
101 |
+
maxNbPanels: maxNbPanelsPerPage * maxNbPages,
|
102 |
+
})
|
103 |
+
},
|
104 |
+
setCurrentNbPages: (currentNbPages: number) => {
|
105 |
+
const { currentNbPanelsPerPage } = get()
|
106 |
+
set({
|
107 |
+
currentNbPages,
|
108 |
+
currentNbPanels: currentNbPanelsPerPage * currentNbPages
|
109 |
+
})
|
110 |
+
},
|
111 |
+
setMaxNbPages: (maxNbPages: number) => {
|
112 |
+
const { maxNbPanelsPerPage } = get()
|
113 |
set({
|
114 |
+
maxNbPages,
|
115 |
+
maxNbPanels: maxNbPanelsPerPage * maxNbPages,
|
116 |
})
|
117 |
},
|
118 |
+
setCurrentNbPanels: (currentNbPanels: number) => {
|
|
|
119 |
set({
|
120 |
+
currentNbPanels,
|
|
|
121 |
})
|
122 |
},
|
123 |
+
setMaxNbPanels: (maxNbPanels: number) => {
|
124 |
set({
|
125 |
+
maxNbPanels
|
126 |
})
|
127 |
},
|
128 |
+
|
129 |
setRendered: (panelId: string, renderedScene: RenderedScene) => {
|
130 |
const { renderedScenes } = get()
|
131 |
set({
|
|
|
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()
|
|
|
250 |
|
251 |
|
252 |
const canvas = await html2canvas(page)
|
253 |
+
// console.log("canvas:", canvas)
|
254 |
|
255 |
+
const data = canvas.toDataURL('image/jpeg', 0.97)
|
256 |
return data
|
257 |
},
|
258 |
download: async () => {
|
|
|
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()
|
src/components/ui/dialog.tsx
CHANGED
@@ -41,7 +41,7 @@ const DialogContent = React.forwardRef<
|
|
41 |
<DialogPrimitive.Content
|
42 |
ref={ref}
|
43 |
className={cn(
|
44 |
-
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-stone-200 bg-white p-6 shadow-
|
45 |
className
|
46 |
)}
|
47 |
{...props}
|
|
|
41 |
<DialogPrimitive.Content
|
42 |
ref={ref}
|
43 |
className={cn(
|
44 |
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-stone-200 bg-white p-6 shadow-2xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-3xl md:w-full dark:border-stone-800 dark:bg-stone-950",
|
45 |
className
|
46 |
)}
|
47 |
{...props}
|
src/components/ui/select.tsx
CHANGED
@@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
|
|
19 |
<SelectPrimitive.Trigger
|
20 |
ref={ref}
|
21 |
className={cn(
|
22 |
-
"flex h-10 w-full items-center justify-between rounded-md border border-stone-200 border-stone-200 bg-transparent px-3 py-2 text-
|
23 |
className
|
24 |
)}
|
25 |
{...props}
|
@@ -68,7 +68,7 @@ const SelectLabel = React.forwardRef<
|
|
68 |
>(({ className, ...props }, ref) => (
|
69 |
<SelectPrimitive.Label
|
70 |
ref={ref}
|
71 |
-
className={cn("py-1.5 pl-8 pr-2 text-
|
72 |
{...props}
|
73 |
/>
|
74 |
))
|
@@ -81,7 +81,7 @@ const SelectItem = React.forwardRef<
|
|
81 |
<SelectPrimitive.Item
|
82 |
ref={ref}
|
83 |
className={cn(
|
84 |
-
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-
|
85 |
className
|
86 |
)}
|
87 |
{...props}
|
|
|
19 |
<SelectPrimitive.Trigger
|
20 |
ref={ref}
|
21 |
className={cn(
|
22 |
+
"flex h-10 w-full items-center justify-between rounded-md border border-stone-200 border-stone-200 bg-transparent px-3 py-2 text-base ring-offset-white placeholder:text-stone-500 focus:outline-none focus:ring-2 focus:ring-stone-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-stone-800 dark:border-stone-800 dark:ring-offset-stone-950 dark:placeholder:text-stone-400 dark:focus:ring-stone-800",
|
23 |
className
|
24 |
)}
|
25 |
{...props}
|
|
|
68 |
>(({ className, ...props }, ref) => (
|
69 |
<SelectPrimitive.Label
|
70 |
ref={ref}
|
71 |
+
className={cn("py-1.5 pl-8 pr-2 text-base font-semibold", className)}
|
72 |
{...props}
|
73 |
/>
|
74 |
))
|
|
|
81 |
<SelectPrimitive.Item
|
82 |
ref={ref}
|
83 |
className={cn(
|
84 |
+
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-base outline-none focus:bg-stone-100 focus:text-stone-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-stone-800 dark:focus:text-stone-50",
|
85 |
className
|
86 |
)}
|
87 |
{...props}
|
src/lib/usePageOrientation.ts
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useEffect } from "react";
|
2 |
+
|
3 |
+
/**
|
4 |
+
* This will turn the page in portrait or landscape depending on the number of pages
|
5 |
+
*/
|
6 |
+
const usePageOrientation = () => {
|
7 |
+
useEffect(() => {
|
8 |
+
const updatePageOrientation = () => {
|
9 |
+
const pages = document.querySelectorAll(".comic-page");
|
10 |
+
const styleEl = document.createElement("style");
|
11 |
+
|
12 |
+
// Append style element to the head
|
13 |
+
document.head.appendChild(styleEl);
|
14 |
+
|
15 |
+
// Get the style sheet created in the above step
|
16 |
+
const styleSheet = styleEl.sheet as CSSStyleSheet;
|
17 |
+
|
18 |
+
if (pages.length >= 2) {
|
19 |
+
styleSheet.insertRule("@page { size: landscape }", 0);
|
20 |
+
} else {
|
21 |
+
styleSheet.insertRule("@page { size: portrait }", 0);
|
22 |
+
}
|
23 |
+
};
|
24 |
+
|
25 |
+
// Execute when the DOM is fully loaded
|
26 |
+
updatePageOrientation();
|
27 |
+
|
28 |
+
// Also execute when the window is resized
|
29 |
+
window.addEventListener("resize", updatePageOrientation);
|
30 |
+
|
31 |
+
// Clean up event listener on unmount
|
32 |
+
return () => {
|
33 |
+
window.removeEventListener("resize", updatePageOrientation);
|
34 |
+
};
|
35 |
+
}, []); // Empty dependency array ensures this runs once on mount and cleanup on unmount
|
36 |
+
};
|
37 |
+
|
38 |
+
export default usePageOrientation;
|
src/types.ts
CHANGED
@@ -174,6 +174,7 @@ export type Settings = {
|
|
174 |
groqApiKey: string
|
175 |
groqApiLanguageModel: string
|
176 |
hasGeneratedAtLeastOnce: boolean
|
|
|
177 |
}
|
178 |
|
179 |
export type DynamicConfig = {
|
|
|
174 |
groqApiKey: string
|
175 |
groqApiLanguageModel: string
|
176 |
hasGeneratedAtLeastOnce: boolean
|
177 |
+
userDefinedMaxNumberOfPages: number
|
178 |
}
|
179 |
|
180 |
export type DynamicConfig = {
|
tailwind.config.js
CHANGED
@@ -17,6 +17,16 @@ module.exports = {
|
|
17 |
},
|
18 |
},
|
19 |
extend: {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
fontFamily: {
|
21 |
indieflower: ['var(--font-indieflower)'],
|
22 |
thegirlnextdoor: ['var(--font-the-girl-next-door)'],
|
|
|
17 |
},
|
18 |
},
|
19 |
extend: {
|
20 |
+
spacing: {
|
21 |
+
17: '4.25rem', // 68px
|
22 |
+
18: '4.5rem', // 72px
|
23 |
+
19: '4.75rem', // 76px
|
24 |
+
20: '5rem', // 80px
|
25 |
+
21: '5.25rem', // 84px
|
26 |
+
22: '5.5rem', // 88px
|
27 |
+
22: '5.5rem', // 88px
|
28 |
+
26: '6.5rem', // 104px
|
29 |
+
},
|
30 |
fontFamily: {
|
31 |
indieflower: ['var(--font-indieflower)'],
|
32 |
thegirlnextdoor: ['var(--font-the-girl-next-door)'],
|