Spaces:
Paused
Paused
Commit
β’
8f24b44
1
Parent(s):
3689a9d
preparing repo for multivideo
Browse files- next.config.js +2 -0
- scripts/test.js +1 -1
- src/app/api/tasks/[uuid]/route.ts +2 -2
- src/app/api/tasks/route.ts +6 -7
- src/app/studio/[ownerId]/main.tsx +5 -5
- src/app/studio/[ownerId]/page.tsx +4 -3
- src/app/types.ts +30 -4
- src/components/business/video-form.tsx +2 -2
- src/components/business/video-player.tsx +2 -2
- src/components/business/{tasks/data-table-column-header.tsx β videos/column-header.tsx} +0 -0
- src/components/business/{tasks β videos}/columns.tsx +33 -38
- src/components/business/{tasks/data-table-row-actions.tsx β videos/video-actions.tsx} +7 -3
- src/components/business/{tasks/video-tasks-queue.tsx β videos/video-table.tsx} +9 -9
- src/server/actions.ts +3 -7
- src/server/base.ts +96 -2
- src/server/index.ts +35 -14
next.config.js
CHANGED
@@ -6,6 +6,7 @@ const nextConfig = {
|
|
6 |
serverActions: true,
|
7 |
},
|
8 |
|
|
|
9 |
async redirects() {
|
10 |
return [
|
11 |
{
|
@@ -15,6 +16,7 @@ const nextConfig = {
|
|
15 |
},
|
16 |
]
|
17 |
},
|
|
|
18 |
}
|
19 |
|
20 |
module.exports = nextConfig
|
|
|
6 |
serverActions: true,
|
7 |
},
|
8 |
|
9 |
+
/*
|
10 |
async redirects() {
|
11 |
return [
|
12 |
{
|
|
|
16 |
},
|
17 |
]
|
18 |
},
|
19 |
+
*/
|
20 |
}
|
21 |
|
22 |
module.exports = nextConfig
|
scripts/test.js
CHANGED
@@ -2,7 +2,7 @@ const { promises: fs } = require("node:fs")
|
|
2 |
|
3 |
const main = async () => {
|
4 |
console.log('generating shot..')
|
5 |
-
const response = await fetch(
|
6 |
method: "POST",
|
7 |
headers: {
|
8 |
"Accept": "application/json",
|
|
|
2 |
|
3 |
const main = async () => {
|
4 |
console.log('generating shot..')
|
5 |
+
const response = await fetch("http://localhost:3000/api/shot", {
|
6 |
method: "POST",
|
7 |
headers: {
|
8 |
"Accept": "application/json",
|
src/app/api/tasks/[uuid]/route.ts
CHANGED
@@ -1,9 +1,9 @@
|
|
1 |
import { NextRequest, NextResponse } from "next/server"
|
2 |
-
import {
|
3 |
|
4 |
// TODO: implement some kind of quota system
|
5 |
export async function GET(req: NextRequest) {
|
6 |
return NextResponse.json({
|
7 |
-
|
8 |
})
|
9 |
}
|
|
|
1 |
import { NextRequest, NextResponse } from "next/server"
|
2 |
+
import { getVideo } from "@/server"
|
3 |
|
4 |
// TODO: implement some kind of quota system
|
5 |
export async function GET(req: NextRequest) {
|
6 |
return NextResponse.json({
|
7 |
+
video: await getVideo(`${req.url?.split('/').pop() || ""}`)
|
8 |
})
|
9 |
}
|
src/app/api/tasks/route.ts
CHANGED
@@ -1,24 +1,23 @@
|
|
1 |
-
import {
|
2 |
-
import {
|
3 |
import { NextApiResponse } from "next"
|
4 |
import { NextRequest, NextResponse } from "next/server"
|
5 |
|
6 |
// TODO: implement some kind of quota system
|
7 |
export async function GET() {
|
8 |
return NextResponse.json({
|
9 |
-
|
10 |
})
|
11 |
}
|
12 |
|
13 |
// TODO: implement some kind of quota system
|
14 |
export async function POST(
|
15 |
req: NextRequest,
|
16 |
-
res: NextApiResponse<
|
17 |
error?: string
|
18 |
}>
|
19 |
) {
|
20 |
-
|
21 |
-
const
|
22 |
-
const task = await submitNewTask(taskRequest)
|
23 |
res.status(200).json(task)
|
24 |
}
|
|
|
1 |
+
import { Video, VideoRequest } from "@/app/types"
|
2 |
+
import { getAllVideos, createNewVideo } from "@/server"
|
3 |
import { NextApiResponse } from "next"
|
4 |
import { NextRequest, NextResponse } from "next/server"
|
5 |
|
6 |
// TODO: implement some kind of quota system
|
7 |
export async function GET() {
|
8 |
return NextResponse.json({
|
9 |
+
videos: await getAllVideos()
|
10 |
})
|
11 |
}
|
12 |
|
13 |
// TODO: implement some kind of quota system
|
14 |
export async function POST(
|
15 |
req: NextRequest,
|
16 |
+
res: NextApiResponse<Video | {
|
17 |
error?: string
|
18 |
}>
|
19 |
) {
|
20 |
+
const taskRequest = req.body as VideoRequest
|
21 |
+
const task = await createNewVideo(taskRequest)
|
|
|
22 |
res.status(200).json(task)
|
23 |
}
|
src/app/studio/[ownerId]/main.tsx
CHANGED
@@ -2,20 +2,20 @@
|
|
2 |
|
3 |
import { useState } from "react"
|
4 |
|
5 |
-
import {
|
6 |
import { RefreshStudio } from "@/components/business/refresh"
|
7 |
import { VideoForm } from "@/components/business/video-form"
|
8 |
-
import {
|
9 |
import { VideoPlayer } from "@/components/business/video-player"
|
10 |
|
11 |
-
export default function Main({
|
12 |
-
const [selectedVideo, selectVideo] = useState<
|
13 |
|
14 |
return (
|
15 |
<div className="flex flex-col md:flex-row">
|
16 |
<div className="h-full flex flex-col space-y-4 w-full md:w-[800px] px-4 py-8">
|
17 |
<VideoForm />
|
18 |
-
<
|
19 |
<RefreshStudio />
|
20 |
</div>
|
21 |
<div className="flex flex-col w-auto">
|
|
|
2 |
|
3 |
import { useState } from "react"
|
4 |
|
5 |
+
import { VideosQueue } from "@/components/business/videos/video-table"
|
6 |
import { RefreshStudio } from "@/components/business/refresh"
|
7 |
import { VideoForm } from "@/components/business/video-form"
|
8 |
+
import { Video } from "@/app/types"
|
9 |
import { VideoPlayer } from "@/components/business/video-player"
|
10 |
|
11 |
+
export default function Main({ videos }: { videos: Video[] }) {
|
12 |
+
const [selectedVideo, selectVideo] = useState<Video>()
|
13 |
|
14 |
return (
|
15 |
<div className="flex flex-col md:flex-row">
|
16 |
<div className="h-full flex flex-col space-y-4 w-full md:w-[800px] px-4 py-8">
|
17 |
<VideoForm />
|
18 |
+
<VideosQueue videos={videos} onSelectVideo={selectVideo} />
|
19 |
<RefreshStudio />
|
20 |
</div>
|
21 |
<div className="flex flex-col w-auto">
|
src/app/studio/[ownerId]/page.tsx
CHANGED
@@ -2,11 +2,12 @@
|
|
2 |
|
3 |
import Head from "next/head"
|
4 |
|
|
|
|
|
5 |
import Main from "./main"
|
6 |
-
import { getTasks } from "@/server"
|
7 |
|
8 |
export default async function StudioPage({ params: { ownerId } }: { params: { ownerId: string }}) {
|
9 |
-
const
|
10 |
|
11 |
return (
|
12 |
<div>
|
@@ -14,7 +15,7 @@ export default async function StudioPage({ params: { ownerId } }: { params: { ow
|
|
14 |
<meta name="viewport" content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86" />
|
15 |
</Head>
|
16 |
<main className="dark fixed inset-0 flex flex-col items-center bg-stone-900 text-stone-10 overflow-y-scroll">
|
17 |
-
<Main
|
18 |
</main>
|
19 |
</div>
|
20 |
)
|
|
|
2 |
|
3 |
import Head from "next/head"
|
4 |
|
5 |
+
import { getVideos } from "@/server"
|
6 |
+
|
7 |
import Main from "./main"
|
|
|
8 |
|
9 |
export default async function StudioPage({ params: { ownerId } }: { params: { ownerId: string }}) {
|
10 |
+
const videos = await getVideos(ownerId)
|
11 |
|
12 |
return (
|
13 |
<div>
|
|
|
15 |
<meta name="viewport" content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86" />
|
16 |
</Head>
|
17 |
<main className="dark fixed inset-0 flex flex-col items-center bg-stone-900 text-stone-10 overflow-y-scroll">
|
18 |
+
<Main videos={videos} />
|
19 |
</main>
|
20 |
</div>
|
21 |
)
|
src/app/types.ts
CHANGED
@@ -1,3 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
export type VideoTransition =
|
2 |
| 'dissolve'
|
3 |
| 'bookflip'
|
@@ -139,6 +148,8 @@ export interface VideoShotMeta {
|
|
139 |
export interface VideoShotData {
|
140 |
// must be unique
|
141 |
id: string
|
|
|
|
|
142 |
|
143 |
fileName: string
|
144 |
|
@@ -158,6 +169,7 @@ export interface VideoShotData {
|
|
158 |
nbCompletedSteps: number
|
159 |
nbTotalSteps: number
|
160 |
progressPercent: number
|
|
|
161 |
completedAt: string
|
162 |
completed: boolean
|
163 |
error: string
|
@@ -204,15 +216,21 @@ export interface VideoSequenceMeta {
|
|
204 |
export interface VideoSequenceData {
|
205 |
// must be unique
|
206 |
id: string
|
207 |
-
|
|
|
|
|
208 |
fileName: string
|
209 |
|
210 |
// used to check compatibility
|
211 |
version: number
|
212 |
|
|
|
|
|
|
|
213 |
hasAssembledVideo: boolean
|
214 |
nbCompletedShots: number
|
215 |
progressPercent: number
|
|
|
216 |
completedAt: string
|
217 |
completed: boolean
|
218 |
error: string
|
@@ -220,13 +238,21 @@ export interface VideoSequenceData {
|
|
220 |
|
221 |
export type VideoSequence = VideoSequenceMeta & VideoSequenceData
|
222 |
|
223 |
-
export type
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
224 |
prompt: string
|
225 |
-
ownerId: string
|
226 |
sequence: Partial<VideoSequenceMeta>
|
227 |
shots: Array<Partial<VideoShotMeta>>
|
228 |
}>
|
229 |
|
230 |
-
export type
|
231 |
shots: VideoShot[]
|
232 |
}
|
|
|
1 |
+
export type VideoStatus =
|
2 |
+
| 'pending'
|
3 |
+
| 'abort' // this is an order (the video might still being processed by a task)
|
4 |
+
| 'delete' // this is an order (the video might still being processed by a task)
|
5 |
+
| 'pause' // this is an order (the video might still being processed by a task)
|
6 |
+
| 'completed'
|
7 |
+
| 'unknown'
|
8 |
+
|
9 |
+
|
10 |
export type VideoTransition =
|
11 |
| 'dissolve'
|
12 |
| 'bookflip'
|
|
|
148 |
export interface VideoShotData {
|
149 |
// must be unique
|
150 |
id: string
|
151 |
+
sequenceId: string
|
152 |
+
ownerId: string
|
153 |
|
154 |
fileName: string
|
155 |
|
|
|
169 |
nbCompletedSteps: number
|
170 |
nbTotalSteps: number
|
171 |
progressPercent: number
|
172 |
+
createdAt: string
|
173 |
completedAt: string
|
174 |
completed: boolean
|
175 |
error: string
|
|
|
216 |
export interface VideoSequenceData {
|
217 |
// must be unique
|
218 |
id: string
|
219 |
+
|
220 |
+
ownerId: string
|
221 |
+
|
222 |
fileName: string
|
223 |
|
224 |
// used to check compatibility
|
225 |
version: number
|
226 |
|
227 |
+
status: VideoStatus
|
228 |
+
|
229 |
+
hasGeneratedSpecs: boolean
|
230 |
hasAssembledVideo: boolean
|
231 |
nbCompletedShots: number
|
232 |
progressPercent: number
|
233 |
+
createdAt: string
|
234 |
completedAt: string
|
235 |
completed: boolean
|
236 |
error: string
|
|
|
238 |
|
239 |
export type VideoSequence = VideoSequenceMeta & VideoSequenceData
|
240 |
|
241 |
+
export type VideoStatusRequest = {
|
242 |
+
status: VideoStatus
|
243 |
+
}
|
244 |
+
|
245 |
+
export type GenericAPIResponse = {
|
246 |
+
success?: boolean
|
247 |
+
error?: string
|
248 |
+
}
|
249 |
+
|
250 |
+
export type VideoAPIRequest = Partial<{
|
251 |
prompt: string
|
|
|
252 |
sequence: Partial<VideoSequenceMeta>
|
253 |
shots: Array<Partial<VideoShotMeta>>
|
254 |
}>
|
255 |
|
256 |
+
export type Video = VideoSequence & {
|
257 |
shots: VideoShot[]
|
258 |
}
|
src/components/business/video-form.tsx
CHANGED
@@ -6,7 +6,7 @@ import { usePathname } from "next/navigation"
|
|
6 |
import { experimental_useFormStatus as useFormStatus } from "react-dom"
|
7 |
import { Textarea } from "@/components/ui/textarea"
|
8 |
import { Button } from "@/components/ui/button"
|
9 |
-
import {
|
10 |
|
11 |
export const VideoForm = () => {
|
12 |
const pathname = usePathname()
|
@@ -15,7 +15,7 @@ export const VideoForm = () => {
|
|
15 |
|
16 |
return (
|
17 |
<form
|
18 |
-
action={
|
19 |
>
|
20 |
<div className="flex flex-col md:hidden w-full text-center">
|
21 |
<h2 className="text-4xl font-thin tracking-tight">VideoChain UI</h2>
|
|
|
6 |
import { experimental_useFormStatus as useFormStatus } from "react-dom"
|
7 |
import { Textarea } from "@/components/ui/textarea"
|
8 |
import { Button } from "@/components/ui/button"
|
9 |
+
import { handleFormSubmit } from "@/server/actions"
|
10 |
|
11 |
export const VideoForm = () => {
|
12 |
const pathname = usePathname()
|
|
|
15 |
|
16 |
return (
|
17 |
<form
|
18 |
+
action={handleFormSubmit}
|
19 |
>
|
20 |
<div className="flex flex-col md:hidden w-full text-center">
|
21 |
<h2 className="text-4xl font-thin tracking-tight">VideoChain UI</h2>
|
src/components/business/video-player.tsx
CHANGED
@@ -1,8 +1,8 @@
|
|
1 |
"use client"
|
2 |
|
3 |
-
import {
|
4 |
|
5 |
-
export const VideoPlayer = ({ video }: { video?:
|
6 |
|
7 |
if (typeof video === "undefined") {
|
8 |
return <div className="flex w-full h-screen items-center justify-center text-center">
|
|
|
1 |
"use client"
|
2 |
|
3 |
+
import { Video } from "@/app/types"
|
4 |
|
5 |
+
export const VideoPlayer = ({ video }: { video?: Video }) => {
|
6 |
|
7 |
if (typeof video === "undefined") {
|
8 |
return <div className="flex w-full h-screen items-center justify-center text-center">
|
src/components/business/{tasks/data-table-column-header.tsx β videos/column-header.tsx}
RENAMED
File without changes
|
src/components/business/{tasks β videos}/columns.tsx
RENAMED
@@ -3,12 +3,13 @@
|
|
3 |
import { ColumnDef } from "@tanstack/react-table"
|
4 |
import { Checkbox } from "@/components/ui/checkbox"
|
5 |
|
6 |
-
import { DataTableColumnHeader } from "./
|
7 |
-
import {
|
8 |
|
9 |
-
import {
|
|
|
10 |
|
11 |
-
export const columns: ColumnDef<
|
12 |
{
|
13 |
id: "select",
|
14 |
header: ({ table }) => (
|
@@ -42,15 +43,11 @@ export const columns: ColumnDef<VideoTask>[] = [
|
|
42 |
header: ({ column }) => (
|
43 |
<DataTableColumnHeader column={column} title="Prompt" />
|
44 |
),
|
45 |
-
cell: ({ row }) =>
|
46 |
-
|
47 |
-
<
|
48 |
-
|
49 |
-
|
50 |
-
</span>
|
51 |
-
</div>
|
52 |
-
)
|
53 |
-
},
|
54 |
enableSorting: false,
|
55 |
},
|
56 |
{
|
@@ -58,52 +55,50 @@ export const columns: ColumnDef<VideoTask>[] = [
|
|
58 |
header: ({ column }) => (
|
59 |
<DataTableColumnHeader column={column} title="Progress" />
|
60 |
),
|
61 |
-
cell: ({ row }) =>
|
62 |
-
|
63 |
-
|
64 |
-
return (
|
65 |
-
<div className="flex items-center">
|
66 |
-
<span>{progress}%</span>
|
67 |
-
</div>
|
68 |
-
)
|
69 |
-
},
|
70 |
enableSorting: false,
|
71 |
},
|
72 |
{
|
73 |
-
accessorKey: "
|
74 |
-
header: ({ column }) =>
|
75 |
-
|
76 |
-
),
|
77 |
-
cell: ({ row }) => <div className="w-[100px]">
|
78 |
<a
|
79 |
className="hover:underline cursor-pointer"
|
80 |
target="_blank"
|
81 |
-
href={`${process.env.NEXT_PUBLIC_DOWNLOAD_URL}/${
|
82 |
-
<video src={`${process.env.NEXT_PUBLIC_DOWNLOAD_URL}/${
|
83 |
</a>
|
84 |
</div>,
|
85 |
enableSorting: false,
|
86 |
enableHiding: false,
|
87 |
},
|
|
|
88 |
{
|
89 |
accessorKey: "fileName",
|
90 |
-
header: ({ column }) =>
|
91 |
-
|
92 |
-
),
|
93 |
-
cell: ({ row }) => <div className="">
|
94 |
<a
|
95 |
className="hover:underline cursor-pointer"
|
96 |
target="_blank"
|
97 |
-
href={`${process.env.NEXT_PUBLIC_DOWNLOAD_URL}/${
|
98 |
</div>,
|
99 |
enableSorting: false,
|
100 |
enableHiding: false,
|
101 |
},
|
102 |
-
|
103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
104 |
{
|
105 |
id: "actions",
|
106 |
-
cell: ({ row }) => <
|
107 |
},
|
108 |
-
*/
|
109 |
]
|
|
|
3 |
import { ColumnDef } from "@tanstack/react-table"
|
4 |
import { Checkbox } from "@/components/ui/checkbox"
|
5 |
|
6 |
+
import { DataTableColumnHeader } from "./column-header"
|
7 |
+
import { VideoActions } from "./video-actions"
|
8 |
|
9 |
+
import { Video } from "@/app/types"
|
10 |
+
import { deleteVideo } from "@/server"
|
11 |
|
12 |
+
export const columns: ColumnDef<Video>[] = [
|
13 |
{
|
14 |
id: "select",
|
15 |
header: ({ table }) => (
|
|
|
43 |
header: ({ column }) => (
|
44 |
<DataTableColumnHeader column={column} title="Prompt" />
|
45 |
),
|
46 |
+
cell: ({ row: { original: { videoPrompt }} }) => (
|
47 |
+
<div className="flex space-x-2">
|
48 |
+
<span className="max-w-[500px] font-medium">{videoPrompt}</span>
|
49 |
+
</div>
|
50 |
+
),
|
|
|
|
|
|
|
|
|
51 |
enableSorting: false,
|
52 |
},
|
53 |
{
|
|
|
55 |
header: ({ column }) => (
|
56 |
<DataTableColumnHeader column={column} title="Progress" />
|
57 |
),
|
58 |
+
cell: ({ row: { original: { progressPercent }} }) => (
|
59 |
+
<div className="flex items-center"><span>{Number(progressPercent || 0)}%</span></div>
|
60 |
+
),
|
|
|
|
|
|
|
|
|
|
|
|
|
61 |
enableSorting: false,
|
62 |
},
|
63 |
{
|
64 |
+
accessorKey: "fileName",
|
65 |
+
header: ({ column }) => null,// no header
|
66 |
+
cell: ({ row: { original: { ownerId, id, progressPercent } } }) => <div className="w-[100px]">
|
|
|
|
|
67 |
<a
|
68 |
className="hover:underline cursor-pointer"
|
69 |
target="_blank"
|
70 |
+
href={`${process.env.NEXT_PUBLIC_DOWNLOAD_URL}/${ownerId}/${id}.mp4`}>
|
71 |
+
<video src={`${process.env.NEXT_PUBLIC_DOWNLOAD_URL}/${ownerId}/${id}.mp4?progress=${progressPercent || 0}`} muted />
|
72 |
</a>
|
73 |
</div>,
|
74 |
enableSorting: false,
|
75 |
enableHiding: false,
|
76 |
},
|
77 |
+
/*
|
78 |
{
|
79 |
accessorKey: "fileName",
|
80 |
+
header: ({ column }) => null,
|
81 |
+
cell: ({ row: { original: { fileName, ownerId, id }} }) => <div className="">
|
|
|
|
|
82 |
<a
|
83 |
className="hover:underline cursor-pointer"
|
84 |
target="_blank"
|
85 |
+
href={`${process.env.NEXT_PUBLIC_DOWNLOAD_URL}/${ownerId}/${id}.mp4`}>Save</a>
|
86 |
</div>,
|
87 |
enableSorting: false,
|
88 |
enableHiding: false,
|
89 |
},
|
90 |
+
*/
|
91 |
+
{
|
92 |
+
accessorKey: "delete",
|
93 |
+
header: ({ column }) => null, // no header
|
94 |
+
cell: ({ row: { original } }) => <div
|
95 |
+
className="hover:underline cursor-pointer"
|
96 |
+
onClick={() => { deleteVideo(original.ownerId, original.id) }}>Delete</div>,
|
97 |
+
enableSorting: false,
|
98 |
+
enableHiding: false,
|
99 |
+
},
|
100 |
{
|
101 |
id: "actions",
|
102 |
+
cell: ({ row }) => <VideoActions row={row} />,
|
103 |
},
|
|
|
104 |
]
|
src/components/business/{tasks/data-table-row-actions.tsx β videos/video-actions.tsx}
RENAMED
@@ -13,12 +13,12 @@ import {
|
|
13 |
DropdownMenuTrigger,
|
14 |
} from "@/components/ui/dropdown-menu"
|
15 |
|
16 |
-
import {
|
17 |
|
18 |
-
export function
|
19 |
row,
|
20 |
}: {
|
21 |
-
row: Row<
|
22 |
}) {
|
23 |
const task = row.original
|
24 |
|
@@ -34,6 +34,10 @@ export function DataTableRowActions({
|
|
34 |
</Button>
|
35 |
</DropdownMenuTrigger>
|
36 |
<DropdownMenuContent align="end" className="w-[160px]">
|
|
|
|
|
|
|
|
|
37 |
<DropdownMenuItem>Download</DropdownMenuItem>
|
38 |
<DropdownMenuSeparator />
|
39 |
<DropdownMenuItem>
|
|
|
13 |
DropdownMenuTrigger,
|
14 |
} from "@/components/ui/dropdown-menu"
|
15 |
|
16 |
+
import { Video } from "@/app/types"
|
17 |
|
18 |
+
export function VideoActions({
|
19 |
row,
|
20 |
}: {
|
21 |
+
row: Row<Video>
|
22 |
}) {
|
23 |
const task = row.original
|
24 |
|
|
|
34 |
</Button>
|
35 |
</DropdownMenuTrigger>
|
36 |
<DropdownMenuContent align="end" className="w-[160px]">
|
37 |
+
<DropdownMenuItem>
|
38 |
+
Pause generation
|
39 |
+
<DropdownMenuShortcut>ββ«</DropdownMenuShortcut>
|
40 |
+
</DropdownMenuItem>
|
41 |
<DropdownMenuItem>Download</DropdownMenuItem>
|
42 |
<DropdownMenuSeparator />
|
43 |
<DropdownMenuItem>
|
src/components/business/{tasks/video-tasks-queue.tsx β videos/video-table.tsx}
RENAMED
@@ -24,16 +24,16 @@ import {
|
|
24 |
TableRow,
|
25 |
} from "@/components/ui/table"
|
26 |
|
27 |
-
import { columns } from "@/components/business/
|
28 |
-
import {
|
29 |
import { useState } from "react"
|
30 |
|
31 |
-
export function
|
32 |
-
|
33 |
onSelectVideo,
|
34 |
}: {
|
35 |
-
|
36 |
-
onSelectVideo: (task:
|
37 |
}) {
|
38 |
const [rowSelection, setRowSelection] = useState({})
|
39 |
const [columnVisibility, setColumnVisibility] =
|
@@ -44,8 +44,8 @@ export function VideoTasksQueue({
|
|
44 |
const [sorting, setSorting] = useState<SortingState>([])
|
45 |
|
46 |
const table = useReactTable({
|
47 |
-
data:
|
48 |
-
columns: columns as ColumnDef<
|
49 |
state: {
|
50 |
sorting,
|
51 |
columnVisibility,
|
@@ -98,7 +98,7 @@ export function VideoTasksQueue({
|
|
98 |
className="cursor-pointer"
|
99 |
onClick={() => {
|
100 |
const videoId = `${row.getValue("id") || ""}`
|
101 |
-
const video =
|
102 |
if (video) {
|
103 |
onSelectVideo(video)
|
104 |
}
|
|
|
24 |
TableRow,
|
25 |
} from "@/components/ui/table"
|
26 |
|
27 |
+
import { columns } from "@/components/business/videos/columns"
|
28 |
+
import { Video } from "@/app/types"
|
29 |
import { useState } from "react"
|
30 |
|
31 |
+
export function VideosQueue({
|
32 |
+
videos = [],
|
33 |
onSelectVideo,
|
34 |
}: {
|
35 |
+
videos: Video[]
|
36 |
+
onSelectVideo: (task: Video) => void
|
37 |
}) {
|
38 |
const [rowSelection, setRowSelection] = useState({})
|
39 |
const [columnVisibility, setColumnVisibility] =
|
|
|
44 |
const [sorting, setSorting] = useState<SortingState>([])
|
45 |
|
46 |
const table = useReactTable({
|
47 |
+
data: videos,
|
48 |
+
columns: columns as ColumnDef<Video, any>[],
|
49 |
state: {
|
50 |
sorting,
|
51 |
columnVisibility,
|
|
|
98 |
className="cursor-pointer"
|
99 |
onClick={() => {
|
100 |
const videoId = `${row.getValue("id") || ""}`
|
101 |
+
const video = videos.find(({ id }) => id === videoId)
|
102 |
if (video) {
|
103 |
onSelectVideo(video)
|
104 |
}
|
src/server/actions.ts
CHANGED
@@ -1,17 +1,13 @@
|
|
1 |
"use server"
|
2 |
|
3 |
import { revalidatePath } from "next/cache"
|
4 |
-
import {
|
5 |
-
|
6 |
-
export async function formSubmit(formData: FormData) {
|
7 |
|
|
|
8 |
const ownerId = `${formData.get("ownerId") || ""}`
|
9 |
-
|
10 |
-
await submitNewTask({
|
11 |
prompt: `${formData.get("prompt") || ""}`,
|
12 |
-
ownerId,
|
13 |
})
|
14 |
-
console.log('calling revalidate', ownerId)
|
15 |
// for doc see https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions
|
16 |
revalidatePath(`/studio/${ownerId}`)
|
17 |
}
|
|
|
1 |
"use server"
|
2 |
|
3 |
import { revalidatePath } from "next/cache"
|
4 |
+
import { createNewVideo } from "."
|
|
|
|
|
5 |
|
6 |
+
export async function handleFormSubmit(formData: FormData) {
|
7 |
const ownerId = `${formData.get("ownerId") || ""}`
|
8 |
+
await createNewVideo(ownerId, {
|
|
|
9 |
prompt: `${formData.get("prompt") || ""}`,
|
|
|
10 |
})
|
|
|
11 |
// for doc see https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions
|
12 |
revalidatePath(`/studio/${ownerId}`)
|
13 |
}
|
src/server/base.ts
CHANGED
@@ -3,7 +3,7 @@
|
|
3 |
// so we have to add it ourselves if needed
|
4 |
const apiUrl = process.env.VC_VIDEOCHAIN_API_URL
|
5 |
|
6 |
-
export const
|
7 |
try {
|
8 |
const res = await fetch(`${apiUrl}/${path}`, {
|
9 |
method: "GET",
|
@@ -35,7 +35,38 @@ export const get = async <T>(path: string = '', defaultValue: T): Promise<T> =>
|
|
35 |
}
|
36 |
|
37 |
|
38 |
-
export const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
try {
|
40 |
const res = await fetch(`${apiUrl}/${path}`, {
|
41 |
method: "POST",
|
@@ -60,6 +91,69 @@ export const post = async <S, T>(path: string = '', payload: S, defaultValue: T)
|
|
60 |
|
61 |
const data = await res.json()
|
62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
63 |
return ((data as T) || defaultValue)
|
64 |
} catch (err) {
|
65 |
return defaultValue
|
|
|
3 |
// so we have to add it ourselves if needed
|
4 |
const apiUrl = process.env.VC_VIDEOCHAIN_API_URL
|
5 |
|
6 |
+
export const GET = async <T>(path: string = '', defaultValue: T): Promise<T> => {
|
7 |
try {
|
8 |
const res = await fetch(`${apiUrl}/${path}`, {
|
9 |
method: "GET",
|
|
|
35 |
}
|
36 |
|
37 |
|
38 |
+
export const DELETE = async <T>(path: string = '', defaultValue: T): Promise<T> => {
|
39 |
+
try {
|
40 |
+
const res = await fetch(`${apiUrl}/${path}`, {
|
41 |
+
method: "DELETE",
|
42 |
+
headers: {
|
43 |
+
Accept: "application/json",
|
44 |
+
Authorization: `Bearer ${process.env.VC_SECRET_ACCESS_TOKEN}`,
|
45 |
+
},
|
46 |
+
cache: 'no-store',
|
47 |
+
// we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
|
48 |
+
// next: { revalidate: 1 }
|
49 |
+
})
|
50 |
+
|
51 |
+
// The return value is *not* serialized
|
52 |
+
// You can return Date, Map, Set, etc.
|
53 |
+
|
54 |
+
// Recommendation: handle errors
|
55 |
+
if (res.status !== 200) {
|
56 |
+
// This will activate the closest `error.js` Error Boundary
|
57 |
+
throw new Error('Failed to fetch data')
|
58 |
+
}
|
59 |
+
|
60 |
+
const data = await res.json()
|
61 |
+
|
62 |
+
return ((data as T) || defaultValue)
|
63 |
+
} catch (err) {
|
64 |
+
console.error(err)
|
65 |
+
return defaultValue
|
66 |
+
}
|
67 |
+
}
|
68 |
+
|
69 |
+
export const POST = async <S, T>(path: string = '', payload: S, defaultValue: T): Promise<T> => {
|
70 |
try {
|
71 |
const res = await fetch(`${apiUrl}/${path}`, {
|
72 |
method: "POST",
|
|
|
91 |
|
92 |
const data = await res.json()
|
93 |
|
94 |
+
return ((data as T) || defaultValue)
|
95 |
+
} catch (err) {
|
96 |
+
return defaultValue
|
97 |
+
}
|
98 |
+
}
|
99 |
+
|
100 |
+
|
101 |
+
export const PUT = async <S, T>(path: string = '', payload: S, defaultValue: T): Promise<T> => {
|
102 |
+
try {
|
103 |
+
const res = await fetch(`${apiUrl}/${path}`, {
|
104 |
+
method: "PUT",
|
105 |
+
headers: {
|
106 |
+
Accept: "application/json",
|
107 |
+
"Content-Type": "application/json",
|
108 |
+
Authorization: `Bearer ${process.env.VC_SECRET_ACCESS_TOKEN}`,
|
109 |
+
},
|
110 |
+
body: JSON.stringify(payload),
|
111 |
+
// cache: 'no-store',
|
112 |
+
// we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
|
113 |
+
next: { revalidate: 1 }
|
114 |
+
})
|
115 |
+
// The return value is *not* serialized
|
116 |
+
// You can return Date, Map, Set, etc.
|
117 |
+
|
118 |
+
// Recommendation: handle errors
|
119 |
+
if (res.status !== 200) {
|
120 |
+
// This will activate the closest `error.js` Error Boundary
|
121 |
+
throw new Error('Failed to post data')
|
122 |
+
}
|
123 |
+
|
124 |
+
const data = await res.json()
|
125 |
+
|
126 |
+
return ((data as T) || defaultValue)
|
127 |
+
} catch (err) {
|
128 |
+
return defaultValue
|
129 |
+
}
|
130 |
+
}
|
131 |
+
|
132 |
+
export const PATCH = async <S, T>(path: string = '', payload: S, defaultValue: T): Promise<T> => {
|
133 |
+
try {
|
134 |
+
const res = await fetch(`${apiUrl}/${path}`, {
|
135 |
+
method: "PATCH",
|
136 |
+
headers: {
|
137 |
+
Accept: "application/json",
|
138 |
+
"Content-Type": "application/json",
|
139 |
+
Authorization: `Bearer ${process.env.VC_SECRET_ACCESS_TOKEN}`,
|
140 |
+
},
|
141 |
+
body: JSON.stringify(payload),
|
142 |
+
// cache: 'no-store',
|
143 |
+
// we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
|
144 |
+
next: { revalidate: 1 }
|
145 |
+
})
|
146 |
+
// The return value is *not* serialized
|
147 |
+
// You can return Date, Map, Set, etc.
|
148 |
+
|
149 |
+
// Recommendation: handle errors
|
150 |
+
if (res.status !== 200) {
|
151 |
+
// This will activate the closest `error.js` Error Boundary
|
152 |
+
throw new Error('Failed to post data')
|
153 |
+
}
|
154 |
+
|
155 |
+
const data = await res.json()
|
156 |
+
|
157 |
return ((data as T) || defaultValue)
|
158 |
} catch (err) {
|
159 |
return defaultValue
|
src/server/index.ts
CHANGED
@@ -1,39 +1,60 @@
|
|
1 |
"use server"
|
2 |
|
3 |
-
import {
|
4 |
|
5 |
-
import {
|
6 |
|
7 |
// note: for security purposes we do not directly expose the VideoChain API:
|
8 |
// all calls are protected with a token, that way it the VideooChain API can stay
|
9 |
// lightweight, security and quotas are handled outside
|
10 |
|
11 |
-
//
|
12 |
-
export const
|
13 |
-
const tasks = await
|
14 |
|
15 |
return tasks
|
16 |
}
|
17 |
|
18 |
// return all tasks of a owner
|
19 |
-
export const
|
20 |
-
const tasks = await
|
21 |
|
22 |
return tasks
|
23 |
}
|
24 |
|
25 |
-
export const
|
26 |
-
const task = await
|
27 |
|
28 |
return task
|
29 |
}
|
30 |
|
31 |
-
export const
|
32 |
-
const task = await
|
33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
taskRequest,
|
35 |
-
null as unknown as
|
36 |
)
|
37 |
|
38 |
return task
|
39 |
-
}
|
|
|
|
1 |
"use server"
|
2 |
|
3 |
+
import { Video, VideoAPIRequest, GenericAPIResponse, VideoStatusRequest, VideoStatus } from "@/app/types"
|
4 |
|
5 |
+
import { GET, POST, DELETE, PATCH } from "./base"
|
6 |
|
7 |
// note: for security purposes we do not directly expose the VideoChain API:
|
8 |
// all calls are protected with a token, that way it the VideooChain API can stay
|
9 |
// lightweight, security and quotas are handled outside
|
10 |
|
11 |
+
// this should be used by the admin only
|
12 |
+
export const getAllVideos = async () => {
|
13 |
+
const tasks = await GET<Video[]>("", [])
|
14 |
|
15 |
return tasks
|
16 |
}
|
17 |
|
18 |
// return all tasks of a owner
|
19 |
+
export const getVideos = async (ownerId: string) => {
|
20 |
+
const tasks = await GET<Video[]>(ownerId, [])
|
21 |
|
22 |
return tasks
|
23 |
}
|
24 |
|
25 |
+
export const getVideo = async (ownerId: string, videoId: string) => {
|
26 |
+
const task = await GET<Video>(`${ownerId}/${videoId}`, null as unknown as Video)
|
27 |
|
28 |
return task
|
29 |
}
|
30 |
|
31 |
+
export const setVideoStatus = async (ownerId: string, videoId: string, status: VideoStatus) => {
|
32 |
+
const task = await PATCH<VideoStatusRequest, GenericAPIResponse>(`${ownerId}/${videoId}`, { status }, null as unknown as Video)
|
33 |
+
|
34 |
+
return task
|
35 |
+
}
|
36 |
+
|
37 |
+
export const deleteVideo = async (ownerId: string, videoId: string) => {
|
38 |
+
const task = await DELETE<GenericAPIResponse>(`${ownerId}/${videoId}`, { success: true })
|
39 |
+
|
40 |
+
return task
|
41 |
+
}
|
42 |
+
|
43 |
+
/*
|
44 |
+
export async function deleteVideos(ownerId: string, videoIds: string[]) {
|
45 |
+
const task = await DELETE<GenericAPIResponse>(ownerAndVideoId, { success: true })
|
46 |
+
|
47 |
+
return task
|
48 |
+
}
|
49 |
+
*/
|
50 |
+
|
51 |
+
export const createNewVideo = async (ownerId: string, taskRequest: VideoAPIRequest) => {
|
52 |
+
const task = await POST<VideoAPIRequest, Video>(
|
53 |
+
ownerId,
|
54 |
taskRequest,
|
55 |
+
null as unknown as Video
|
56 |
)
|
57 |
|
58 |
return task
|
59 |
+
}
|
60 |
+
|