jbilcke-hf HF staff commited on
Commit
bf988e1
1 Parent(s): 2f436dd

added a firehose

Browse files
.env CHANGED
@@ -12,9 +12,10 @@ REPLICATE_API_MODEL="lucataco/sdxl-panoramic"
12
  REPLICATE_API_MODEL_VERSION="76acc4075d0633dcb3823c1fed0419de21d42001b65c816c7b5b9beff30ec8cd"
13
 
14
  # ----------- CENSORSHIP -------
15
- # Due to abuse by users, I've had to add a censorshi/fingerprinting mechanism
16
  ENABLE_CENSORSHIP="false"
17
  FINGERPRINT_KEY=
 
18
 
19
  # ----------- COMMUNITY SHARING (OPTIONAL, YOU DON'T NEED THIS IN LOCAL) -----------
20
  NEXT_PUBLIC_ENABLE_COMMUNITY_SHARING="false"
 
12
  REPLICATE_API_MODEL_VERSION="76acc4075d0633dcb3823c1fed0419de21d42001b65c816c7b5b9beff30ec8cd"
13
 
14
  # ----------- CENSORSHIP -------
15
+ # Due to abuse by users, I've had to add a censorship/fingerprinting mechanism
16
  ENABLE_CENSORSHIP="false"
17
  FINGERPRINT_KEY=
18
+ MODERATION_KEY=
19
 
20
  # ----------- COMMUNITY SHARING (OPTIONAL, YOU DON'T NEED THIS IN LOCAL) -----------
21
  NEXT_PUBLIC_ENABLE_COMMUNITY_SHARING="false"
src/app/engine/community.ts CHANGED
@@ -2,12 +2,13 @@
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,
@@ -23,7 +24,7 @@ export async function postToCommunity({
23
  if (prompt !== before) {
24
  console.log(`user attempted to use bad words! their original prompt is: ${before}`)
25
  }
26
-
27
  // if the community API is disabled,
28
  // we don't fail, we just mock
29
  if (!apiUrl) {
@@ -137,4 +138,108 @@ export async function getLatestPosts(visibility?: PostVisibility): Promise<Post[
137
  // throw new Error(error)
138
  return []
139
  }
140
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  import { v4 as uuidv4 } from "uuid"
4
 
5
+ import { CreatePostResponse, GetAppPostResponse, 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
+ const secretModerationKey = `${process.env.MODERATION_KEY || ""}`
12
 
13
  export async function postToCommunity({
14
  prompt,
 
24
  if (prompt !== before) {
25
  console.log(`user attempted to use bad words! their original prompt is: ${before}`)
26
  }
27
+
28
  // if the community API is disabled,
29
  // we don't fail, we just mock
30
  if (!apiUrl) {
 
138
  // throw new Error(error)
139
  return []
140
  }
141
+ }
142
+
143
+ export async function getPost(postId: string): Promise<Post> {
144
+
145
+ // if the community API is disabled we don't fail,
146
+ // we just mock
147
+ if (!apiUrl) {
148
+ throw new Error("community API is not enabled")
149
+ }
150
+
151
+ try {
152
+ // console.log(`calling GET ${apiUrl}/posts with renderId: ${renderId}`)
153
+ const res = await fetch(`${apiUrl}/posts/${appId}/${postId}`, {
154
+ method: "GET",
155
+ headers: {
156
+ Accept: "application/json",
157
+ "Content-Type": "application/json",
158
+ Authorization: `Bearer ${apiToken}`,
159
+ },
160
+ cache: 'no-store',
161
+ // we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
162
+ // next: { revalidate: 1 }
163
+ })
164
+
165
+ // console.log("res:", res)
166
+ // The return value is *not* serialized
167
+ // You can return Date, Map, Set, etc.
168
+
169
+ // Recommendation: handle errors
170
+ if (res.status !== 200) {
171
+ // This will activate the closest `error.js` Error Boundary
172
+ throw new Error('Failed to fetch data')
173
+ }
174
+
175
+ const response = (await res.json()) as GetAppPostResponse
176
+ // console.log("response:", response)
177
+ return response.post
178
+ } catch (err) {
179
+ const error = `failed to get post: ${err}`
180
+ console.error(error)
181
+ throw new Error(error)
182
+ }
183
+ }
184
+
185
+ export async function deletePost({
186
+ postId,
187
+ moderationKey,
188
+ }: {
189
+ postId: string
190
+ moderationKey: string
191
+ }): Promise<boolean> {
192
+
193
+ // if the community API is disabled,
194
+ // we don't fail, we just mock
195
+ if (!apiUrl) {
196
+ return false
197
+ }
198
+
199
+ if (!postId) {
200
+ console.error(`cannot delete a post without a postId, aborting..`)
201
+ throw new Error(`cannot delete a post without a postId, aborting..`)
202
+ }
203
+ if (!moderationKey) {
204
+ console.error(`cannot delete a post without a moderationKey, aborting..`)
205
+ throw new Error(`cannot delete a post without a moderationKey, aborting..`)
206
+ }
207
+
208
+ if (moderationKey !== secretModerationKey) {
209
+ console.error(`invalid moderation key, operation denied! please ask a Panoremix admin for the mdoeration key`)
210
+ throw new Error(`invalid moderation key, operation denied! please ask a Panoremix admin for the mdoeration key`)
211
+ }
212
+
213
+ try {
214
+ console.log(`calling DELETE ${apiUrl}/posts/${appId}/${postId}`)
215
+
216
+ const res = await fetch(`${apiUrl}/posts/${appId}/${postId}`, {
217
+ method: "DELETE",
218
+ headers: {
219
+ Accept: "application/json",
220
+ "Content-Type": "application/json",
221
+ Authorization: `Bearer ${apiToken}`,
222
+ },
223
+ cache: 'no-store',
224
+ // we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
225
+ // next: { revalidate: 1 }
226
+ })
227
+
228
+ // console.log("res:", res)
229
+ // The return value is *not* serialized
230
+ // You can return Date, Map, Set, etc.
231
+
232
+ // Recommendation: handle errors
233
+ if (res.status !== 200) {
234
+ // This will activate the closest `error.js` Error Boundary
235
+ throw new Error('Failed to fetch data')
236
+ }
237
+
238
+ const response = (await res.json()) as CreatePostResponse
239
+ return true
240
+ } catch (err) {
241
+ const error = `failed to delete the post: ${err}`
242
+ console.error(error)
243
+ throw new Error(error)
244
+ }
245
+ }
src/app/firehose/delete.tsx ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { startTransition, useEffect, useState } from "react"
2
+
3
+ import { Button } from "@/components/ui/button"
4
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
5
+ import { cn } from "@/lib/utils"
6
+ import { Post } from "@/types"
7
+
8
+ import { deletePost } from "../engine/community"
9
+
10
+ export function Delete({ post, moderationKey = "", onDelete = () => {} }: { post?: Post, moderationKey?: string; onDelete: (post: Post) => void }) {
11
+ const [isOpen, setOpen] = useState(false)
12
+
13
+ useEffect(() => {
14
+ if (post?.postId && !isOpen) {
15
+ setOpen(true)
16
+ }
17
+ }, [post?.postId])
18
+
19
+ const handleDelete = () => {
20
+ startTransition(() => {
21
+ const fn = async () => {
22
+ setOpen(false)
23
+ if (!post) { return }
24
+ const postId = post.postId
25
+ await deletePost({ postId, moderationKey })
26
+ onDelete(post)
27
+ }
28
+ fn()
29
+ })
30
+ }
31
+
32
+ return (
33
+ <Dialog open={isOpen} onOpenChange={setOpen}>
34
+ <DialogContent className="sm:max-w-[800px]">
35
+ <DialogHeader>
36
+ <DialogTitle>Delete</DialogTitle>
37
+ </DialogHeader>
38
+ {post ?<div className="flex flex-col py-4 text-stone-800">
39
+
40
+ <div className="w-full h-64">
41
+ <img
42
+ src={post.assetUrl}
43
+ className={cn(
44
+ `w-full h-64 rounded-xl overflow-hidden object-cover`,
45
+ `border border-zinc-900/70`
46
+ )}
47
+ />
48
+ </div>
49
+ <div className="text-lg text-stone-800/80 word-break w-full py-6">{post.prompt}</div>
50
+ </div> : null}
51
+ <DialogFooter>
52
+ <div className="w-full flex flex-row space-x-6 items-center justify-center">
53
+ <Button type="submit" className="text-xl bg-green-800 text-green-100 hover:bg-green-700 hover:text-green-50" onClick={() => setOpen(false)}>Keep</Button>
54
+ <Button type="submit" className="text-xl bg-red-800 text-red-100 hover:bg-red-700 hover:text-red-50" onClick={handleDelete}>Delete</Button>
55
+ </div>
56
+ </DialogFooter>
57
+ </DialogContent>
58
+ </Dialog>
59
+ )
60
+ }
src/app/{landing.tsx → firehose/page.tsx} RENAMED
@@ -6,12 +6,18 @@ 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 () => {
@@ -20,6 +26,11 @@ export default function Landing() {
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`,
@@ -35,27 +46,42 @@ export default function Landing() {
35
 
36
  <div className="w-full grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-x-4 gap-y-6 px-12">
37
  {posts.map(post => (
38
- <div
39
  key={post.postId}
40
- className="flex flex-col space-y-3 cursor-pointer"
41
- onClick={() => {
42
- // TODO
43
- }}
44
- >
45
- <div className="w-full h-24">
46
- <img
47
- src={post.assetUrl}
48
- className={cn(
49
- `w-full h-24 rounded-xl overflow-hidden object-cover`,
50
- `border border-zinc-900/70`
51
- )}
52
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  </div>
54
- <div className="text-sm text-stone-800/80 truncate w-full">{post.prompt}</div>
55
- </div>
56
  ))}
57
  </div>
58
  </div>
 
59
  </div>
60
  )
61
  }
 
6
  import { cn } from "@/lib/utils"
7
  import { actionman } from "@/lib/fonts"
8
 
9
+ import { getLatestPosts } from "../engine/community"
10
+ import { useSearchParams } from "next/navigation"
11
+ import { Button } from "@/components/ui/button"
12
+ import { Delete } from "./delete"
13
+ import Link from "next/link"
14
 
15
+ export default function FirehosePage() {
16
+ const searchParams = useSearchParams()
 
17
  const [_isPending, startTransition] = useTransition()
18
  const [posts, setPosts] = useState<Post[]>([])
19
+ const moderationKey = (searchParams.get("moderationKey") as string) || ""
20
+ const [toDelete, setToDelete] = useState<Post>()
21
 
22
  useEffect(() => {
23
  startTransition(async () => {
 
26
  })
27
  }, [])
28
 
29
+ const handleOnDelete = ({ postId }: Post) => {
30
+ setPosts(posts.filter(post => post.postId !== postId))
31
+ setToDelete(undefined)
32
+ }
33
+
34
  return (
35
  <div className={cn(
36
  `light fixed w-full h-full flex flex-col items-center bg-slate-300 text-slate-800`,
 
46
 
47
  <div className="w-full grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-x-4 gap-y-6 px-12">
48
  {posts.map(post => (
49
+ <Link
50
  key={post.postId}
51
+ href={`/?postId=${post.postId}`}
52
+ target="_blank">
53
+ <div
54
+ key={post.postId}
55
+ className="group flex flex-col space-y-3 cursor-pointer"
56
+ >
57
+ <div className="w-full h-24">
58
+ {moderationKey ? <div className="relative -mb-8 ml-2">
59
+ <Button
60
+ className="z-30 bg-red-200 text-red-700 hover:bg-red-300 hover:text-red-800 text-2xs px-2 h-7"
61
+ onClick={(e) => {
62
+ e.preventDefault()
63
+ setToDelete(post)
64
+ return false
65
+ }}>Delete</Button>
66
+ </div> : null}
67
+ <img
68
+ src={post.assetUrl}
69
+ className={cn(
70
+ `w-full h-24 rounded-xl overflow-hidden object-cover`,
71
+ `border border-zinc-900/70`,
72
+ // `group-hover:brightness-105`
73
+ )}
74
+ />
75
+ </div>
76
+ <div
77
+ className="text-sm text-stone-800/80 truncate w-full group-hover:underline underline-offset-2"
78
+ >{post.prompt}</div>
79
  </div>
80
+ </Link>
 
81
  ))}
82
  </div>
83
  </div>
84
+ <Delete post={toDelete} moderationKey={moderationKey} onDelete={handleOnDelete} />
85
  </div>
86
  )
87
  }
src/app/generate/page.tsx CHANGED
@@ -11,12 +11,16 @@ 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)
 
20
  const setRendered = useStore(state => state.setRendered)
21
  const renderedScene = useStore(state => state.renderedScene)
22
  const isLoading = useStore(state => state.isLoading)
@@ -35,6 +39,10 @@ export default function GeneratePage() {
35
  useEffect(() => {
36
  if (!prompt) { return }
37
 
 
 
 
 
38
  startTransition(async () => {
39
 
40
  try {
@@ -52,10 +60,16 @@ export default function GeneratePage() {
52
  startTransition(async () => {
53
  clearTimeout(timeoutRef.current)
54
 
 
 
 
 
 
55
  if (!renderedRef.current?.renderId || renderedRef.current?.status !== "pending") {
56
  timeoutRef.current = setTimeout(checkStatus, delay)
57
  return
58
  }
 
59
  try {
60
  // console.log(`Checking job status API for job ${renderedRef.current?.renderId}`)
61
  const newRendered = await getRender(renderedRef.current.renderId)
@@ -109,6 +123,39 @@ export default function GeneratePage() {
109
  }
110
  }, [prompt])
111
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  return (
113
  <div className="">
114
  <TopMenu />
 
11
  import { SphericalImage } from "../interface/spherical-image"
12
  import { getRender, newRender } from "../engine/render"
13
  import { RenderedScene } from "@/types"
14
+ import { getPost, postToCommunity } from "../engine/community"
15
+ import { useSearchParams } from "next/navigation"
16
 
17
  export default function GeneratePage() {
18
+ const searchParams = useSearchParams()
19
  const [_isPending, startTransition] = useTransition()
20
+ const postId = (searchParams.get("postId") as string) || ""
21
 
22
  const prompt = useStore(state => state.prompt)
23
+ const setPrompt = useStore(state => state.setPrompt)
24
  const setRendered = useStore(state => state.setRendered)
25
  const renderedScene = useStore(state => state.renderedScene)
26
  const isLoading = useStore(state => state.isLoading)
 
39
  useEffect(() => {
40
  if (!prompt) { return }
41
 
42
+ // to prevent loading a new prompt if we are already loading
43
+ // (eg. the initial one, from a community post)
44
+ // if (isLoading) { return }
45
+
46
  startTransition(async () => {
47
 
48
  try {
 
60
  startTransition(async () => {
61
  clearTimeout(timeoutRef.current)
62
 
63
+ if (renderedRef.current?.status === "completed") {
64
+ console.log("rendering job is already completed")
65
+ return
66
+ }
67
+
68
  if (!renderedRef.current?.renderId || renderedRef.current?.status !== "pending") {
69
  timeoutRef.current = setTimeout(checkStatus, delay)
70
  return
71
  }
72
+
73
  try {
74
  // console.log(`Checking job status API for job ${renderedRef.current?.renderId}`)
75
  const newRendered = await getRender(renderedRef.current.renderId)
 
123
  }
124
  }, [prompt])
125
 
126
+ useEffect(() => {
127
+ if (!postId) {
128
+ return
129
+ }
130
+ setLoading(true)
131
+
132
+ startTransition(async () => {
133
+ try {
134
+ console.log(`loading post ${postId}`)
135
+ const post = await getPost(postId)
136
+
137
+ // setting the prompt here will mess-up with everything
138
+ // normally this shouldn't trigger the normal prompt update workflow,
139
+ // because we are set the app to "is loading"
140
+ // setPrompt(post.prompt)
141
+
142
+ setRendered({
143
+ renderId: postId,
144
+ status: "completed",
145
+ assetUrl: post.assetUrl,
146
+ alt: post.prompt,
147
+ error: "",
148
+ maskUrl: "",
149
+ segments: []
150
+ })
151
+ setLoading(false)
152
+ } catch (err) {
153
+ console.error("failed to get post: ", err)
154
+ setLoading(false)
155
+ }
156
+ })
157
+ }, [postId])
158
+
159
  return (
160
  <div className="">
161
  <TopMenu />
src/app/interface/top-menu/index.tsx CHANGED
@@ -10,7 +10,7 @@ import { Button } from "@/components/ui/button"
10
  export function TopMenu() {
11
  const prompt = useStore(state => state.prompt)
12
 
13
- const generate = useStore(state => state.generate)
14
 
15
  const isLoading = useStore(state => state.isLoading)
16
  const setLoading = useStore(state => state.setLoading)
@@ -24,7 +24,7 @@ export function TopMenu() {
24
  const handleSubmit = () => {
25
  const promptChanged = draftPrompt.trim() !== prompt.trim()
26
  if (!isLoading && (promptChanged)) {
27
- generate(draftPrompt)
28
  }
29
  }
30
 
 
10
  export function TopMenu() {
11
  const prompt = useStore(state => state.prompt)
12
 
13
+ const setPrompt = useStore(state => state.setPrompt)
14
 
15
  const isLoading = useStore(state => state.isLoading)
16
  const setLoading = useStore(state => state.setLoading)
 
24
  const handleSubmit = () => {
25
  const promptChanged = draftPrompt.trim() !== prompt.trim()
26
  if (!isLoading && (promptChanged)) {
27
+ setPrompt(draftPrompt)
28
  }
29
  }
30
 
src/app/page.tsx CHANGED
@@ -2,7 +2,7 @@
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"
 
2
 
3
  import Head from "next/head"
4
 
5
+ // import Firehose from "./firehose/page"
6
  import Generate from "./generate/page"
7
 
8
  import { TooltipProvider } from "@/components/ui/tooltip"
src/app/store/index.ts CHANGED
@@ -10,7 +10,7 @@ export const useStore = create<{
10
  isLoading: boolean
11
  setLoading: (isLoading: boolean) => void
12
  setRendered: (renderedScene: RenderedScene) => void
13
- generate: (prompt: string) => void
14
  }>((set, get) => ({
15
  prompt: "",
16
  renderedScene: {
@@ -31,7 +31,7 @@ export const useStore = create<{
31
  renderedScene
32
  })
33
  },
34
- generate: (prompt: string) => {
35
  set({
36
  prompt,
37
  })
 
10
  isLoading: boolean
11
  setLoading: (isLoading: boolean) => void
12
  setRendered: (renderedScene: RenderedScene) => void
13
+ setPrompt: (prompt: string) => void
14
  }>((set, get) => ({
15
  prompt: "",
16
  renderedScene: {
 
31
  renderedScene
32
  })
33
  },
34
+ setPrompt: (prompt: string) => {
35
  set({
36
  prompt,
37
  })
src/types.ts CHANGED
@@ -115,4 +115,10 @@ export type GetAppPostsResponse = {
115
  success?: boolean
116
  error?: string
117
  posts: Post[]
 
 
 
 
 
 
118
  }
 
115
  success?: boolean
116
  error?: string
117
  posts: Post[]
118
+ }
119
+
120
+ export type GetAppPostResponse = {
121
+ success?: boolean
122
+ error?: string
123
+ post: Post
124
  }