Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Commit
β’
b48537f
1
Parent(s):
8cd2153
add a login wall
Browse files- .env +3 -3
- src/app/engine/presets.ts +1 -1
- src/app/interface/about/index.tsx +8 -4
- src/app/interface/auth-wall/index.tsx +24 -0
- src/app/interface/bottom-bar/bottom-bar.tsx +162 -0
- src/app/interface/bottom-bar/index.tsx +6 -143
- src/app/interface/edit-modal/index.tsx +1 -4
- src/app/interface/login/index.tsx +5 -27
- src/app/interface/login/login.tsx +32 -0
- src/app/interface/settings-dialog/defaultSettings.ts +1 -0
- src/app/interface/settings-dialog/getSettings.ts +1 -0
- src/app/interface/settings-dialog/localStorageKeys.ts +1 -0
- src/app/interface/top-menu/index.tsx +38 -15
- src/app/main.tsx +5 -4
- src/app/queries/getStory.ts +0 -90
- src/app/queries/getStoryContinuation.ts +1 -1
- src/app/queries/predictNextPanels.ts +1 -1
- src/config.ts +1 -1
- src/lib/useOAuth.ts +5 -4
- src/types.ts +1 -0
.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 |
-
NEXT_PUBLIC_MAX_NB_PAGES=
|
18 |
|
19 |
# Not implemented for the Inference API yet - you can submit a PR if you have some ideas
|
20 |
NEXT_PUBLIC_CAN_UPSCALE="false"
|
@@ -26,7 +26,8 @@ NEXT_PUBLIC_CAN_REDRAW="false"
|
|
26 |
NEXT_PUBLIC_ENABLE_RATE_LIMITER="false"
|
27 |
|
28 |
# ------------- HUGGING FACE OAUTH -------------
|
29 |
-
NEXT_PUBLIC_ENABLE_HUGGING_FACE_OAUTH=
|
|
|
30 |
NEXT_PUBLIC_HUGGING_FACE_OAUTH_CLIENT_ID=""
|
31 |
HUGGING_FACE_OAUTH_SECRET=""
|
32 |
|
@@ -45,7 +46,6 @@ AUTH_OPENAI_API_KEY=
|
|
45 |
# An experimental RENDERING engine (sorry it is not very documented yet, so you can use one of the other engines)
|
46 |
AUTH_VIDEOCHAIN_API_TOKEN=
|
47 |
|
48 |
-
|
49 |
# Groq.com key: available for the LLM engine
|
50 |
AUTH_GROQ_API_KEY=
|
51 |
|
|
|
14 |
LLM_ENGINE="INFERENCE_API"
|
15 |
|
16 |
# set this to control the number of pages
|
17 |
+
NEXT_PUBLIC_MAX_NB_PAGES=
|
18 |
|
19 |
# Not implemented for the Inference API yet - you can submit a PR if you have some ideas
|
20 |
NEXT_PUBLIC_CAN_UPSCALE="false"
|
|
|
26 |
NEXT_PUBLIC_ENABLE_RATE_LIMITER="false"
|
27 |
|
28 |
# ------------- HUGGING FACE OAUTH -------------
|
29 |
+
NEXT_PUBLIC_ENABLE_HUGGING_FACE_OAUTH=
|
30 |
+
NEXT_PUBLIC_ENABLE_HUGGING_FACE_OAUTH_WALL=
|
31 |
NEXT_PUBLIC_HUGGING_FACE_OAUTH_CLIENT_ID=""
|
32 |
HUGGING_FACE_OAUTH_SECRET=""
|
33 |
|
|
|
46 |
# An experimental RENDERING engine (sorry it is not very documented yet, so you can use one of the other engines)
|
47 |
AUTH_VIDEOCHAIN_API_TOKEN=
|
48 |
|
|
|
49 |
# Groq.com key: available for the LLM engine
|
50 |
AUTH_GROQ_API_KEY=
|
51 |
|
src/app/engine/presets.ts
CHANGED
@@ -667,7 +667,7 @@ export const presets: Record<string, Preset> = {
|
|
667 |
|
668 |
export type PresetName = keyof typeof presets
|
669 |
|
670 |
-
export const defaultPreset: PresetName = "
|
671 |
|
672 |
export const nonRandomPresets = Object.keys(presets).filter(p => p !== "random")
|
673 |
|
|
|
667 |
|
668 |
export type PresetName = keyof typeof presets
|
669 |
|
670 |
+
export const defaultPreset: PresetName = "american_comic_50"
|
671 |
|
672 |
export const nonRandomPresets = Object.keys(presets).filter(p => p !== "random")
|
673 |
|
src/app/interface/about/index.tsx
CHANGED
@@ -1,6 +1,7 @@
|
|
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 |
|
5 |
export function About() {
|
6 |
const [isOpen, setOpen] = useState(false)
|
@@ -13,19 +14,22 @@ export function About() {
|
|
13 |
<span className="inline md:hidden">About</span>
|
14 |
</Button>
|
15 |
</DialogTrigger>
|
16 |
-
<DialogContent className="sm:max-w-[425px]">
|
17 |
<DialogHeader>
|
18 |
<DialogTitle>The AI Comic Factory</DialogTitle>
|
19 |
<DialogDescription className="w-full text-center text-lg font-bold text-stone-800">
|
20 |
What is the AI Comic Factory?
|
21 |
</DialogDescription>
|
22 |
</DialogHeader>
|
23 |
-
<div className="grid gap-4 py-4 text-stone-800">
|
24 |
<p className="">
|
25 |
-
The AI Comic Factory is
|
|
|
|
|
|
|
26 |
</p>
|
27 |
<p>
|
28 |
-
|
29 |
</p>
|
30 |
<p>
|
31 |
π The language model used to generate the story is <a className="text-stone-600 underline" href="https://huggingface.co/HuggingFaceH4/zephyr-7b-beta" target="_blank">Zephyr-7b-beta</a>.
|
|
|
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)
|
|
|
14 |
<span className="inline md:hidden">About</span>
|
15 |
</Button>
|
16 |
</DialogTrigger>
|
17 |
+
<DialogContent className="sm:max-w-[425px] md:max-w-[600px]">
|
18 |
<DialogHeader>
|
19 |
<DialogTitle>The AI Comic Factory</DialogTitle>
|
20 |
<DialogDescription className="w-full text-center text-lg font-bold text-stone-800">
|
21 |
What is the AI Comic Factory?
|
22 |
</DialogDescription>
|
23 |
</DialogHeader>
|
24 |
+
<div className="grid gap-4 py-4 text-stone-800 text-sm">
|
25 |
<p className="">
|
26 |
+
The AI Comic Factory is an app to generate stories using AI in a few clicks.
|
27 |
+
</p>
|
28 |
+
<p>
|
29 |
+
It is free for all Hugging Face users: <Login />
|
30 |
</p>
|
31 |
<p>
|
32 |
+
As an artist, you can use your <a className="text-stone-600 underline" href="https://huggingface.co/spaces/jbilcke-hf/ai-comic-factory/discussions/402#654ab848fa25dfb780aa19fb" target="_blank">own art to generate comic panels.</a>
|
33 |
</p>
|
34 |
<p>
|
35 |
π The language model used to generate the story is <a className="text-stone-600 underline" href="https://huggingface.co/HuggingFaceH4/zephyr-7b-beta" target="_blank">Zephyr-7b-beta</a>.
|
src/app/interface/auth-wall/index.tsx
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
3 |
+
|
4 |
+
import { Login } from "../login"
|
5 |
+
|
6 |
+
export function AuthWall({ show }: { show: boolean }) {
|
7 |
+
return (
|
8 |
+
<Dialog open={show}>
|
9 |
+
<DialogContent className="sm:max-w-[425px]">
|
10 |
+
<div className="grid gap-4 py-4 text-stone-800">
|
11 |
+
<p className="">
|
12 |
+
The AI Comic Factory is a free app available to all Hugging Face users!
|
13 |
+
</p>
|
14 |
+
<p>
|
15 |
+
Please sign-in to continue:
|
16 |
+
</p>
|
17 |
+
<p>
|
18 |
+
<Login />
|
19 |
+
</p>
|
20 |
+
</div>
|
21 |
+
</DialogContent>
|
22 |
+
</Dialog>
|
23 |
+
)
|
24 |
+
}
|
src/app/interface/bottom-bar/bottom-bar.tsx
ADDED
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { startTransition, useEffect, useState } from "react"
|
2 |
+
|
3 |
+
import { useStore } from "@/app/store"
|
4 |
+
import { Button } from "@/components/ui/button"
|
5 |
+
import { cn } from "@/lib/utils"
|
6 |
+
import { upscaleImage } from "@/app/engine/render"
|
7 |
+
import { sleep } from "@/lib/sleep"
|
8 |
+
|
9 |
+
import { Share } from "../share"
|
10 |
+
import { About } from "../about"
|
11 |
+
import { SettingsDialog } from "../settings-dialog"
|
12 |
+
import { useLocalStorage } from "usehooks-ts"
|
13 |
+
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)
|
27 |
+
|
28 |
+
const upscaleQueue = useStore(state => state.upscaleQueue)
|
29 |
+
const renderedScenes = useStore(state => state.renderedScenes)
|
30 |
+
const removeFromUpscaleQueue = useStore(state => state.removeFromUpscaleQueue)
|
31 |
+
const setRendered = useStore(state => state.setRendered)
|
32 |
+
const [isUpscaling, setUpscaling] = useState(false)
|
33 |
+
|
34 |
+
const [hasGeneratedAtLeastOnce, setHasGeneratedAtLeastOnce] = useLocalStorage<boolean>(
|
35 |
+
localStorageKeys.hasGeneratedAtLeastOnce,
|
36 |
+
defaultSettings.hasGeneratedAtLeastOnce
|
37 |
+
)
|
38 |
+
|
39 |
+
const handleUpscale = () => {
|
40 |
+
setUpscaling(true)
|
41 |
+
startTransition(() => {
|
42 |
+
const fn = async () => {
|
43 |
+
for (let [panelId, renderedScene] of Object.entries(upscaleQueue)) {
|
44 |
+
try {
|
45 |
+
console.log(`upscaling panel ${panelId} (${renderedScene.renderId})`)
|
46 |
+
const result = await upscaleImage(renderedScene.assetUrl)
|
47 |
+
await sleep(1000)
|
48 |
+
if (result.assetUrl) {
|
49 |
+
console.log(`upscale successful, removing ${panelId} (${renderedScene.renderId}) from upscale queue`)
|
50 |
+
setRendered(panelId, {
|
51 |
+
...renderedScene,
|
52 |
+
assetUrl: result.assetUrl
|
53 |
+
})
|
54 |
+
removeFromUpscaleQueue(panelId)
|
55 |
+
}
|
56 |
+
|
57 |
+
} catch (err) {
|
58 |
+
console.error(`failed to upscale: ${err}`)
|
59 |
+
}
|
60 |
+
}
|
61 |
+
|
62 |
+
setUpscaling(false)
|
63 |
+
}
|
64 |
+
|
65 |
+
fn()
|
66 |
+
})
|
67 |
+
}
|
68 |
+
|
69 |
+
const handlePrint = () => {
|
70 |
+
window.print()
|
71 |
+
}
|
72 |
+
const hasFinishedGeneratingImages = allStatus.length > 0 && (allStatus.length - remainingImages) === allStatus.length
|
73 |
+
|
74 |
+
// keep track of the first generation, independently of the login status
|
75 |
+
useEffect(() => {
|
76 |
+
if (hasFinishedGeneratingImages && !hasGeneratedAtLeastOnce) {
|
77 |
+
setHasGeneratedAtLeastOnce(true)
|
78 |
+
}
|
79 |
+
}, [hasFinishedGeneratingImages, hasGeneratedAtLeastOnce])
|
80 |
+
|
81 |
+
return (
|
82 |
+
<div className={cn(
|
83 |
+
`print:hidden`,
|
84 |
+
`fixed bottom-2 md:bottom-4 left-2 right-0 md:left-3 md:right-1`,
|
85 |
+
`flex flex-row`,
|
86 |
+
`justify-between`,
|
87 |
+
`pointer-events-none`
|
88 |
+
)}>
|
89 |
+
<div className={cn(
|
90 |
+
`flex flex-row`,
|
91 |
+
`items-end`,
|
92 |
+
`pointer-events-auto`,
|
93 |
+
`animation-all duration-300 ease-in-out`,
|
94 |
+
isGeneratingStory ? `scale-0 opacity-0` : ``,
|
95 |
+
`space-x-3`,
|
96 |
+
`scale-[0.9]`
|
97 |
+
)}>
|
98 |
+
<About />
|
99 |
+
{/*
|
100 |
+
Thank you clip factory for your service π«‘
|
101 |
+
<AIClipFactory />
|
102 |
+
*/}
|
103 |
+
</div>
|
104 |
+
<div className={cn(
|
105 |
+
`flex flex-row`,
|
106 |
+
`pointer-events-auto`,
|
107 |
+
`animation-all duration-300 ease-in-out`,
|
108 |
+
isGeneratingStory ? `scale-0 opacity-0` : ``,
|
109 |
+
`space-x-3`,
|
110 |
+
`scale-[0.9]`
|
111 |
+
)}>
|
112 |
+
<SettingsDialog />
|
113 |
+
{/*<Button
|
114 |
+
onClick={handleUpscale}
|
115 |
+
disabled={!prompt?.length || remainingImages > 0 || isUpscaling || !Object.values(upscaleQueue).length}
|
116 |
+
>
|
117 |
+
{isUpscaling
|
118 |
+
? `${allStatus.length - Object.values(upscaleQueue).length}/${allStatus.length} β`
|
119 |
+
: "Upscale"}
|
120 |
+
</Button>*/}
|
121 |
+
|
122 |
+
{/*
|
123 |
+
<div>
|
124 |
+
<Button
|
125 |
+
onClick={handlePrint}
|
126 |
+
disabled={!prompt?.length}
|
127 |
+
>
|
128 |
+
Print
|
129 |
+
</Button>
|
130 |
+
</div>
|
131 |
+
<div>
|
132 |
+
<Button
|
133 |
+
onClick={download}
|
134 |
+
disabled={!prompt?.length}
|
135 |
+
>
|
136 |
+
<span className="hidden md:inline">{
|
137 |
+
remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} panels β` : `Save`
|
138 |
+
}</span>
|
139 |
+
<span className="inline md:hidden">{
|
140 |
+
remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} β` : `Save`
|
141 |
+
}</span>
|
142 |
+
</Button>
|
143 |
+
</div>
|
144 |
+
*/}
|
145 |
+
<Button
|
146 |
+
onClick={handlePrint}
|
147 |
+
disabled={!prompt?.length}
|
148 |
+
>
|
149 |
+
<span className="hidden md:inline">{
|
150 |
+
remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} panels β` : `Save PDF`
|
151 |
+
}</span>
|
152 |
+
<span className="inline md:hidden">{
|
153 |
+
remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} β` : `Save`
|
154 |
+
}</span>
|
155 |
+
</Button>
|
156 |
+
<Share />
|
157 |
+
</div>
|
158 |
+
</div>
|
159 |
+
)
|
160 |
+
}
|
161 |
+
|
162 |
+
export default BottomBar
|
src/app/interface/bottom-bar/index.tsx
CHANGED
@@ -1,145 +1,8 @@
|
|
1 |
-
|
2 |
-
import { Button } from "@/components/ui/button"
|
3 |
-
import { cn } from "@/lib/utils"
|
4 |
-
import { About } from "../about"
|
5 |
-
import { startTransition, useState } from "react"
|
6 |
-
import { upscaleImage } from "@/app/engine/render"
|
7 |
-
import { sleep } from "@/lib/sleep"
|
8 |
-
import { AIClipFactory } from "../ai-clip-factory"
|
9 |
-
import { Share } from "../share"
|
10 |
-
import { SettingsDialog } from "../settings-dialog"
|
11 |
-
import { Login } from "../login"
|
12 |
|
13 |
-
|
14 |
-
const download = useStore(state => state.download)
|
15 |
-
const isGeneratingStory = useStore(state => state.isGeneratingStory)
|
16 |
-
const prompt = useStore(state => state.prompt)
|
17 |
-
const panelGenerationStatus = useStore(state => state.panelGenerationStatus)
|
18 |
-
const page = useStore(state => state.page)
|
19 |
-
const preset = useStore(state => state.preset)
|
20 |
-
const pageToImage = useStore(state => state.pageToImage)
|
21 |
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
const renderedScenes = useStore(state => state.renderedScenes)
|
27 |
-
const removeFromUpscaleQueue = useStore(state => state.removeFromUpscaleQueue)
|
28 |
-
const setRendered = useStore(state => state.setRendered)
|
29 |
-
const [isUpscaling, setUpscaling] = useState(false)
|
30 |
-
|
31 |
-
const handleUpscale = () => {
|
32 |
-
setUpscaling(true)
|
33 |
-
startTransition(() => {
|
34 |
-
const fn = async () => {
|
35 |
-
for (let [panelId, renderedScene] of Object.entries(upscaleQueue)) {
|
36 |
-
try {
|
37 |
-
console.log(`upscaling panel ${panelId} (${renderedScene.renderId})`)
|
38 |
-
const result = await upscaleImage(renderedScene.assetUrl)
|
39 |
-
await sleep(1000)
|
40 |
-
if (result.assetUrl) {
|
41 |
-
console.log(`upscale successful, removing ${panelId} (${renderedScene.renderId}) from upscale queue`)
|
42 |
-
setRendered(panelId, {
|
43 |
-
...renderedScene,
|
44 |
-
assetUrl: result.assetUrl
|
45 |
-
})
|
46 |
-
removeFromUpscaleQueue(panelId)
|
47 |
-
}
|
48 |
-
|
49 |
-
} catch (err) {
|
50 |
-
console.error(`failed to upscale: ${err}`)
|
51 |
-
}
|
52 |
-
}
|
53 |
-
|
54 |
-
setUpscaling(false)
|
55 |
-
}
|
56 |
-
|
57 |
-
fn()
|
58 |
-
})
|
59 |
-
}
|
60 |
-
|
61 |
-
const handlePrint = () => {
|
62 |
-
window.print()
|
63 |
-
}
|
64 |
-
|
65 |
-
return (
|
66 |
-
<div className={cn(
|
67 |
-
`print:hidden`,
|
68 |
-
`fixed bottom-2 md:bottom-4 left-2 right-0 md:left-3 md:right-1`,
|
69 |
-
`flex flex-row`,
|
70 |
-
`justify-between`,
|
71 |
-
`pointer-events-none`
|
72 |
-
)}>
|
73 |
-
<div className={cn(
|
74 |
-
`flex flex-row`,
|
75 |
-
`items-end`,
|
76 |
-
`pointer-events-auto`,
|
77 |
-
`animation-all duration-300 ease-in-out`,
|
78 |
-
isGeneratingStory ? `scale-0 opacity-0` : ``,
|
79 |
-
`space-x-3`,
|
80 |
-
`scale-[0.9]`
|
81 |
-
)}>
|
82 |
-
<About />
|
83 |
-
<Login />
|
84 |
-
{/*
|
85 |
-
Thank you clip factory for your service π«‘
|
86 |
-
<AIClipFactory />
|
87 |
-
*/}
|
88 |
-
</div>
|
89 |
-
<div className={cn(
|
90 |
-
`flex flex-row`,
|
91 |
-
`pointer-events-auto`,
|
92 |
-
`animation-all duration-300 ease-in-out`,
|
93 |
-
isGeneratingStory ? `scale-0 opacity-0` : ``,
|
94 |
-
`space-x-3`,
|
95 |
-
`scale-[0.9]`
|
96 |
-
)}>
|
97 |
-
<SettingsDialog />
|
98 |
-
{/*<Button
|
99 |
-
onClick={handleUpscale}
|
100 |
-
disabled={!prompt?.length || remainingImages > 0 || isUpscaling || !Object.values(upscaleQueue).length}
|
101 |
-
>
|
102 |
-
{isUpscaling
|
103 |
-
? `${allStatus.length - Object.values(upscaleQueue).length}/${allStatus.length} β`
|
104 |
-
: "Upscale"}
|
105 |
-
</Button>*/}
|
106 |
-
|
107 |
-
{/*
|
108 |
-
<div>
|
109 |
-
<Button
|
110 |
-
onClick={handlePrint}
|
111 |
-
disabled={!prompt?.length}
|
112 |
-
>
|
113 |
-
Print
|
114 |
-
</Button>
|
115 |
-
</div>
|
116 |
-
<div>
|
117 |
-
<Button
|
118 |
-
onClick={download}
|
119 |
-
disabled={!prompt?.length}
|
120 |
-
>
|
121 |
-
<span className="hidden md:inline">{
|
122 |
-
remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} panels β` : `Save`
|
123 |
-
}</span>
|
124 |
-
<span className="inline md:hidden">{
|
125 |
-
remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} β` : `Save`
|
126 |
-
}</span>
|
127 |
-
</Button>
|
128 |
-
</div>
|
129 |
-
*/}
|
130 |
-
<Button
|
131 |
-
onClick={handlePrint}
|
132 |
-
disabled={!prompt?.length}
|
133 |
-
>
|
134 |
-
<span className="hidden md:inline">{
|
135 |
-
remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} panels β` : `Save PDF`
|
136 |
-
}</span>
|
137 |
-
<span className="inline md:hidden">{
|
138 |
-
remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} β` : `Save`
|
139 |
-
}</span>
|
140 |
-
</Button>
|
141 |
-
<Share />
|
142 |
-
</div>
|
143 |
-
</div>
|
144 |
-
)
|
145 |
-
}
|
|
|
1 |
+
"use client"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
+
import dynamic from "next/dynamic";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
|
5 |
+
export const BottomBar = dynamic(() => import("./bottom-bar"), {
|
6 |
+
// Make sure we turn SSR off
|
7 |
+
ssr: false,
|
8 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/interface/edit-modal/index.tsx
CHANGED
@@ -1,12 +1,9 @@
|
|
1 |
import { ReactNode, useState } from "react"
|
2 |
-
import { RxReload, RxPencil2 } from "react-icons/rx"
|
3 |
|
4 |
import { Button } from "@/components/ui/button"
|
5 |
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
6 |
-
import { Input } from "@/components/ui/input"
|
7 |
-
import { cn } from "@/lib/utils"
|
8 |
-
import { Textarea } from "@/components/ui/textarea"
|
9 |
|
|
|
10 |
|
11 |
export function EditModal({
|
12 |
existingPrompt,
|
|
|
1 |
import { ReactNode, 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 { Textarea } from "@/components/ui/textarea"
|
7 |
|
8 |
export function EditModal({
|
9 |
existingPrompt,
|
src/app/interface/login/index.tsx
CHANGED
@@ -1,30 +1,8 @@
|
|
1 |
"use client"
|
2 |
|
3 |
-
import
|
4 |
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
const { canLogin, login, isLoggedIn, oauthResult } = useOAuth({ debug: false })
|
10 |
-
|
11 |
-
useEffect(() => {
|
12 |
-
if (!oauthResult) {
|
13 |
-
return
|
14 |
-
}
|
15 |
-
|
16 |
-
const { userInfo } = oauthResult
|
17 |
-
|
18 |
-
// TODO use the Inference API
|
19 |
-
|
20 |
-
if (userInfo.isPro) {
|
21 |
-
// TODO we could do something with the fact the user is PRO versus other types of users
|
22 |
-
}
|
23 |
-
}, [canLogin, isLoggedIn, oauthResult])
|
24 |
-
|
25 |
-
if (isLoggedIn || canLogin) {
|
26 |
-
return <Button onClick={login}>Sign-in with Hugging Face</Button>
|
27 |
-
} else {
|
28 |
-
return null
|
29 |
-
}
|
30 |
-
}
|
|
|
1 |
"use client"
|
2 |
|
3 |
+
import dynamic from "next/dynamic";
|
4 |
|
5 |
+
export const Login = dynamic(() => import("./login"), {
|
6 |
+
// Make sure we turn SSR off
|
7 |
+
ssr: false,
|
8 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/interface/login/login.tsx
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { useEffect } from "react"
|
4 |
+
|
5 |
+
import { Button } from "@/components/ui/button"
|
6 |
+
import { useOAuth } from "@/lib/useOAuth"
|
7 |
+
|
8 |
+
function Login() {
|
9 |
+
const { canLogin, login, isLoggedIn, oauthResult } = useOAuth({ debug: false })
|
10 |
+
|
11 |
+
useEffect(() => {
|
12 |
+
if (!oauthResult) {
|
13 |
+
return
|
14 |
+
}
|
15 |
+
|
16 |
+
const { userInfo } = oauthResult
|
17 |
+
|
18 |
+
// TODO use the Inference API
|
19 |
+
|
20 |
+
if (userInfo.isPro) {
|
21 |
+
// TODO we could do something with the fact the user is PRO versus other types of users
|
22 |
+
}
|
23 |
+
}, [canLogin, isLoggedIn, oauthResult])
|
24 |
+
|
25 |
+
if (isLoggedIn || canLogin) {
|
26 |
+
return <Button onClick={login}>Sign-in with Hugging Face</Button>
|
27 |
+
} else {
|
28 |
+
return null
|
29 |
+
}
|
30 |
+
}
|
31 |
+
|
32 |
+
export default Login
|
src/app/interface/settings-dialog/defaultSettings.ts
CHANGED
@@ -17,4 +17,5 @@ export const defaultSettings: Settings = {
|
|
17 |
openaiApiLanguageModel: "gpt-4",
|
18 |
groqApiKey: "",
|
19 |
groqApiLanguageModel: "mixtral-8x7b-32768",
|
|
|
20 |
}
|
|
|
17 |
openaiApiLanguageModel: "gpt-4",
|
18 |
groqApiKey: "",
|
19 |
groqApiLanguageModel: "mixtral-8x7b-32768",
|
20 |
+
hasGeneratedAtLeastOnce: false,
|
21 |
}
|
src/app/interface/settings-dialog/getSettings.ts
CHANGED
@@ -24,6 +24,7 @@ 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 |
}
|
28 |
} catch (err) {
|
29 |
return {
|
|
|
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 {
|
src/app/interface/settings-dialog/localStorageKeys.ts
CHANGED
@@ -17,4 +17,5 @@ export const localStorageKeys: Record<keyof Settings, string> = {
|
|
17 |
openaiApiLanguageModel: "CONF_AUTH_OPENAI_API_LANGUAGE_MODEL",
|
18 |
groqApiKey: "CONF_AUTH_GROQ_API_KEY",
|
19 |
groqApiLanguageModel: "CONF_AUTH_GROQ_API_LANGUAGE_MODEL",
|
|
|
20 |
}
|
|
|
17 |
openaiApiLanguageModel: "CONF_AUTH_OPENAI_API_LANGUAGE_MODEL",
|
18 |
groqApiKey: "CONF_AUTH_GROQ_API_KEY",
|
19 |
groqApiLanguageModel: "CONF_AUTH_GROQ_API_LANGUAGE_MODEL",
|
20 |
+
hasGeneratedAtLeastOnce: "CONF_HAS_GENERATED_AT_LEAST_ONCE",
|
21 |
}
|
src/app/interface/top-menu/index.tsx
CHANGED
@@ -26,6 +26,11 @@ import layoutPreview2 from "../../../../public/layouts/layout2.jpg"
|
|
26 |
import layoutPreview3 from "../../../../public/layouts/layout3.jpg"
|
27 |
import { StaticImageData } from "next/image"
|
28 |
import { Switch } from "@/components/ui/switch"
|
|
|
|
|
|
|
|
|
|
|
29 |
|
30 |
const layoutIcons: Partial<Record<LayoutName, StaticImageData>> = {
|
31 |
Layout0: layoutPreview0,
|
@@ -65,9 +70,25 @@ export function TopMenu() {
|
|
65 |
|
66 |
const [draftPreset, setDraftPreset] = useState<PresetName>(requestedPreset)
|
67 |
const [draftLayout, setDraftLayout] = useState<LayoutName>(requestedLayout)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
|
69 |
const handleSubmit = () => {
|
|
|
70 |
|
|
|
|
|
|
|
|
|
|
|
71 |
const promptChanged = draftPrompt.trim() !== prompt.trim()
|
72 |
const presetChanged = draftPreset !== preset.id
|
73 |
const layoutChanged = draftLayout !== layout
|
@@ -202,7 +223,7 @@ export function TopMenu() {
|
|
202 |
<div className="flex flex-row flex-grow w-full">
|
203 |
<div className="flex flex-row flex-grow w-full">
|
204 |
<Input
|
205 |
-
placeholder="1. Story
|
206 |
className="w-1/2 bg-neutral-300 text-neutral-800 dark:bg-neutral-300 dark:text-neutral-800 rounded-r-none border-r-stone-100"
|
207 |
// disabled={atLeastOnePanelIsBusy}
|
208 |
onChange={(e) => {
|
@@ -216,7 +237,7 @@ export function TopMenu() {
|
|
216 |
value={draftPromptB}
|
217 |
/>
|
218 |
<Input
|
219 |
-
placeholder="2. Style
|
220 |
className="w-1/2 bg-neutral-300 text-neutral-800 dark:bg-neutral-300 dark:text-neutral-800 border-l-stone-100 rounded-l-none rounded-r-none"
|
221 |
// disabled={atLeastOnePanelIsBusy}
|
222 |
onChange={(e) => {
|
@@ -230,19 +251,21 @@ export function TopMenu() {
|
|
230 |
value={draftPromptA}
|
231 |
/>
|
232 |
</div>
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
|
|
|
|
246 |
</div>
|
247 |
</div>
|
248 |
{/*
|
|
|
26 |
import layoutPreview3 from "../../../../public/layouts/layout3.jpg"
|
27 |
import { StaticImageData } from "next/image"
|
28 |
import { Switch } from "@/components/ui/switch"
|
29 |
+
import { useLocalStorage } from "usehooks-ts"
|
30 |
+
import { useOAuth } from "@/lib/useOAuth"
|
31 |
+
import { localStorageKeys } from "../settings-dialog/localStorageKeys"
|
32 |
+
import { defaultSettings } from "../settings-dialog/defaultSettings"
|
33 |
+
import { AuthWall } from "../auth-wall"
|
34 |
|
35 |
const layoutIcons: Partial<Record<LayoutName, StaticImageData>> = {
|
36 |
Layout0: layoutPreview0,
|
|
|
70 |
|
71 |
const [draftPreset, setDraftPreset] = useState<PresetName>(requestedPreset)
|
72 |
const [draftLayout, setDraftLayout] = useState<LayoutName>(requestedLayout)
|
73 |
+
|
74 |
+
|
75 |
+
const { canLogin, login, isLoggedIn, oauthResult } = useOAuth({ debug: false })
|
76 |
+
|
77 |
+
const [hasGeneratedAtLeastOnce, setHasGeneratedAtLeastOnce] = useLocalStorage<boolean>(
|
78 |
+
localStorageKeys.hasGeneratedAtLeastOnce,
|
79 |
+
defaultSettings.hasGeneratedAtLeastOnce
|
80 |
+
)
|
81 |
+
|
82 |
+
const [showAuthWall, setShowAuthWall] = useState(false)
|
83 |
|
84 |
const handleSubmit = () => {
|
85 |
+
const enableAuthWall = `${process.env.NEXT_PUBLIC_ENABLE_HUGGING_FACE_OAUTH_WALL || "false"}` === "true"
|
86 |
|
87 |
+
if (enableAuthWall && hasGeneratedAtLeastOnce && !isLoggedIn) {
|
88 |
+
setShowAuthWall(true)
|
89 |
+
return
|
90 |
+
}
|
91 |
+
|
92 |
const promptChanged = draftPrompt.trim() !== prompt.trim()
|
93 |
const presetChanged = draftPreset !== preset.id
|
94 |
const layoutChanged = draftLayout !== layout
|
|
|
223 |
<div className="flex flex-row flex-grow w-full">
|
224 |
<div className="flex flex-row flex-grow w-full">
|
225 |
<Input
|
226 |
+
placeholder="1. Story (eg. detective dog)"
|
227 |
className="w-1/2 bg-neutral-300 text-neutral-800 dark:bg-neutral-300 dark:text-neutral-800 rounded-r-none border-r-stone-100"
|
228 |
// disabled={atLeastOnePanelIsBusy}
|
229 |
onChange={(e) => {
|
|
|
237 |
value={draftPromptB}
|
238 |
/>
|
239 |
<Input
|
240 |
+
placeholder="2. Style (eg 'rain, shiba inu')"
|
241 |
className="w-1/2 bg-neutral-300 text-neutral-800 dark:bg-neutral-300 dark:text-neutral-800 border-l-stone-100 rounded-l-none rounded-r-none"
|
242 |
// disabled={atLeastOnePanelIsBusy}
|
243 |
onChange={(e) => {
|
|
|
251 |
value={draftPromptA}
|
252 |
/>
|
253 |
</div>
|
254 |
+
<Button
|
255 |
+
className={cn(
|
256 |
+
`rounded-l-none cursor-pointer`,
|
257 |
+
`transition-all duration-200 ease-in-out`,
|
258 |
+
`bg-[rgb(59,134,247)] hover:bg-[rgb(69,144,255)] disabled:bg-[rgb(59,134,247)]`
|
259 |
+
)}
|
260 |
+
onClick={() => {
|
261 |
+
handleSubmit()
|
262 |
+
}}
|
263 |
+
disabled={!draftPrompt?.trim().length || isBusy}
|
264 |
+
>
|
265 |
+
Go
|
266 |
+
</Button>
|
267 |
+
|
268 |
+
<AuthWall show={showAuthWall} />
|
269 |
</div>
|
270 |
</div>
|
271 |
{/*
|
src/app/main.tsx
CHANGED
@@ -3,16 +3,17 @@
|
|
3 |
import { useEffect, useState, useTransition } from "react"
|
4 |
|
5 |
import { cn } from "@/lib/utils"
|
6 |
-
import { TopMenu } from "./interface/top-menu"
|
7 |
import { fonts } from "@/lib/fonts"
|
|
|
|
|
|
|
|
|
|
|
8 |
import { useStore } from "./store"
|
9 |
import { Zoom } from "./interface/zoom"
|
10 |
import { BottomBar } from "./interface/bottom-bar"
|
11 |
import { Page } from "./interface/page"
|
12 |
-
import { GeneratedPanel } from "@/types"
|
13 |
-
import { joinWords } from "@/lib/joinWords"
|
14 |
import { getStoryContinuation } from "./queries/getStoryContinuation"
|
15 |
-
import { MAX_NB_PAGES, NB_TOTAL_PANELS_TO_GENERATE } from "@/config"
|
16 |
|
17 |
export default function Main() {
|
18 |
const [_isPending, startTransition] = useTransition()
|
|
|
3 |
import { 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 |
+
import { MAX_NB_PAGES } from "@/config"
|
10 |
+
|
11 |
+
import { TopMenu } from "./interface/top-menu"
|
12 |
import { useStore } from "./store"
|
13 |
import { Zoom } from "./interface/zoom"
|
14 |
import { BottomBar } from "./interface/bottom-bar"
|
15 |
import { Page } from "./interface/page"
|
|
|
|
|
16 |
import { getStoryContinuation } from "./queries/getStoryContinuation"
|
|
|
17 |
|
18 |
export default function Main() {
|
19 |
const [_isPending, startTransition] = useTransition()
|
src/app/queries/getStory.ts
DELETED
@@ -1,90 +0,0 @@
|
|
1 |
-
|
2 |
-
import { predict } from "./predict"
|
3 |
-
import { Preset } from "../engine/presets"
|
4 |
-
import { GeneratedPanels } from "@/types"
|
5 |
-
import { cleanJson } from "@/lib/cleanJson"
|
6 |
-
import { createZephyrPrompt } from "@/lib/createZephyrPrompt"
|
7 |
-
|
8 |
-
import { dirtyGeneratedPanelCleaner } from "@/lib/dirtyGeneratedPanelCleaner"
|
9 |
-
import { dirtyGeneratedPanelsParser } from "@/lib/dirtyGeneratedPanelsParser"
|
10 |
-
|
11 |
-
export const getStory = async ({
|
12 |
-
preset,
|
13 |
-
prompt = "",
|
14 |
-
nbTotalPanels = 4,
|
15 |
-
}: {
|
16 |
-
preset: Preset;
|
17 |
-
prompt: string;
|
18 |
-
nbTotalPanels: number;
|
19 |
-
}): Promise<GeneratedPanels> => {
|
20 |
-
throw new Error("legacy, deprecated")
|
21 |
-
|
22 |
-
// In case you need to quickly debug the RENDERING engine you can uncomment this:
|
23 |
-
// return mockGeneratedPanels
|
24 |
-
|
25 |
-
const query = createZephyrPrompt([
|
26 |
-
{
|
27 |
-
role: "system",
|
28 |
-
content: [
|
29 |
-
`You are a writer specialized in ${preset.llmPrompt}`,
|
30 |
-
`Please write detailed drawing instructions and short (2-3 sentences long) speech captions for the ${nbTotalPanels} panels of a new story. Please make sure each of the ${nbTotalPanels} panels include info about character gender, age, origin, clothes, colors, location, lights, etc.`,
|
31 |
-
`Give your response as a VALID JSON array like this: \`Array<{ panel: number; instructions: string; caption: string; }>\`.`,
|
32 |
-
// `Give your response as Markdown bullet points.`,
|
33 |
-
`Be brief in your ${nbTotalPanels} instructions and narrative captions, don't add your own comments. The whole story must be captivating, smart, entertaining. Be straight to the point, and never reply things like "Sure, I can.." etc. Reply using valid JSON.`
|
34 |
-
].filter(item => item).join("\n")
|
35 |
-
},
|
36 |
-
{
|
37 |
-
role: "user",
|
38 |
-
content: `The story is: ${prompt}`,
|
39 |
-
}
|
40 |
-
]) + "\n```[{"
|
41 |
-
|
42 |
-
|
43 |
-
let result = ""
|
44 |
-
|
45 |
-
try {
|
46 |
-
// console.log(`calling predict(${query}, ${nbTotalPanels})`)
|
47 |
-
result = `${await predict(query, nbTotalPanels) || ""}`.trim()
|
48 |
-
if (!result.length) {
|
49 |
-
throw new Error("empty result!")
|
50 |
-
}
|
51 |
-
} catch (err) {
|
52 |
-
// console.log(`prediction of the story failed, trying again..`)
|
53 |
-
try {
|
54 |
-
result = `${await predict(query+".", nbTotalPanels) || ""}`.trim()
|
55 |
-
if (!result.length) {
|
56 |
-
throw new Error("empty result!")
|
57 |
-
}
|
58 |
-
} catch (err) {
|
59 |
-
console.error(`prediction of the story failed again π©`)
|
60 |
-
throw new Error(`failed to generate the story ${err}`)
|
61 |
-
}
|
62 |
-
}
|
63 |
-
|
64 |
-
// console.log("Raw response from LLM:", result)
|
65 |
-
const tmp = cleanJson(result)
|
66 |
-
|
67 |
-
let GeneratedPanels: GeneratedPanels = []
|
68 |
-
|
69 |
-
try {
|
70 |
-
GeneratedPanels = dirtyGeneratedPanelsParser(tmp)
|
71 |
-
} catch (err) {
|
72 |
-
// console.log(`failed to read LLM response: ${err}`)
|
73 |
-
// console.log(`original response was:`, result)
|
74 |
-
|
75 |
-
// in case of failure here, it might be because the LLM hallucinated a completely different response,
|
76 |
-
// such as markdown. There is no real solution.. but we can try a fallback:
|
77 |
-
|
78 |
-
GeneratedPanels = (
|
79 |
-
tmp.split("*")
|
80 |
-
.map(item => item.trim())
|
81 |
-
.map((cap, i) => ({
|
82 |
-
panel: i,
|
83 |
-
caption: cap,
|
84 |
-
instructions: cap,
|
85 |
-
}))
|
86 |
-
)
|
87 |
-
}
|
88 |
-
|
89 |
-
return GeneratedPanels.map(res => dirtyGeneratedPanelCleaner(res))
|
90 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/queries/getStoryContinuation.ts
CHANGED
@@ -7,7 +7,7 @@ export const getStoryContinuation = async ({
|
|
7 |
preset,
|
8 |
stylePrompt = "",
|
9 |
userStoryPrompt = "",
|
10 |
-
nbPanelsToGenerate =
|
11 |
nbTotalPanels = 4,
|
12 |
existingPanels = [],
|
13 |
}: {
|
|
|
7 |
preset,
|
8 |
stylePrompt = "",
|
9 |
userStoryPrompt = "",
|
10 |
+
nbPanelsToGenerate = 2,
|
11 |
nbTotalPanels = 4,
|
12 |
existingPanels = [],
|
13 |
}: {
|
src/app/queries/predictNextPanels.ts
CHANGED
@@ -11,7 +11,7 @@ import { sleep } from "@/lib/sleep"
|
|
11 |
export const predictNextPanels = async ({
|
12 |
preset,
|
13 |
prompt = "",
|
14 |
-
nbPanelsToGenerate =
|
15 |
nbTotalPanels = 4,
|
16 |
existingPanels = [],
|
17 |
}: {
|
|
|
11 |
export const predictNextPanels = async ({
|
12 |
preset,
|
13 |
prompt = "",
|
14 |
+
nbPanelsToGenerate = 2,
|
15 |
nbTotalPanels = 4,
|
16 |
existingPanels = [],
|
17 |
}: {
|
src/config.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
import { getValidNumber } from "./lib/getValidNumber"
|
2 |
|
3 |
-
export const MAX_NB_PAGES = getValidNumber(process.env.NEXT_PUBLIC_MAX_NB_PAGES, 1,
|
4 |
|
5 |
// TODO: this one should be dynamic and depend upon the page layout type
|
6 |
export const NB_PANELS_PER_PAGE = 4
|
|
|
1 |
import { getValidNumber } from "./lib/getValidNumber"
|
2 |
|
3 |
+
export const MAX_NB_PAGES = getValidNumber(process.env.NEXT_PUBLIC_MAX_NB_PAGES, 1, 8, 1)
|
4 |
|
5 |
// TODO: this one should be dynamic and depend upon the page layout type
|
6 |
export const NB_PANELS_PER_PAGE = 4
|
src/lib/useOAuth.ts
CHANGED
@@ -31,7 +31,6 @@ export function useOAuth({
|
|
31 |
const scopes = "openid profile inference-api"
|
32 |
|
33 |
const isOAuthEnabled = useOAuthEnabled()
|
34 |
-
const isBetaEnabled = useBetaEnabled()
|
35 |
|
36 |
const searchParams = useSearchParams()
|
37 |
const code = searchParams.get("code")
|
@@ -39,7 +38,7 @@ export function useOAuth({
|
|
39 |
|
40 |
const hasReceivedFreshOAuth = Boolean(code && state)
|
41 |
|
42 |
-
const canLogin: boolean = Boolean(clientId && isOAuthEnabled
|
43 |
const isLoggedIn = Boolean(oauthResult)
|
44 |
|
45 |
if (debug) {
|
@@ -49,7 +48,6 @@ export function useOAuth({
|
|
49 |
redirectUrl,
|
50 |
scopes,
|
51 |
isOAuthEnabled,
|
52 |
-
isBetaEnabled,
|
53 |
code,
|
54 |
state,
|
55 |
hasReceivedFreshOAuth,
|
@@ -64,7 +62,6 @@ export function useOAuth({
|
|
64 |
redirectUrl: 'http://localhost:3000',
|
65 |
scopes: 'openid profile inference-api',
|
66 |
isOAuthEnabled: true,
|
67 |
-
isBetaEnabled: false,
|
68 |
code: '...........',
|
69 |
state: '{"nonce":".........","redirectUri":"http://localhost:3000"}',
|
70 |
hasReceivedFreshOAuth: true,
|
@@ -77,6 +74,7 @@ export function useOAuth({
|
|
77 |
useEffect(() => {
|
78 |
// no need to perfor the rest if the operation is there is nothing in the url
|
79 |
if (hasReceivedFreshOAuth) {
|
|
|
80 |
(async () => {
|
81 |
const maybeValidOAuth = await oauthHandleRedirectIfPresent()
|
82 |
|
@@ -91,6 +89,9 @@ export function useOAuth({
|
|
91 |
console.log("useOAuth::useEffect 1: correctly received the new oauth result, saving it to local storage:", newOAuth)
|
92 |
}
|
93 |
setOAuthResult(newOAuth)
|
|
|
|
|
|
|
94 |
}
|
95 |
})()
|
96 |
}
|
|
|
31 |
const scopes = "openid profile inference-api"
|
32 |
|
33 |
const isOAuthEnabled = useOAuthEnabled()
|
|
|
34 |
|
35 |
const searchParams = useSearchParams()
|
36 |
const code = searchParams.get("code")
|
|
|
38 |
|
39 |
const hasReceivedFreshOAuth = Boolean(code && state)
|
40 |
|
41 |
+
const canLogin: boolean = Boolean(clientId && isOAuthEnabled)
|
42 |
const isLoggedIn = Boolean(oauthResult)
|
43 |
|
44 |
if (debug) {
|
|
|
48 |
redirectUrl,
|
49 |
scopes,
|
50 |
isOAuthEnabled,
|
|
|
51 |
code,
|
52 |
state,
|
53 |
hasReceivedFreshOAuth,
|
|
|
62 |
redirectUrl: 'http://localhost:3000',
|
63 |
scopes: 'openid profile inference-api',
|
64 |
isOAuthEnabled: true,
|
|
|
65 |
code: '...........',
|
66 |
state: '{"nonce":".........","redirectUri":"http://localhost:3000"}',
|
67 |
hasReceivedFreshOAuth: true,
|
|
|
74 |
useEffect(() => {
|
75 |
// no need to perfor the rest if the operation is there is nothing in the url
|
76 |
if (hasReceivedFreshOAuth) {
|
77 |
+
|
78 |
(async () => {
|
79 |
const maybeValidOAuth = await oauthHandleRedirectIfPresent()
|
80 |
|
|
|
89 |
console.log("useOAuth::useEffect 1: correctly received the new oauth result, saving it to local storage:", newOAuth)
|
90 |
}
|
91 |
setOAuthResult(newOAuth)
|
92 |
+
|
93 |
+
// once set we can (brutally) reload the page
|
94 |
+
window.location.href = `//${window.location.host}${window.location.pathname}`
|
95 |
}
|
96 |
})()
|
97 |
}
|
src/types.ts
CHANGED
@@ -173,4 +173,5 @@ export type Settings = {
|
|
173 |
openaiApiLanguageModel: string
|
174 |
groqApiKey: string
|
175 |
groqApiLanguageModel: string
|
|
|
176 |
}
|
|
|
173 |
openaiApiLanguageModel: string
|
174 |
groqApiKey: string
|
175 |
groqApiLanguageModel: string
|
176 |
+
hasGeneratedAtLeastOnce: boolean
|
177 |
}
|