File size: 5,392 Bytes
15bfa8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
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>
  )
}