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