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}
|