jbilcke-hf HF staff commited on
Commit
b48537f
1 Parent(s): 8cd2153

add a login wall

Browse files
.env CHANGED
@@ -14,7 +14,7 @@ RENDERING_ENGINE="INFERENCE_API"
14
  LLM_ENGINE="INFERENCE_API"
15
 
16
  # set this to control the number of pages
17
- NEXT_PUBLIC_MAX_NB_PAGES="2"
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="false"
 
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 = "american_comic_90"
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 a free and open-source application made to demonstrate the capabilities of AI models.
 
 
 
26
  </p>
27
  <p>
28
- And yes, 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>
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
- import { useStore } from "@/app/store"
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
- export function BottomBar() {
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
- const allStatus = Object.values(panelGenerationStatus)
23
- const remainingImages = allStatus.reduce((acc, s) => (acc + (s ? 1 : 0)), 0)
24
-
25
- const upscaleQueue = useStore(state => state.upscaleQueue)
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 { useEffect } from "react"
4
 
5
- import { Button } from "@/components/ui/button"
6
- import { useOAuth } from "@/lib/useOAuth"
7
-
8
- export 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
- }
 
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 prompt"
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/character prompt"
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
- <Button
234
- className={cn(
235
- `rounded-l-none cursor-pointer`,
236
- `transition-all duration-200 ease-in-out`,
237
- `bg-[rgb(59,134,247)] hover:bg-[rgb(69,144,255)] disabled:bg-[rgb(59,134,247)]`
238
- )}
239
- onClick={() => {
240
- handleSubmit()
241
- }}
242
- disabled={!draftPrompt?.trim().length || isBusy}
243
- >
244
- Go
245
- </Button>
 
 
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 = 1,
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 = 1,
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, 2, 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 && isBetaEnabled)
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
  }