Julian Bilcke
		
	commited on
		
		
					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)'],
         
     |