wuyiqunLu commited on
Commit
bc0330e
1 Parent(s): d49cc42

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 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 ?? nanoid()}/${fileName}`;
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 { id, signedUrl, publicUrl, fields } =
33
- await fetcher<SignedPayload>('/api/sign', {
34
- method: 'POST',
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
- const resp = await fetcher<ChatEntity>('/api/upload', {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- if (resp) {
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, { useEffect, useState } from '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={isVideo ? thumbnail : 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}