Skip to content

Commit

Permalink
feat: Add video edit form
Browse files Browse the repository at this point in the history
  • Loading branch information
diego3g committed Jul 15, 2023
1 parent 6ddd2de commit 24626f1
Show file tree
Hide file tree
Showing 7 changed files with 305 additions and 6 deletions.
83 changes: 80 additions & 3 deletions src/app/(app)/videos/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import { Button } from '@/components/ui/button'

import { prisma } from '@/lib/prisma'
import { VideoIcon } from '@radix-ui/react-icons'
import { Music2 } from 'lucide-react'
import { GitHubLogoIcon, MagicWandIcon, VideoIcon } from '@radix-ui/react-icons'
import { Loader2, Music2, Save } from 'lucide-react'
import { Metadata } from 'next'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { DeleteVideoButton } from './delete-video-button'
import { TranscriptionCard } from './transcription-card'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { VideoTagInput } from './video-tag-input'
import { VideoDescriptionInput } from './video-description-input'

dayjs.extend(relativeTime)

Expand Down Expand Up @@ -67,7 +79,72 @@ export default async function VideoPage({ params }: VideoPageProps) {
</div>
</div>

<div className="grid flex-1 grid-cols-3 gap-4">
<div className="grid flex-1 grid-cols-[1fr_minmax(320px,480px)] gap-4">
<Card className="self-start">
<CardHeader>
<CardTitle>Edit video</CardTitle>
<CardDescription>Update video details</CardDescription>
</CardHeader>
<CardContent>
<form className="space-y-4">
<div className="space-y-2">
<Label htmlFor="title">
Title{' '}
<span className="text-muted-foreground">
(synced with Skylab)
</span>
</Label>
<Input defaultValue={video.title} id="title" />
</div>

<div className="space-y-2">
<Label htmlFor="description">
Description{' '}
<span className="text-muted-foreground">
(synced with Skylab)
</span>
</Label>
<VideoDescriptionInput
videoId={video.id}
id="description"
defaultValue={video?.description ?? ''}
/>
</div>

<div className="space-y-2">
<Label htmlFor="externalProviderId">External ID (Panda)</Label>
<Input
data-empty={!video.externalProviderId}
value={video.externalProviderId ?? '(not generated yet)'}
id="externalProviderId"
className="data-[empty=true]:italic data-[empty=true]:text-muted-foreground"
readOnly
/>
</div>

<div className="space-y-2">
<Label htmlFor="commit">Commit reference</Label>
<div className="flex items-center gap-2">
<Input id="commit" className="flex-1" />
<Button variant="secondary">
<GitHubLogoIcon className="mr-2 h-3 w-3" />
Connect Github
</Button>
</div>
<p className="text-sm text-slate-500 dark:text-slate-400">
Link to Github commit of this lesson with the file diff
</p>
</div>

<div className="space-y-2">
<Label htmlFor="commit">Tags</Label>
<VideoTagInput />
</div>

<Button type="submit">Save video</Button>
</form>
</CardContent>
</Card>
<TranscriptionCard videoId={video.id} />
</div>
</>
Expand Down
2 changes: 1 addition & 1 deletion src/app/(app)/videos/[id]/transcription-card/segment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function Segment({
>
<Badge
variant="outline"
className="px-1.5 py-0 transition-none group-hover:border-white/40 group-focus:border-white/60"
className="px-1.5 py-0 transition-none group-hover:border-primary/20 group-focus:border-white/80 group-focus:text-white"
>
{formatSecondsToMinutes(start)}
</Badge>
Expand Down
48 changes: 48 additions & 0 deletions src/app/(app)/videos/[id]/video-description-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use client'

import { useCompletion } from 'ai/react'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { MagicWandIcon } from '@radix-ui/react-icons'
import { ComponentPropsWithoutRef } from 'react'
import { Loader2 } from 'lucide-react'

export interface VideoDescriptionInputProps
extends ComponentPropsWithoutRef<'textarea'> {
videoId: string
}

export function VideoDescriptionInput({
videoId,
...props
}: VideoDescriptionInputProps) {
const { completion, complete, isLoading } = useCompletion({
api: `/api/ai/generate/description?videoId=${videoId}`,
})

return (
<>
<Textarea
disabled={isLoading}
className="min-h-[160px] leading-relaxed"
value={completion}
{...props}
/>
<div>
<Button
disabled={isLoading}
onClick={() => complete(videoId)}
size="sm"
variant="outline"
>
{isLoading ? (
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
) : (
<MagicWandIcon className="mr-2 h-3 w-3" />
)}
Generate with AI
</Button>
</div>
</>
)
}
118 changes: 118 additions & 0 deletions src/app/(app)/videos/[id]/video-tag-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
'use client'

import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { Separator } from '@/components/ui/separator'
import { cn } from '@/lib/utils'
import { CheckIcon } from '@radix-ui/react-icons'
import { Tag } from 'lucide-react'

const options = [
'ignite',
'node',
'react',
'react-native',
'explorer',
'plus',
'node-ddd',
]

export function VideoTagInput() {
const error: any = false
const tags = ['ignite', 'node']

return (
<Popover>
<PopoverTrigger asChild>
<Button
data-error={!!error}
variant="outline"
size="sm"
className="flex h-8 items-center border-dashed px-2 data-[error=true]:border-red-400 data-[error=true]:bg-red-50"
>
<Tag className="mr-2 h-3 w-3" />
<span className="text-xs">Tags</span>

{!!error && (
<span className="ml-2 text-xs font-normal">{error.message}</span>
)}

{tags.length > 0 && (
<>
<Separator orientation="vertical" className="mx-2 h-4" />
<div className="flex gap-1">
{tags.length > 2 ? (
<Badge
variant="secondary"
className="pointer-events-none rounded-sm px-1 font-normal"
>
{tags.length} selected
</Badge>
) : (
options
.filter((option) => tags.includes(option))
.map((option) => (
<Badge
variant="secondary"
key={option}
className="pointer-events-none rounded-sm px-1 font-normal"
>
{option}
</Badge>
))
)}
</div>
</>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandInput placeholder="Tags" />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{options.map((option) => {
const isSelected = tags.includes(option)

return (
<CommandItem
key={option}
onSelect={() => {
// TODO: make it work
}}
>
<div
className={cn(
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
isSelected
? 'bg-primary text-primary-foreground'
: 'opacity-50 [&_svg]:invisible',
)}
>
<CheckIcon className={cn('h-4 w-4')} />
</div>
<span>{option}</span>
</CommandItem>
)
})}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
56 changes: 56 additions & 0 deletions src/app/api/ai/generate/description/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { openai } from '@/lib/openai'
import { prisma } from '@/lib/prisma'
import { OpenAIStream, StreamingTextResponse } from 'ai'
import { NextResponse } from 'next/server'

export async function POST(request: Request) {
const { prompt } = await request.json()

const [transcription] = await prisma.$queryRaw<[{ text: string }]>/* sql */ `
SELECT
STRING_AGG("public"."TranscriptionSegment"."text", '') as "text"
FROM "public"."TranscriptionSegment"
JOIN "public"."Transcription" ON "public"."Transcription"."id" = "public"."TranscriptionSegment"."transcriptionId"
WHERE "public"."Transcription"."videoId" = ${prompt}
`

if (!transcription.text) {
return NextResponse.json(
{ message: 'Transcription not found.' },
{
status: 400,
},
)
}

const response = await openai.createChatCompletion({
model: 'gpt-3.5-turbo',
stream: true,
messages: [
{
role: 'system',
content:
'Se comporte como um especialista em programação que cria resumos a partir da transcrição de uma aula.',
},
{
role: 'system',
content:
'Responda em primeira pessoa como se você fosse o instrutor da aula. Utilize uma linguagem menos formal.',
},
{
role: 'system',
content:
'Seja sucinto e retorne no máximo 80 palavras em markdown sem cabeçalhos.',
},
{
role: 'user',
content: `Gere um resumo da transcrição abaixo: \n\n ${transcription.text}`,
},
],
temperature: 0,
})

const stream = OpenAIStream(response)

return new StreamingTextResponse(stream)
}
3 changes: 1 addition & 2 deletions src/components/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import { useEffect, useState } from 'react'
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
Expand Down Expand Up @@ -62,7 +61,7 @@ export function Search() {
>
Search videos...
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
<span className="text-base"></span>K
<span className="text-sm"></span>K
</kbd>
</Button>

Expand Down
1 change: 1 addition & 0 deletions src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export default withAuth({

export const config = {
matcher: ['/((?!api/webhooks|_next/static|_next/image|favicon.ico).*)'],
// matcher: ['/none'],
}

0 comments on commit 24626f1

Please sign in to comment.