Spaces:
Sleeping
Sleeping
| import { | |
| useEffect, | |
| useState, | |
| useCallback, | |
| ChangeEvent, | |
| ClipboardEvent, | |
| MouseEventHandler, | |
| FormEvent, | |
| useRef | |
| } from "react" | |
| import Image from 'next/image' | |
| import PasteIcon from '@/assets/images/paste.svg' | |
| import UploadIcon from '@/assets/images/upload.svg' | |
| import CameraIcon from '@/assets/images/camera.svg' | |
| import { useBing } from '@/lib/hooks/use-bing' | |
| import { cn } from '@/lib/utils' | |
| interface ChatImageProps extends Pick<ReturnType<typeof useBing>, 'uploadImage'> {} | |
| const preventDefault: MouseEventHandler<HTMLDivElement> = (event) => { | |
| event.nativeEvent.stopImmediatePropagation() | |
| } | |
| const toBase64 = (file: File): Promise<string> => new Promise((resolve, reject) => { | |
| const reader = new FileReader() | |
| reader.readAsDataURL(file) | |
| reader.onload = () => resolve(reader.result as string) | |
| reader.onerror = reject | |
| }) | |
| export function ChatImage({ children, uploadImage }: React.PropsWithChildren<ChatImageProps>) { | |
| const videoRef = useRef<HTMLVideoElement>(null) | |
| const canvasRef = useRef<HTMLCanvasElement>(null) | |
| const mediaStream = useRef<MediaStream>() | |
| const [panel, setPanel] = useState('none') | |
| const upload = useCallback((url: string) => { | |
| if (url) { | |
| uploadImage(url) | |
| } | |
| setPanel('none') | |
| }, [panel]) | |
| const onUpload = useCallback(async (event: ChangeEvent<HTMLInputElement>) => { | |
| const file = event.target.files?.[0] | |
| if (file) { | |
| const fileDataUrl = await toBase64(file) | |
| if (fileDataUrl) { | |
| upload(fileDataUrl) | |
| } | |
| } | |
| }, []) | |
| const onPaste = useCallback((event: ClipboardEvent<HTMLInputElement>) => { | |
| const pasteUrl = event.clipboardData.getData('text') ?? '' | |
| upload(pasteUrl) | |
| }, []) | |
| const onEnter = useCallback((event: FormEvent<HTMLFormElement>) => { | |
| event.preventDefault() | |
| event.stopPropagation() | |
| // @ts-ignore | |
| const inputUrl = event.target.elements.image.value | |
| if (inputUrl) { | |
| upload(inputUrl) | |
| } | |
| }, []) | |
| const openVideo: MouseEventHandler<HTMLButtonElement> = async (event) => { | |
| event.stopPropagation() | |
| setPanel('camera-mode') | |
| } | |
| const onCapture = () => { | |
| if (canvasRef.current && videoRef.current) { | |
| const canvas = canvasRef.current | |
| canvas.width = videoRef.current!.videoWidth | |
| canvas.height = videoRef.current!.videoHeight | |
| canvas.getContext('2d')?.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height) | |
| const cameraUrl = canvas.toDataURL('image/jpeg') | |
| upload(cameraUrl) | |
| } | |
| } | |
| useEffect(() => { | |
| const handleBlur = () => { | |
| if (panel !== 'none') { | |
| setPanel('none') | |
| } | |
| } | |
| document.addEventListener('click', handleBlur) | |
| return () => { | |
| document.removeEventListener('click', handleBlur) | |
| } | |
| }, [panel]) | |
| useEffect(() => { | |
| if (panel === 'camera-mode') { | |
| navigator.mediaDevices.getUserMedia({ video: true, audio: false }) | |
| .then(videoStream => { | |
| mediaStream.current = videoStream | |
| if (videoRef.current) { | |
| videoRef.current.srcObject = videoStream | |
| } | |
| }) | |
| } else { | |
| if (mediaStream.current) { | |
| mediaStream.current.getTracks().forEach(function(track) { | |
| track.stop() | |
| }) | |
| mediaStream.current = undefined | |
| } | |
| } | |
| }, [panel]) | |
| return ( | |
| <div className="visual-search-container"> | |
| <div onClick={() => panel === 'none' ? setPanel('normal') : setPanel('none')}>{children}</div> | |
| <div className={cn('visual-search', panel)} onClick={preventDefault}> | |
| <div className="normal-content"> | |
| <div className="header"> | |
| <h4>添加图像</h4> | |
| </div> | |
| <div className="paste"> | |
| <Image alt="paste" src={PasteIcon} width={24} /> | |
| <form onSubmitCapture={onEnter}> | |
| <input | |
| className="paste-input" | |
| id="sb_imgpst" | |
| type="text" | |
| name="image" | |
| placeholder="粘贴图像 URL" | |
| aria-label="粘贴图像 URL" | |
| onPaste={onPaste} | |
| onClickCapture={(e) => e.stopPropagation()} | |
| /> | |
| </form> | |
| </div> | |
| <div className="buttons"> | |
| <button type="button" aria-label="从此设备上传"> | |
| <input | |
| id="vs_fileinput" | |
| className="fileinput" | |
| type="file" | |
| accept="image/gif, image/jpeg, image/png, image/webp" | |
| onChange={onUpload} | |
| /> | |
| <Image alt="uplaod" src={UploadIcon} width={20} /> | |
| 从此设备上传 | |
| </button> | |
| <button type="button" aria-label="拍照" onClick={openVideo}> | |
| <Image alt="camera" src={CameraIcon} width={20} /> | |
| 拍照 | |
| </button> | |
| </div> | |
| </div> | |
| {panel === 'camera-mode' && <div className="cam-content"> | |
| <div className="webvideo-container"> | |
| <video className="webvideo" autoPlay muted playsInline ref={videoRef} /> | |
| <canvas className="webcanvas" ref={canvasRef} /> | |
| </div> | |
| <div className="cambtn" role="button" aria-label="拍照" onClick={onCapture}> | |
| <div className="cam-btn-circle-large"></div> | |
| <div className="cam-btn-circle-small"></div> | |
| </div> | |
| </div>} | |
| </div> | |
| </div> | |
| ) | |
| } | |