jbilcke-hf HF staff commited on
Commit
d160b97
1 Parent(s): 5f6a9eb

improve mobile support

Browse files
TODO.md CHANGED
@@ -1,6 +1,6 @@
1
 
2
- Allow browsing some loras
3
 
4
- Funny use cases to try:
5
- - Hugging Face
6
- - Zelda 64 (will be a bit more tricky since it uses some custom Replicate stuff)
 
 
1
 
 
2
 
3
+ ## Video quality improvements
4
+
5
+ - Add a "style prompt" to do it like the AI Comic Factory
6
+ - Make it easier to pick a LoRA
src/app/config.ts CHANGED
@@ -1,3 +1,7 @@
1
  export const showBetaFeatures = `${
2
  process.env.NEXT_PUBLIC_SHOW_BETA_FEATURES || ""
3
  }`.trim().toLowerCase() === "true"
 
 
 
 
 
1
  export const showBetaFeatures = `${
2
  process.env.NEXT_PUBLIC_SHOW_BETA_FEATURES || ""
3
  }`.trim().toLowerCase() === "true"
4
+
5
+
6
+ export const defaultVideoModel = "SVD"
7
+ export const defaultVoice = "Julian"
src/app/interface/action-button/index.tsx CHANGED
@@ -1,7 +1,15 @@
1
  import { ReactNode } from "react"
2
 
3
  import { cn } from "@/lib/utils"
4
- import Link from "next/link"
 
 
 
 
 
 
 
 
5
 
6
  export function ActionButton({
7
  className,
@@ -14,12 +22,7 @@ export function ActionButton({
14
  }) {
15
 
16
  const classNames = cn(
17
- `flex flex-row space-x-2 pl-3 pr-4 h-9`,
18
- `items-center justify-center text-center`,
19
- `rounded-2xl`,
20
- `cursor-pointer`,
21
- `text-sm font-medium`,
22
- `bg-neutral-700/50 hover:bg-neutral-700/90 text-zinc-100`,
23
  className,
24
  )
25
 
 
1
  import { ReactNode } from "react"
2
 
3
  import { cn } from "@/lib/utils"
4
+
5
+ export const actionButtonClassName = cn(
6
+ `flex flex-row space-x-1.5 lg:space-x-2 pl-2 lg:pl-3 pr-3 lg:pr-4 h-8 lg:h-9`,
7
+ `items-center justify-center text-center`,
8
+ `rounded-2xl`,
9
+ `cursor-pointer`,
10
+ `text-xs lg:text-sm font-medium`,
11
+ `bg-neutral-700/50 hover:bg-neutral-700/90 text-zinc-100`,
12
+ )
13
 
14
  export function ActionButton({
15
  className,
 
22
  }) {
23
 
24
  const classNames = cn(
25
+ actionButtonClassName,
 
 
 
 
 
26
  className,
27
  )
28
 
src/app/interface/channel-card/index.tsx CHANGED
@@ -1,10 +1,12 @@
1
  import { useState } from "react"
2
  import dynamic from "next/dynamic"
3
 
 
 
 
4
  import { cn } from "@/lib/utils"
5
  import { ChannelInfo } from "@/types"
6
  import { isCertifiedUser } from "@/app/certification"
7
- import { RiCheckboxCircleFill } from "react-icons/ri"
8
 
9
  const DefaultAvatar = dynamic(() => import("../default-avatar"), {
10
  loading: () => null,
@@ -31,6 +33,8 @@ export function ChannelCard({
31
  }
32
  }
33
 
 
 
34
  return (
35
  <div
36
  className={cn(
@@ -39,8 +43,8 @@ export function ChannelCard({
39
  `space-y-1`,
40
  `w-52 h-52`,
41
  `rounded-lg`,
42
- `hover:bg-neutral-800/30`,
43
- `text-neutral-100/80 hover:text-neutral-100/100`,
44
  `cursor-pointer`,
45
  className,
46
  )}
@@ -57,8 +61,17 @@ export function ChannelCard({
57
  `w-26 h-26`
58
  )}
59
  >
60
- {channelThumbnail ?
61
- <img
 
 
 
 
 
 
 
 
 
62
  src={channelThumbnail}
63
  onError={handleBadChannelThumbnail}
64
  />
@@ -76,22 +89,27 @@ export function ChannelCard({
76
  `items-center justify-center text-center`,
77
  `space-y-1`
78
  )}>
79
- <div className="text-center text-base font-medium text-zinc-100">{channel.label}</div>
 
 
 
 
 
80
  {/*<div className="text-center text-sm font-semibold">
81
  by <a href={
82
  `https://huggingface.co/${channel.datasetUser}`
83
  } target="_blank">@{channel.datasetUser}</a>
84
  </div>
85
  */}
86
- <div className="flex flex-row items-center space-x-0.5">
87
  <div className="flex flex-row items-center text-center text-xs font-medium">@{channel.datasetUser}</div>
88
  {isCertifiedUser(channel.datasetUser) ? <div className="text-xs text-neutral-400"><RiCheckboxCircleFill className="" /></div> : null}
89
- </div>
90
- <div className="flex flex-row items-center justify-center text-neutral-400">
91
  <div className="text-center text-xs">{0} videos</div>
92
  <div className="px-1">-</div>
93
  <div className="text-center text-xs">{channel.likes} likes</div>
94
- </div>
95
  </div>
96
  </div>
97
  )
 
1
  import { useState } from "react"
2
  import dynamic from "next/dynamic"
3
 
4
+ import { RiCheckboxCircleFill } from "react-icons/ri"
5
+ import { IoAdd } from "react-icons/io5"
6
+
7
  import { cn } from "@/lib/utils"
8
  import { ChannelInfo } from "@/types"
9
  import { isCertifiedUser } from "@/app/certification"
 
10
 
11
  const DefaultAvatar = dynamic(() => import("../default-avatar"), {
12
  loading: () => null,
 
33
  }
34
  }
35
 
36
+ const isCreateButton = !channel.id
37
+
38
  return (
39
  <div
40
  className={cn(
 
43
  `space-y-1`,
44
  `w-52 h-52`,
45
  `rounded-lg`,
46
+ `text-neutral-100/80`,
47
+ isCreateButton ? '' : `hover:bg-neutral-800/30 hover:text-neutral-100/100`,
48
  `cursor-pointer`,
49
  className,
50
  )}
 
61
  `w-26 h-26`
62
  )}
63
  >
64
+ {isCreateButton
65
+ ? <div className={cn(
66
+ `flex flex-col justify-center items-center text-center`,
67
+ `w-full h-full rounded-full`,
68
+ `bg-neutral-700 hover:bg-neutral-600`,
69
+ `border-2 border-neutral-400 hover:border-neutral-300`
70
+ )}>
71
+ <IoAdd className="w-8 h-8" />
72
+ </div>
73
+ : channelThumbnail
74
+ ? <img
75
  src={channelThumbnail}
76
  onError={handleBadChannelThumbnail}
77
  />
 
89
  `items-center justify-center text-center`,
90
  `space-y-1`
91
  )}>
92
+ <div className={cn(
93
+ `text-center text-base font-medium text-zinc-100`,
94
+ isCreateButton ? 'mt-2' : ''
95
+ )}>{
96
+ isCreateButton ? "Create a channel" : channel.label
97
+ }</div>
98
  {/*<div className="text-center text-sm font-semibold">
99
  by <a href={
100
  `https://huggingface.co/${channel.datasetUser}`
101
  } target="_blank">@{channel.datasetUser}</a>
102
  </div>
103
  */}
104
+ {!isCreateButton && <div className="flex flex-row items-center space-x-0.5">
105
  <div className="flex flex-row items-center text-center text-xs font-medium">@{channel.datasetUser}</div>
106
  {isCertifiedUser(channel.datasetUser) ? <div className="text-xs text-neutral-400"><RiCheckboxCircleFill className="" /></div> : null}
107
+ </div>}
108
+ {!isCreateButton && <div className="flex flex-row items-center justify-center text-neutral-400">
109
  <div className="text-center text-xs">{0} videos</div>
110
  <div className="px-1">-</div>
111
  <div className="text-center text-xs">{channel.likes} likes</div>
112
+ </div>}
113
  </div>
114
  </div>
115
  )
src/app/interface/left-menu/index.tsx CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import { GrChannel } from "react-icons/gr"
2
  import { MdVideoLibrary } from "react-icons/md"
3
  import { RiHome8Line } from "react-icons/ri"
@@ -6,17 +8,17 @@ import { CgProfile } from "react-icons/cg"
6
 
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
- import Link from "next/link"
12
 
 
13
 
14
  export function LeftMenu() {
15
  const view = useStore(s => s.view)
16
 
17
  return (
18
  <div className={cn(
19
- `flex flex-col`,
 
20
  `w-24 px-1 pt-4`,
21
  `justify-between`
22
  // `bg-orange-500`,
 
1
+ import Link from "next/link"
2
+
3
  import { GrChannel } from "react-icons/gr"
4
  import { MdVideoLibrary } from "react-icons/md"
5
  import { RiHome8Line } from "react-icons/ri"
 
8
 
9
  import { useStore } from "@/app/state/useStore"
10
  import { cn } from "@/lib/utils"
 
11
  import { showBetaFeatures } from "@/app/config"
 
12
 
13
+ import { MenuItem } from "./menu-item"
14
 
15
  export function LeftMenu() {
16
  const view = useStore(s => s.view)
17
 
18
  return (
19
  <div className={cn(
20
+ `hidden sm:flex`,
21
+ `flex-col`,
22
  `w-24 px-1 pt-4`,
23
  `justify-between`
24
  // `bg-orange-500`,
src/app/interface/left-menu/menu-item/index.tsx CHANGED
@@ -22,7 +22,7 @@ export function MenuItem({
22
  `items-center justify-center justify-items-stretch`,
23
  // `bg-green-500`,
24
  `cursor-pointer`,
25
- `w-full h-21`,
26
  `p-1`,
27
  `group`
28
  )}
 
22
  `items-center justify-center justify-items-stretch`,
23
  // `bg-green-500`,
24
  `cursor-pointer`,
25
+ `w-20 h-18 sm:w-full sm:h-21`,
26
  `p-1`,
27
  `group`
28
  )}
src/app/interface/mobile-bottom-menu/index.tsx ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from "next/link"
2
+
3
+ import { GrChannel } from "react-icons/gr"
4
+ import { MdVideoLibrary } from "react-icons/md"
5
+ import { RiHome8Line } from "react-icons/ri"
6
+ import { PiRobot } from "react-icons/pi"
7
+ import { CgProfile } from "react-icons/cg"
8
+
9
+ import { useStore } from "@/app/state/useStore"
10
+ import { cn } from "@/lib/utils"
11
+ import { showBetaFeatures } from "@/app/config"
12
+
13
+ import { MenuItem } from "../left-menu/menu-item"
14
+
15
+ export function MobileBottomMenu() {
16
+ const view = useStore(s => s.view)
17
+
18
+ return (
19
+ <div className={cn(
20
+ `flex sm:hidden`,
21
+ `flex-row`,
22
+ `w-full`,
23
+ `justify-between`
24
+ )}>
25
+ <Link href={{
26
+ pathname: '/',
27
+ query: { v: undefined },
28
+ }}>
29
+ <MenuItem
30
+ icon={<RiHome8Line className="h-6 w-6" />}
31
+ selected={view === "home"}
32
+ >
33
+ Discover
34
+ </MenuItem>
35
+ </Link>
36
+ <Link href="/channels">
37
+ <MenuItem
38
+ icon={<GrChannel className="h-5 w-5" />}
39
+ selected={view === "public_channels"}
40
+ >
41
+ Channels
42
+ </MenuItem>
43
+ </Link>
44
+ <Link href="/account">
45
+ <MenuItem
46
+ icon={<CgProfile className="h-6 w-6" />}
47
+ selected={view === "user_account" || view === "user_channel"}
48
+ >
49
+ Account
50
+ </MenuItem>
51
+ </Link>
52
+ </div>
53
+ )
54
+ }
src/app/interface/top-header/index.tsx CHANGED
@@ -98,10 +98,13 @@ export function TopHeader() {
98
  </div>
99
  </div>
100
  <div className={cn(
101
- `transition-all duration-200 ease-in-out`,
102
- `flex flex-col items-center justify-center`,
103
- `px-4 py-2 w-max-64`,
104
- `text-neutral-400 text-sm italic`
 
 
 
105
  )}>
106
  All the videos are generated using AI, for research purposes only. Some models might produce factually incorrect or biased outputs.
107
  </div>
 
98
  </div>
99
  </div>
100
  <div className={cn(
101
+ // TODO: show the disclaimer on mobile too, maybe with a modal or something
102
+ `hidden sm:flex`,
103
+ `flex-col`,
104
+ `items-center justify-center`,
105
+ `transition-all duration-200 ease-in-out`,
106
+ `px-4 py-2 w-max-64`,
107
+ `text-neutral-400 text-2xs sm:text-xs lg:text-sm italic`
108
  )}>
109
  All the videos are generated using AI, for research purposes only. Some models might produce factually incorrect or biased outputs.
110
  </div>
src/app/interface/tube-layout/index.tsx CHANGED
@@ -5,8 +5,10 @@ import { cn } from "@/lib/utils"
5
  import { useStore } from "@/app/state/useStore"
6
 
7
  import { LeftMenu } from "../left-menu"
 
8
  import { TopHeader } from "../top-header"
9
 
 
10
  export function TubeLayout({ children }: { children?: ReactNode }) {
11
  const headerMode = useStore(s => s.headerMode)
12
  const view = useStore(s => s.view)
@@ -21,7 +23,7 @@ export function TubeLayout({ children }: { children?: ReactNode }) {
21
  <LeftMenu />
22
  <div className={cn(
23
  `flex flex-col`,
24
- `w-[calc(100vw-96px)]`,
25
  `px-2`
26
  )}>
27
  <TopHeader />
@@ -33,6 +35,7 @@ export function TubeLayout({ children }: { children?: ReactNode }) {
33
  )}>
34
  {children}
35
  </main>
 
36
  </div>
37
  </div>
38
  )
 
5
  import { useStore } from "@/app/state/useStore"
6
 
7
  import { LeftMenu } from "../left-menu"
8
+ import { MobileBottomMenu } from "../mobile-bottom-menu"
9
  import { TopHeader } from "../top-header"
10
 
11
+
12
  export function TubeLayout({ children }: { children?: ReactNode }) {
13
  const headerMode = useStore(s => s.headerMode)
14
  const view = useStore(s => s.view)
 
23
  <LeftMenu />
24
  <div className={cn(
25
  `flex flex-col`,
26
+ `w-full sm:w-[calc(100vw-96px)]`,
27
  `px-2`
28
  )}>
29
  <TopHeader />
 
35
  )}>
36
  {children}
37
  </main>
38
+ <MobileBottomMenu />
39
  </div>
40
  </div>
41
  )
src/app/interface/video-card/index.tsx CHANGED
@@ -118,7 +118,7 @@ export function VideoCard({
118
  {/* TEXT BLOCK */}
119
  <div className={cn(
120
  `flex flex-row`,
121
- isCompact ? `w-51` : `space-x-4`,
122
  )}>
123
  {
124
  isCompact ? null
@@ -143,12 +143,12 @@ export function VideoCard({
143
  )}>
144
  <h3 className={cn(
145
  `text-zinc-100 font-medium mb-0 line-clamp-2`,
146
- isCompact ? `text-sm mb-1.5` : `text-base`
147
  )}>{video.label}</h3>
148
  <div className={cn(
149
  `flex flex-row items-center`,
150
  `text-neutral-400 font-normal space-x-1`,
151
- isCompact ? `text-xs` : `text-sm`
152
  )}>
153
  <div>{video.channel.label}</div>
154
  {isCertifiedUser(video.channel.datasetUser) ? <div><RiCheckboxCircleFill className="" /></div> : null}
@@ -157,7 +157,7 @@ export function VideoCard({
157
  <div className={cn(
158
  `flex flex-row`,
159
  `text-neutral-400 font-normal`,
160
- isCompact ? `text-xs` : `text-sm`,
161
  `space-x-1`
162
  )}>
163
  <div>0 views</div>
 
118
  {/* TEXT BLOCK */}
119
  <div className={cn(
120
  `flex flex-row`,
121
+ isCompact ? `w-40 lg:w-44 xl:w-51` : `space-x-4`,
122
  )}>
123
  {
124
  isCompact ? null
 
143
  )}>
144
  <h3 className={cn(
145
  `text-zinc-100 font-medium mb-0 line-clamp-2`,
146
+ isCompact ? `text-2xs md:text-xs lg:text-sm mb-1.5` : `text-base`
147
  )}>{video.label}</h3>
148
  <div className={cn(
149
  `flex flex-row items-center`,
150
  `text-neutral-400 font-normal space-x-1`,
151
+ isCompact ? `text-3xs md:text-2xs lg:text-xs` : `text-sm`
152
  )}>
153
  <div>{video.channel.label}</div>
154
  {isCertifiedUser(video.channel.datasetUser) ? <div><RiCheckboxCircleFill className="" /></div> : null}
 
157
  <div className={cn(
158
  `flex flex-row`,
159
  `text-neutral-400 font-normal`,
160
+ isCompact ? `text-2xs lg:text-xs` : `text-sm`,
161
  `space-x-1`
162
  )}>
163
  <div>0 views</div>
src/app/interface/video-list/index.tsx CHANGED
@@ -29,7 +29,7 @@ export function VideoList({
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
  : layout === "vertical"
34
  ? `grid grid-cols-1 gap-2`
35
  : `flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4`,
 
29
  <div
30
  className={cn(
31
  layout === "grid"
32
+ ? `grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4`
33
  : layout === "vertical"
34
  ? `grid grid-cols-1 gap-2`
35
  : `flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4`,
src/app/views/public-video-view/index.tsx CHANGED
@@ -13,7 +13,7 @@ import { useStore } from "@/app/state/useStore"
13
  import { cn } from "@/lib/utils"
14
  import { VideoPlayer } from "@/app/interface/video-player"
15
  import { VideoInfo } from "@/types"
16
- import { ActionButton } from "@/app/interface/action-button"
17
  import { RecommendedVideos } from "@/app/interface/recommended-videos"
18
  import { isCertifiedUser } from "@/app/certification"
19
 
@@ -78,6 +78,8 @@ export function PublicVideoView() {
78
  <div className={cn(
79
  `flex-grow`,
80
  `flex flex-col`,
 
 
81
  )}>
82
  {/** VIDEO PLAYER - HORIZONTAL */}
83
  <VideoPlayer
@@ -88,8 +90,9 @@ export function PublicVideoView() {
88
  {/** VIDEO TITLE - HORIZONTAL */}
89
  <div className={cn(
90
  `flex flew-row space-x-2`,
91
- `text-xl text-zinc-100 font-medium mb-0 line-clamp-2`,
92
- `mb-2`
 
93
  )}>
94
  <div className="">{video.label}</div>
95
  {/*
@@ -108,21 +111,26 @@ export function PublicVideoView() {
108
 
109
  {/** VIDEO TOOLBAR - HORIZONTAL */}
110
  <div className={cn(
111
- `flex flex-row`,
112
- `items-center`,
 
113
  `justify-between`,
114
- `mb-4`
115
  )}>
116
- {/** LEFT PART FO THE TOOLBARR */}
117
  <div className={cn(
118
  `flex flex-row`,
119
  `items-center`
120
  )}>
121
  {/** CHANNEL LOGO - VERTICAL */}
122
- <div className={cn(
123
- `flex flex-col`,
124
- `mr-3`
125
- )}>
 
 
 
 
126
  <div className="flex w-10 rounded-full overflow-hidden">
127
  {
128
  channelThumbnail ? <div className="flex flex-col">
@@ -141,15 +149,20 @@ export function PublicVideoView() {
141
  roundShape
142
  />}
143
  </div>
144
- </div>
145
 
146
  {/** CHANNEL INFO - VERTICAL */}
147
- <div className={cn(
148
- `flex flex-col`
149
- )}>
 
 
 
 
150
  <div className={cn(
151
  `flex flex-row items-center`,
152
- `text-zinc-100 text-base font-medium space-x-1`,
 
153
  )}>
154
  <div>{video.channel.label}</div>
155
  {isCertifiedUser(video.channel.datasetUser) ? <div className="text-sm text-neutral-400"><RiCheckboxCircleFill className="" /></div> : null}
@@ -161,10 +174,12 @@ export function PublicVideoView() {
161
  <div>0 followers</div>
162
  <div></div>
163
  </div>
164
- </div>
 
 
165
  </div>
166
 
167
- {/** RIGHT PART FO THE TOOLBAR */}
168
  <div className={cn(
169
  `flex flex-row`,
170
  `items-center`,
@@ -178,14 +193,7 @@ export function PublicVideoView() {
178
  <CopyToClipboard
179
  text={`https://huggingface.co/spaces/jbilcke-hf/ai-tube?v=${video.id}`}
180
  onCopy={() => setCopied(true)}>
181
- <div className={cn(
182
- `flex flex-row space-x-2 pl-3 pr-4 h-9`,
183
- `items-center justify-center text-center`,
184
- `rounded-2xl`,
185
- `cursor-pointer`,
186
- `text-sm font-medium`,
187
- `bg-neutral-700/50 hover:bg-neutral-700/90 text-zinc-100`
188
- )}>
189
  <div className="flex items-center justify-center">
190
  {
191
  copied ? <LuCopyCheck className="w-4 h-4" />
@@ -234,17 +242,19 @@ export function PublicVideoView() {
234
  {/** VIDEO DESCRIPTION - VERTICAL */}
235
  <div className={cn(
236
  `flex flex-col p-3`,
 
237
  `rounded-xl`,
238
  `bg-neutral-700/50`,
239
- `text-sm`
240
  )}>
241
  <p>{video.description}</p>
242
  </div>
243
  </div>
244
  <div className={cn(
245
- `sm:w-56 md:w-[450px]`,
 
246
  `hidden sm:flex flex-col`,
247
- `pl-5 pr-8`,
248
  )}>
249
  <RecommendedVideos video={video} />
250
  </div>
 
13
  import { cn } from "@/lib/utils"
14
  import { VideoPlayer } from "@/app/interface/video-player"
15
  import { VideoInfo } from "@/types"
16
+ import { ActionButton, actionButtonClassName } from "@/app/interface/action-button"
17
  import { RecommendedVideos } from "@/app/interface/recommended-videos"
18
  import { isCertifiedUser } from "@/app/certification"
19
 
 
78
  <div className={cn(
79
  `flex-grow`,
80
  `flex flex-col`,
81
+ `transition-all duration-200 ease-in-out`,
82
+ `px-2 sm:px-0`
83
  )}>
84
  {/** VIDEO PLAYER - HORIZONTAL */}
85
  <VideoPlayer
 
90
  {/** VIDEO TITLE - HORIZONTAL */}
91
  <div className={cn(
92
  `flex flew-row space-x-2`,
93
+ `transition-all duration-200 ease-in-out`,
94
+ `text-lg lg:text-xl text-zinc-100 font-medium mb-0 line-clamp-2`,
95
+ `mb-2`,
96
  )}>
97
  <div className="">{video.label}</div>
98
  {/*
 
111
 
112
  {/** VIDEO TOOLBAR - HORIZONTAL */}
113
  <div className={cn(
114
+ `flex flex-col space-y-3 xl:space-y-0 xl:flex-row`,
115
+ `transition-all duration-200 ease-in-out`,
116
+ `items-start xl:items-center`,
117
  `justify-between`,
118
+ `mb-4`,
119
  )}>
120
+ {/** LEFT PART OF THE TOOLBAR */}
121
  <div className={cn(
122
  `flex flex-row`,
123
  `items-center`
124
  )}>
125
  {/** CHANNEL LOGO - VERTICAL */}
126
+ <a
127
+ className={cn(
128
+ `flex flex-col`,
129
+ `mr-3`,
130
+ `cursor-pointer`
131
+ )}
132
+ href={`https://huggingface.co/datasets/${video.channel.datasetUser}/${video.channel.datasetName}`}
133
+ target="_blank">
134
  <div className="flex w-10 rounded-full overflow-hidden">
135
  {
136
  channelThumbnail ? <div className="flex flex-col">
 
149
  roundShape
150
  />}
151
  </div>
152
+ </a>
153
 
154
  {/** CHANNEL INFO - VERTICAL */}
155
+ <a className={cn(
156
+ `flex flex-row sm:flex-col`,
157
+ `transition-all duration-200 ease-in-out`,
158
+ `cursor-pointer`,
159
+ )}
160
+ href={`https://huggingface.co/datasets/${video.channel.datasetUser}/${video.channel.datasetName}`}
161
+ target="_blank">
162
  <div className={cn(
163
  `flex flex-row items-center`,
164
+ `transition-all duration-200 ease-in-out`,
165
+ `text-zinc-100 text-sm lg:text-base font-medium space-x-1`,
166
  )}>
167
  <div>{video.channel.label}</div>
168
  {isCertifiedUser(video.channel.datasetUser) ? <div className="text-sm text-neutral-400"><RiCheckboxCircleFill className="" /></div> : null}
 
174
  <div>0 followers</div>
175
  <div></div>
176
  </div>
177
+ </a>
178
+
179
+
180
  </div>
181
 
182
+ {/** RIGHT PART OF THE TOOLBAR */}
183
  <div className={cn(
184
  `flex flex-row`,
185
  `items-center`,
 
193
  <CopyToClipboard
194
  text={`https://huggingface.co/spaces/jbilcke-hf/ai-tube?v=${video.id}`}
195
  onCopy={() => setCopied(true)}>
196
+ <div className={actionButtonClassName}>
 
 
 
 
 
 
 
197
  <div className="flex items-center justify-center">
198
  {
199
  copied ? <LuCopyCheck className="w-4 h-4" />
 
242
  {/** VIDEO DESCRIPTION - VERTICAL */}
243
  <div className={cn(
244
  `flex flex-col p-3`,
245
+ `transition-all duration-200 ease-in-out`,
246
  `rounded-xl`,
247
  `bg-neutral-700/50`,
248
+ `text-sm`,
249
  )}>
250
  <p>{video.description}</p>
251
  </div>
252
  </div>
253
  <div className={cn(
254
+ `w-40 sm:w-56 md:w-64 lg:w-72 xl:w-[450px]`,
255
+ `transition-all duration-200 ease-in-out`,
256
  `hidden sm:flex flex-col`,
257
+ `pl-5 pr-1 sm:pr-2 md:pr-3 lg:pr-4 xl:pr-6 2xl:pr-8`,
258
  )}>
259
  <RecommendedVideos video={video} />
260
  </div>
src/app/views/user-account-view/index.tsx CHANGED
@@ -10,6 +10,7 @@ import { ChannelList } from "@/app/interface/channel-list"
10
  import { localStorageKeys } from "@/app/state/localStorageKeys"
11
  import { defaultSettings } from "@/app/state/defaultSettings"
12
  import { Input } from "@/components/ui/input"
 
13
 
14
  export function UserAccountView() {
15
  const [_isPending, startTransition] = useTransition()
@@ -73,13 +74,22 @@ export function UserAccountView() {
73
  <h2 className="text-3xl font-bold">Your custom channels:</h2>
74
  {userChannels?.length ? <ChannelList
75
  layout="grid"
76
- channels={userChannels}
 
 
 
 
 
77
  onSelect={(userChannel) => {
78
- setUserChannel(userChannel)
 
 
79
  setView("user_channel")
80
  }}
81
- /> : <p>Ask <span className="font-mono">@jbilcke-hf</span> for help to create a channel!</p>}
 
82
  </div> : null}
 
83
  </div>
84
  )
85
  }
 
10
  import { localStorageKeys } from "@/app/state/localStorageKeys"
11
  import { defaultSettings } from "@/app/state/defaultSettings"
12
  import { Input } from "@/components/ui/input"
13
+ import { ChannelInfo } from "@/types"
14
 
15
  export function UserAccountView() {
16
  const [_isPending, startTransition] = useTransition()
 
74
  <h2 className="text-3xl font-bold">Your custom channels:</h2>
75
  {userChannels?.length ? <ChannelList
76
  layout="grid"
77
+ channels={[
78
+ // add a fake button to the list, at the beginning
79
+ // { id: "" } as ChannelInfo,
80
+
81
+ ...userChannels
82
+ ]}
83
  onSelect={(userChannel) => {
84
+ if (userChannel.id) {
85
+ setUserChannel(userChannel)
86
+ }
87
  setView("user_channel")
88
  }}
89
+ />
90
+ : isLoaded ? <p>You don&apos;t seem to have any channel yet. See @flngr on X to learn more about how to do this!</p> : <p>Loading channels..</p>}
91
  </div> : null}
92
+
93
  </div>
94
  )
95
  }
src/app/views/user-channel-view/index.tsx CHANGED
@@ -17,6 +17,7 @@ import { PendingVideoList } from "@/app/interface/pending-video-list"
17
  import { getChannelVideos } from "@/app/server/actions/ai-tube-hf/getChannelVideos"
18
  import { parseVideoModelName } from "@/app/server/actions/utils/parseVideoModelName"
19
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
 
20
 
21
  export function UserChannelView() {
22
  const [_isPending, startTransition] = useTransition()
@@ -24,11 +25,7 @@ export function UserChannelView() {
24
  localStorageKeys.huggingfaceApiKey,
25
  defaultSettings.huggingfaceApiKey
26
  )
27
-
28
- const defaultVideoModel = "SVD"
29
- const defaultVoice = "Julian"
30
 
31
-
32
  const [titleDraft, setTitleDraft] = useState("")
33
  const [descriptionDraft, setDescriptionDraft] = useState("")
34
  const [tagsDraft, setTagsDraft] = useState("")
 
17
  import { getChannelVideos } from "@/app/server/actions/ai-tube-hf/getChannelVideos"
18
  import { parseVideoModelName } from "@/app/server/actions/utils/parseVideoModelName"
19
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
20
+ import { defaultVideoModel, defaultVoice } from "@/app/config"
21
 
22
  export function UserChannelView() {
23
  const [_isPending, startTransition] = useTransition()
 
25
  localStorageKeys.huggingfaceApiKey,
26
  defaultSettings.huggingfaceApiKey
27
  )
 
 
 
28
 
 
29
  const [titleDraft, setTitleDraft] = useState("")
30
  const [descriptionDraft, setDescriptionDraft] = useState("")
31
  const [tagsDraft, setTagsDraft] = useState("")