jbilcke-hf HF staff commited on
Commit
026baa0
·
1 Parent(s): f39b1f2

working on community sharing features

Browse files
.env CHANGED
@@ -7,12 +7,12 @@ RENDERING_ENGINE="REPLICATE"
7
  VIDEOCHAIN_API_URL="http://localhost:7860"
8
  VIDEOCHAIN_API_TOKEN=
9
 
10
- # Not supported yet
11
  REPLICATE_API_TOKEN=
12
  REPLICATE_API_MODEL="lucataco/sdxl-panoramic"
13
  REPLICATE_API_MODEL_VERSION="76acc4075d0633dcb3823c1fed0419de21d42001b65c816c7b5b9beff30ec8cd"
14
 
15
  # ----------- COMMUNITY SHARING (OPTIONAL, YOU DON'T NEED THIS IN LOCAL) -----------
 
16
  # You don't need those community sharing options to run Panoremix
17
  # locally or on your own server (they are meant to be used by the Hugging Face team)
18
  COMMUNITY_API_URL=
 
7
  VIDEOCHAIN_API_URL="http://localhost:7860"
8
  VIDEOCHAIN_API_TOKEN=
9
 
 
10
  REPLICATE_API_TOKEN=
11
  REPLICATE_API_MODEL="lucataco/sdxl-panoramic"
12
  REPLICATE_API_MODEL_VERSION="76acc4075d0633dcb3823c1fed0419de21d42001b65c816c7b5b9beff30ec8cd"
13
 
14
  # ----------- COMMUNITY SHARING (OPTIONAL, YOU DON'T NEED THIS IN LOCAL) -----------
15
+ NEXT_PUBLIC_ENABLE_COMMUNITY_SHARING="false"
16
  # You don't need those community sharing options to run Panoremix
17
  # locally or on your own server (they are meant to be used by the Hugging Face team)
18
  COMMUNITY_API_URL=
src/app/engine/censorship.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // I don't want to be banned by Replicate because bad actors are asking
2
+ // for some naked anime stuff or whatever
3
+ // I also want to avoid a PR scandal due to some bad user generated content
4
+
5
+ const forbiddenWords = [
6
+ // those keywords have been generated by looking at the logs of the AI Comic Factory
7
+ // those are real requests some users tried to attempt.. :|
8
+ "nazi",
9
+ "hitler",
10
+ "boob",
11
+ "boobs",
12
+ "boobies",
13
+ "nipple",
14
+ "nipples",
15
+ "nude",
16
+ "nudes",
17
+ "naked",
18
+ "pee",
19
+ "peeing",
20
+ "erotic",
21
+ "sexy"
22
+ ]
23
+
24
+ // temporary utility to make sure Replicate doesn't ban my account
25
+ // because of what users do in their prompt
26
+ export const filterOutBadWords = (sentence: string) => {
27
+ const words = sentence.split(" ")
28
+ return words.filter(word => {
29
+ const lowerCase = word.toLocaleLowerCase()
30
+ return !forbiddenWords.includes(lowerCase)
31
+ }).join(" ")
32
+ }
src/app/engine/community.ts CHANGED
@@ -2,11 +2,12 @@
2
 
3
  import { v4 as uuidv4 } from "uuid"
4
 
5
- import { CreatePostResponse, GetAppPostsResponse, Post } from "@/types"
 
6
 
7
  const apiUrl = `${process.env.COMMUNITY_API_URL || ""}`
8
  const apiToken = `${process.env.COMMUNITY_API_TOKEN || ""}`
9
- const appId = `${process.env.APP_ID || ""}`
10
 
11
  export async function postToCommunity({
12
  prompt,
@@ -15,6 +16,9 @@ export async function postToCommunity({
15
  prompt: string
16
  assetUrl: string
17
  }): Promise<Post> {
 
 
 
18
  // if the community API is disabled,
19
  // we don't fail, we just mock
20
  if (!apiUrl) {
@@ -25,6 +29,7 @@ export async function postToCommunity({
25
  previewUrl: assetUrl,
26
  assetUrl,
27
  createdAt: new Date().toISOString(),
 
28
  upvotes: 0,
29
  downvotes: 0
30
  }
@@ -41,7 +46,7 @@ export async function postToCommunity({
41
  }
42
 
43
  try {
44
- console.log(`calling POST ${apiUrl}/post with prompt: ${prompt}`)
45
 
46
  const postId = uuidv4()
47
 
@@ -49,7 +54,7 @@ export async function postToCommunity({
49
 
50
  console.table(post)
51
 
52
- const res = await fetch(`${apiUrl}/post`, {
53
  method: "POST",
54
  headers: {
55
  Accept: "application/json",
@@ -82,7 +87,7 @@ export async function postToCommunity({
82
  }
83
  }
84
 
85
- export async function getLatestPosts(): Promise<Post[]> {
86
 
87
  let posts: Post[] = []
88
 
@@ -94,7 +99,9 @@ export async function getLatestPosts(): Promise<Post[]> {
94
 
95
  try {
96
  // console.log(`calling GET ${apiUrl}/posts with renderId: ${renderId}`)
97
- const res = await fetch(`${apiUrl}/posts/${appId}`, {
 
 
98
  method: "GET",
99
  headers: {
100
  Accept: "application/json",
@@ -120,8 +127,9 @@ export async function getLatestPosts(): Promise<Post[]> {
120
  // console.log("response:", response)
121
  return Array.isArray(response?.posts) ? response?.posts : []
122
  } catch (err) {
123
- const error = `failed to get posts: ${err}`
124
- console.error(error)
125
- throw new Error(error)
 
126
  }
127
  }
 
2
 
3
  import { v4 as uuidv4 } from "uuid"
4
 
5
+ import { CreatePostResponse, GetAppPostsResponse, Post, PostVisibility } from "@/types"
6
+ import { filterOutBadWords } from "./censorship"
7
 
8
  const apiUrl = `${process.env.COMMUNITY_API_URL || ""}`
9
  const apiToken = `${process.env.COMMUNITY_API_TOKEN || ""}`
10
+ const appId = `${process.env.COMMUNITY_API_ID || ""}`
11
 
12
  export async function postToCommunity({
13
  prompt,
 
16
  prompt: string
17
  assetUrl: string
18
  }): Promise<Post> {
19
+
20
+ prompt = filterOutBadWords(prompt)
21
+
22
  // if the community API is disabled,
23
  // we don't fail, we just mock
24
  if (!apiUrl) {
 
29
  previewUrl: assetUrl,
30
  assetUrl,
31
  createdAt: new Date().toISOString(),
32
+ visibility: "normal",
33
  upvotes: 0,
34
  downvotes: 0
35
  }
 
46
  }
47
 
48
  try {
49
+ console.log(`calling POST ${apiUrl}/posts/${appId} with prompt: ${prompt}`)
50
 
51
  const postId = uuidv4()
52
 
 
54
 
55
  console.table(post)
56
 
57
+ const res = await fetch(`${apiUrl}/posts/${appId}`, {
58
  method: "POST",
59
  headers: {
60
  Accept: "application/json",
 
87
  }
88
  }
89
 
90
+ export async function getLatestPosts(visibility?: PostVisibility): Promise<Post[]> {
91
 
92
  let posts: Post[] = []
93
 
 
99
 
100
  try {
101
  // console.log(`calling GET ${apiUrl}/posts with renderId: ${renderId}`)
102
+ const res = await fetch(`${apiUrl}/posts/${appId}/${
103
+ visibility || "all"
104
+ }`, {
105
  method: "GET",
106
  headers: {
107
  Accept: "application/json",
 
127
  // console.log("response:", response)
128
  return Array.isArray(response?.posts) ? response?.posts : []
129
  } catch (err) {
130
+ // const error = `failed to get posts: ${err}`
131
+ // console.error(error)
132
+ // throw new Error(error)
133
+ return []
134
  }
135
  }
src/app/engine/render.ts CHANGED
@@ -5,6 +5,7 @@ import Replicate, { Prediction } from "replicate"
5
  import { RenderRequest, RenderedScene, RenderingEngine } from "@/types"
6
  import { generateSeed } from "@/lib/generateSeed"
7
  import { sleep } from "@/lib/sleep"
 
8
 
9
  const renderingEngine = `${process.env.RENDERING_ENGINE || ""}` as RenderingEngine
10
 
@@ -32,7 +33,7 @@ export async function newRender({
32
  `hdri view`,
33
  `highly detailed`,
34
  `intricate details`,
35
- prompt
36
  ].join(', ')
37
 
38
  // return await Gorgon.get(cacheKey, async () => {
@@ -240,6 +241,7 @@ export async function getRender(renderId: string) {
240
 
241
  const response = (await res.json()) as RenderedScene
242
  // console.log("response:", response)
 
243
  return response
244
  }
245
  } catch (err) {
 
5
  import { RenderRequest, RenderedScene, RenderingEngine } from "@/types"
6
  import { generateSeed } from "@/lib/generateSeed"
7
  import { sleep } from "@/lib/sleep"
8
+ import { filterOutBadWords } from "./censorship"
9
 
10
  const renderingEngine = `${process.env.RENDERING_ENGINE || ""}` as RenderingEngine
11
 
 
33
  `hdri view`,
34
  `highly detailed`,
35
  `intricate details`,
36
+ filterOutBadWords(prompt)
37
  ].join(', ')
38
 
39
  // return await Gorgon.get(cacheKey, async () => {
 
241
 
242
  const response = (await res.json()) as RenderedScene
243
  // console.log("response:", response)
244
+
245
  return response
246
  }
247
  } catch (err) {
src/app/{main.tsx → generate/page.tsx} RENAMED
@@ -1,18 +1,19 @@
1
  "use client"
2
 
3
- import { useEffect, useRef, useState, useTransition } from "react"
4
 
5
  import { cn } from "@/lib/utils"
6
- import { TopMenu } from "./interface/top-menu"
7
  import { fonts } from "@/lib/fonts"
8
 
9
- import { useStore } from "./store"
10
- import { BottomBar } from "./interface/bottom-bar"
11
- import { SphericalImage } from "./interface/spherical-image"
12
- import { getRender, newRender } from "./engine/render"
13
  import { RenderedScene } from "@/types"
 
14
 
15
- export default function Generator() {
16
  const [_isPending, startTransition] = useTransition()
17
 
18
  const prompt = useStore(state => state.prompt)
@@ -78,6 +79,14 @@ export default function Generator() {
78
  setLoading(false)
79
  } else {
80
  console.log("panorama finished:", newRendered)
 
 
 
 
 
 
 
 
81
  setRendered(newRendered)
82
  setLoading(false)
83
  }
@@ -101,7 +110,7 @@ export default function Generator() {
101
  }, [prompt])
102
 
103
  return (
104
- <div>
105
  <TopMenu />
106
  <div className={cn(
107
  `fixed inset-0 w-screen h-screen overflow-y-scroll`,
@@ -129,7 +138,7 @@ export default function Generator() {
129
  isLoading ? ``: `scale-0 opacity-0`,
130
  `transition-all duration-300 ease-in-out`,
131
  )}>
132
- {isLoading ? 'Generating new metaverse location..' : ''}
133
  </div>
134
  </div>
135
  </div>
 
1
  "use client"
2
 
3
+ import { useEffect, useRef, useTransition } from "react"
4
 
5
  import { cn } from "@/lib/utils"
6
+ import { TopMenu } from "../interface/top-menu"
7
  import { fonts } from "@/lib/fonts"
8
 
9
+ import { useStore } from "../store"
10
+ import { BottomBar } from "../interface/bottom-bar"
11
+ import { SphericalImage } from "../interface/spherical-image"
12
+ import { getRender, newRender } from "../engine/render"
13
  import { RenderedScene } from "@/types"
14
+ import { postToCommunity } from "../engine/community"
15
 
16
+ export default function GeneratePage() {
17
  const [_isPending, startTransition] = useTransition()
18
 
19
  const prompt = useStore(state => state.prompt)
 
79
  setLoading(false)
80
  } else {
81
  console.log("panorama finished:", newRendered)
82
+ try {
83
+ await postToCommunity({
84
+ prompt,
85
+ assetUrl: newRendered.assetUrl,
86
+ })
87
+ } catch (err) {
88
+ console.log("failed to post to community, but it's no big deal")
89
+ }
90
  setRendered(newRendered)
91
  setLoading(false)
92
  }
 
110
  }, [prompt])
111
 
112
  return (
113
+ <div className="">
114
  <TopMenu />
115
  <div className={cn(
116
  `fixed inset-0 w-screen h-screen overflow-y-scroll`,
 
138
  isLoading ? ``: `scale-0 opacity-0`,
139
  `transition-all duration-300 ease-in-out`,
140
  )}>
141
+ {isLoading ? 'Generating metaverse location in the latent space..' : ''}
142
  </div>
143
  </div>
144
  </div>
src/app/interface/about/index.tsx CHANGED
@@ -8,7 +8,7 @@ export function About() {
8
  return (
9
  <Dialog open={isOpen} onOpenChange={setOpen}>
10
  <DialogTrigger asChild>
11
- <Button variant="outline">
12
  <span className="hidden md:inline">About this project</span>
13
  <span className="inline md:hidden">About</span>
14
  </Button>
 
8
  return (
9
  <Dialog open={isOpen} onOpenChange={setOpen}>
10
  <DialogTrigger asChild>
11
+ <Button variant="outline" className="text-stone-800 dark:text-stone-200">
12
  <span className="hidden md:inline">About this project</span>
13
  <span className="inline md:hidden">About</span>
14
  </Button>
src/app/interface/panel/index.txt DELETED
@@ -1,303 +0,0 @@
1
- "use client"
2
-
3
- import { useEffect, useRef, useState, useTransition } from "react"
4
- // import AutoSizer from "react-virtualized-auto-sizer"
5
-
6
- import { RenderedScene } from "@/types"
7
-
8
- import { getRender, newRender } from "@/app/engine/render"
9
- import { useStore } from "@/app/store"
10
-
11
- import { cn } from "@/lib/utils"
12
- import { getInitialRenderedScene } from "@/lib/getInitialRenderedScene"
13
- import { Progress } from "@/app/interface/progress"
14
-
15
- // import { see } from "@/app/engine/caption"
16
- // import { replaceTextInSpeechBubbles } from "@/lib/replaceTextInSpeechBubbles"
17
-
18
- export function Panel({
19
- panel,
20
- className = "",
21
- width = 1,
22
- height = 1,
23
- }: {
24
- panel: number
25
- className?: string
26
- width?: number
27
- height?: number
28
- }) {
29
- const panelId = `${panel}`
30
-
31
- const ref = useRef<HTMLImageElement>(null)
32
- const font = useStore(state => state.font)
33
- const preset = useStore(state => state.preset)
34
-
35
- const setGeneratingImages = useStore(state => state.setGeneratingImages)
36
-
37
- const [imageWithText, setImageWithText] = useState("")
38
- const panels = useStore(state => state.panels)
39
- const prompt = panels[panel] || ""
40
-
41
- const captions = useStore(state => state.captions)
42
- const caption = captions[panel] || ""
43
-
44
- const zoomLevel = useStore(state => state.zoomLevel)
45
- const showCaptions = useStore(state => state.showCaptions)
46
-
47
- const addToUpscaleQueue = useStore(state => state.addToUpscaleQueue)
48
-
49
- const [_isPending, startTransition] = useTransition()
50
- const renderedScenes = useStore(state => state.renderedScenes)
51
- const setRendered = useStore(state => state.setRendered)
52
-
53
- const rendered = renderedScenes[panel] || getInitialRenderedScene()
54
-
55
- // keep a ref in sync
56
- const renderedRef = useRef<RenderedScene>()
57
- const renderedKey = JSON.stringify(rendered)
58
- useEffect(() => { renderedRef.current = rendered }, [renderedKey])
59
-
60
- const timeoutRef = useRef<any>(null)
61
-
62
- const delay = 3000 + (1000 * panel)
63
-
64
- // since this run in its own loop, we need to use references everywhere
65
- // but perhaps this could be refactored
66
- useEffect(() => {
67
- // console.log("Panel prompt: "+ prompt)
68
- if (!prompt?.length) { return }
69
-
70
- // important: update the status, and clear the scene
71
- setGeneratingImages(panelId, true)
72
-
73
- // just to empty it
74
- setRendered(panelId, getInitialRenderedScene())
75
-
76
- setTimeout(() => {
77
- startTransition(async () => {
78
-
79
- // console.log(`Loading panel ${panel}..`)
80
-
81
- let newRendered: RenderedScene
82
- try {
83
- newRendered = await newRender({ prompt, width, height })
84
- } catch (err) {
85
- // "Failed to load the panel! Don't worry, we are retrying..")
86
- newRendered = await newRender({ prompt, width, height })
87
- }
88
-
89
- if (newRendered) {
90
- // console.log("newRendered:", newRendered)
91
- setRendered(panelId, newRendered)
92
-
93
- // but we are still loading!
94
- } else {
95
- setRendered(panelId, {
96
- renderId: "",
97
- status: "pending",
98
- assetUrl: "",
99
- alt: "",
100
- maskUrl: "",
101
- error: "",
102
- segments: []
103
- })
104
- setGeneratingImages(panelId, false)
105
- return
106
- }
107
- })
108
- }, 2000 * panel)
109
- }, [prompt, width, height])
110
-
111
-
112
- const checkStatus = () => {
113
- startTransition(async () => {
114
- clearTimeout(timeoutRef.current)
115
-
116
- if (!renderedRef.current?.renderId || renderedRef.current?.status !== "pending") {
117
- timeoutRef.current = setTimeout(checkStatus, delay)
118
- return
119
- }
120
- try {
121
- setGeneratingImages(panelId, true)
122
- // console.log(`Checking job status API for job ${renderedRef.current?.renderId}`)
123
- const newRendered = await getRender(renderedRef.current.renderId)
124
- // console.log("got a response!", newRendered)
125
-
126
- if (JSON.stringify(renderedRef.current) !== JSON.stringify(newRendered)) {
127
- // console.log("updated panel:", newRendered)
128
- setRendered(panelId, renderedRef.current = newRendered)
129
- setGeneratingImages(panelId, true)
130
- }
131
- // console.log("status:", newRendered.status)
132
-
133
- if (newRendered.status === "pending") {
134
- // console.log("job not finished")
135
- timeoutRef.current = setTimeout(checkStatus, delay)
136
- } else if (newRendered.status === "error" ||
137
- (newRendered.status === "completed" && !newRendered.assetUrl?.length)) {
138
- // console.log(`panel got an error and/or an empty asset url :/ "${newRendered.error}", but let's try to recover..`)
139
- try {
140
- const newAttempt = await newRender({ prompt, width, height })
141
- setRendered(panelId, newAttempt)
142
- } catch (err) {
143
- console.error("yeah sorry, something is wrong.. aborting", err)
144
- setGeneratingImages(panelId, false)
145
- }
146
- } else {
147
- console.log("panel finished!")
148
- setGeneratingImages(panelId, false)
149
- addToUpscaleQueue(panelId, newRendered)
150
- }
151
- } catch (err) {
152
- console.error(err)
153
- timeoutRef.current = setTimeout(checkStatus, delay)
154
- }
155
- })
156
- }
157
-
158
- useEffect(() => {
159
- // console.log("starting timeout")
160
- clearTimeout(timeoutRef.current)
161
-
162
- // normally it should reply in < 1sec, but we could also use an interval
163
- timeoutRef.current = setTimeout(checkStatus, delay)
164
-
165
- return () => {
166
- clearTimeout(timeoutRef.current)
167
- }
168
- }, [prompt, width, height])
169
-
170
- /*
171
- doing the captionning from the browser is expensive
172
- a simpler solution is to caption directly during SDXL generation
173
-
174
- useEffect(() => {
175
- if (!rendered.assetUrl) { return }
176
- // the asset url can evolve with time (link to a better resolution image)
177
- // however it would be costly to ask for the caption, the low resolution is enough for the semantic resolution
178
- // so we just do nothing if we already have the caption
179
- if (caption) { return }
180
- startTransition(async () => {
181
- try {
182
- const newCaption = await see({
183
- prompt: "please caption the following image",
184
- imageBase64: rendered.assetUrl
185
- })
186
- if (newCaption) {
187
- setCaption(newCaption)
188
- }
189
- } catch (err) {
190
- console.error(`failed to generate the caption:`, err)
191
- }
192
- })
193
- }, [rendered.assetUrl, caption])
194
- */
195
-
196
- const frameClassName = cn(
197
- //`flex`,
198
- `w-full h-full`,
199
- `border-stone-800`,
200
- `transition-all duration-200 ease-in-out`,
201
- zoomLevel > 140 ? `border-[2px] md:border-[4px] rounded-sm md:rounded-md` :
202
- zoomLevel > 120 ? `border-[1.5px] md:border-[3px] rounded-xs md:rounded-sm` :
203
- zoomLevel > 90 ? `border-[1px] md:border-[2px] rounded-xs md:rounded-sm` :
204
- zoomLevel > 40 ? `border-[0.5px] md:border-[1px] rounded-none md:rounded-xs` :
205
- `border-transparent md:border-[0.5px] rounded-none md:rounded-none`,
206
- `shadow-sm`,
207
- `overflow-hidden`,
208
- `print:border-[1.5px] print:shadow-none`,
209
- )
210
-
211
-
212
- /*
213
- text detection (doesn't work)
214
- useEffect(() => {
215
- const fn = async () => {
216
- if (!rendered.assetUrl || !ref.current) {
217
- return
218
- }
219
-
220
- const result = await replaceTextInSpeechBubbles(
221
- rendered.assetUrl,
222
- "Lorem ipsum dolor sit amet, dolor ipsum. Sit amet? Ipsum! Dolor!!!"
223
- )
224
- if (result) {
225
- setImageWithText(result)
226
- }
227
- }
228
- fn()
229
-
230
- }, [rendered.assetUrl, ref.current])
231
- */
232
-
233
- if (prompt && !rendered.assetUrl) {
234
- return (
235
- <div className={cn(
236
- frameClassName,
237
- `flex flex-col items-center justify-center`,
238
- className,
239
- )}>
240
- <Progress isLoading />
241
- </div>
242
- )
243
- }
244
-
245
- return (
246
- <div className={cn(
247
- frameClassName,
248
- { "grayscale": preset.color === "grayscale" },
249
- className
250
- )}>
251
- <div className={cn(
252
- ``,
253
- `bg-stone-50`,
254
- `border-stone-800`,
255
- `transition-all duration-200 ease-in-out`,
256
- zoomLevel > 140 ? `border-b-[2px] md:border-b-[4px]` :
257
- zoomLevel > 120 ? `border-b-[1.5px] md:border-b-[3px]` :
258
- zoomLevel > 90 ? `border-b-[1px] md:border-b-[2px]` :
259
- zoomLevel > 40 ? `border-b-[0.5px] md:border-b-[1px]` :
260
- `border-transparent md:border-b-[0.5px]`,
261
- `print:border-b-[1.5px]`,
262
- `truncate`,
263
-
264
- zoomLevel > 200 ? `p-4 md:p-8` :
265
- zoomLevel > 180 ? `p-[14px] md:p-8` :
266
- zoomLevel > 160 ? `p-[12px] md:p-[28px]` :
267
- zoomLevel > 140 ? `p-[10px] md:p-[26px]` :
268
- zoomLevel > 120 ? `p-2 md:p-6` :
269
- zoomLevel > 100 ? `p-1.5 md:p-[20px]` :
270
- zoomLevel > 90 ? `p-1.5 md:p-4` :
271
- zoomLevel > 40 ? `p-1 md:p-2` :
272
- `p-0.5 md:p-2`,
273
-
274
- zoomLevel > 220 ? `text-xl md:text-4xl` :
275
- zoomLevel > 200 ? `text-lg md:text-3xl` :
276
- zoomLevel > 180 ? `text-md md:text-2xl` :
277
- zoomLevel > 140 ? `text-2xs md:text-2xl` :
278
- zoomLevel > 120 ? `text-3xs md:text-xl` :
279
- zoomLevel > 100 ? `text-4xs md:text-lg` :
280
- zoomLevel > 90 ? `text-5xs md:text-sm` :
281
- zoomLevel > 40 ? `md:text-xs` : `md:text-2xs`,
282
-
283
- showCaptions ? (
284
- zoomLevel > 90 ? `block` : `hidden md:block`
285
- ) : `hidden`,
286
- )}
287
- >{caption || ""}
288
- </div>
289
- {rendered.assetUrl &&
290
- <img
291
- ref={ref}
292
- src={imageWithText || rendered.assetUrl}
293
- width={width}
294
- height={height}
295
- alt={rendered.alt}
296
- className={cn(
297
- `comic-panel w-full h-full object-cover max-w-max`,
298
- // showCaptions ? `-mt-11` : ''
299
- )}
300
- />}
301
- </div>
302
- )
303
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/interface/top-menu/index.tsx CHANGED
@@ -45,7 +45,7 @@ export function TopMenu() {
45
  )}>
46
  <div className="flex flex-row flex-grow w-full">
47
  <Input
48
- placeholder={`Type a location e.g. "Jurassic Park entrance", "Spaceport in Mos Eisley"..`}
49
  className="w-full bg-neutral-300 text-neutral-800 dark:bg-neutral-300 dark:text-neutral-800 rounded-r-none"
50
  // disabled={atLeastOnePanelIsBusy}
51
  onChange={(e) => {
 
45
  )}>
46
  <div className="flex flex-row flex-grow w-full">
47
  <Input
48
+ placeholder={`Invent a location e.g. Jurassic Park entrance, Spaceport in Mos Eisley..`}
49
  className="w-full bg-neutral-300 text-neutral-800 dark:bg-neutral-300 dark:text-neutral-800 rounded-r-none"
50
  // disabled={atLeastOnePanelIsBusy}
51
  onChange={(e) => {
src/app/landing.tsx ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useEffect, useState, useTransition } from "react"
4
+
5
+ import { Post } from "@/types"
6
+ import { cn } from "@/lib/utils"
7
+ import { actionman } from "@/lib/fonts"
8
+
9
+
10
+ import { getLatestPosts } from "./engine/community"
11
+
12
+ export default function Landing() {
13
+ const [_isPending, startTransition] = useTransition()
14
+ const [posts, setPosts] = useState<Post[]>([])
15
+
16
+ useEffect(() => {
17
+ startTransition(async () => {
18
+ const newPosts = await getLatestPosts()
19
+ setPosts(newPosts)
20
+ })
21
+ }, [])
22
+
23
+ return (
24
+ <div className={cn(
25
+ `light fixed w-full h-full flex flex-col items-center bg-slate-300 text-slate-800`,
26
+ `pt-24`,
27
+ actionman.className
28
+ )}>
29
+ <div className="w-full flex flex-col items-center">
30
+ <h1 className="text-[100px] text-cyan-700">🌐 Panoremix</h1>
31
+ <h2 className="text-3xl mb-12">Generate cool panoramas using AI!</h2>
32
+
33
+ <h2 className="text-2xl">Latest locations synthesized:</h2>
34
+
35
+ <div className="grid grid-col-2 sm:grid-col-3 md:grid-col-4 lg:grid-cols-5 gap-4">
36
+ {posts.map(post => (
37
+ <div key={post.postId} className="flex flex-col space-y-3">
38
+ <div className="w-full h-24">
39
+ <img
40
+ src={post.assetUrl}
41
+ className="w-full h-full rounded-xl overflow-hidden"
42
+ />
43
+ </div>
44
+ <div className="text-base truncate w-full">{post.prompt}</div>
45
+ </div>
46
+ ))}
47
+ </div>
48
+ </div>
49
+ </div>
50
+ )
51
+ }
src/app/page.tsx CHANGED
@@ -2,12 +2,14 @@
2
 
3
  import Head from "next/head"
4
 
5
- import Main from "./main"
 
 
6
  import { TooltipProvider } from "@/components/ui/tooltip"
7
 
8
  // https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
9
 
10
- export default async function IndexPage({ params: { ownerId } }: { params: { ownerId: string }}) {
11
  return (
12
  <>
13
  <Head>
@@ -19,7 +21,7 @@ export default async function IndexPage({ params: { ownerId } }: { params: { own
19
  `light bg-zinc-50 text-stone-900
20
  `}>
21
  <TooltipProvider delayDuration={100}>
22
- <Main />
23
  </TooltipProvider>
24
  </main>
25
  </>
 
2
 
3
  import Head from "next/head"
4
 
5
+ // import Landing from "./landing"
6
+ import Generate from "./generate/page"
7
+
8
  import { TooltipProvider } from "@/components/ui/tooltip"
9
 
10
  // https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
11
 
12
+ export default async function Page() {
13
  return (
14
  <>
15
  <Head>
 
21
  `light bg-zinc-50 text-stone-900
22
  `}>
23
  <TooltipProvider delayDuration={100}>
24
+ <Generate />
25
  </TooltipProvider>
26
  </main>
27
  </>
src/app/todo.tsx DELETED
@@ -1,21 +0,0 @@
1
- "use client"
2
-
3
- import { useState, useTransition } from "react"
4
-
5
- import { Post } from "@/types"
6
-
7
- export default function Main() {
8
- const [_isPending, startTransition] = useTransition()
9
- const posts = useState<Post[]>([])
10
-
11
- return (
12
- <div>
13
- <h1>Panoremix</h1>
14
- <h2>Generate 360° panoramas from text!</h2>
15
-
16
- <h2>Explore latent locations discovered by the community</h2>
17
-
18
-
19
- </div>
20
- )
21
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/types.ts CHANGED
@@ -88,26 +88,31 @@ export type RenderingEngine =
88
  | "OPENAI"
89
  | "REPLICATE"
90
 
91
- export type Post = {
92
- postId: string
93
- appId: string
94
- prompt: string
95
- previewUrl: string
96
- assetUrl: string
97
- createdAt: string
98
- upvotes: number
99
- downvotes: number
100
- }
101
-
102
- export type CreatePostResponse = {
103
- success?: boolean
104
- error?: string
105
- post: Post
106
- }
107
-
108
- export type GetAppPostsResponse = {
109
- success?: boolean
110
- error?: string
111
- posts: Post[]
112
- }
113
-
 
 
 
 
 
 
88
  | "OPENAI"
89
  | "REPLICATE"
90
 
91
+ export type PostVisibility =
92
+ | "featured" // featured by admins
93
+ | "trending" // top trending / received more than 10 upvotes
94
+ | "normal" // default visibility
95
+
96
+ export type Post = {
97
+ postId: string
98
+ appId: string
99
+ prompt: string
100
+ previewUrl: string
101
+ assetUrl: string
102
+ createdAt: string
103
+ visibility: PostVisibility
104
+ upvotes: number
105
+ downvotes: number
106
+ }
107
+
108
+ export type CreatePostResponse = {
109
+ success?: boolean
110
+ error?: string
111
+ post: Post
112
+ }
113
+
114
+ export type GetAppPostsResponse = {
115
+ success?: boolean
116
+ error?: string
117
+ posts: Post[]
118
+ }