jbilcke-hf HF staff commited on
Commit
f4dea7d
1 Parent(s): a79c634

add LLM-generated story

Browse files
src/app/interface/top-menu/index.tsx CHANGED
@@ -1,5 +1,7 @@
1
  "use client"
2
 
 
 
3
  import {
4
  Select,
5
  SelectContent,
@@ -12,7 +14,6 @@ import { cn } from "@/lib/utils"
12
  import { FontName, fontList, fonts } from "@/lib/fonts"
13
  import { Input } from "@/components/ui/input"
14
  import { defaultPreset, getPreset, presets } from "@/app/engine/presets"
15
- import { useState } from "react"
16
  import { useStore } from "@/app/store"
17
 
18
  export function TopMenu() {
@@ -119,7 +120,7 @@ export function TopMenu() {
119
  )}>
120
  <Label className="flex text-sm w-24">Font:</Label>
121
  <Select
122
- defaultValue={fontList.includes(preset.font) ? preset.font : "cartoonist"}
123
  onValueChange={(value) => { setFont(value as FontName) }}
124
  // disabled={atLeastOnePanelIsBusy}
125
  disabled={true}
 
1
  "use client"
2
 
3
+ import { useState } from "react"
4
+
5
  import {
6
  Select,
7
  SelectContent,
 
14
  import { FontName, fontList, fonts } from "@/lib/fonts"
15
  import { Input } from "@/components/ui/input"
16
  import { defaultPreset, getPreset, presets } from "@/app/engine/presets"
 
17
  import { useStore } from "@/app/store"
18
 
19
  export function TopMenu() {
 
120
  )}>
121
  <Label className="flex text-sm w-24">Font:</Label>
122
  <Select
123
+ defaultValue={fontList.includes(preset.font) ? preset.font : "actionman"}
124
  onValueChange={(value) => { setFont(value as FontName) }}
125
  // disabled={atLeastOnePanelIsBusy}
126
  disabled={true}
src/app/main.tsx CHANGED
@@ -7,10 +7,11 @@ import { PresetName, defaultPreset, getPreset } from "@/app/engine/presets"
7
 
8
  import { cn } from "@/lib/utils"
9
  import { TopMenu } from "./interface/top-menu"
10
- import { FontName, defaultFont } from "@/lib/fonts"
11
  import { getRandomLayoutName, layouts } from "./layouts"
12
  import { useStore } from "./store"
13
  import { Zoom } from "./interface/zoom"
 
14
 
15
  export default function Main() {
16
  const [_isPending, startTransition] = useTransition()
@@ -20,6 +21,9 @@ export default function Main() {
20
  const requestedFont = (searchParams.get('font') as FontName) || defaultFont
21
  const requestedPrompt = (searchParams.get('prompt') as string) || ""
22
 
 
 
 
23
  const font = useStore(state => state.font)
24
  const setFont = useStore(state => state.setFont)
25
 
@@ -53,22 +57,37 @@ export default function Main() {
53
  useEffect(() => {
54
  if (!prompt) { return }
55
 
56
- const newLayout = getRandomLayoutName()
57
- console.log("using layout " + newLayout)
58
- setLayout(newLayout)
59
-
60
- // TODO call the LLM here!
61
- const panelPrompt = preset.imagePrompt(prompt).join(", ")
62
- console.log("panelPrompt:", panelPrompt)
63
-
64
- // what we want is for it to invent a small "story"
65
- // we are going to use a LLM for this, but until then let's do this:
66
- setPanels([
67
- `introduction scene, ${panelPrompt}`,
68
- panelPrompt,
69
- panelPrompt,
70
- `final scene, ${panelPrompt}`,
71
- ])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  }, [prompt, preset?.label]) // important: we need to react to preset changes too
73
 
74
  const LayoutElement = (layouts as any)[layout]
@@ -78,7 +97,9 @@ export default function Main() {
78
  <TopMenu />
79
  <div className={cn(
80
  `flex items-start w-screen h-screen pt-[120px] px-16 md:pt-[72px] overflow-y-scroll`,
81
- `transition-all duration-200 ease-in-out`
 
 
82
  )}>
83
  <div className="flex flex-col items-center w-full">
84
  <div
@@ -105,6 +126,23 @@ export default function Main() {
105
  </div>
106
  </div>
107
  <Zoom />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  </div>
109
  )
110
  }
 
7
 
8
  import { cn } from "@/lib/utils"
9
  import { TopMenu } from "./interface/top-menu"
10
+ import { FontName, defaultFont, fontList, fonts } from "@/lib/fonts"
11
  import { getRandomLayoutName, layouts } from "./layouts"
12
  import { useStore } from "./store"
13
  import { Zoom } from "./interface/zoom"
14
+ import { getStory } from "./queries/getStory"
15
 
16
  export default function Main() {
17
  const [_isPending, startTransition] = useTransition()
 
21
  const requestedFont = (searchParams.get('font') as FontName) || defaultFont
22
  const requestedPrompt = (searchParams.get('prompt') as string) || ""
23
 
24
+ const isGeneratingStory = useStore(state => state.isGeneratingStory)
25
+ const setGeneratingStory = useStore(state => state.setGeneratingStory)
26
+
27
  const font = useStore(state => state.font)
28
  const setFont = useStore(state => state.setFont)
29
 
 
57
  useEffect(() => {
58
  if (!prompt) { return }
59
 
60
+ startTransition(async () => {
61
+
62
+ setGeneratingStory(true)
63
+
64
+ const newLayout = getRandomLayoutName()
65
+ console.log("using layout " + newLayout)
66
+ setLayout(newLayout)
67
+
68
+ try {
69
+ const llmResponse = await getStory({ preset, prompt })
70
+ console.log("response:", llmResponse)
71
+
72
+ // TODO call the LLM here!
73
+ const panelPromptPrefix = preset.imagePrompt(prompt).join(", ")
74
+ console.log("panel prompt prefix:", panelPromptPrefix)
75
+
76
+ const nbPanels = 4
77
+ const newPanels: string[] = []
78
+
79
+ for (let p = 0; p < nbPanels; p++) {
80
+ const newPanel = [panelPromptPrefix, llmResponse[p] || ""]
81
+ newPanels.push(newPanel.map(chunk => chunk).join(", "))
82
+ }
83
+ console.log("newPanels:", newPanels)
84
+ setPanels(newPanels)
85
+ } catch (err) {
86
+ console.error(err)
87
+ } finally {
88
+ setGeneratingStory(false)
89
+ }
90
+ })
91
  }, [prompt, preset?.label]) // important: we need to react to preset changes too
92
 
93
  const LayoutElement = (layouts as any)[layout]
 
97
  <TopMenu />
98
  <div className={cn(
99
  `flex items-start w-screen h-screen pt-[120px] px-16 md:pt-[72px] overflow-y-scroll`,
100
+ `transition-all duration-200 ease-in-out`,
101
+
102
+ fonts.actionman.className
103
  )}>
104
  <div className="flex flex-col items-center w-full">
105
  <div
 
126
  </div>
127
  </div>
128
  <Zoom />
129
+ <div className={cn(
130
+ `z-20 fixed inset-0`,
131
+ `flex flex-row items-center justify-center`,
132
+ `transition-all duration-300 ease-in-out`,
133
+ isGeneratingStory
134
+ ? `bg-zinc-100/10 backdrop-blur-md`
135
+ : `bg-zinc-100/0 backdrop-blur-none pointer-events-none`,
136
+ fonts.actionman.className
137
+ )}>
138
+ <div className={cn(
139
+ `text-center text-lg text-stone-600 w-[70%]`,
140
+ isGeneratingStory ? ``: `scale-0 opacity-0`,
141
+ `transition-all duration-300 ease-in-out`,
142
+ )}>
143
+ Generating your story.. (hold tight)
144
+ </div>
145
+ </div>
146
  </div>
147
  )
148
  }
src/app/queries/getStory.ts ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createLlamaPrompt } from "@/lib/createLlamaPrompt"
2
+
3
+ import { predict } from "./predict"
4
+ import { Preset } from "../engine/presets"
5
+
6
+ type LLMResponse = Array<{panel: number; caption: string }>
7
+
8
+ export const getStory = async ({
9
+ preset,
10
+ prompt = "",
11
+ }: {
12
+ preset: Preset;
13
+ prompt: string;
14
+ }): Promise<string[]> => {
15
+
16
+ const query = createLlamaPrompt([
17
+ {
18
+ role: "system",
19
+ content: [
20
+ `You are a comic book author specialized in ${preset.llmPrompt}`,
21
+ `Please generate detailed drawing instructions for the 4 panels of a new silent comic book page.`,
22
+ `Give your response as a JSON array like this: \`Array<{ panel: number; caption: string}>\`.`,
23
+ // `Give your response as Markdown bullet points.`,
24
+ `Be brief in your caption don't add your own comments. Be straight to the point, and never reply things like "Sure, I can.." etc.`
25
+ ].filter(item => item).join("\n")
26
+ },
27
+ {
28
+ role: "user",
29
+ content: `The story is: ${prompt}`,
30
+ }
31
+ ])
32
+
33
+
34
+ let result = ""
35
+ try {
36
+ result = await predict(query)
37
+ if (!result.trim().length) {
38
+ throw new Error("empty result!")
39
+ }
40
+ } catch (err) {
41
+ console.log(`prediction of the story failed, trying again..`)
42
+ try {
43
+ result = await predict(query+".")
44
+ if (!result.trim().length) {
45
+ throw new Error("empty result!")
46
+ }
47
+ } catch (err) {
48
+ console.error(`prediction of the story failed again!`)
49
+ throw new Error(`failed to generate the story ${err}`)
50
+ }
51
+ }
52
+
53
+ console.log("Raw response from LLM:", result)
54
+ let tmp = result // result.split("Caption:").pop() || result
55
+ tmp = tmp
56
+ .replaceAll("}}", "}")
57
+ .replaceAll("]]", "]")
58
+ .replaceAll(",,", ",")
59
+
60
+ try {
61
+ // we only keep what's after the first [
62
+ let jsonOrNot = `[${tmp.split("[").pop() || ""}`
63
+
64
+ // and before the first ]
65
+ jsonOrNot = `${jsonOrNot.split("]").shift() || ""}]`
66
+
67
+ const jsonData = JSON.parse(jsonOrNot) as LLMResponse
68
+ const captions = jsonData.map(item => item.caption.trim())
69
+ return captions.map(caption => caption.split(":").pop()?.trim() || "")
70
+ } catch (err) {
71
+ console.log(`failed to read LLM response: ${err}`)
72
+ return []
73
+ }
74
+ }
src/app/queries/{getBackground.ts → getStyle.ts} RENAMED
@@ -3,54 +3,50 @@ import { createLlamaPrompt } from "@/lib/createLlamaPrompt"
3
  import { predict } from "./predict"
4
  import { Preset } from "../engine/presets"
5
 
6
- export const getBackground = async ({
7
  preset,
8
- storyPrompt = "",
9
- previousPanelPrompt = "",
10
- newPanelPrompt = "",
11
  }: {
12
  preset: Preset;
13
- storyPrompt: string;
14
- previousPanelPrompt: string;
15
- newPanelPrompt: string;
16
  }) => {
17
 
18
- const prompt = createLlamaPrompt([
19
  {
20
  role: "system",
21
  content: [
22
- `You are a comic book author for a ${preset.llmPrompt}`,
23
- `Please write in a single sentence a photo caption for the next plausible page, using a few words for each of those categories: the environment, era, characters, objects, textures, lighting.`,
24
- `Separate each of those category descriptions using a comma.`,
25
- `Be brief in your caption don't add your own comments. Be straight to the point, and never reply things like "As the player approaches.." or "As the player clicks.." or "the scene shifts to.." (the best is not not mention the player at all)`
26
  ].filter(item => item).join("\n")
27
  },
28
  {
29
  role: "user",
30
- content: storyPrompt
31
  }
32
  ])
33
 
34
 
35
  let result = ""
36
  try {
37
- result = await predict(prompt)
38
  if (!result.trim().length) {
39
  throw new Error("empty result!")
40
  }
41
  } catch (err) {
42
- console.log(`prediction of the background failed, trying again..`)
43
  try {
44
- result = await predict(prompt+".")
45
  if (!result.trim().length) {
46
  throw new Error("empty result!")
47
  }
48
  } catch (err) {
49
- console.error(`prediction of the background failed again!`)
50
- throw new Error(`failed to generate the background ${err}`)
51
  }
52
  }
53
 
54
- const tmp = result.split("Caption:").pop() || result
55
  return tmp.replaceAll("\n", ", ")
56
  }
 
3
  import { predict } from "./predict"
4
  import { Preset } from "../engine/presets"
5
 
6
+ export const getStory = async ({
7
  preset,
8
+ prompt = "",
 
 
9
  }: {
10
  preset: Preset;
11
+ prompt: string;
 
 
12
  }) => {
13
 
14
+ const query = createLlamaPrompt([
15
  {
16
  role: "system",
17
  content: [
18
+ `You are a comic book author specialized in ${preset.llmPrompt}`,
19
+ `You are going to be asked to write a comic book page, your mission is to answer a JSON array containing 4 items, to describe the page (one item per panel).`,
20
+ `Each array item should be a comic book panel caption the describe the environment, era, characters, objects, textures, lighting.`,
21
+ `Be brief in your caption don't add your own comments. Be straight to the point, and never reply things like "Sure, I can.." etc.`
22
  ].filter(item => item).join("\n")
23
  },
24
  {
25
  role: "user",
26
+ content: `The story is: ${prompt}`,
27
  }
28
  ])
29
 
30
 
31
  let result = ""
32
  try {
33
+ result = await predict(query)
34
  if (!result.trim().length) {
35
  throw new Error("empty result!")
36
  }
37
  } catch (err) {
38
+ console.log(`prediction of the story failed, trying again..`)
39
  try {
40
+ result = await predict(query+".")
41
  if (!result.trim().length) {
42
  throw new Error("empty result!")
43
  }
44
  } catch (err) {
45
+ console.error(`prediction of the story failed again!`)
46
+ throw new Error(`failed to generate the story ${err}`)
47
  }
48
  }
49
 
50
+ const tmp = result // result.split("Caption:").pop() || result
51
  return tmp.replaceAll("\n", ", ")
52
  }
src/app/queries/predict.ts CHANGED
@@ -17,7 +17,7 @@ export async function predict(inputs: string) {
17
  do_sample: true,
18
 
19
  // hard limit for max_new_tokens is 1512
20
- max_new_tokens: 200, // 1150,
21
  return_full_text: false,
22
  }
23
  })) {
 
17
  do_sample: true,
18
 
19
  // hard limit for max_new_tokens is 1512
20
+ max_new_tokens: 300, // 1150,
21
  return_full_text: false,
22
  }
23
  })) {
src/app/store/index.ts CHANGED
@@ -14,7 +14,7 @@ export const useStore = create<{
14
  captions: Record<string, string>
15
  layout: LayoutName
16
  zoomLevel: number
17
- isGeneratingLogic: boolean
18
  panelGenerationStatus: Record<number, boolean>
19
  isGeneratingText: boolean
20
  atLeastOnePanelIsBusy: boolean
@@ -25,7 +25,7 @@ export const useStore = create<{
25
  setLayout: (layout: LayoutName) => void
26
  setCaption: (panelId: number, caption: string) => void
27
  setZoomLevel: (zoomLevel: number) => void
28
- setGeneratingLogic: (isGeneratingLogic: boolean) => void
29
  setGeneratingImages: (panelId: number, value: boolean) => void
30
  setGeneratingText: (isGeneratingText: boolean) => void
31
  }>((set, get) => ({
@@ -36,7 +36,7 @@ export const useStore = create<{
36
  captions: {},
37
  layout: getRandomLayoutName(),
38
  zoomLevel: 50,
39
- isGeneratingLogic: false,
40
  panelGenerationStatus: {},
41
  isGeneratingText: false,
42
  atLeastOnePanelIsBusy: false,
@@ -81,7 +81,7 @@ export const useStore = create<{
81
  },
82
  setLayout: (layout: LayoutName) => set({ layout }),
83
  setZoomLevel: (zoomLevel: number) => set({ zoomLevel }),
84
- setGeneratingLogic: (isGeneratingLogic: boolean) => set({ isGeneratingLogic }),
85
  setGeneratingImages: (panelId: number, value: boolean) => {
86
 
87
  const panelGenerationStatus: Record<number, boolean> = {
 
14
  captions: Record<string, string>
15
  layout: LayoutName
16
  zoomLevel: number
17
+ isGeneratingStory: boolean
18
  panelGenerationStatus: Record<number, boolean>
19
  isGeneratingText: boolean
20
  atLeastOnePanelIsBusy: boolean
 
25
  setLayout: (layout: LayoutName) => void
26
  setCaption: (panelId: number, caption: string) => void
27
  setZoomLevel: (zoomLevel: number) => void
28
+ setGeneratingStory: (isGeneratingStory: boolean) => void
29
  setGeneratingImages: (panelId: number, value: boolean) => void
30
  setGeneratingText: (isGeneratingText: boolean) => void
31
  }>((set, get) => ({
 
36
  captions: {},
37
  layout: getRandomLayoutName(),
38
  zoomLevel: 50,
39
+ isGeneratingStory: false,
40
  panelGenerationStatus: {},
41
  isGeneratingText: false,
42
  atLeastOnePanelIsBusy: false,
 
81
  },
82
  setLayout: (layout: LayoutName) => set({ layout }),
83
  setZoomLevel: (zoomLevel: number) => set({ zoomLevel }),
84
+ setGeneratingStory: (isGeneratingStory: boolean) => set({ isGeneratingStory }),
85
  setGeneratingImages: (panelId: number, value: boolean) => {
86
 
87
  const panelGenerationStatus: Record<number, boolean> = {
src/components/ui/toast.tsx ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as ToastPrimitives from "@radix-ui/react-toast"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+ import { X } from "lucide-react"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const ToastProvider = ToastPrimitives.Provider
9
+
10
+ const ToastViewport = React.forwardRef<
11
+ React.ElementRef<typeof ToastPrimitives.Viewport>,
12
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
13
+ >(({ className, ...props }, ref) => (
14
+ <ToastPrimitives.Viewport
15
+ ref={ref}
16
+ className={cn(
17
+ "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
18
+ className
19
+ )}
20
+ {...props}
21
+ />
22
+ ))
23
+ ToastViewport.displayName = ToastPrimitives.Viewport.displayName
24
+
25
+ const toastVariants = cva(
26
+ "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border border-stone-200 p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full dark:border-stone-800",
27
+ {
28
+ variants: {
29
+ variant: {
30
+ default: "border bg-white text-stone-950 dark:bg-stone-950 dark:text-stone-50",
31
+ destructive:
32
+ "destructive group border-red-500 bg-red-500 text-stone-50 dark:border-red-900 dark:bg-red-900 dark:text-stone-50",
33
+ },
34
+ },
35
+ defaultVariants: {
36
+ variant: "default",
37
+ },
38
+ }
39
+ )
40
+
41
+ const Toast = React.forwardRef<
42
+ React.ElementRef<typeof ToastPrimitives.Root>,
43
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
44
+ VariantProps<typeof toastVariants>
45
+ >(({ className, variant, ...props }, ref) => {
46
+ return (
47
+ <ToastPrimitives.Root
48
+ ref={ref}
49
+ className={cn(toastVariants({ variant }), className)}
50
+ {...props}
51
+ />
52
+ )
53
+ })
54
+ Toast.displayName = ToastPrimitives.Root.displayName
55
+
56
+ const ToastAction = React.forwardRef<
57
+ React.ElementRef<typeof ToastPrimitives.Action>,
58
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
59
+ >(({ className, ...props }, ref) => (
60
+ <ToastPrimitives.Action
61
+ ref={ref}
62
+ className={cn(
63
+ "inline-flex h-8 shrink-0 items-center justify-center rounded-md border border-stone-200 bg-transparent px-3 text-sm font-medium ring-offset-white transition-colors hover:bg-stone-100 focus:outline-none focus:ring-2 focus:ring-stone-950 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-stone-100/40 group-[.destructive]:hover:border-red-500/30 group-[.destructive]:hover:bg-red-500 group-[.destructive]:hover:text-stone-50 group-[.destructive]:focus:ring-red-500 dark:border-stone-800 dark:ring-offset-stone-950 dark:hover:bg-stone-800 dark:focus:ring-stone-300 dark:group-[.destructive]:border-stone-800/40 dark:group-[.destructive]:hover:border-red-900/30 dark:group-[.destructive]:hover:bg-red-900 dark:group-[.destructive]:hover:text-stone-50 dark:group-[.destructive]:focus:ring-red-900",
64
+ className
65
+ )}
66
+ {...props}
67
+ />
68
+ ))
69
+ ToastAction.displayName = ToastPrimitives.Action.displayName
70
+
71
+ const ToastClose = React.forwardRef<
72
+ React.ElementRef<typeof ToastPrimitives.Close>,
73
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
74
+ >(({ className, ...props }, ref) => (
75
+ <ToastPrimitives.Close
76
+ ref={ref}
77
+ className={cn(
78
+ "absolute right-2 top-2 rounded-md p-1 text-stone-950/50 opacity-0 transition-opacity hover:text-stone-950 focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 dark:text-stone-50/50 dark:hover:text-stone-50",
79
+ className
80
+ )}
81
+ toast-close=""
82
+ {...props}
83
+ >
84
+ <X className="h-4 w-4" />
85
+ </ToastPrimitives.Close>
86
+ ))
87
+ ToastClose.displayName = ToastPrimitives.Close.displayName
88
+
89
+ const ToastTitle = React.forwardRef<
90
+ React.ElementRef<typeof ToastPrimitives.Title>,
91
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
92
+ >(({ className, ...props }, ref) => (
93
+ <ToastPrimitives.Title
94
+ ref={ref}
95
+ className={cn("text-sm font-semibold", className)}
96
+ {...props}
97
+ />
98
+ ))
99
+ ToastTitle.displayName = ToastPrimitives.Title.displayName
100
+
101
+ const ToastDescription = React.forwardRef<
102
+ React.ElementRef<typeof ToastPrimitives.Description>,
103
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
104
+ >(({ className, ...props }, ref) => (
105
+ <ToastPrimitives.Description
106
+ ref={ref}
107
+ className={cn("text-sm opacity-90", className)}
108
+ {...props}
109
+ />
110
+ ))
111
+ ToastDescription.displayName = ToastPrimitives.Description.displayName
112
+
113
+ type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
114
+
115
+ type ToastActionElement = React.ReactElement<typeof ToastAction>
116
+
117
+ export {
118
+ type ToastProps,
119
+ type ToastActionElement,
120
+ ToastProvider,
121
+ ToastViewport,
122
+ Toast,
123
+ ToastTitle,
124
+ ToastDescription,
125
+ ToastClose,
126
+ ToastAction,
127
+ }
src/components/ui/toaster.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import {
4
+ Toast,
5
+ ToastClose,
6
+ ToastDescription,
7
+ ToastProvider,
8
+ ToastTitle,
9
+ ToastViewport,
10
+ } from "@/components/ui/toast"
11
+ import { useToast } from "@/components/ui/use-toast"
12
+
13
+ export function Toaster() {
14
+ const { toasts } = useToast()
15
+
16
+ return (
17
+ <ToastProvider>
18
+ {toasts.map(function ({ id, title, description, action, ...props }) {
19
+ return (
20
+ <Toast key={id} {...props}>
21
+ <div className="grid gap-1">
22
+ {title && <ToastTitle>{title}</ToastTitle>}
23
+ {description && (
24
+ <ToastDescription>{description}</ToastDescription>
25
+ )}
26
+ </div>
27
+ {action}
28
+ <ToastClose />
29
+ </Toast>
30
+ )
31
+ })}
32
+ <ToastViewport />
33
+ </ToastProvider>
34
+ )
35
+ }
src/components/ui/use-toast.ts ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Inspired by react-hot-toast library
2
+ import * as React from "react"
3
+
4
+ import type {
5
+ ToastActionElement,
6
+ ToastProps,
7
+ } from "@/components/ui/toast"
8
+
9
+ const TOAST_LIMIT = 1
10
+ const TOAST_REMOVE_DELAY = 1000000
11
+
12
+ type ToasterToast = ToastProps & {
13
+ id: string
14
+ title?: React.ReactNode
15
+ description?: React.ReactNode
16
+ action?: ToastActionElement
17
+ }
18
+
19
+ const actionTypes = {
20
+ ADD_TOAST: "ADD_TOAST",
21
+ UPDATE_TOAST: "UPDATE_TOAST",
22
+ DISMISS_TOAST: "DISMISS_TOAST",
23
+ REMOVE_TOAST: "REMOVE_TOAST",
24
+ } as const
25
+
26
+ let count = 0
27
+
28
+ function genId() {
29
+ count = (count + 1) % Number.MAX_VALUE
30
+ return count.toString()
31
+ }
32
+
33
+ type ActionType = typeof actionTypes
34
+
35
+ type Action =
36
+ | {
37
+ type: ActionType["ADD_TOAST"]
38
+ toast: ToasterToast
39
+ }
40
+ | {
41
+ type: ActionType["UPDATE_TOAST"]
42
+ toast: Partial<ToasterToast>
43
+ }
44
+ | {
45
+ type: ActionType["DISMISS_TOAST"]
46
+ toastId?: ToasterToast["id"]
47
+ }
48
+ | {
49
+ type: ActionType["REMOVE_TOAST"]
50
+ toastId?: ToasterToast["id"]
51
+ }
52
+
53
+ interface State {
54
+ toasts: ToasterToast[]
55
+ }
56
+
57
+ const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
58
+
59
+ const addToRemoveQueue = (toastId: string) => {
60
+ if (toastTimeouts.has(toastId)) {
61
+ return
62
+ }
63
+
64
+ const timeout = setTimeout(() => {
65
+ toastTimeouts.delete(toastId)
66
+ dispatch({
67
+ type: "REMOVE_TOAST",
68
+ toastId: toastId,
69
+ })
70
+ }, TOAST_REMOVE_DELAY)
71
+
72
+ toastTimeouts.set(toastId, timeout)
73
+ }
74
+
75
+ export const reducer = (state: State, action: Action): State => {
76
+ switch (action.type) {
77
+ case "ADD_TOAST":
78
+ return {
79
+ ...state,
80
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
81
+ }
82
+
83
+ case "UPDATE_TOAST":
84
+ return {
85
+ ...state,
86
+ toasts: state.toasts.map((t) =>
87
+ t.id === action.toast.id ? { ...t, ...action.toast } : t
88
+ ),
89
+ }
90
+
91
+ case "DISMISS_TOAST": {
92
+ const { toastId } = action
93
+
94
+ // ! Side effects ! - This could be extracted into a dismissToast() action,
95
+ // but I'll keep it here for simplicity
96
+ if (toastId) {
97
+ addToRemoveQueue(toastId)
98
+ } else {
99
+ state.toasts.forEach((toast) => {
100
+ addToRemoveQueue(toast.id)
101
+ })
102
+ }
103
+
104
+ return {
105
+ ...state,
106
+ toasts: state.toasts.map((t) =>
107
+ t.id === toastId || toastId === undefined
108
+ ? {
109
+ ...t,
110
+ open: false,
111
+ }
112
+ : t
113
+ ),
114
+ }
115
+ }
116
+ case "REMOVE_TOAST":
117
+ if (action.toastId === undefined) {
118
+ return {
119
+ ...state,
120
+ toasts: [],
121
+ }
122
+ }
123
+ return {
124
+ ...state,
125
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
126
+ }
127
+ }
128
+ }
129
+
130
+ const listeners: Array<(state: State) => void> = []
131
+
132
+ let memoryState: State = { toasts: [] }
133
+
134
+ function dispatch(action: Action) {
135
+ memoryState = reducer(memoryState, action)
136
+ listeners.forEach((listener) => {
137
+ listener(memoryState)
138
+ })
139
+ }
140
+
141
+ type Toast = Omit<ToasterToast, "id">
142
+
143
+ function toast({ ...props }: Toast) {
144
+ const id = genId()
145
+
146
+ const update = (props: ToasterToast) =>
147
+ dispatch({
148
+ type: "UPDATE_TOAST",
149
+ toast: { ...props, id },
150
+ })
151
+ const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
152
+
153
+ dispatch({
154
+ type: "ADD_TOAST",
155
+ toast: {
156
+ ...props,
157
+ id,
158
+ open: true,
159
+ onOpenChange: (open) => {
160
+ if (!open) dismiss()
161
+ },
162
+ },
163
+ })
164
+
165
+ return {
166
+ id: id,
167
+ dismiss,
168
+ update,
169
+ }
170
+ }
171
+
172
+ function useToast() {
173
+ const [state, setState] = React.useState<State>(memoryState)
174
+
175
+ React.useEffect(() => {
176
+ listeners.push(setState)
177
+ return () => {
178
+ const index = listeners.indexOf(setState)
179
+ if (index > -1) {
180
+ listeners.splice(index, 1)
181
+ }
182
+ }
183
+ }, [state])
184
+
185
+ return {
186
+ ...state,
187
+ toast,
188
+ dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
189
+ }
190
+ }
191
+
192
+ export { useToast, toast }
src/lib/fonts.ts CHANGED
@@ -78,16 +78,16 @@ export const digitalstrip = localFont({
78
  export const fonts = {
79
  indieflower,
80
  thegirlnextdoor,
81
- komika,
82
  actionman,
83
  karantula,
84
  manoskope,
85
- paeteround,
86
- qarmic,
87
- archrival,
88
- cartoonist,
89
- toontime,
90
- vtc,
91
  digitalstrip
92
  }
93
 
 
78
  export const fonts = {
79
  indieflower,
80
  thegirlnextdoor,
81
+ // komika,
82
  actionman,
83
  karantula,
84
  manoskope,
85
+ // paeteround,
86
+ // qarmic,
87
+ // archrival,
88
+ // cartoonist,
89
+ // toontime,
90
+ // vtc,
91
  digitalstrip
92
  }
93