Spaces:
Running
Running
wuyiqunLu
commited on
feat: generate thumbnail and save on video upload (#43)
Browse files<img width="1035" alt="image"
src="https://github.com/landing-ai/vision-agent-ui/assets/132986242/71c19637-149a-4349-9c4d-0ac9db031231">
- app/api/sign/route.ts +2 -2
- components/chat/ImageSelector.tsx +60 -27
- components/ui/Img.tsx +6 -24
app/api/sign/route.ts
CHANGED
|
@@ -23,9 +23,9 @@ export const POST = withLogging(
|
|
| 23 |
// }
|
| 24 |
|
| 25 |
try {
|
| 26 |
-
const { fileName, fileType, id } = json;
|
| 27 |
|
| 28 |
-
const signedFileName = `${user}/${id
|
| 29 |
const res = await getPresignedUrl(signedFileName, fileType);
|
| 30 |
return Response.json({
|
| 31 |
id,
|
|
|
|
| 23 |
// }
|
| 24 |
|
| 25 |
try {
|
| 26 |
+
const { fileName, fileType, id = nanoid() } = json;
|
| 27 |
|
| 28 |
+
const signedFileName = `${user}/${id}/${fileName}`;
|
| 29 |
const res = await getPresignedUrl(signedFileName, fileType);
|
| 30 |
return Response.json({
|
| 31 |
id,
|
components/chat/ImageSelector.tsx
CHANGED
|
@@ -1,12 +1,16 @@
|
|
| 1 |
'use client';
|
| 2 |
|
| 3 |
-
import React, { useState } from 'react';
|
| 4 |
import useImageUpload from '../../lib/hooks/useImageUpload';
|
| 5 |
import { cn, fetcher } from '@/lib/utils';
|
| 6 |
import { SignedPayload, MessageBase, ChatEntity } from '@/lib/types';
|
| 7 |
import { useRouter } from 'next/navigation';
|
| 8 |
import Loading from '../ui/Loading';
|
| 9 |
import toast from 'react-hot-toast';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
export interface ImageSelectorProps {}
|
| 12 |
|
|
@@ -18,6 +22,39 @@ type Example = {
|
|
| 18 |
const ImageSelector: React.FC<ImageSelectorProps> = () => {
|
| 19 |
const router = useRouter();
|
| 20 |
const [isUploading, setUploading] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
const { getRootProps, getInputProps, isDragActive } = useImageUpload(
|
| 22 |
undefined,
|
| 23 |
async files => {
|
|
@@ -29,42 +66,38 @@ const ImageSelector: React.FC<ImageSelectorProps> = () => {
|
|
| 29 |
const reader = new FileReader();
|
| 30 |
reader.readAsDataURL(files[0]);
|
| 31 |
reader.onload = async () => {
|
| 32 |
-
const
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
body: JSON.stringify({
|
| 36 |
-
fileType: files[0].type,
|
| 37 |
-
fileName: files[0].name,
|
| 38 |
-
}),
|
| 39 |
-
});
|
| 40 |
-
const formData = new FormData();
|
| 41 |
-
Object.entries(fields).forEach(([key, value]) => {
|
| 42 |
-
formData.append(key, value as string);
|
| 43 |
-
});
|
| 44 |
-
formData.append('file', files[0]);
|
| 45 |
-
|
| 46 |
-
const uploadResponse = await fetch(signedUrl, {
|
| 47 |
-
method: 'POST',
|
| 48 |
-
body: formData,
|
| 49 |
-
});
|
| 50 |
-
if (!uploadResponse.ok) {
|
| 51 |
-
toast.error(uploadResponse.statusText);
|
| 52 |
return;
|
| 53 |
}
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
method: 'POST',
|
| 56 |
headers: {
|
| 57 |
'Content-Type': 'application/json',
|
| 58 |
},
|
| 59 |
body: JSON.stringify({
|
| 60 |
-
id,
|
| 61 |
-
url: publicUrl,
|
| 62 |
}),
|
| 63 |
});
|
| 64 |
setUploading(false);
|
| 65 |
-
|
| 66 |
-
router.push(`/chat/${resp.id}`);
|
| 67 |
-
}
|
| 68 |
};
|
| 69 |
},
|
| 70 |
);
|
|
|
|
| 1 |
'use client';
|
| 2 |
|
| 3 |
+
import React, { useCallback, useState } from 'react';
|
| 4 |
import useImageUpload from '../../lib/hooks/useImageUpload';
|
| 5 |
import { cn, fetcher } from '@/lib/utils';
|
| 6 |
import { SignedPayload, MessageBase, ChatEntity } from '@/lib/types';
|
| 7 |
import { useRouter } from 'next/navigation';
|
| 8 |
import Loading from '../ui/Loading';
|
| 9 |
import toast from 'react-hot-toast';
|
| 10 |
+
import {
|
| 11 |
+
generateVideoThumbnails,
|
| 12 |
+
getVideoCover,
|
| 13 |
+
} from '@rajesh896/video-thumbnails-generator';
|
| 14 |
|
| 15 |
export interface ImageSelectorProps {}
|
| 16 |
|
|
|
|
| 22 |
const ImageSelector: React.FC<ImageSelectorProps> = () => {
|
| 23 |
const router = useRouter();
|
| 24 |
const [isUploading, setUploading] = useState(false);
|
| 25 |
+
|
| 26 |
+
const upload = useCallback(async (file: File, chatId?: string) => {
|
| 27 |
+
const { id, signedUrl, publicUrl, fields } = await fetcher<SignedPayload>(
|
| 28 |
+
'/api/sign',
|
| 29 |
+
{
|
| 30 |
+
method: 'POST',
|
| 31 |
+
body: JSON.stringify({
|
| 32 |
+
id: chatId,
|
| 33 |
+
fileType: file.type,
|
| 34 |
+
fileName: file.name,
|
| 35 |
+
}),
|
| 36 |
+
},
|
| 37 |
+
);
|
| 38 |
+
const formData = new FormData();
|
| 39 |
+
Object.entries(fields).forEach(([key, value]) => {
|
| 40 |
+
formData.append(key, value as string);
|
| 41 |
+
});
|
| 42 |
+
formData.append('file', file);
|
| 43 |
+
|
| 44 |
+
const uploadResponse = await fetch(signedUrl, {
|
| 45 |
+
method: 'POST',
|
| 46 |
+
body: formData,
|
| 47 |
+
});
|
| 48 |
+
if (!uploadResponse.ok) {
|
| 49 |
+
toast.error(uploadResponse.statusText);
|
| 50 |
+
return;
|
| 51 |
+
}
|
| 52 |
+
return {
|
| 53 |
+
id,
|
| 54 |
+
publicUrl,
|
| 55 |
+
};
|
| 56 |
+
}, []);
|
| 57 |
+
|
| 58 |
const { getRootProps, getInputProps, isDragActive } = useImageUpload(
|
| 59 |
undefined,
|
| 60 |
async files => {
|
|
|
|
| 66 |
const reader = new FileReader();
|
| 67 |
reader.readAsDataURL(files[0]);
|
| 68 |
reader.onload = async () => {
|
| 69 |
+
const file = files[0];
|
| 70 |
+
const resp = await upload(file);
|
| 71 |
+
if (!resp) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
return;
|
| 73 |
}
|
| 74 |
+
if (file.type === 'video/mp4') {
|
| 75 |
+
const thumbnails = await generateVideoThumbnails(file, 1, 'file');
|
| 76 |
+
fetch(thumbnails[0])
|
| 77 |
+
.then(res => res.blob())
|
| 78 |
+
.then(blob => {
|
| 79 |
+
const thumbnailFile = new File(
|
| 80 |
+
[blob],
|
| 81 |
+
file.name.replace('.mp4', '.png').replace('.MP4', '.png'),
|
| 82 |
+
{
|
| 83 |
+
type: 'image/png',
|
| 84 |
+
},
|
| 85 |
+
);
|
| 86 |
+
return upload(thumbnailFile, resp.id);
|
| 87 |
+
});
|
| 88 |
+
}
|
| 89 |
+
await fetcher<ChatEntity>('/api/upload', {
|
| 90 |
method: 'POST',
|
| 91 |
headers: {
|
| 92 |
'Content-Type': 'application/json',
|
| 93 |
},
|
| 94 |
body: JSON.stringify({
|
| 95 |
+
id: resp.id,
|
| 96 |
+
url: resp.publicUrl,
|
| 97 |
}),
|
| 98 |
});
|
| 99 |
setUploading(false);
|
| 100 |
+
router.push(`/chat/${resp.id}`);
|
|
|
|
|
|
|
| 101 |
};
|
| 102 |
},
|
| 103 |
);
|
components/ui/Img.tsx
CHANGED
|
@@ -1,10 +1,8 @@
|
|
| 1 |
'use client';
|
| 2 |
|
| 3 |
-
import React
|
| 4 |
import Image from 'next/image';
|
| 5 |
import { cn } from '@/lib/utils';
|
| 6 |
-
import { toast } from 'react-hot-toast';
|
| 7 |
-
import { getVideoCover } from '@rajesh896/video-thumbnails-generator';
|
| 8 |
|
| 9 |
const placeholder =
|
| 10 |
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAgEASABIAAD/4QDWRXhpZgAATU0AKgAAAAgABwEGAAMAAAABAAIAAAESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAITAAMAAAABAAEAAIdpAAQAAAABAAAAcgAAAAAAAABIAAAAAQAAAEgAAAABAAeQAAAHAAAABDAyMjGRAQAHAAAABAECAwCgAAAHAAAABDAxMDCgAQADAAAAAQABAACgAgAEAAAAAQAAADKgAwAEAAAAAQAAADKkBgADAAAAAQAAAAAAAAAAAAD/wAARCAAyADIDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9sAQwABAQEBAQECAQECAwICAgMEAwMDAwQGBAQEBAQGBwYGBgYGBgcHBwcHBwcHCAgICAgICQkJCQkLCwsLCwsLCwsL/9sAQwECAgIDAwMFAwMFCwgGCAsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsL/90ABAAE/9oADAMBAAIRAxEAPwD+nbSfDst/NnB5Ne5+G/htJMqsVOD610fw/wDCS3Lq7L3r6y0Lw7b20SgqMiluB4hpvwti2Asn51fufhVbsmQlfSCQQQjHFOIt24OKLAfEXiL4XtCjMiGvnrxH4SlsJG+UjFfqZf6Va3UZUgV84/ETwTGY3kRaVrbAfAp058//AK6T+zn/AM5/wr1uTw0BIwx3P+elM/4Roen+fyo5gP/Q/tp+G1vGIVJFer6n4lstGizM4XFeH/DzVUW3HPavmL9pX4sXvhmKV0Yqq5qbgfXeq/GjRLJipmH51yMn7Qvh2NtrTL+dfzYfGL9uqTw1PLGZjkE96+M7j/gotqtzf+WkrYz60rsD+1bwt8YtD12ZYYZlJPvXY+KlivdOMy8giv5q/wBjD9prxB8QNcgXezKxHOa/obsdZa48KRPMTuKiqA8dm09PNbjuaj/s9PSp5rtDM5B7nvUf2tfX9f8A69QB/9H+v3wLrW1RHuxmvJv2iPh43jLRZhEuSynpWN4X8Rm0lCs2BnivoKy1iy1W1ENyQcjFQB/J7+0x+x34yvtVmlsY3OScYBr418KfsLfES81xFmgkALDPBr+1rXPhR4d15jJJEj59qwtP+BXhiznE4t0BHsKEugH54fsJfsqS/Dqzt7m/TDgAnIr9j9Q1BNN0kW4OAq1yun2mk+G7fZAFG0dq868Y+LwyMit+FFtAEl1tfNb5x1NR/wBtr/fH+fwrxptbnLE5703+2p6QH//S/pMs+JjivYfDjv5Q5NePWn+uNev+HP8AVCkwPWdPdtvU1fnkk2n5j09aztP+7V6f7p+lLoBxeuu4jOCa8N1xmMjEnNe4a7/qzXh2t/6xqfQDlqKKKgD/2Q==';
|
|
@@ -20,32 +18,16 @@ const Img = React.forwardRef<
|
|
| 20 |
});
|
| 21 |
// const [isLoading, setIsLoading] = React.useState(true);
|
| 22 |
const [_, startTransition] = React.useTransition();
|
| 23 |
-
const [thumbnail, setThumbnail] = useState<string>('');
|
| 24 |
const isVideo =
|
| 25 |
typeof src === 'string' ? src.toLowerCase().endsWith('.mp4') : false;
|
| 26 |
|
| 27 |
-
useEffect(() => {
|
| 28 |
-
if (!isVideo) {
|
| 29 |
-
return;
|
| 30 |
-
}
|
| 31 |
-
const generateThumbnail = async () => {
|
| 32 |
-
try {
|
| 33 |
-
const cover = await getVideoCover(src as string);
|
| 34 |
-
setThumbnail(cover);
|
| 35 |
-
} catch (e) {
|
| 36 |
-
toast.error((e as Error).message);
|
| 37 |
-
}
|
| 38 |
-
};
|
| 39 |
-
generateThumbnail();
|
| 40 |
-
}, [isVideo, src]);
|
| 41 |
-
|
| 42 |
-
if (isVideo && !thumbnail) {
|
| 43 |
-
return null;
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
return (
|
| 47 |
<Image
|
| 48 |
-
src={
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
placeholder={placeholder}
|
| 50 |
width={dimensions.width}
|
| 51 |
height={dimensions.height}
|
|
|
|
| 1 |
'use client';
|
| 2 |
|
| 3 |
+
import React from 'react';
|
| 4 |
import Image from 'next/image';
|
| 5 |
import { cn } from '@/lib/utils';
|
|
|
|
|
|
|
| 6 |
|
| 7 |
const placeholder =
|
| 8 |
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAgEASABIAAD/4QDWRXhpZgAATU0AKgAAAAgABwEGAAMAAAABAAIAAAESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAITAAMAAAABAAEAAIdpAAQAAAABAAAAcgAAAAAAAABIAAAAAQAAAEgAAAABAAeQAAAHAAAABDAyMjGRAQAHAAAABAECAwCgAAAHAAAABDAxMDCgAQADAAAAAQABAACgAgAEAAAAAQAAADKgAwAEAAAAAQAAADKkBgADAAAAAQAAAAAAAAAAAAD/wAARCAAyADIDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9sAQwABAQEBAQECAQECAwICAgMEAwMDAwQGBAQEBAQGBwYGBgYGBgcHBwcHBwcHCAgICAgICQkJCQkLCwsLCwsLCwsL/9sAQwECAgIDAwMFAwMFCwgGCAsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsL/90ABAAE/9oADAMBAAIRAxEAPwD+nbSfDst/NnB5Ne5+G/htJMqsVOD610fw/wDCS3Lq7L3r6y0Lw7b20SgqMiluB4hpvwti2Asn51fufhVbsmQlfSCQQQjHFOIt24OKLAfEXiL4XtCjMiGvnrxH4SlsJG+UjFfqZf6Va3UZUgV84/ETwTGY3kRaVrbAfAp058//AK6T+zn/AM5/wr1uTw0BIwx3P+elM/4Roen+fyo5gP/Q/tp+G1vGIVJFer6n4lstGizM4XFeH/DzVUW3HPavmL9pX4sXvhmKV0Yqq5qbgfXeq/GjRLJipmH51yMn7Qvh2NtrTL+dfzYfGL9uqTw1PLGZjkE96+M7j/gotqtzf+WkrYz60rsD+1bwt8YtD12ZYYZlJPvXY+KlivdOMy8giv5q/wBjD9prxB8QNcgXezKxHOa/obsdZa48KRPMTuKiqA8dm09PNbjuaj/s9PSp5rtDM5B7nvUf2tfX9f8A69QB/9H+v3wLrW1RHuxmvJv2iPh43jLRZhEuSynpWN4X8Rm0lCs2BnivoKy1iy1W1ENyQcjFQB/J7+0x+x34yvtVmlsY3OScYBr418KfsLfES81xFmgkALDPBr+1rXPhR4d15jJJEj59qwtP+BXhiznE4t0BHsKEugH54fsJfsqS/Dqzt7m/TDgAnIr9j9Q1BNN0kW4OAq1yun2mk+G7fZAFG0dq868Y+LwyMit+FFtAEl1tfNb5x1NR/wBtr/fH+fwrxptbnLE5703+2p6QH//S/pMs+JjivYfDjv5Q5NePWn+uNev+HP8AVCkwPWdPdtvU1fnkk2n5j09aztP+7V6f7p+lLoBxeuu4jOCa8N1xmMjEnNe4a7/qzXh2t/6xqfQDlqKKKgD/2Q==';
|
|
|
|
| 18 |
});
|
| 19 |
// const [isLoading, setIsLoading] = React.useState(true);
|
| 20 |
const [_, startTransition] = React.useTransition();
|
|
|
|
| 21 |
const isVideo =
|
| 22 |
typeof src === 'string' ? src.toLowerCase().endsWith('.mp4') : false;
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
return (
|
| 25 |
<Image
|
| 26 |
+
src={
|
| 27 |
+
isVideo
|
| 28 |
+
? (src as string).replace('.mp4', '.png').replace('.MP4', '.png')
|
| 29 |
+
: src
|
| 30 |
+
}
|
| 31 |
placeholder={placeholder}
|
| 32 |
width={dimensions.width}
|
| 33 |
height={dimensions.height}
|