Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Commit
•
f27679f
1
Parent(s):
ac83b4d
hide some features for the beta + improve player
Browse files- .env +2 -3
- package-lock.json +28 -0
- package.json +2 -0
- public/huggingface-avatar.jpeg +0 -0
- src/app/config.ts +3 -0
- src/app/interface/channel-card/index.tsx +21 -5
- src/app/interface/left-menu/index.tsx +12 -12
- src/app/interface/pending-video-card/index.tsx +44 -0
- src/app/interface/pending-video-card/truncate.ts +5 -0
- src/app/interface/pending-video-list/index.tsx +39 -0
- src/app/interface/{top-menu → top-header}/index.tsx +78 -12
- src/app/interface/video-card/index.tsx +91 -6
- src/app/interface/video-list/index.tsx +6 -2
- src/app/interface/video-player/index.tsx +46 -0
- src/app/layout.tsx +8 -3
- src/app/main.tsx +10 -5
- src/app/page.tsx +5 -1
- src/app/server/actions/ai-tube-hf/deleteFileFromDataset.ts +42 -0
- src/app/server/actions/ai-tube-hf/deleteVideoRequest.ts +24 -0
- src/app/server/actions/ai-tube-hf/downloadFileAsText.ts +59 -0
- src/app/server/actions/ai-tube-hf/getChannels.ts +13 -10
- src/app/server/actions/ai-tube-hf/getCredentials.ts +38 -0
- src/app/server/actions/ai-tube-hf/getVideoRequestsFromChannel.ts +87 -89
- src/app/server/actions/ai-tube-hf/getVideos.ts +46 -0
- src/app/server/actions/ai-tube-hf/uploadVideoRequestToDataset.ts +8 -19
- src/app/server/actions/submitVideoRequest.ts +2 -11
- src/app/server/actions/utils/formatPromptFileName.ts +14 -0
- src/app/server/actions/utils/parseDatasetPrompt.ts +11 -4
- src/app/server/actions/utils/parseDatasetReadme.ts +0 -2
- src/app/server/actions/utils/parsePromptFileName.ts +3 -0
- src/app/state/useStore.ts +17 -1
- src/app/views/home-view/index.tsx +26 -27
- src/app/views/public-channel-view/index.tsx +2 -0
- src/app/views/public-channels-view/index.tsx +3 -3
- src/app/views/public-video-view/index.tsx +78 -15
- src/app/views/user-account-view/index.tsx +2 -2
- src/app/views/user-channel-view/index.tsx +130 -57
- src/app/views/user-channels-view/index.tsx +31 -7
- src/components/ui/table.tsx +8 -5
- src/huggingface/hub/src/lib/list-datasets.ts +1 -0
- src/lib/fonts.ts +0 -29
- src/lib/formatDuration.ts +12 -0
- src/lib/formatTimeAgo.ts +5 -0
- src/types.ts +12 -0
- 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-
|
20 |
`rounded-lg`,
|
21 |
-
`bg-neutral-
|
|
|
22 |
`cursor-pointer`,
|
23 |
className,
|
24 |
)}
|
@@ -37,10 +38,25 @@ export function ChannelCard({
|
|
37 |
</div>
|
38 |
|
39 |
<div className={cn(
|
40 |
-
`
|
|
|
|
|
41 |
)}>
|
42 |
-
<
|
43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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={<
|
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
|
|
|
|
|
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 |
-
`
|
|
|
|
|
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 |
-
`
|
29 |
)}>
|
30 |
-
<div className=
|
31 |
-
|
32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
{/* 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-
|
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 |
+
{/* 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 |
-
`
|
|
|
23 |
)}
|
24 |
>
|
25 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
</div>
|
27 |
<div className={cn(
|
28 |
-
|
29 |
)}>
|
30 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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-
|
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 {
|
3 |
|
4 |
import { cn } from '@/lib/utils'
|
5 |
|
6 |
import './globals.css'
|
7 |
|
8 |
-
const
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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 {
|
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(
|
27 |
`px-2`
|
28 |
)}>
|
29 |
-
<
|
30 |
-
<div className=
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
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 |
-
|
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
|
87 |
|
88 |
-
// console.log("
|
89 |
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
description = ParsedDatasetReadme.description
|
94 |
|
95 |
const prefix = "ai-tube:"
|
96 |
|
97 |
-
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 {
|
|
|
|
|
|
|
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(
|
16 |
-
channel
|
17 |
-
apiKey
|
|
|
|
|
|
|
|
|
|
|
18 |
renewCache?: boolean
|
19 |
-
|
|
|
20 |
|
21 |
-
|
|
|
22 |
|
23 |
-
|
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 |
-
|
37 |
|
38 |
-
|
39 |
|
40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
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 |
-
|
80 |
-
const response = await downloadFile({
|
81 |
repo,
|
82 |
path: file.path,
|
83 |
-
|
|
|
|
|
84 |
})
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
prompt:
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
103 |
}
|
|
|
104 |
|
105 |
-
|
106 |
-
|
107 |
-
|
|
|
|
|
|
|
|
|
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
|
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
|
86 |
label: title,
|
87 |
description,
|
88 |
prompt,
|
89 |
thumbnailUrl: "",
|
90 |
updatedAt: new Date().toISOString(),
|
91 |
-
tags
|
92 |
channel,
|
93 |
}
|
94 |
|
95 |
const newVideo: VideoInfo = {
|
96 |
-
id
|
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
|
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 |
-
|
37 |
-
|
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):
|
|
|
|
|
|
|
|
|
|
|
28 |
// Regular expression to find markdown sections based on the provided structure
|
29 |
-
const sectionRegex =
|
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 |
-
|
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 |
-
|
|
|
|
|
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 |
-
|
22 |
-
|
23 |
-
const newCategoryVideos: VideoInfo[] = []
|
24 |
-
setCurrentVideos(newCategoryVideos)
|
25 |
}, [currentTag])
|
26 |
|
|
|
|
|
|
|
|
|
|
|
27 |
return (
|
28 |
<div className={cn(
|
29 |
-
|
30 |
)}>
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
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
|
9 |
-
const
|
10 |
-
const
|
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 |
-
`
|
|
|
24 |
)}>
|
25 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
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
|
40 |
channel: currentChannel,
|
41 |
apiKey: huggingfaceApiKey,
|
|
|
42 |
})
|
43 |
-
|
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 |
-
<
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
<
|
105 |
-
|
106 |
-
<
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
</p>
|
117 |
</div>
|
118 |
-
</div>
|
119 |
|
120 |
-
|
121 |
-
<
|
122 |
-
|
123 |
-
<
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
<
|
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. "Education, Sports")
|
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 |
-
|
46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
47 |
{huggingfaceApiKey ?
|
48 |
-
|
|
|
|
|
49 |
channels={currentChannels}
|
50 |
onSelect={(channel) => {
|
51 |
setCurrentChannel(channel)
|
52 |
setView("user_channel")
|
53 |
}}
|
54 |
-
/> : <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(
|
|
|
|
|
|
|
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-
|
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-
|
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-
|
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 |
-
|
21 |
-
|
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": {
|