jbilcke-hf HF staff commited on
Commit
f27679f
1 Parent(s): ac83b4d

hide some features for the beta + improve player

Browse files
Files changed (45) hide show
  1. .env +2 -3
  2. package-lock.json +28 -0
  3. package.json +2 -0
  4. public/huggingface-avatar.jpeg +0 -0
  5. src/app/config.ts +3 -0
  6. src/app/interface/channel-card/index.tsx +21 -5
  7. src/app/interface/left-menu/index.tsx +12 -12
  8. src/app/interface/pending-video-card/index.tsx +44 -0
  9. src/app/interface/pending-video-card/truncate.ts +5 -0
  10. src/app/interface/pending-video-list/index.tsx +39 -0
  11. src/app/interface/{top-menu → top-header}/index.tsx +78 -12
  12. src/app/interface/video-card/index.tsx +91 -6
  13. src/app/interface/video-list/index.tsx +6 -2
  14. src/app/interface/video-player/index.tsx +46 -0
  15. src/app/layout.tsx +8 -3
  16. src/app/main.tsx +10 -5
  17. src/app/page.tsx +5 -1
  18. src/app/server/actions/ai-tube-hf/deleteFileFromDataset.ts +42 -0
  19. src/app/server/actions/ai-tube-hf/deleteVideoRequest.ts +24 -0
  20. src/app/server/actions/ai-tube-hf/downloadFileAsText.ts +59 -0
  21. src/app/server/actions/ai-tube-hf/getChannels.ts +13 -10
  22. src/app/server/actions/ai-tube-hf/getCredentials.ts +38 -0
  23. src/app/server/actions/ai-tube-hf/getVideoRequestsFromChannel.ts +87 -89
  24. src/app/server/actions/ai-tube-hf/getVideos.ts +46 -0
  25. src/app/server/actions/ai-tube-hf/uploadVideoRequestToDataset.ts +8 -19
  26. src/app/server/actions/submitVideoRequest.ts +2 -11
  27. src/app/server/actions/utils/formatPromptFileName.ts +14 -0
  28. src/app/server/actions/utils/parseDatasetPrompt.ts +11 -4
  29. src/app/server/actions/utils/parseDatasetReadme.ts +0 -2
  30. src/app/server/actions/utils/parsePromptFileName.ts +3 -0
  31. src/app/state/useStore.ts +17 -1
  32. src/app/views/home-view/index.tsx +26 -27
  33. src/app/views/public-channel-view/index.tsx +2 -0
  34. src/app/views/public-channels-view/index.tsx +3 -3
  35. src/app/views/public-video-view/index.tsx +78 -15
  36. src/app/views/user-account-view/index.tsx +2 -2
  37. src/app/views/user-channel-view/index.tsx +130 -57
  38. src/app/views/user-channels-view/index.tsx +31 -7
  39. src/components/ui/table.tsx +8 -5
  40. src/huggingface/hub/src/lib/list-datasets.ts +1 -0
  41. src/lib/fonts.ts +0 -29
  42. src/lib/formatDuration.ts +12 -0
  43. src/lib/formatTimeAgo.ts +5 -0
  44. src/types.ts +12 -0
  45. tailwind.config.js +3 -3
.env CHANGED
@@ -1,12 +1,11 @@
1
 
 
 
2
  ADMIN_HUGGING_FACE_API_TOKEN=""
3
  ADMIN_HUGGING_FACE_USERNAME=""
4
 
5
  AI_TUBE_ROBOT_API="https://jbilcke-hf-ai-tube-robot.hf.space"
6
 
7
- VIDEOCHAIN_API_URL=""
8
- VIDEOCHAIN_API_TOKEN=""
9
-
10
  # ----------- CENSORSHIP -------
11
  ENABLE_CENSORSHIP=
12
  FINGERPRINT_KEY=
 
1
 
2
+ NEXT_PUBLIC_SHOW_BETA_FEATURES="false"
3
+
4
  ADMIN_HUGGING_FACE_API_TOKEN=""
5
  ADMIN_HUGGING_FACE_USERNAME=""
6
 
7
  AI_TUBE_ROBOT_API="https://jbilcke-hf-ai-tube-robot.hf.space"
8
 
 
 
 
9
  # ----------- CENSORSHIP -------
10
  ENABLE_CENSORSHIP=
11
  FINGERPRINT_KEY=
package-lock.json CHANGED
@@ -36,6 +36,7 @@
36
  "clsx": "^2.0.0",
37
  "cmdk": "^0.2.0",
38
  "cookies-next": "^2.1.2",
 
39
  "eslint": "8.45.0",
40
  "eslint-config-next": "13.4.10",
41
  "hash-wasm": "^4.11.0",
@@ -50,6 +51,7 @@
50
  "react-dom": "18.2.0",
51
  "react-icons": "^4.12.0",
52
  "react-smooth-scroll-hook": "^1.3.4",
 
53
  "react-virtualized-auto-sizer": "^1.0.20",
54
  "replicate": "^0.17.0",
55
  "sbd": "^1.0.19",
@@ -2910,6 +2912,21 @@
2910
  "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
2911
  "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="
2912
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2913
  "node_modules/debug": {
2914
  "version": "4.3.4",
2915
  "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -5639,6 +5656,17 @@
5639
  }
5640
  }
5641
  },
 
 
 
 
 
 
 
 
 
 
 
5642
  "node_modules/react-virtualized-auto-sizer": {
5643
  "version": "1.0.20",
5644
  "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.20.tgz",
 
36
  "clsx": "^2.0.0",
37
  "cmdk": "^0.2.0",
38
  "cookies-next": "^2.1.2",
39
+ "date-fns": "^2.30.0",
40
  "eslint": "8.45.0",
41
  "eslint-config-next": "13.4.10",
42
  "hash-wasm": "^4.11.0",
 
51
  "react-dom": "18.2.0",
52
  "react-icons": "^4.12.0",
53
  "react-smooth-scroll-hook": "^1.3.4",
54
+ "react-tuby": "^0.1.24",
55
  "react-virtualized-auto-sizer": "^1.0.20",
56
  "replicate": "^0.17.0",
57
  "sbd": "^1.0.19",
 
2912
  "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
2913
  "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="
2914
  },
2915
+ "node_modules/date-fns": {
2916
+ "version": "2.30.0",
2917
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
2918
+ "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
2919
+ "dependencies": {
2920
+ "@babel/runtime": "^7.21.0"
2921
+ },
2922
+ "engines": {
2923
+ "node": ">=0.11"
2924
+ },
2925
+ "funding": {
2926
+ "type": "opencollective",
2927
+ "url": "https://opencollective.com/date-fns"
2928
+ }
2929
+ },
2930
  "node_modules/debug": {
2931
  "version": "4.3.4",
2932
  "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
 
5656
  }
5657
  }
5658
  },
5659
+ "node_modules/react-tuby": {
5660
+ "version": "0.1.24",
5661
+ "resolved": "https://registry.npmjs.org/react-tuby/-/react-tuby-0.1.24.tgz",
5662
+ "integrity": "sha512-NbCZSgzzeP1vnXpeb6mWlTmIbJVYcputd7Sck14ZN12mREO+IndPQHLfDL3S/l1CKuualFyK+fvpoq79EyYx0A==",
5663
+ "engines": {
5664
+ "node": ">=10"
5665
+ },
5666
+ "peerDependencies": {
5667
+ "react": ">=16"
5668
+ }
5669
+ },
5670
  "node_modules/react-virtualized-auto-sizer": {
5671
  "version": "1.0.20",
5672
  "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.20.tgz",
package.json CHANGED
@@ -37,6 +37,7 @@
37
  "clsx": "^2.0.0",
38
  "cmdk": "^0.2.0",
39
  "cookies-next": "^2.1.2",
 
40
  "eslint": "8.45.0",
41
  "eslint-config-next": "13.4.10",
42
  "hash-wasm": "^4.11.0",
@@ -51,6 +52,7 @@
51
  "react-dom": "18.2.0",
52
  "react-icons": "^4.12.0",
53
  "react-smooth-scroll-hook": "^1.3.4",
 
54
  "react-virtualized-auto-sizer": "^1.0.20",
55
  "replicate": "^0.17.0",
56
  "sbd": "^1.0.19",
 
37
  "clsx": "^2.0.0",
38
  "cmdk": "^0.2.0",
39
  "cookies-next": "^2.1.2",
40
+ "date-fns": "^2.30.0",
41
  "eslint": "8.45.0",
42
  "eslint-config-next": "13.4.10",
43
  "hash-wasm": "^4.11.0",
 
52
  "react-dom": "18.2.0",
53
  "react-icons": "^4.12.0",
54
  "react-smooth-scroll-hook": "^1.3.4",
55
+ "react-tuby": "^0.1.24",
56
  "react-virtualized-auto-sizer": "^1.0.20",
57
  "replicate": "^0.17.0",
58
  "sbd": "^1.0.19",
public/huggingface-avatar.jpeg ADDED
src/app/config.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export const showBetaFeatures = `${
2
+ process.env.NEXT_PUBLIC_SHOW_BETA_FEATURES || ""
3
+ }`.trim().toLowerCase() === "true"
src/app/interface/channel-card/index.tsx CHANGED
@@ -16,9 +16,10 @@ export function ChannelCard({
16
  className={cn(
17
  `flex flex-col`,
18
  `items-center justify-center`,
19
- `w-[300px] h-[200px]`,
20
  `rounded-lg`,
21
- `bg-neutral-800 hover:bg-neutral-500/80`,
 
22
  `cursor-pointer`,
23
  className,
24
  )}
@@ -37,10 +38,25 @@ export function ChannelCard({
37
  </div>
38
 
39
  <div className={cn(
40
- `text-center`
 
 
41
  )}>
42
- <h3>{channel.label}</h3>
43
- <p>{channel.likes} likes</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  </div>
45
  </div>
46
  )
 
16
  className={cn(
17
  `flex flex-col`,
18
  `items-center justify-center`,
19
+ `w-52 h-52`,
20
  `rounded-lg`,
21
+ `bg-neutral-900 hover:bg-neutral-700/80`,
22
+ `text-neutral-100/80 hover:text-neutral-100/100`,
23
  `cursor-pointer`,
24
  className,
25
  )}
 
38
  </div>
39
 
40
  <div className={cn(
41
+ `flex flex-col`,
42
+ `items-center justify-center text-center`,
43
+ `space-y-2`
44
  )}>
45
+ <div className="text-center text-lg">{channel.label}</div>
46
+ {/*<div className="text-center text-sm font-semibold">
47
+ by <a href={
48
+ `https://huggingface.co/${channel.datasetUser}`
49
+ } target="_blank">@{channel.datasetUser}</a>
50
+ </div>
51
+ */}
52
+ <div className="text-center text-sm font-semibold">
53
+ @{channel.datasetUser}
54
+ </div>
55
+ <div className="flex flex-row items-center justify-center">
56
+ <div className="text-center text-sm">{0} videos</div>
57
+ <div className="px-1">-</div>
58
+ <div className="text-center text-sm">{channel.likes} likes</div>
59
+ </div>
60
  </div>
61
  </div>
62
  )
src/app/interface/left-menu/index.tsx CHANGED
@@ -7,14 +7,20 @@ import { CgProfile } from "react-icons/cg"
7
  import { useStore } from "@/app/state/useStore"
8
  import { cn } from "@/lib/utils"
9
  import { MenuItem } from "./menu-item"
 
 
10
 
11
  export function LeftMenu() {
12
  const view = useStore(s => s.view)
13
  const setView = useStore(s => s.setView)
 
 
 
14
  return (
15
  <div className={cn(
16
  `flex flex-col`,
17
  `w-24 px-1 pt-4`,
 
18
  // `bg-orange-500`,
19
  )}>
20
  <div className={cn(
@@ -27,16 +33,17 @@ export function LeftMenu() {
27
  >
28
  Discover
29
  </MenuItem>
30
- <MenuItem
31
  icon={<GrChannel className="h-5 w-5" />}
32
  selected={view === "public_channels"}
33
  onClick={() => setView("public_channels")}
34
  >
35
  Channels
36
- </MenuItem>
37
  </div>
38
  <div className={cn(
39
  `flex flex-col w-full`,
 
40
  )}>
41
  {/*<MenuItem
42
  icon={<MdVideoLibrary className="h-6 w-6" />}
@@ -46,20 +53,13 @@ export function LeftMenu() {
46
  My Videos
47
  </MenuItem>
48
  */}
49
- <MenuItem
50
- icon={<PiRobot className="h-6 w-6" />}
51
  selected={view === "user_channels"}
52
  onClick={() => setView("user_channels")}
53
  >
54
- My Robots
55
- </MenuItem>
56
- <MenuItem
57
- icon={<CgProfile className="h-6 w-6" />}
58
- selected={view === "user_account"}
59
- onClick={() => setView("user_account")}
60
- >
61
  Account
62
- </MenuItem>
63
  </div>
64
  </div>
65
  )
 
7
  import { useStore } from "@/app/state/useStore"
8
  import { cn } from "@/lib/utils"
9
  import { MenuItem } from "./menu-item"
10
+ import { showBetaFeatures } from "@/app/config"
11
+
12
 
13
  export function LeftMenu() {
14
  const view = useStore(s => s.view)
15
  const setView = useStore(s => s.setView)
16
+ const menuMode = useStore(s => s.menuMode)
17
+ const setMenuMode = useStore(s => s.setMenuMode)
18
+
19
  return (
20
  <div className={cn(
21
  `flex flex-col`,
22
  `w-24 px-1 pt-4`,
23
+ `justify-between`
24
  // `bg-orange-500`,
25
  )}>
26
  <div className={cn(
 
33
  >
34
  Discover
35
  </MenuItem>
36
+ {showBetaFeatures && <MenuItem
37
  icon={<GrChannel className="h-5 w-5" />}
38
  selected={view === "public_channels"}
39
  onClick={() => setView("public_channels")}
40
  >
41
  Channels
42
+ </MenuItem>}
43
  </div>
44
  <div className={cn(
45
  `flex flex-col w-full`,
46
+
47
  )}>
48
  {/*<MenuItem
49
  icon={<MdVideoLibrary className="h-6 w-6" />}
 
53
  My Videos
54
  </MenuItem>
55
  */}
56
+ {showBetaFeatures && <MenuItem
57
+ icon={<CgProfile className="h-6 w-6" />}
58
  selected={view === "user_channels"}
59
  onClick={() => setView("user_channels")}
60
  >
 
 
 
 
 
 
 
61
  Account
62
+ </MenuItem>}
63
  </div>
64
  </div>
65
  )
src/app/interface/pending-video-card/index.tsx ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PiTrashBold } from "react-icons/pi"
2
+
3
+ import { TableCell, TableRow } from "@/components/ui/table"
4
+ import { cn } from "@/lib/utils"
5
+ import { MdLockClock } from "react-icons/md"
6
+ import { VideoInfo } from "@/types"
7
+ import { truncate } from "./truncate"
8
+
9
+ export function PendingVideoCard({
10
+ video,
11
+ onDelete,
12
+ className = "",
13
+ }: {
14
+ video: VideoInfo
15
+ onDelete?: (video: VideoInfo) => void
16
+ className?: string
17
+ }) {
18
+
19
+ const isBusy = video.status === "queued" || video.status === "generating"
20
+ const hasError = video.status === "error"
21
+ const isNotGeneratedYet = video.status === "submitted" || video.status === "queued" || video.status === "generating"
22
+ const isGenerated = video.status === "published"
23
+
24
+ return (
25
+ <TableRow className={cn(
26
+ className,
27
+ )}>
28
+ <TableCell className="w-[100px] text-xs">{truncate(video.id, 8)}</TableCell>
29
+ <TableCell className="w-[120px]">{video.updatedAt || "N.A."}</TableCell>
30
+ <TableCell className="w-[150px] truncate">{truncate(video.description, 20)}</TableCell>
31
+ <TableCell className="w-[150px] truncate">{truncate(video.description, 45)}</TableCell>
32
+ <TableCell className="w-[100px]">{video.status}</TableCell>
33
+ <TableCell>
34
+ {
35
+ isBusy
36
+ ? <MdLockClock className="h-5 w-5" />
37
+ : <div
38
+ className="h-8 w-8 rounded-full cursor-pointer hover:bg-neutral-600 flex flex-col items-center justify-center"
39
+ onClick={() => { onDelete?.(video) }}><PiTrashBold className="h-6 w-6" />
40
+ </div>
41
+ }</TableCell>
42
+ </TableRow>
43
+ )
44
+ }
src/app/interface/pending-video-card/truncate.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export function truncate(text: string, length: number): string {
2
+ const truncated = text.slice(0, length)
3
+
4
+ return `${truncated}${truncated !== text ? '...' : ''}`
5
+ }
src/app/interface/pending-video-list/index.tsx ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cn } from "@/lib/utils"
2
+ import { VideoInfo } from "@/types"
3
+ import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
4
+
5
+ import { PendingVideoCard } from "../pending-video-card"
6
+
7
+ export function PendingVideoList({
8
+ videos,
9
+ onDelete,
10
+ className = "",
11
+ }: {
12
+ videos: VideoInfo[]
13
+ onDelete?: (video: VideoInfo) => void
14
+ className?: string
15
+ }) {
16
+ return (
17
+ <Table>
18
+ <TableHeader>
19
+ <TableRow>
20
+ <TableHead className="w-[100px]">ID</TableHead>
21
+ <TableHead className="w-[120px]">Updated at</TableHead>
22
+ <TableHead className="w-[150px]">Title</TableHead>
23
+ <TableHead className="w-[150px]">Description</TableHead>
24
+ <TableHead className="w-[100px]">Status</TableHead>
25
+ </TableRow>
26
+ </TableHeader>
27
+ <TableBody>
28
+ {videos.map((video) => (
29
+ <PendingVideoCard
30
+ key={video.id}
31
+ video={video}
32
+ className=""
33
+ onDelete={onDelete}
34
+ />
35
+ ))}
36
+ </TableBody>
37
+ </Table>
38
+ )
39
+ }
src/app/interface/{top-menu → top-header}/index.tsx RENAMED
@@ -1,23 +1,57 @@
 
 
 
 
 
 
 
 
 
 
1
  import { videoCategoriesWithLabels } from "@/app/state/categories"
2
  import { useStore } from "@/app/state/useStore"
3
  import { cn } from "@/lib/utils"
 
4
 
5
- export function TopMenu() {
 
 
6
  const displayMode = useStore(s => s.displayMode)
7
  const setDisplayMode = useStore(s => s.setDisplayMode)
 
 
 
 
 
 
 
8
  const currentChannel = useStore(s => s.currentChannel)
9
  const setCurrentChannel = useStore(s => s.setCurrentChannel)
10
  const currentTag = useStore(s => s.currentTag)
11
  const setCurrentTag = useStore(s => s.setCurrentTag)
12
  const currentVideos = useStore(s => s.currentVideos)
13
  const currentVideo = useStore(s => s.currentVideo)
14
- const setCurrentVideo = useStore(s => s.setCurrentVideo)
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  return (
17
  <div className={cn(
18
  `flex flex-col`,
19
  `overflow-hidden`,
20
- `w-full h-28 pt-4`,
 
 
21
  )}>
22
  <div className={cn(
23
  `flex flex-row justify-between`,
@@ -25,26 +59,58 @@ export function TopMenu() {
25
  )}>
26
  <div className={cn(
27
  `flex flex-col items-start justify-center`,
28
- `py-2 w-64`,
29
  )}>
30
- <div className="flex flex-row items-center justify-start">
31
- <span className="text-4xl mr-1">🍿 </span>
32
- <span className="text-4xl font-semibold">AI Tube</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  </div>
34
  </div>
35
  <div className={cn(
 
36
  `flex flex-col items-center justify-center`,
37
  `px-4 py-2 w-max-64`,
38
  )}>
39
- [ Search bar goes here ]
40
  </div>
41
  <div className={cn()}>
42
- &nbsp; {/* unused for now */}
43
  </div>
44
  </div>
 
 
45
  <div className={cn(
46
  `flex flex-row space-x-3`,
47
  `text-[13px] font-semibold`,
 
48
  )}>
49
  {Object.entries(videoCategoriesWithLabels)
50
  .map(([ key, label ]) => (
@@ -54,7 +120,7 @@ export function TopMenu() {
54
  `flex flex-col items-center justify-center`,
55
  `rounded-lg px-3 py-1 h-8`,
56
  `cursor-pointer`,
57
- `transition-all duration-300 ease-in-out`,
58
  currentTag === key
59
  ? `bg-neutral-100 text-neutral-800`
60
  : `bg-neutral-800 text-neutral-50/90 hover:bg-neutral-700 hover:text-neutral-50/90`,
@@ -69,7 +135,7 @@ export function TopMenu() {
69
  )}>{label}</span>
70
  </div>
71
  ))}
72
- </div>
73
  </div>
74
  )
75
- }
 
1
+ import { Pathway_Gothic_One } from 'next/font/google'
2
+ import { PiPopcornBold } from "react-icons/pi"
3
+
4
+ const pathway = Pathway_Gothic_One({
5
+ weight: "400",
6
+ style: "normal",
7
+ subsets: ["latin"],
8
+ display: "swap"
9
+ })
10
+
11
  import { videoCategoriesWithLabels } from "@/app/state/categories"
12
  import { useStore } from "@/app/state/useStore"
13
  import { cn } from "@/lib/utils"
14
+ import { useEffect } from 'react'
15
 
16
+ export function TopHeader() {
17
+ const view = useStore(s => s.view)
18
+ const setView = useStore(s => s.setView)
19
  const displayMode = useStore(s => s.displayMode)
20
  const setDisplayMode = useStore(s => s.setDisplayMode)
21
+
22
+ const headerMode = useStore(s => s.headerMode)
23
+ const setHeaderMode = useStore(s => s.setHeaderMode)
24
+
25
+ const setMenuMode = useStore(s => s.setMenuMode)
26
+
27
+
28
  const currentChannel = useStore(s => s.currentChannel)
29
  const setCurrentChannel = useStore(s => s.setCurrentChannel)
30
  const currentTag = useStore(s => s.currentTag)
31
  const setCurrentTag = useStore(s => s.setCurrentTag)
32
  const currentVideos = useStore(s => s.currentVideos)
33
  const currentVideo = useStore(s => s.currentVideo)
 
34
 
35
+ const isNormalSize = headerMode === "normal"
36
+
37
+
38
+ useEffect(() => {
39
+ if (view === "public_video") {
40
+ setHeaderMode("compact")
41
+ setMenuMode("slider_hidden")
42
+ } else {
43
+ setHeaderMode("normal")
44
+ setMenuMode("normal_icon")
45
+ }
46
+ }, [view])
47
+
48
  return (
49
  <div className={cn(
50
  `flex flex-col`,
51
  `overflow-hidden`,
52
+ `transition-all duration-200 ease-in-out`,
53
+ `w-full`,
54
+
55
  )}>
56
  <div className={cn(
57
  `flex flex-row justify-between`,
 
59
  )}>
60
  <div className={cn(
61
  `flex flex-col items-start justify-center`,
62
+ `w-64`,
63
  )}>
64
+ <div className={cn(
65
+ `flex flex-row items-center justify-start`,
66
+ `transition-all duration-200 ease-in-out`,
67
+ `cursor-pointer`,
68
+ "pt-2 text-3xl space-x-1",
69
+ pathway.className,
70
+ isNormalSize
71
+ ? "scale-125 ml-4 mb-4" : "scale-100 mb-2"
72
+ )}
73
+ onClick={() => {
74
+ setView("home")
75
+ }}
76
+ >
77
+ <div className="mr-1">
78
+ <div className={cn(
79
+ `flex flex-col items-center justify-center`,
80
+ `bg-yellow-300 text-neutral-950`,
81
+ `rounded-lg w-6 h-7`
82
+ )}>
83
+ <PiPopcornBold className={cn(
84
+ `w-5 h-5`
85
+ )} />
86
+ </div>
87
+ </div>
88
+ <div className="font-semibold">
89
+ {view === "user_channels"
90
+ ? "My account"
91
+ : view === "public_channels"
92
+ ? "AI Channels"
93
+ : "AiTube" }
94
+ </div>
95
  </div>
96
  </div>
97
  <div className={cn(
98
+ `transition-all duration-200 ease-in-out`,
99
  `flex flex-col items-center justify-center`,
100
  `px-4 py-2 w-max-64`,
101
  )}>
102
+ {/*[ Search bar goes here ]*/}
103
  </div>
104
  <div className={cn()}>
105
+ &nbsp; {/* more buttons? unused for now */}
106
  </div>
107
  </div>
108
+ {
109
+ isNormalSize ?
110
  <div className={cn(
111
  `flex flex-row space-x-3`,
112
  `text-[13px] font-semibold`,
113
+ `mb-4`
114
  )}>
115
  {Object.entries(videoCategoriesWithLabels)
116
  .map(([ key, label ]) => (
 
120
  `flex flex-col items-center justify-center`,
121
  `rounded-lg px-3 py-1 h-8`,
122
  `cursor-pointer`,
123
+ `transition-all duration-200 ease-in-out`,
124
  currentTag === key
125
  ? `bg-neutral-100 text-neutral-800`
126
  : `bg-neutral-800 text-neutral-50/90 hover:bg-neutral-700 hover:text-neutral-50/90`,
 
135
  )}>{label}</span>
136
  </div>
137
  ))}
138
+ </div> : null}
139
  </div>
140
  )
141
+ }
src/app/interface/video-card/index.tsx CHANGED
@@ -1,33 +1,118 @@
 
 
 
1
  import { cn } from "@/lib/utils"
2
  import { VideoInfo } from "@/types"
 
 
3
 
4
  export function VideoCard({
5
  video,
6
  className = "",
 
7
  }: {
8
  video: VideoInfo
9
  className?: string
 
10
  }) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  return (
13
  <div
14
  className={cn(
 
15
  `flex flex-col`,
16
- `w-[300px] h-[400px]`,
17
  `bg-line-900`,
 
 
18
  className,
19
- )}>
 
 
 
 
20
  <div
21
  className={cn(
22
- `rounded-lg overflow-hidden`
 
23
  )}
24
  >
25
- <img src="" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  </div>
27
  <div className={cn(
28
-
29
  )}>
30
- <h3>{video.label}</h3>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  </div>
32
  </div>
33
  )
 
1
+ import { useRef, useState } from "react"
2
+ import { RiCheckboxCircleFill } from "react-icons/ri"
3
+
4
  import { cn } from "@/lib/utils"
5
  import { VideoInfo } from "@/types"
6
+ import { formatDuration } from "@/lib/formatDuration"
7
+ import { formatTimeAgo } from "@/lib/formatTimeAgo"
8
 
9
  export function VideoCard({
10
  video,
11
  className = "",
12
+ onSelect,
13
  }: {
14
  video: VideoInfo
15
  className?: string
16
+ onSelect?: (video: VideoInfo) => void
17
  }) {
18
+ const ref = useRef<HTMLVideoElement>(null)
19
+ const [duration, setDuration] = useState(0)
20
+
21
+ const handlePointerEnter = () => {
22
+ // ref.current?.load()
23
+ ref.current?.play()
24
+ }
25
+ const handlePointerLeave = () => {
26
+ ref.current?.pause()
27
+ // ref.current?.load()
28
+ }
29
+ const handleLoad = () => {
30
+ if (ref.current?.readyState) {
31
+ setDuration(ref.current.duration)
32
+ }
33
+ }
34
+
35
+ const handleClick = () => {
36
+ onSelect?.(video)
37
+ }
38
 
39
  return (
40
  <div
41
  className={cn(
42
+ `w-full`,
43
  `flex flex-col`,
 
44
  `bg-line-900`,
45
+ `space-y-3`,
46
+ `cursor-pointer`,
47
  className,
48
+ )}
49
+ onPointerEnter={handlePointerEnter}
50
+ onPointerLeave={handlePointerLeave}
51
+ onClick={handleClick}
52
+ >
53
  <div
54
  className={cn(
55
+ `flex flex-col aspect-video items-center justify-center`,
56
+ `rounded-xl overflow-hidden`,
57
  )}
58
  >
59
+ <video
60
+ ref={ref}
61
+ src={video.assetUrl}
62
+ className="w-full"
63
+ onLoadedMetadata={handleLoad}
64
+ muted
65
+ />
66
+
67
+ <div className={cn(
68
+ ``,
69
+ `w-full flex flex-row items-end justify-end`
70
+ )}>
71
+ <div className={cn(
72
+ `-mt-8`,
73
+ `mr-0`,
74
+ )}
75
+ >
76
+ <div className={cn(
77
+ `mb-[5px]`,
78
+ `mr-[5px]`,
79
+ `flex flex-col items-center justify-center text-center`,
80
+ `bg-neutral-900 rounded`,
81
+ `text-2xs font-semibold px-[3px] py-[1px]`,
82
+ )}
83
+ >{formatDuration(duration)}</div>
84
+ </div>
85
+ </div>
86
  </div>
87
  <div className={cn(
88
+ `flex flex-row space-x-4`,
89
  )}>
90
+ <div className="flex flex-col">
91
+ <div className="flex w-9 rounded-full overflow-hidden">
92
+ <img
93
+ src="huggingface-avatar.jpeg"
94
+ />
95
+ </div>
96
+ </div>
97
+ <div className="flex flex-col flex-grow">
98
+ <h3 className="text-zinc-100 text-base font-medium mb-0 line-clamp-2">{video.label}</h3>
99
+ <div className={cn(
100
+ `flex flex-row items-center`,
101
+ `text-neutral-400 text-sm font-normal space-x-1`,
102
+ )}>
103
+ <div>{video.channel.label}</div>
104
+ <div><RiCheckboxCircleFill className="" /></div>
105
+ </div>
106
+ <div className={cn(
107
+ `flex flex-row`,
108
+ `text-neutral-400 text-sm font-normal`,
109
+ `space-x-1`
110
+ )}>
111
+ <div>0 views</div>
112
+ <div className="font-semibold scale-125">·</div>
113
+ <div>{formatTimeAgo(video.updatedAt)}</div>
114
+ </div>
115
+ </div>
116
  </div>
117
  </div>
118
  )
src/app/interface/video-list/index.tsx CHANGED
@@ -7,6 +7,7 @@ export function VideoList({
7
  videos,
8
  layout = "flex",
9
  className = "",
 
10
  }: {
11
  videos: VideoInfo[]
12
 
@@ -20,13 +21,15 @@ export function VideoList({
20
  layout?: "grid" | "flex"
21
 
22
  className?: string
 
 
23
  }) {
24
 
25
  return (
26
  <div
27
  className={cn(
28
  layout === "grid"
29
- ? `grid grid-cols-4 gap-4`
30
  : `flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4`,
31
  className,
32
  )}
@@ -35,7 +38,8 @@ export function VideoList({
35
  <VideoCard
36
  key={video.id}
37
  video={video}
38
- className=""
 
39
  />
40
  ))}
41
  </div>
 
7
  videos,
8
  layout = "flex",
9
  className = "",
10
+ onSelect,
11
  }: {
12
  videos: VideoInfo[]
13
 
 
21
  layout?: "grid" | "flex"
22
 
23
  className?: string
24
+
25
+ onSelect?: (video: VideoInfo) => void
26
  }) {
27
 
28
  return (
29
  <div
30
  className={cn(
31
  layout === "grid"
32
+ ? `grid grid-cols-2 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4`
33
  : `flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4`,
34
  className,
35
  )}
 
38
  <VideoCard
39
  key={video.id}
40
  video={video}
41
+ className="w-full"
42
+ onSelect={onSelect}
43
  />
44
  ))}
45
  </div>
src/app/interface/video-player/index.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { Player } from "react-tuby"
4
+ import "react-tuby/css/main.css"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { VideoInfo } from "@/types"
8
+
9
+ export function VideoPlayer({
10
+ video,
11
+ className = ""
12
+ }: {
13
+ video?: VideoInfo
14
+ className?: string
15
+ }) {
16
+
17
+ // TODO: keep the same form factor?
18
+ if (!video) { return null }
19
+
20
+ return (
21
+ <div className={cn(
22
+ `w-full`,
23
+ `flex flex-col items-center justify-center`,
24
+ `rounded-xl overflow-hidden`,
25
+ className
26
+ )}>
27
+ <div className={cn(
28
+ `w-[calc(100%+16px)]`,
29
+ `-ml-2 -mr-2`,
30
+ `flex flex-col items-center justify-center`,
31
+ )}>
32
+ <Player
33
+
34
+ src={[
35
+ {
36
+ quality: "Full HD",
37
+ url: video.assetUrl,
38
+ }
39
+ ]}
40
+ subtitles={[]}
41
+ // poster="https://cdn.jsdelivr.net/gh/naptestdev/video-examples@master/poster.png"
42
+ />
43
+ </div>
44
+ </div>
45
+ )
46
+ }
src/app/layout.tsx CHANGED
@@ -1,11 +1,16 @@
1
  import type { Metadata } from 'next'
2
- import { Inter } from 'next/font/google'
3
 
4
  import { cn } from '@/lib/utils'
5
 
6
  import './globals.css'
7
 
8
- const inter = Inter({ subsets: ['latin'] })
 
 
 
 
 
9
 
10
  export const metadata: Metadata = {
11
  title: '🍿 AI Tube',
@@ -21,7 +26,7 @@ export default function RootLayout({
21
  <html lang="en">
22
  <body className={cn(
23
  `h-full w-full overflow-auto`,
24
- inter.className
25
  )}>
26
  {children}
27
  </body>
 
1
  import type { Metadata } from 'next'
2
+ import { Roboto } from 'next/font/google'
3
 
4
  import { cn } from '@/lib/utils'
5
 
6
  import './globals.css'
7
 
8
+ const roboto = Roboto({
9
+ weight: ['100', '300', '400', '500', '700', '900'],
10
+ style: ['normal', 'italic'],
11
+ subsets: ['latin'],
12
+ display: 'swap',
13
+ })
14
 
15
  export const metadata: Metadata = {
16
  title: '🍿 AI Tube',
 
26
  <html lang="en">
27
  <body className={cn(
28
  `h-full w-full overflow-auto`,
29
+ roboto.className
30
  )}>
31
  {children}
32
  </body>
src/app/main.tsx CHANGED
@@ -1,7 +1,7 @@
1
  "use client"
2
 
3
  import { cn } from "@/lib/utils"
4
- import { TopMenu } from "./interface/top-menu"
5
  import { LeftMenu } from "./interface/left-menu"
6
  import { useStore } from "./state/useStore"
7
  import { HomeView } from "./views/home-view"
@@ -14,7 +14,7 @@ import { UserAccountView } from "./views/user-account-view"
14
 
15
  export function Main() {
16
  const view = useStore(s => s.view)
17
-
18
  return (
19
  <div className={cn(
20
  `flex flex-row h-screen w-screen inset-0 overflow-hidden`,
@@ -23,11 +23,16 @@ export function Main() {
23
  <LeftMenu />
24
  <div className={cn(
25
  `flex flex-col`,
26
- `w-[calc(100vh-96px)]`,
27
  `px-2`
28
  )}>
29
- <TopMenu />
30
- <div className="pt-4">
 
 
 
 
 
31
  {view === "home" && <HomeView />}
32
  {view === "public_video" && <PublicVideoView />}
33
  {view === "public_channels" && <PublicChannelsView />}
 
1
  "use client"
2
 
3
  import { cn } from "@/lib/utils"
4
+ import { TopHeader } from "./interface/top-header"
5
  import { LeftMenu } from "./interface/left-menu"
6
  import { useStore } from "./state/useStore"
7
  import { HomeView } from "./views/home-view"
 
14
 
15
  export function Main() {
16
  const view = useStore(s => s.view)
17
+ const headerMode = useStore(s => s.headerMode)
18
  return (
19
  <div className={cn(
20
  `flex flex-row h-screen w-screen inset-0 overflow-hidden`,
 
23
  <LeftMenu />
24
  <div className={cn(
25
  `flex flex-col`,
26
+ `w-[calc(100vw-96px)]`,
27
  `px-2`
28
  )}>
29
+ <TopHeader />
30
+ <div className={cn(
31
+ `w-full overflow-x-hidden overflow-y-scroll`,
32
+ headerMode === "normal"
33
+ ? `h-[calc(100vh-112px)]`
34
+ : `h-[calc(100vh-48px)]`
35
+ )}>
36
  {view === "home" && <HomeView />}
37
  {view === "public_video" && <PublicVideoView />}
38
  {view === "public_channels" && <PublicChannelsView />}
src/app/page.tsx CHANGED
@@ -5,12 +5,14 @@ import Head from "next/head"
5
  import Script from "next/script"
6
 
7
  import { cn } from "@/lib/utils"
 
8
 
9
  import { Main } from "./main"
10
 
11
  // https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
12
 
13
  export default function Page() {
 
14
  const [isLoaded, setLoaded] = useState(false)
15
  useEffect(() => { setLoaded(true) }, [])
16
  return (
@@ -23,7 +25,9 @@ export default function Page() {
23
  <main className={cn(
24
  `light text-neutral-100`,
25
  // `bg-gradient-to-r from-green-500 to-yellow-400`,
26
- `bg-gradient-to-r from-neutral-950 to-neutral-950`,
 
 
27
  )}>
28
  {isLoaded && <Main />}
29
  {/*
 
5
  import Script from "next/script"
6
 
7
  import { cn } from "@/lib/utils"
8
+ import { useStore } from "@/app/state/useStore"
9
 
10
  import { Main } from "./main"
11
 
12
  // https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
13
 
14
  export default function Page() {
15
+ const view = useStore(s => s.view)
16
  const [isLoaded, setLoaded] = useState(false)
17
  useEffect(() => { setLoaded(true) }, [])
18
  return (
 
25
  <main className={cn(
26
  `light text-neutral-100`,
27
  // `bg-gradient-to-r from-green-500 to-yellow-400`,
28
+ view === "public_video"
29
+ ? `bg-gradient-radial from-neutral-900 to-neutral-950`
30
+ : `bg-neutral-950` // bg-gradient-to-br from-neutral-950 via-neutral-950 to-neutral-950`
31
  )}>
32
  {isLoaded && <Main />}
33
  {/*
src/app/server/actions/ai-tube-hf/deleteFileFromDataset.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { deleteFile } from "@/huggingface/hub/src"
2
+
3
+ import { getCredentials } from "./getCredentials"
4
+
5
+ export async function deleteFileFromDataset({
6
+ repo,
7
+ path,
8
+ apiKey,
9
+ neverThrow = false
10
+ }: {
11
+ repo: string
12
+
13
+ path: string
14
+
15
+ apiKey?: string
16
+
17
+ /**
18
+ * If set to true, this function will never throw an exception
19
+ * this is useful in workflow where we don't care about what happened
20
+ *
21
+ * False by default
22
+ */
23
+ neverThrow?: boolean
24
+ }): Promise<boolean> {
25
+ try {
26
+ const { credentials } = await getCredentials(apiKey)
27
+
28
+ await deleteFile({
29
+ repo,
30
+ path,
31
+ credentials
32
+ })
33
+ return true
34
+ } catch (err) {
35
+ if (neverThrow) {
36
+ console.error(`deleteFileFromDataset():`, err)
37
+ return false
38
+ } else {
39
+ throw err
40
+ }
41
+ }
42
+ }
src/app/server/actions/ai-tube-hf/deleteVideoRequest.ts ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { VideoInfo } from "@/types"
3
+
4
+ import { deleteFileFromDataset } from "./deleteFileFromDataset"
5
+ import { formatPromptFileName } from "../utils/formatPromptFileName"
6
+
7
+ export async function deleteVideoRequest({
8
+ video,
9
+ apiKey,
10
+ neverThrow,
11
+ }: {
12
+ video: VideoInfo
13
+ apiKey: string
14
+ neverThrow?: boolean
15
+ }): Promise<boolean> {
16
+ const repo = `datasets/${video.channel.datasetUser}/${video.channel.datasetName}`
17
+ const { fileName } = formatPromptFileName(video.id)
18
+ return deleteFileFromDataset({
19
+ repo,
20
+ path: fileName,
21
+ apiKey,
22
+ neverThrow,
23
+ })
24
+ }
src/app/server/actions/ai-tube-hf/downloadFileAsText.ts ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { downloadFile } from "@/huggingface/hub/src"
2
+ import { getCredentials } from "./getCredentials"
3
+
4
+ export async function downloadFileAsText({
5
+ repo,
6
+ path,
7
+ apiKey,
8
+ renewCache = false,
9
+ neverThrow = false
10
+ }: {
11
+ repo: string
12
+
13
+ path: string
14
+
15
+ apiKey?: string
16
+
17
+ /**
18
+ * Force renewing the cache
19
+ *
20
+ * False by default
21
+ */
22
+ renewCache?: boolean
23
+
24
+ /**
25
+ * If set to true, this function will never throw an exception
26
+ * this is useful in workflow where we don't care about what happened
27
+ *
28
+ * False by default
29
+ */
30
+ neverThrow?: boolean
31
+ }): Promise<string> {
32
+ try {
33
+ const { credentials } = await getCredentials(apiKey)
34
+
35
+ const response = await downloadFile({
36
+ repo,
37
+ path,
38
+ credentials,
39
+ requestInit: renewCache
40
+ ? { cache: "no-cache" }
41
+ : undefined
42
+ })
43
+
44
+ const text = await response?.text()
45
+
46
+ if (typeof text !== "string") {
47
+ throw new Error(`file has no text content`)
48
+ }
49
+
50
+ return text
51
+ } catch (err) {
52
+ if (neverThrow) {
53
+ console.error(`downloadFileAsText():`, err)
54
+ return ""
55
+ } else {
56
+ throw err
57
+ }
58
+ }
59
+ }
src/app/server/actions/ai-tube-hf/getChannels.ts CHANGED
@@ -9,6 +9,7 @@ import { adminCredentials } from "../config"
9
  export async function getChannels(options: {
10
  apiKey?: string
11
  owner?: string
 
12
  } = {}): Promise<ChannelInfo[]> {
13
 
14
  let credentials: Credentials = adminCredentials
@@ -37,11 +38,14 @@ export async function getChannels(options: {
37
  ? { owner } // search channels of a specific user
38
  : prefix // global search (note: might be costly?)
39
 
40
- // console.log("search:", search)
41
 
42
  for await (const { id, name, likes, updatedAt } of listDatasets({
43
  search,
44
- credentials
 
 
 
45
  })) {
46
 
47
  // TODO: need to handle better cases where the username is missing
@@ -83,25 +87,24 @@ export async function getChannels(options: {
83
  })
84
  const readme = await response?.text()
85
 
86
- const ParsedDatasetReadme = parseDatasetReadme(readme)
87
 
88
- // console.log("ParsedDatasetReadme: ", ParsedDatasetReadme)
89
 
90
-
91
- prompt = ParsedDatasetReadme.prompt
92
- label = ParsedDatasetReadme.pretty_name
93
- description = ParsedDatasetReadme.description
94
 
95
  const prefix = "ai-tube:"
96
 
97
- tags = ParsedDatasetReadme.tags
98
  .filter(tag => tag.startsWith(prefix)) // remove any tag not belonging to us
99
  .map(tag => tag.replaceAll(prefix, "").trim()) // remove the prefix
100
  .filter(tag => tag) // remove empty tags
101
 
102
 
103
  } catch (err) {
104
- console.log("failed to read the readme:", err)
105
  }
106
 
107
  const channel: ChannelInfo = {
 
9
  export async function getChannels(options: {
10
  apiKey?: string
11
  owner?: string
12
+ renewCache?: boolean
13
  } = {}): Promise<ChannelInfo[]> {
14
 
15
  let credentials: Credentials = adminCredentials
 
38
  ? { owner } // search channels of a specific user
39
  : prefix // global search (note: might be costly?)
40
 
41
+ console.log("search:", search)
42
 
43
  for await (const { id, name, likes, updatedAt } of listDatasets({
44
  search,
45
+ credentials,
46
+ requestInit: options?.renewCache
47
+ ? { cache: "no-cache" }
48
+ : undefined
49
  })) {
50
 
51
  // TODO: need to handle better cases where the username is missing
 
87
  })
88
  const readme = await response?.text()
89
 
90
+ const parsedDatasetReadme = parseDatasetReadme(readme)
91
 
92
+ // console.log("parsedDatasetReadme: ", parsedDatasetReadme)
93
 
94
+ prompt = parsedDatasetReadme.prompt
95
+ label = parsedDatasetReadme.pretty_name
96
+ description = parsedDatasetReadme.description
 
97
 
98
  const prefix = "ai-tube:"
99
 
100
+ tags = parsedDatasetReadme.tags
101
  .filter(tag => tag.startsWith(prefix)) // remove any tag not belonging to us
102
  .map(tag => tag.replaceAll(prefix, "").trim()) // remove the prefix
103
  .filter(tag => tag) // remove empty tags
104
 
105
 
106
  } catch (err) {
107
+ // console.log("failed to read the readme:", err)
108
  }
109
 
110
  const channel: ChannelInfo = {
src/app/server/actions/ai-tube-hf/getCredentials.ts ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ // safe way to get the credentials
3
+
4
+ import { Credentials, WhoAmIUser, whoAmI } from "@/huggingface/hub/src"
5
+
6
+ import { adminCredentials, adminUsername } from "../config"
7
+
8
+ export async function getCredentials(apiKey?: string): Promise<{
9
+ username: string
10
+ avatarUrl: string
11
+ credentials: Credentials
12
+ }> {
13
+ let username = adminUsername
14
+ let credentials: Credentials = adminCredentials
15
+ let avatarUrl = ""
16
+
17
+ if (apiKey) {
18
+ try {
19
+ credentials = { accessToken: apiKey }
20
+ const user = await whoAmI({ credentials }) as unknown as WhoAmIUser
21
+ if (!user.name) {
22
+ throw new Error(`couldn't get the username`)
23
+ }
24
+ username = user.name
25
+ avatarUrl = user.avatarUrl || ""
26
+ } catch (err) {
27
+ console.error(err)
28
+ // important: we throw an error if an apiKey was explicitely given but is empty
29
+ throw new Error(`the provided Hugging Face API key is invalid or has expired`)
30
+ }
31
+ }
32
+
33
+ return {
34
+ username,
35
+ avatarUrl,
36
+ credentials
37
+ }
38
+ }
src/app/server/actions/ai-tube-hf/getVideoRequestsFromChannel.ts CHANGED
@@ -1,112 +1,110 @@
1
  "use server"
2
 
3
- import { Credentials, downloadFile, listFiles, whoAmI } from "@/huggingface/hub/src"
4
- import { parseDatasetReadme } from "@/app/server/actions/utils/parseDatasetReadme"
5
  import { ChannelInfo, VideoRequest } from "@/types"
6
-
7
- import { adminCredentials } from "../config"
 
 
 
8
 
9
  /**
10
  * Return all the videos requests created by a user on their channel
11
- *
12
- * @param options
13
- * @returns
14
  */
15
- export async function getVideoRequestsFromChannel(options: {
16
- channel: ChannelInfo,
17
- apiKey?: string,
 
 
 
 
 
18
  renewCache?: boolean
19
- }): Promise<Record<string, VideoRequest>> {
 
20
 
21
- let credentials: Credentials = adminCredentials
 
22
 
23
- if (options?.apiKey) {
24
- try {
25
- credentials = { accessToken: options.apiKey }
26
- const { name: username } = await whoAmI({ credentials })
27
- if (!username) {
28
- throw new Error(`couldn't get the username`)
29
- }
30
- } catch (err) {
31
- console.error(err)
32
- return {}
33
- }
34
- }
35
 
36
- let videos: Record<string, VideoRequest> = {}
37
 
38
- const repo = `datasets/${options.channel.datasetUser}/${options.channel.datasetName}`
39
 
40
- console.log(`scanning ${repo}`)
 
 
 
 
 
 
 
 
41
 
42
- for await (const file of listFiles({
43
- repo,
44
- // recursive: true,
45
- // expand: true,
46
- credentials,
47
- requestInit: {
48
- // cache invalidation should be called right after adding a new video
49
- cache: options?.renewCache ? "no-cache" : "default",
50
- next: {
51
- revalidate: 10, // otherwise we only update very 10 seconds by default
52
- // tags: [] // tags used for cache invalidation (ie. this is added to the cache key)
53
- }
54
- }
55
- })) {
56
-
57
- // TODO we should add some safety mechanisms here:
58
- // skip lists of files that are too long
59
- // skip files that are too big
60
- // skip files with file.security.safe !== true
61
-
62
- console.log("file.path:", file.path)
63
- /// { type, oid, size, path }
64
- if (file.path === "README.md") {
65
- console.log("found the README")
66
- // TODO: read this readme
67
- } else if (file.path.startsWith("prompt_") && file.path.endsWith(".txt")) {
68
- console.log("yes!!")
69
- const fileWithoutSuffix = file.path.split(".txt").shift() || ""
70
- const words = fileWithoutSuffix.split("_")
71
- console.log("debug:", { path: file.path, fileWithoutSuffix, words })
72
- if (words.length !== 3) {
73
- console.log("found an invalid prompt file format: " + file.path)
74
- continue
75
- }
76
- const [_prefix, date, id] = words
77
- console.log("found a prompt:", file.path)
78
 
79
- try {
80
- const response = await downloadFile({
81
  repo,
82
  path: file.path,
83
- credentials
 
 
84
  })
85
- const rawMarkdown = await response?.text()
86
-
87
- const parsedDatasetReadme = parseDatasetReadme(rawMarkdown)
88
- console.log("prompt parsed markdown:", parsedDatasetReadme)
89
- } catch (err) {
90
- console.log("failed to parse the prompt file")
91
- continue
92
- }
93
- const video: VideoRequest = {
94
- id,
95
- label: "",
96
- description: "",
97
- prompt: "",
98
- thumbnailUrl: "",
99
-
100
- updatedAt: file.lastCommit?.date || "",
101
- tags: [], // read them from the file?
102
- channel: options.channel
 
 
 
 
 
 
 
 
 
 
 
 
103
  }
 
104
 
105
- videos[id] = video
106
- } else if (file.path.endsWith(".mp4")) {
107
- console.log("found a video:", file.path)
 
 
 
 
108
  }
109
  }
110
-
111
- return videos
112
  }
 
1
  "use server"
2
 
 
 
3
  import { ChannelInfo, VideoRequest } from "@/types"
4
+ import { getCredentials } from "./getCredentials"
5
+ import { listFiles } from "@/huggingface/hub/src"
6
+ import { parsePromptFileName } from "../utils/parsePromptFileName"
7
+ import { downloadFileAsText } from "./downloadFileAsText"
8
+ import { parseDatasetPrompt } from "../utils/parseDatasetPrompt"
9
 
10
  /**
11
  * Return all the videos requests created by a user on their channel
12
+ *
 
 
13
  */
14
+ export async function getVideoRequestsFromChannel({
15
+ channel,
16
+ apiKey,
17
+ renewCache,
18
+ neverThrow,
19
+ }: {
20
+ channel: ChannelInfo
21
+ apiKey?: string
22
  renewCache?: boolean
23
+ neverThrow?: boolean
24
+ }): Promise<VideoRequest[]> {
25
 
26
+ try {
27
+ const { credentials } = await getCredentials(apiKey)
28
 
29
+ let videos: Record<string, VideoRequest> = {}
 
 
 
 
 
 
 
 
 
 
 
30
 
31
+ const repo = `datasets/${channel.datasetUser}/${channel.datasetName}`
32
 
33
+ // console.log(`scanning ${repo}`)
34
 
35
+ for await (const file of listFiles({
36
+ repo,
37
+ // recursive: true,
38
+ // expand: true,
39
+ credentials,
40
+ requestInit: renewCache
41
+ ? { cache: "no-cache" }
42
+ : undefined
43
+ })) {
44
 
45
+ // TODO we should add some safety mechanisms here:
46
+ // skip lists of files that are too long
47
+ // skip files that are too big
48
+ // skip files with file.security.safe !== true
49
+
50
+ // console.log("file.path:", file.path)
51
+ /// { type, oid, size, path }
52
+ if (file.path === "README.md") {
53
+ // console.log("found the README")
54
+ // TODO: read this readme
55
+ } else if (file.path.startsWith("prompt_") && file.path.endsWith(".md")) {
56
+
57
+ const id = parsePromptFileName(file.path)
58
+
59
+ if (!id) { continue }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
+ const rawMarkdown = await downloadFileAsText({
 
62
  repo,
63
  path: file.path,
64
+ apiKey,
65
+ renewCache,
66
+ neverThrow: true,
67
  })
68
+
69
+ if (!rawMarkdown) {
70
+ // console.log(`markdown file is empty, skipping`)
71
+ continue
72
+ }
73
+
74
+ const { title, description, tags, prompt } = parseDatasetPrompt(rawMarkdown)
75
+
76
+ if (!title || !description || !prompt) {
77
+ // console.log("dataset prompt is incomplete or unparseable")
78
+ continue
79
+ }
80
+ // console.log("prompt parsed markdown:", { title, description, tags })
81
+
82
+ const video: VideoRequest = {
83
+ id,
84
+ label: title,
85
+ description,
86
+ prompt,
87
+ thumbnailUrl: "",
88
+
89
+ updatedAt: file.lastCommit?.date || "",
90
+ tags, // read them from the file?
91
+ channel,
92
+ }
93
+
94
+ videos[id] = video
95
+
96
+ } else if (file.path.endsWith(".mp4")) {
97
+ // console.log("found a video:", file.path)
98
  }
99
+ }
100
 
101
+ return Object.values(videos)
102
+ } catch (err) {
103
+ if (neverThrow) {
104
+ console.error(`getVideoRequestsFromChannel():`, err)
105
+ return []
106
+ } else {
107
+ throw err
108
  }
109
  }
 
 
110
  }
src/app/server/actions/ai-tube-hf/getVideos.ts ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use server"
2
+
3
+ import { VideoInfo } from "@/types"
4
+
5
+ import { getIndex } from "./getIndex"
6
+
7
+ const HARD_LIMIT = 100
8
+
9
+ // this just return ALL videos on the platform
10
+ export async function getVideos({
11
+ tag = "",
12
+ sortBy = "date",
13
+ maxVideos = HARD_LIMIT,
14
+ }: {
15
+ tag?: string
16
+ sortBy?: "random" | "date",
17
+ maxVideos?: number
18
+ }): Promise<VideoInfo[]> {
19
+ // the index is gonna grow more and more,
20
+ // but in the future we will use some DB eg. Prisma or sqlite
21
+ const published = await getIndex({
22
+ status: "published",
23
+ renewCache: true
24
+ })
25
+
26
+
27
+ let videos = Object.values(published)
28
+
29
+ // filter videos by tag, or else we return everything
30
+ const requestedTag = tag.toLowerCase().trim()
31
+ if (requestedTag) {
32
+ videos = videos
33
+ .filter(v =>
34
+ v.tags.map(t => t.toLowerCase().trim()).includes(requestedTag)
35
+ )
36
+ }
37
+
38
+ if (sortBy === "date") {
39
+ videos.sort(((a, b) => a.updatedAt.localeCompare(b.updatedAt)))
40
+ } else {
41
+ videos.sort(() => Math.random() - 0.5)
42
+ }
43
+
44
+ // we enforce a max limit of HARD_LIMIT (eg. 100)
45
+ return videos.slice(0, Math.min(HARD_LIMIT, maxVideos))
46
+ }
src/app/server/actions/ai-tube-hf/uploadVideoRequestToDataset.ts CHANGED
@@ -1,16 +1,14 @@
1
  "use server"
2
 
3
  import { Blob } from "buffer"
4
- import { v4 as uuidv4 } from "uuid"
5
 
6
  import { Credentials, uploadFile, whoAmI } from "@/huggingface/hub/src"
7
  import { ChannelInfo, VideoInfo, VideoRequest } from "@/types"
 
8
 
9
  /**
10
  * Save the video request to the user's own dataset
11
- *
12
- * @param param0
13
- * @returns
14
  */
15
  export async function uploadVideoRequestToDataset({
16
  channel,
@@ -41,16 +39,7 @@ export async function uploadVideoRequestToDataset({
41
  throw new Error(`couldn't get the username`)
42
  }
43
 
44
- const date = new Date()
45
- const dateSlug = date.toISOString().replace(/[^0-9]/gi, '').slice(0, 12);
46
-
47
- // there is a bug in the [^] maybe, because all characters are removed
48
- // const nameSlug = title.replaceAll(/\S+/gi, "-").replaceAll(/[^A-Za-z0-9\-_]/gi, "")
49
- // const fileName = `prompt-${dateSlug}-${nameSlug}.txt`
50
-
51
- const videoId = uuidv4()
52
-
53
- const fileName = `prompt_${dateSlug}_${videoId}.txt`
54
 
55
  // Convert string to a Buffer
56
  const blob = new Blob([`
@@ -62,7 +51,7 @@ ${description}
62
 
63
  # Tags
64
 
65
- ${tags.map(tag => `- ${tag}\n`)}
66
 
67
  # Prompt
68
  ${prompt}
@@ -82,18 +71,18 @@ ${prompt}
82
  // TODO: now we ping the robot to come read our prompt
83
 
84
  const newVideoRequest: VideoRequest = {
85
- id: videoId,
86
  label: title,
87
  description,
88
  prompt,
89
  thumbnailUrl: "",
90
  updatedAt: new Date().toISOString(),
91
- tags: [...channel.tags],
92
  channel,
93
  }
94
 
95
  const newVideo: VideoInfo = {
96
- id: videoId,
97
  status: "submitted",
98
  label: title,
99
  description,
@@ -103,7 +92,7 @@ ${prompt}
103
  numberOfViews: 0,
104
  numberOfLikes: 0,
105
  updatedAt: new Date().toISOString(),
106
- tags: [...channel.tags],
107
  channel,
108
  }
109
 
 
1
  "use server"
2
 
3
  import { Blob } from "buffer"
 
4
 
5
  import { Credentials, uploadFile, whoAmI } from "@/huggingface/hub/src"
6
  import { ChannelInfo, VideoInfo, VideoRequest } from "@/types"
7
+ import { formatPromptFileName } from "../utils/formatPromptFileName"
8
 
9
  /**
10
  * Save the video request to the user's own dataset
11
+ *
 
 
12
  */
13
  export async function uploadVideoRequestToDataset({
14
  channel,
 
39
  throw new Error(`couldn't get the username`)
40
  }
41
 
42
+ const { id, fileName } = formatPromptFileName()
 
 
 
 
 
 
 
 
 
43
 
44
  // Convert string to a Buffer
45
  const blob = new Blob([`
 
51
 
52
  # Tags
53
 
54
+ ${tags.map(tag => `- ${tag}`).join("\n")}
55
 
56
  # Prompt
57
  ${prompt}
 
71
  // TODO: now we ping the robot to come read our prompt
72
 
73
  const newVideoRequest: VideoRequest = {
74
+ id,
75
  label: title,
76
  description,
77
  prompt,
78
  thumbnailUrl: "",
79
  updatedAt: new Date().toISOString(),
80
+ tags,
81
  channel,
82
  }
83
 
84
  const newVideo: VideoInfo = {
85
+ id,
86
  status: "submitted",
87
  label: title,
88
  description,
 
92
  numberOfViews: 0,
93
  numberOfLikes: 0,
94
  updatedAt: new Date().toISOString(),
95
+ tags,
96
  channel,
97
  }
98
 
src/app/server/actions/submitVideoRequest.ts CHANGED
@@ -33,15 +33,6 @@ export async function submitVideoRequest({
33
  tags
34
  })
35
 
36
- try {
37
- await updateQueue({ apiKey, channel })
38
-
39
- return {
40
- ...videoInfo,
41
- status: "queued"
42
- }
43
- } catch (err) {
44
- console.error(`failed to update the queue, but this can be done later :)`)
45
- return videoInfo
46
- }
47
  }
 
33
  tags
34
  })
35
 
36
+
37
+ return videoInfo
 
 
 
 
 
 
 
 
 
38
  }
src/app/server/actions/utils/formatPromptFileName.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { v4 as uuidv4 } from "uuid"
3
+
4
+ export function formatPromptFileName(id?: string): { id: string; fileName: string } {
5
+
6
+ const videoId = typeof id === "string" ? id : uuidv4()
7
+
8
+ const fileName = `prompt_${videoId}.md`
9
+
10
+ return {
11
+ id: videoId,
12
+ fileName
13
+ }
14
+ }
src/app/server/actions/utils/parseDatasetPrompt.ts CHANGED
@@ -3,17 +3,19 @@ import { ParsedDatasetPrompt } from "@/types"
3
 
4
  export function parseDatasetPrompt(markdown: string = ""): ParsedDatasetPrompt {
5
  try {
6
- const { title, description, prompt } = parseMarkdown(markdown)
7
 
8
  return {
9
  title: typeof title === "string" && title ? title : "",
10
  description: typeof description === "string" && description ? description : "",
 
11
  prompt: typeof prompt === "string" && prompt ? prompt : "",
12
  }
13
  } catch (err) {
14
  return {
15
  title: "",
16
  description: "",
 
17
  prompt: "",
18
  }
19
  }
@@ -24,9 +26,14 @@ export function parseDatasetPrompt(markdown: string = ""): ParsedDatasetPrompt {
24
  * @param markdown A Markdown string containing Description and Prompt sections
25
  * @returns A JSON object with { "description": "...", "prompt": "..." }
26
  */
27
- function parseMarkdown(markdown: string): ParsedDatasetPrompt {
 
 
 
 
 
28
  // Regular expression to find markdown sections based on the provided structure
29
- const sectionRegex = /^## (.+?)\n\n([\s\S]+?)(?=\n## |$)/gm;
30
 
31
  let match;
32
  const sections: { [key: string]: string } = {};
@@ -41,7 +48,7 @@ function parseMarkdown(markdown: string): ParsedDatasetPrompt {
41
  const result = {
42
  title: sections['title'] || '',
43
  description: sections['description'] || '',
44
- // categories: sections['categories'] || '',
45
  prompt: sections['prompt'] || '',
46
  };
47
 
 
3
 
4
  export function parseDatasetPrompt(markdown: string = ""): ParsedDatasetPrompt {
5
  try {
6
+ const { title, description, tags, prompt } = parseMarkdown(markdown)
7
 
8
  return {
9
  title: typeof title === "string" && title ? title : "",
10
  description: typeof description === "string" && description ? description : "",
11
+ tags: tags && typeof tags === "string" ? tags.split("-").map(x => x.trim()).filter(x => x) : [],
12
  prompt: typeof prompt === "string" && prompt ? prompt : "",
13
  }
14
  } catch (err) {
15
  return {
16
  title: "",
17
  description: "",
18
+ tags: [],
19
  prompt: "",
20
  }
21
  }
 
26
  * @param markdown A Markdown string containing Description and Prompt sections
27
  * @returns A JSON object with { "description": "...", "prompt": "..." }
28
  */
29
+ function parseMarkdown(markdown: string): {
30
+ title: string
31
+ description: string
32
+ tags: string
33
+ prompt: string
34
+ } {
35
  // Regular expression to find markdown sections based on the provided structure
36
+ const sectionRegex = /^#+ (.+?)\n+([\s\S]+?)(?=\n+? |$)/gm;
37
 
38
  let match;
39
  const sections: { [key: string]: string } = {};
 
48
  const result = {
49
  title: sections['title'] || '',
50
  description: sections['description'] || '',
51
+ tags: sections['tags'] || '',
52
  prompt: sections['prompt'] || '',
53
  };
54
 
src/app/server/actions/utils/parseDatasetReadme.ts CHANGED
@@ -7,8 +7,6 @@ export function parseDatasetReadme(markdown: string = ""): ParsedDatasetReadme {
7
  try {
8
  const { metadata, content } = metadataParser(markdown) as ParsedMetadataAndContent
9
 
10
- // console.log("DEBUG README:", { metadata, content })
11
-
12
  const { description, prompt } = parseMarkdown(content)
13
 
14
  return {
 
7
  try {
8
  const { metadata, content } = metadataParser(markdown) as ParsedMetadataAndContent
9
 
 
 
10
  const { description, prompt } = parseMarkdown(content)
11
 
12
  return {
src/app/server/actions/utils/parsePromptFileName.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export function parsePromptFileName(filePath: string): string {
2
+ return (filePath || "").replaceAll("prompt_", "").replaceAll(".md", "")
3
+ }
src/app/state/useStore.ts CHANGED
@@ -2,12 +2,18 @@
2
 
3
  import { create } from "zustand"
4
 
5
- import { ChannelInfo, VideoInfo, InterfaceDisplayMode, InterfaceView } from "@/types"
6
 
7
  export const useStore = create<{
8
  displayMode: InterfaceDisplayMode
9
  setDisplayMode: (displayMode: InterfaceDisplayMode) => void
10
 
 
 
 
 
 
 
11
  view: InterfaceView
12
  setView: (view?: InterfaceView) => void
13
 
@@ -40,6 +46,16 @@ export const useStore = create<{
40
  set({ view: view || "home" })
41
  },
42
 
 
 
 
 
 
 
 
 
 
 
43
  currentChannel: undefined,
44
  setCurrentChannel: (currentChannel?: ChannelInfo) => {
45
  // TODO: download videos for this new channel
 
2
 
3
  import { create } from "zustand"
4
 
5
+ import { ChannelInfo, VideoInfo, InterfaceDisplayMode, InterfaceView, InterfaceMenuMode, InterfaceHeaderMode } from "@/types"
6
 
7
  export const useStore = create<{
8
  displayMode: InterfaceDisplayMode
9
  setDisplayMode: (displayMode: InterfaceDisplayMode) => void
10
 
11
+ headerMode: InterfaceHeaderMode
12
+ setHeaderMode: (headerMode: InterfaceHeaderMode) => void
13
+
14
+ menuMode: InterfaceMenuMode
15
+ setMenuMode: (menuMode: InterfaceMenuMode) => void
16
+
17
  view: InterfaceView
18
  setView: (view?: InterfaceView) => void
19
 
 
46
  set({ view: view || "home" })
47
  },
48
 
49
+ headerMode: "normal",
50
+ setHeaderMode: (headerMode: InterfaceHeaderMode) => {
51
+ set({ headerMode })
52
+ },
53
+
54
+ menuMode: "normal_icon",
55
+ setMenuMode: (menuMode: InterfaceMenuMode) => {
56
+ set({ menuMode })
57
+ },
58
+
59
  currentChannel: undefined,
60
  setCurrentChannel: (currentChannel?: ChannelInfo) => {
61
  // TODO: download videos for this new channel
src/app/views/home-view/index.tsx CHANGED
@@ -1,10 +1,17 @@
1
- import { useEffect } from "react"
 
 
2
 
3
  import { useStore } from "@/app/state/useStore"
4
  import { cn } from "@/lib/utils"
5
  import { VideoInfo } from "@/types"
 
 
6
 
7
  export function HomeView() {
 
 
 
8
  const displayMode = useStore(s => s.displayMode)
9
  const setDisplayMode = useStore(s => s.setDisplayMode)
10
  const currentChannel = useStore(s => s.currentChannel)
@@ -17,38 +24,30 @@ export function HomeView() {
17
  const setCurrentVideo = useStore(s => s.setCurrentVideo)
18
 
19
  useEffect(() => {
 
 
 
 
 
20
 
21
- // we use fake data for now
22
- // this will be pulled from the Hugging Face API
23
- const newCategoryVideos: VideoInfo[] = []
24
- setCurrentVideos(newCategoryVideos)
25
  }, [currentTag])
26
 
 
 
 
 
 
27
  return (
28
  <div className={cn(
29
- `grid grid-cols-4`
30
  )}>
31
- {currentVideos.map((video) => (
32
- <div
33
- key={video.id}
34
- className={cn(
35
- `flex flex-col`,
36
- `w-[300px] h-[400px]`
37
- )}>
38
- <div
39
- className={cn(
40
-
41
- )}
42
- >
43
- <img src="" />
44
- </div>
45
- <div className={cn(
46
-
47
- )}>
48
- <h3>{video.label}</h3>
49
- </div>
50
- </div>
51
- ))}
52
  </div>
53
  )
54
  }
 
1
+ "use client"
2
+
3
+ import { useEffect, useTransition } from "react"
4
 
5
  import { useStore } from "@/app/state/useStore"
6
  import { cn } from "@/lib/utils"
7
  import { VideoInfo } from "@/types"
8
+ import { getVideos } from "@/app/server/actions/ai-tube-hf/getVideos"
9
+ import { VideoList } from "@/app/interface/video-list"
10
 
11
  export function HomeView() {
12
+ const [_isPending, startTransition] = useTransition()
13
+
14
+ const setView = useStore(s => s.setView)
15
  const displayMode = useStore(s => s.displayMode)
16
  const setDisplayMode = useStore(s => s.setDisplayMode)
17
  const currentChannel = useStore(s => s.currentChannel)
 
24
  const setCurrentVideo = useStore(s => s.setCurrentVideo)
25
 
26
  useEffect(() => {
27
+ startTransition(async () => {
28
+ const videos = await getVideos({
29
+ sortBy: "date",
30
+ maxVideos: 25
31
+ })
32
 
33
+ setCurrentVideos(videos)
34
+ })
 
 
35
  }, [currentTag])
36
 
37
+ const handleSelect = (video: VideoInfo) => {
38
+ setCurrentVideo(video)
39
+ setView("public_video")
40
+ }
41
+
42
  return (
43
  <div className={cn(
44
+ // `grid grid-cols-4`
45
  )}>
46
+ <VideoList
47
+ videos={currentVideos}
48
+ layout="grid"
49
+ onSelect={handleSelect}
50
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  </div>
52
  )
53
  }
src/app/views/public-channel-view/index.tsx CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import { useEffect, useTransition } from "react"
2
 
3
  import { useStore } from "@/app/state/useStore"
 
1
+ "use client"
2
+
3
  import { useEffect, useTransition } from "react"
4
 
5
  import { useStore } from "@/app/state/useStore"
src/app/views/public-channels-view/index.tsx CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import { useEffect, useState, useTransition } from "react"
2
 
3
  import { useStore } from "@/app/state/useStore"
@@ -29,9 +31,7 @@ export function PublicChannelsView() {
29
  }, [isLoaded])
30
 
31
  return (
32
- <div className={cn(
33
- `flex flex-col`
34
- )}>
35
  <ChannelList
36
  channels={currentChannels}
37
  />
 
1
+ "use client"
2
+
3
  import { useEffect, useState, useTransition } from "react"
4
 
5
  import { useStore } from "@/app/state/useStore"
 
31
  }, [isLoaded])
32
 
33
  return (
34
+ <div className={cn(`flex flex-col`)}>
 
 
35
  <ChannelList
36
  channels={currentChannels}
37
  />
src/app/views/public-video-view/index.tsx CHANGED
@@ -1,28 +1,91 @@
 
 
1
  import { useEffect } from "react"
 
2
 
3
  import { useStore } from "@/app/state/useStore"
4
  import { cn } from "@/lib/utils"
 
 
5
 
6
  export function PublicVideoView() {
7
  const displayMode = useStore(s => s.displayMode)
8
- const setDisplayMode = useStore(s => s.setDisplayMode)
9
- const currentChannel = useStore(s => s.currentChannel)
10
- const setCurrentChannel = useStore(s => s.setCurrentChannel)
11
- const currentTag = useStore(s => s.currentTag)
12
- const setCurrentTag = useStore(s => s.setCurrentTag)
13
- const currentVideos = useStore(s => s.currentVideos)
14
- const currentVideo = useStore(s => s.currentVideo)
15
- const setCurrentVideo = useStore(s => s.setCurrentVideo)
16
-
17
- useEffect(() => {
18
-
19
- }, [currentTag])
20
 
 
 
21
  return (
22
  <div className={cn(
23
- `grid grid-cols-4`
 
24
  )}>
25
- {null}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  </div>
27
  )
28
- }
 
1
+ "use client"
2
+
3
  import { useEffect } from "react"
4
+ import { RiCheckboxCircleFill } from "react-icons/ri"
5
 
6
  import { useStore } from "@/app/state/useStore"
7
  import { cn } from "@/lib/utils"
8
+ import { VideoPlayer } from "@/app/interface/video-player"
9
+
10
 
11
  export function PublicVideoView() {
12
  const displayMode = useStore(s => s.displayMode)
13
+ const video = useStore(s => s.currentVideo)
14
+ const setMenuMode = useStore(s => s.setMenuMode)
15
+ const setHeaderMode = useStore(s => s.setHeaderMode)
 
 
 
 
 
 
 
 
 
16
 
17
+ if (!video) { return null }
18
+
19
  return (
20
  <div className={cn(
21
+ `w-full`,
22
+ `flex flex-row`
23
  )}>
24
+ <div className={cn(
25
+ `flex-grow`,
26
+ `flex flex-col`,
27
+ )}>
28
+ {/** VIDEO PLAYER - HORIZONTAL */}
29
+ <VideoPlayer
30
+ video={video}
31
+ className="mb-4"
32
+ />
33
+
34
+ {/** VIDEO TITLE - HORIZONTAL */}
35
+ <div className={cn(
36
+ `text-xl text-zinc-100 font-medium mb-0 line-clamp-2`,
37
+ `mb-2`
38
+ )}>
39
+ {video?.label}
40
+ </div>
41
+
42
+ {/** VIDEO TOOLBAR - HORIZONTAL */}
43
+ <div className={cn(
44
+ `flex flex-row`,
45
+ `items-center`
46
+ )}>
47
+ {/** CHANNEL LOGO - VERTICAL */}
48
+ <div className={cn(
49
+ `flex flex-col`,
50
+ `mr-3`
51
+ )}>
52
+ <div className="flex w-10 rounded-full overflow-hidden">
53
+ <img
54
+ src="huggingface-avatar.jpeg"
55
+ />
56
+ </div>
57
+ </div>
58
+
59
+ {/** CHANNEL INFO - VERTICAL */}
60
+ <div className={cn(
61
+ `flex flex-col`
62
+ )}>
63
+ <div className={cn(
64
+ `flex flex-row items-center`,
65
+ `text-zinc-100 text-base font-medium space-x-1`,
66
+ )}>
67
+ <div>{video.channel.label}</div>
68
+ <div className="text-sm text-neutral-400"><RiCheckboxCircleFill className="" /></div>
69
+ </div>
70
+ <div className={cn(
71
+ `flex flex-row items-center`,
72
+ `text-neutral-400 text-xs font-normal space-x-1`,
73
+ )}>
74
+ <div>0 followers</div>
75
+ <div></div>
76
+ </div>
77
+ </div>
78
+
79
+ </div>
80
+
81
+ </div>
82
+ <div className={cn(
83
+ `sm:w-56 md:w-96`,
84
+ `hidden sm:flex flex-col`,
85
+ `px-4`
86
+ )}>
87
+ {/*[ TO BE CONTINUED ]*/}
88
+ </div>
89
  </div>
90
  )
91
+ }
src/app/views/user-account-view/index.tsx CHANGED
@@ -18,7 +18,7 @@ export function UserAccountView() {
18
  <div className={cn(
19
  `flex flex-col space-y-4`
20
  )}>
21
- <div className="flex flex-col space-y-2">
22
  <div className="flex flex-row space-x-2 items-center">
23
  <label className="flex w-64">Hugging Face token:</label>
24
  <Input
@@ -37,7 +37,7 @@ export function UserAccountView() {
37
  </div>
38
  {huggingfaceApiKey
39
  ? <p>You are ready to go!</p>
40
- : <p>Please setup your accountabove to get started</p>}
41
  </div>
42
  )
43
  }
 
18
  <div className={cn(
19
  `flex flex-col space-y-4`
20
  )}>
21
+ <div className="flex flex-col space-y-2 max-w-4xl">
22
  <div className="flex flex-row space-x-2 items-center">
23
  <label className="flex w-64">Hugging Face token:</label>
24
  <Input
 
37
  </div>
38
  {huggingfaceApiKey
39
  ? <p>You are ready to go!</p>
40
+ : <p>Please setup your account (see above) to get started</p>}
41
  </div>
42
  )
43
  }
src/app/views/user-channel-view/index.tsx CHANGED
@@ -1,9 +1,10 @@
 
 
1
  import { useEffect, useState, useTransition } from "react"
2
 
3
  import { useStore } from "@/app/state/useStore"
4
  import { cn } from "@/lib/utils"
5
  import { VideoInfo } from "@/types"
6
- import { VideoList } from "@/app/interface/video-list"
7
 
8
  import { useLocalStorage } from "usehooks-ts"
9
  import { localStorageKeys } from "@/app/state/locaStorageKeys"
@@ -12,6 +13,8 @@ import { Input } from "@/components/ui/input"
12
  import { Textarea } from "@/components/ui/textarea"
13
  import { Button } from "@/components/ui/button"
14
  import { submitVideoRequest } from "@/app/server/actions/submitVideoRequest"
 
 
15
 
16
  export function UserChannelView() {
17
  const [_isPending, startTransition] = useTransition()
@@ -20,8 +23,13 @@ export function UserChannelView() {
20
  defaultSettings.huggingfaceApiKey
21
  )
22
  const [titleDraft, setTitleDraft] = useState("")
 
 
23
  const [promptDraft, setPromptDraft] = useState("")
24
 
 
 
 
25
  const [isSubmitting, setIsSubmitting] = useState(false)
26
 
27
  const currentChannel = useStore(s => s.currentChannel)
@@ -29,22 +37,40 @@ export function UserChannelView() {
29
  const setCurrentVideos = useStore(s => s.setCurrentVideos)
30
  const setCurrentVideo = useStore(s => s.setCurrentVideo)
31
 
 
32
  useEffect(() => {
33
  if (!currentChannel) {
34
  return
35
  }
36
 
37
  startTransition(async () => {
38
- /*
39
- const videos = await getChannelVideos({
40
  channel: currentChannel,
41
  apiKey: huggingfaceApiKey,
 
42
  })
43
- console.log("videos:", videos)
44
- */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  })
46
 
47
- setCurrentVideos([])
48
  }, [huggingfaceApiKey, currentChannel, currentChannel?.id])
49
 
50
  const handleSubmit = () => {
@@ -64,17 +90,18 @@ export function UserChannelView() {
64
  channel: currentChannel,
65
  apiKey: huggingfaceApiKey,
66
  title: titleDraft,
67
- description: "",
68
  prompt: promptDraft,
69
- tags: [],
70
  })
71
 
72
  // in case of success we update the frontend immediately
73
  // with our draft video
74
  setCurrentVideos([newVideo, ...currentVideos])
75
  setPromptDraft("")
 
 
76
  setTitleDraft("")
77
-
78
  // also renew the cache on Next's side
79
  /*
80
  await getChannelVideos({
@@ -91,67 +118,113 @@ export function UserChannelView() {
91
  })
92
  }
93
 
 
 
 
 
 
 
 
 
 
94
  return (
95
  <div className={cn(
96
  `flex flex-col space-y-8`
97
  )}>
98
- <h2 className="text-3xl font-bold">Robot channel settings:</h2>
99
- <p>TODO</p>
100
-
101
- <h2 className="text-3xl font-bold">Schedule a new prompt:</h2>
102
-
103
- <div className="flex flex-row space-x-2 items-start">
104
- <label className="flex w-24">Title:</label>
105
- <div className="flex flex-col space-y-2 flex-grow">
106
- <Input
107
- placeholder="Title"
108
- className="font-mono"
109
- onChange={(x) => {
110
- setTitleDraft(x.target.value)
111
- }}
112
- value={titleDraft}
113
- />
114
- <p className="text-neutral-100/70">
115
- Title of the video, keep it short.
116
- </p>
117
  </div>
118
- </div>
119
 
120
- <div className="flex flex-row space-x-2 items-start">
121
- <label className="flex w-24">Prompt:</label>
122
- <div className="flex flex-col space-y-2 flex-grow">
123
- <Textarea
124
- placeholder="Prompt"
125
- className="font-mono"
126
- rows={6}
127
- onChange={(x) => {
128
- setPromptDraft(x.target.value)
129
- }}
130
- value={promptDraft}
131
- />
132
- <p className="text-neutral-100/70">
133
- Describe your video in natural language.
134
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  </div>
136
- </div>
137
 
138
- <div className="flex flex-row space-x-2 items-center justify-between">
139
- <Button
140
- onClick={handleSubmit}
141
- disabled={isSubmitting}
142
- className={cn(
143
- isSubmitting ? `opacity-50` : `opacity-100`
144
- )}
145
- >
146
- {isSubmitting ? 'Adding to the queue..' : 'Add prompt to the queue'}
147
- </Button>
148
- <p>Note: It can take a few hours for the video to be generated.</p>
149
  </div>
150
 
151
  <h2 className="text-3xl font-bold">Current video prompts:</h2>
152
 
153
- <VideoList
154
  videos={currentVideos}
 
155
  />
156
  </div>
157
  )
 
1
+ "use client"
2
+
3
  import { useEffect, useState, useTransition } from "react"
4
 
5
  import { useStore } from "@/app/state/useStore"
6
  import { cn } from "@/lib/utils"
7
  import { VideoInfo } from "@/types"
 
8
 
9
  import { useLocalStorage } from "usehooks-ts"
10
  import { localStorageKeys } from "@/app/state/locaStorageKeys"
 
13
  import { Textarea } from "@/components/ui/textarea"
14
  import { Button } from "@/components/ui/button"
15
  import { submitVideoRequest } from "@/app/server/actions/submitVideoRequest"
16
+ import { getVideoRequestsFromChannel } from "@/app/server/actions/ai-tube-hf/getVideoRequestsFromChannel"
17
+ import { PendingVideoList } from "@/app/interface/pending-video-list"
18
 
19
  export function UserChannelView() {
20
  const [_isPending, startTransition] = useTransition()
 
23
  defaultSettings.huggingfaceApiKey
24
  )
25
  const [titleDraft, setTitleDraft] = useState("")
26
+ const [descriptionDraft, setDescriptionDraft] = useState("")
27
+ const [tagsDraft, setTagsDraft] = useState("")
28
  const [promptDraft, setPromptDraft] = useState("")
29
 
30
+ // we do not include the tags in the list of required fields
31
+ const missingFields = !titleDraft || !descriptionDraft || !promptDraft
32
+
33
  const [isSubmitting, setIsSubmitting] = useState(false)
34
 
35
  const currentChannel = useStore(s => s.currentChannel)
 
37
  const setCurrentVideos = useStore(s => s.setCurrentVideos)
38
  const setCurrentVideo = useStore(s => s.setCurrentVideo)
39
 
40
+ console.log("CURRENT VIDEOS:", currentVideos)
41
  useEffect(() => {
42
  if (!currentChannel) {
43
  return
44
  }
45
 
46
  startTransition(async () => {
47
+
48
+ const videoRequests = await getVideoRequestsFromChannel({
49
  channel: currentChannel,
50
  apiKey: huggingfaceApiKey,
51
+ renewCache: true
52
  })
53
+
54
+ const videos: VideoInfo[] = Object.values(videoRequests).map(videoRequest => ({
55
+ id: videoRequest.id,
56
+ status: "submitted",
57
+ label: videoRequest.label,
58
+ description: videoRequest.description,
59
+ prompt: videoRequest.prompt,
60
+ thumbnailUrl: videoRequest.thumbnailUrl,
61
+ assetUrl: "",
62
+ numberOfViews: 0,
63
+ numberOfLikes: 0,
64
+ updatedAt: videoRequest.updatedAt,
65
+ tags: videoRequest.tags,
66
+ channel: currentChannel
67
+ }))
68
+
69
+ console.log("setCurrentVideos:", videos)
70
+
71
+ setCurrentVideos(videos)
72
  })
73
 
 
74
  }, [huggingfaceApiKey, currentChannel, currentChannel?.id])
75
 
76
  const handleSubmit = () => {
 
90
  channel: currentChannel,
91
  apiKey: huggingfaceApiKey,
92
  title: titleDraft,
93
+ description: descriptionDraft,
94
  prompt: promptDraft,
95
+ tags: tagsDraft.trim().split(",").map(x => x.trim()).filter(x => x),
96
  })
97
 
98
  // in case of success we update the frontend immediately
99
  // with our draft video
100
  setCurrentVideos([newVideo, ...currentVideos])
101
  setPromptDraft("")
102
+ setDescriptionDraft("")
103
+ setTagsDraft("")
104
  setTitleDraft("")
 
105
  // also renew the cache on Next's side
106
  /*
107
  await getChannelVideos({
 
118
  })
119
  }
120
 
121
+ const handleDelete = (video: VideoInfo) => {
122
+ // step 1: delete it from the dataset
123
+
124
+
125
+ // step 2: if the video has already been generated,
126
+ // we ask the robot to delete it from the index
127
+
128
+ }
129
+
130
  return (
131
  <div className={cn(
132
  `flex flex-col space-y-8`
133
  )}>
134
+ <div className="flex flex-col space-y-8 max-w-4xl">
135
+ <h2 className="text-3xl font-bold">Robot channel settings:</h2>
136
+ <p>TODO</p>
137
+
138
+ <h2 className="text-3xl font-bold">Create a new AI video:</h2>
139
+
140
+ <div className="flex flex-row space-x-2 items-start">
141
+ <label className="flex w-24 pt-1">Title (required):</label>
142
+ <div className="flex flex-col space-y-2 flex-grow">
143
+ <Input
144
+ placeholder="Title"
145
+ className="font-mono"
146
+ onChange={(x) => {
147
+ setTitleDraft(x.target.value)
148
+ }}
149
+ value={titleDraft}
150
+ />
151
+ </div>
 
152
  </div>
 
153
 
154
+
155
+ <div className="flex flex-row space-x-2 items-start">
156
+ <label className="flex w-24 pt-1">Description (required):</label>
157
+ <div className="flex flex-col space-y-2 flex-grow">
158
+ <Textarea
159
+ placeholder="Description"
160
+ className="font-mono"
161
+ rows={2}
162
+ onChange={(x) => {
163
+ setDescriptionDraft(x.target.value)
164
+ }}
165
+ value={descriptionDraft}
166
+ />
167
+ <p className="text-neutral-100/70">
168
+ Short description (visible to humans, and used as context by the AI).
169
+ </p>
170
+ </div>
171
+ </div>
172
+
173
+ <div className="flex flex-row space-x-2 items-start">
174
+ <label className="flex w-24 pt-1">Prompt (required):</label>
175
+ <div className="flex flex-col space-y-2 flex-grow">
176
+ <Textarea
177
+ placeholder="Prompt"
178
+ className="font-mono"
179
+ rows={6}
180
+ onChange={(x) => {
181
+ setPromptDraft(x.target.value)
182
+ }}
183
+ value={promptDraft}
184
+ />
185
+ <p className="text-neutral-100/70">
186
+ Describe your video content, in a synthetic way.
187
+ </p>
188
+ </div>
189
+ </div>
190
+
191
+ <div className="flex flex-row space-x-2 items-start">
192
+ <label className="flex w-24 pt-1">Tags (optional):</label>
193
+ <div className="flex flex-col space-y-2 flex-grow">
194
+ <Input
195
+ placeholder="Tags"
196
+ className="font-mono"
197
+ onChange={(x) => {
198
+ setTagsDraft(x.target.value)
199
+ }}
200
+ value={tagsDraft}
201
+ />
202
+ <p className="text-neutral-100/70">
203
+ Comma-separated tags (eg. &quot;Education, Sports&quot;)
204
+ </p>
205
+ </div>
206
+ </div>
207
+
208
+ <div className="flex flex-row space-x-2 items-center justify-between">
209
+ <Button
210
+ onClick={handleSubmit}
211
+ disabled={isSubmitting}
212
+ className={cn(
213
+ isSubmitting || missingFields ? `opacity-50` : `opacity-100`
214
+ )}
215
+ >
216
+ {missingFields ? 'Please fill the form' : isSubmitting ? 'Adding to the queue..' : 'Add prompt to the queue'}
217
+ </Button>
218
+ <p>Note: It can take a few hours for the video to be generated.</p>
219
  </div>
 
220
 
 
 
 
 
 
 
 
 
 
 
 
221
  </div>
222
 
223
  <h2 className="text-3xl font-bold">Current video prompts:</h2>
224
 
225
+ <PendingVideoList
226
  videos={currentVideos}
227
+ onDelete={handleDelete}
228
  />
229
  </div>
230
  )
src/app/views/user-channels-view/index.tsx CHANGED
@@ -9,14 +9,14 @@ import { getChannels } from "@/app/server/actions/ai-tube-hf/getChannels"
9
  import { ChannelList } from "@/app/interface/channel-list"
10
  import { localStorageKeys } from "@/app/state/locaStorageKeys"
11
  import { defaultSettings } from "@/app/state/defaultSettings"
 
12
 
13
  export function UserChannelsView() {
14
  const [_isPending, startTransition] = useTransition()
15
- const [huggingfaceApiKey,] = useLocalStorage<string>(
16
  localStorageKeys.huggingfaceApiKey,
17
  defaultSettings.huggingfaceApiKey
18
  )
19
-
20
  const setView = useStore(s => s.setView)
21
  const setCurrentChannel = useStore(s => s.setCurrentChannel)
22
 
@@ -41,17 +41,41 @@ export function UserChannelsView() {
41
  }, [isLoaded, huggingfaceApiKey])
42
 
43
  return (
44
- <div className={cn(
45
- `flex flex-col space-y-4`
46
- )}>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  {huggingfaceApiKey ?
48
- <ChannelList
 
 
49
  channels={currentChannels}
50
  onSelect={(channel) => {
51
  setCurrentChannel(channel)
52
  setView("user_channel")
53
  }}
54
- /> : <p>Please setup your account to get started creating robot channels!</p>}
 
55
  </div>
56
  )
57
  }
 
9
  import { ChannelList } from "@/app/interface/channel-list"
10
  import { localStorageKeys } from "@/app/state/locaStorageKeys"
11
  import { defaultSettings } from "@/app/state/defaultSettings"
12
+ import { Input } from "@/components/ui/input"
13
 
14
  export function UserChannelsView() {
15
  const [_isPending, startTransition] = useTransition()
16
+ const [huggingfaceApiKey, setHuggingfaceApiKey] = useLocalStorage<string>(
17
  localStorageKeys.huggingfaceApiKey,
18
  defaultSettings.huggingfaceApiKey
19
  )
 
20
  const setView = useStore(s => s.setView)
21
  const setCurrentChannel = useStore(s => s.setCurrentChannel)
22
 
 
41
  }, [isLoaded, huggingfaceApiKey])
42
 
43
  return (
44
+ <div className={cn(`flex flex-col space-y-4`)}>
45
+ <h2 className="text-3xl font-bold">Want your own channels? Setup your account!</h2>
46
+
47
+ <div className="flex flex-col space-y-4 max-w-2xl">
48
+ <div className="flex flex-row space-x-2 items-center">
49
+ <label className="flex w-64">Hugging Face token:</label>
50
+ <Input
51
+ placeholder="Hugging Face token (with WRITE access)"
52
+ type="password"
53
+ className="font-mono"
54
+ onChange={(x) => {
55
+ setHuggingfaceApiKey(x.target.value)
56
+ }}
57
+ value={huggingfaceApiKey}
58
+ />
59
+ </div>
60
+ <p className="text-neutral-100/70">
61
+ Note: your Hugging Face token must be a <span className="font-bold font-mono text-yellow-300">WRITE</span> access token.
62
+ </p>
63
+ {huggingfaceApiKey
64
+ ? <p className="">Nice, looks like you are ready to go!</p>
65
+ : <p>Please setup your account (see above) to get started</p>}
66
+ </div>
67
+
68
  {huggingfaceApiKey ?
69
+ <div className="flex flex-col space-y-4">
70
+ <h2 className="text-3xl font-bold">Your custom channels:</h2>
71
+ {currentChannels?.length ? <ChannelList
72
  channels={currentChannels}
73
  onSelect={(channel) => {
74
  setCurrentChannel(channel)
75
  setView("user_channel")
76
  }}
77
+ /> : <p>Ask <span className="font-mono">@jbilcke-hf</span> for help to create a channel!</p>}
78
+ </div> : null}
79
  </div>
80
  )
81
  }
src/components/ui/table.tsx CHANGED
@@ -6,7 +6,7 @@ const Table = React.forwardRef<
6
  HTMLTableElement,
7
  React.HTMLAttributes<HTMLTableElement>
8
  >(({ className, ...props }, ref) => (
9
- <div className="w-full overflow-auto">
10
  <table
11
  ref={ref}
12
  className={cn("w-full caption-bottom text-sm", className)}
@@ -42,7 +42,10 @@ const TableFooter = React.forwardRef<
42
  >(({ className, ...props }, ref) => (
43
  <tfoot
44
  ref={ref}
45
- className={cn("bg-neutral-900 font-medium text-neutral-50 dark:bg-neutral-50 dark:text-neutral-900", className)}
 
 
 
46
  {...props}
47
  />
48
  ))
@@ -55,7 +58,7 @@ const TableRow = React.forwardRef<
55
  <tr
56
  ref={ref}
57
  className={cn(
58
- "border-b transition-colors hover:bg-neutral-100/50 data-[state=selected]:bg-neutral-100 dark:hover:bg-neutral-800/50 dark:data-[state=selected]:bg-neutral-800",
59
  className
60
  )}
61
  {...props}
@@ -70,7 +73,7 @@ const TableHead = React.forwardRef<
70
  <th
71
  ref={ref}
72
  className={cn(
73
- "h-12 px-4 text-left align-middle font-medium text-neutral-500 [&:has([role=checkbox])]:pr-0 dark:text-neutral-400",
74
  className
75
  )}
76
  {...props}
@@ -96,7 +99,7 @@ const TableCaption = React.forwardRef<
96
  >(({ className, ...props }, ref) => (
97
  <caption
98
  ref={ref}
99
- className={cn("mt-4 text-sm text-neutral-500 dark:text-neutral-400", className)}
100
  {...props}
101
  />
102
  ))
 
6
  HTMLTableElement,
7
  React.HTMLAttributes<HTMLTableElement>
8
  >(({ className, ...props }, ref) => (
9
+ <div className="relative w-full overflow-auto">
10
  <table
11
  ref={ref}
12
  className={cn("w-full caption-bottom text-sm", className)}
 
42
  >(({ className, ...props }, ref) => (
43
  <tfoot
44
  ref={ref}
45
+ className={cn(
46
+ "border-t bg-stone-100/50 font-medium [&>tr]:last:border-b-0 dark:bg-stone-800/50",
47
+ className
48
+ )}
49
  {...props}
50
  />
51
  ))
 
58
  <tr
59
  ref={ref}
60
  className={cn(
61
+ "border-b transition-colors hover:bg-stone-100/50 data-[state=selected]:bg-stone-100 dark:hover:bg-stone-800/50 dark:data-[state=selected]:bg-stone-800",
62
  className
63
  )}
64
  {...props}
 
73
  <th
74
  ref={ref}
75
  className={cn(
76
+ "h-12 px-4 text-left align-middle font-medium text-stone-500 [&:has([role=checkbox])]:pr-0 dark:text-stone-400",
77
  className
78
  )}
79
  {...props}
 
99
  >(({ className, ...props }, ref) => (
100
  <caption
101
  ref={ref}
102
+ className={cn("mt-4 text-sm text-stone-500 dark:text-stone-400", className)}
103
  {...props}
104
  />
105
  ))
src/huggingface/hub/src/lib/list-datasets.ts CHANGED
@@ -48,6 +48,7 @@ export async function* listDatasets(params?: {
48
  accept: "application/json",
49
  ...(params?.credentials ? { Authorization: `Bearer ${params.credentials.accessToken}` } : undefined),
50
  },
 
51
  });
52
 
53
  if (!res.ok) {
 
48
  accept: "application/json",
49
  ...(params?.credentials ? { Authorization: `Bearer ${params.credentials.accessToken}` } : undefined),
50
  },
51
+ ...params?.requestInit,
52
  });
53
 
54
  if (!res.ok) {
src/lib/fonts.ts DELETED
@@ -1,29 +0,0 @@
1
- import { Ubuntu } from "next/font/google"
2
- import localFont from "next/font/local"
3
-
4
- export const actionman = localFont({
5
- src: "../fonts/Action-Man/Action-Man.woff2",
6
- variable: "--font-action-man"
7
- })
8
-
9
- // https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
10
- // If loading a variable font, you don"t need to specify the font weight
11
- export const fonts = {
12
- actionman,
13
- // ubuntu: Ubuntu
14
- }
15
-
16
- // https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
17
- // If loading a variable font, you don"t need to specify the font weight
18
- export const fontList = Object.keys(fonts)
19
-
20
- export type FontName = keyof typeof fonts
21
-
22
- export const defaultFont = "actionman" as FontName
23
-
24
- export const classNames = Object.values(fonts).map(font => font.className)
25
-
26
- export const className = classNames.join(" ")
27
-
28
- export type FontClass =
29
- | "font-actionman"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/formatDuration.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { intervalToDuration } from 'date-fns'
3
+
4
+ export function formatDuration(seconds: number) {
5
+ const duration = intervalToDuration({ start: 0, end: seconds * 1000 })
6
+
7
+ const zeroPad = (num: any) => String(num).padStart(2, '0')
8
+
9
+ const formatted = `${zeroPad(duration.minutes)}:${zeroPad(duration.seconds)}`
10
+
11
+ return formatted
12
+ }
src/lib/formatTimeAgo.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { formatDistance } from 'date-fns'
2
+
3
+ export function formatTimeAgo(time: string) {
4
+ return formatDistance(new Date(time), new Date(), { addSuffix: true })
5
+ }
src/types.ts CHANGED
@@ -275,6 +275,7 @@ export type VideoRequest = {
275
  channel: ChannelInfo
276
  }
277
 
 
278
  export type VideoInfo = {
279
  /**
280
  * UUID (v4)
@@ -345,6 +346,16 @@ export type InterfaceDisplayMode =
345
  | "desktop"
346
  | "tv"
347
 
 
 
 
 
 
 
 
 
 
 
348
  export type InterfaceView =
349
  | "home"
350
  | "user_channels"
@@ -380,6 +391,7 @@ export type ParsedMetadataAndContent = {
380
  export type ParsedDatasetPrompt = {
381
  title: string
382
  description: string
 
383
  prompt: string
384
  }
385
 
 
275
  channel: ChannelInfo
276
  }
277
 
278
+
279
  export type VideoInfo = {
280
  /**
281
  * UUID (v4)
 
346
  | "desktop"
347
  | "tv"
348
 
349
+ export type InterfaceHeaderMode =
350
+ | "normal"
351
+ | "compact"
352
+
353
+ export type InterfaceMenuMode =
354
+ | "slider_hidden"
355
+ | "slider_text"
356
+ | "normal_icon"
357
+ | "normal_text"
358
+
359
  export type InterfaceView =
360
  | "home"
361
  | "user_channels"
 
391
  export type ParsedDatasetPrompt = {
392
  title: string
393
  description: string
394
+ tags: string[]
395
  prompt: string
396
  }
397
 
tailwind.config.js CHANGED
@@ -17,8 +17,8 @@ module.exports = {
17
  },
18
  },
19
  extend: {
20
- fontFamily: {
21
- actionman: ['var(--font-action-man)'],
22
  },
23
  fontSize: {
24
  "7xs": "5px",
@@ -27,7 +27,7 @@ module.exports = {
27
  "5xs": "8px",
28
  "4xs": "9px",
29
  "3xs": "10px",
30
- "2xs": "11px"
31
  },
32
  keyframes: {
33
  "accordion-down": {
 
17
  },
18
  },
19
  extend: {
20
+ backgroundImage: {
21
+ 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
22
  },
23
  fontSize: {
24
  "7xs": "5px",
 
27
  "5xs": "8px",
28
  "4xs": "9px",
29
  "3xs": "10px",
30
+ "2xs": "11px",
31
  },
32
  keyframes: {
33
  "accordion-down": {