Skip to content

Commit

Permalink
feat: support continous voice chatting
Browse files Browse the repository at this point in the history
  • Loading branch information
weaigc committed Jul 24, 2023
1 parent 2de6b3b commit 5d573f8
Show file tree
Hide file tree
Showing 12 changed files with 138 additions and 6 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,17 @@ https://bing.github1s.tk
- 完全基于 Next.js 重写,高度还原 New Bing Web 版 UI,使用体验和 Bing AI 基本一致。
- 支持 Docker 构建,方便快捷地部署和访问。
- Cookie 可全局配置,全局共享。
- 支持持续语音对话

## RoadMap

- [x] 支持 wss 转发
- [x] 支持一键部署
- [x] 优化移动端展示
- [x] 支持画图
- [x] 支持语音输入(支持语音指令)
- [ ] 适配深色模式
- [ ] 支持内置提示词
- [ ] 支持语音输入
- [ ] 支持图片输入
- [ ] 支持离线访问
- [ ] 国际化翻译
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
"react-hot-toast": "^2.4.1",
"react-intersection-observer": "^9.5.2",
"react-markdown": "^8.0.7",
"react-speech-recognition": "^3.10.0",
"regenerator-runtime": "^0.13.11",
"react-syntax-highlighter": "^15.5.0",
"react-textarea-autosize": "^8.5.0",
"react-viewport-list": "^7.1.1",
Expand Down Expand Up @@ -76,6 +78,7 @@
"@types/react-copy-to-clipboard": "^5.0.4",
"@types/react-dom": "18.2.7",
"@types/react-scroll-to-bottom": "^4.2.0",
"@types/react-speech-recognition": "^3.9.2",
"@types/react-syntax-highlighter": "^15.5.6",
"@types/ws": "^8.5.5",
"@typescript-eslint/eslint-plugin": "^5.60.1",
Expand Down
3 changes: 3 additions & 0 deletions src/app/globals.scss
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,9 @@ p {
gap: 16px;
justify-content: space-between;
align-items: flex-start;
&>*:nth-child(n+5) {
display: none;
}
}

.message-input {
Expand Down
18 changes: 18 additions & 0 deletions src/assets/images/speech.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/images/voice.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 6 additions & 2 deletions src/components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import * as React from 'react'
import Image from 'next/image'
import Textarea from 'react-textarea-autosize'
import { useAtomValue } from 'jotai'
import { useEnterSubmit } from '@/lib/hooks/use-enter-submit'
import { cn } from '@/lib/utils'

Expand All @@ -14,6 +15,8 @@ import PinIcon from '@/assets/images/pin.svg'
import PinFillIcon from '@/assets/images/pin-fill.svg'

import { useBing } from '@/lib/hooks/use-bing'
import { voiceListenAtom } from '@/state'
import Voice from './voice'

export interface ChatPanelProps
extends Pick<
Expand Down Expand Up @@ -42,6 +45,7 @@ export function ChatPanel({
const [active, setActive] = React.useState(false)
const [pin, setPin] = React.useState(false)
const [tid, setTid] = React.useState<any>()
const voiceListening = useAtomValue(voiceListenAtom)

const setBlur = React.useCallback(() => {
clearTimeout(tid)
Expand Down Expand Up @@ -110,19 +114,19 @@ export function ChatPanel({
rows={1}
value={input}
onChange={e => setInput(e.target.value.slice(0, 4000))}
placeholder="Shift + Enter 换行"
placeholder={voiceListening ? '持续对话中...对话完成说“发送”即可' : 'Shift + Enter 换行'}
spellCheck={false}
className="message-input min-h-[24px] -mx-1 w-full text-base resize-none bg-transparent focus-within:outline-none"
/>
<Image alt="visual-search" src={VisualSearchIcon} width={20} />
<Voice setInput={setInput} sendMessage={sendMessage} />
<button type="submit">
<Image alt="send" src={SendIcon} width={20} style={{ marginTop: '2px' }} />
</button>
</div>
<div className="body-1 bottom-bar">
<div className="letter-counter"><span>{input.length}</span>/4000</div>
<button onClick={() => {
console.log('onclick')
setPin(!pin)
}} className="pr-2">
<Image alt="pin" src={pin ? PinFillIcon : PinIcon} width={20} />
Expand Down
2 changes: 1 addition & 1 deletion src/components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export type ChatProps = React.ComponentProps<'div'> & { initialMessages?: ChatMe
export default function Chat({ className }: ChatProps) {
const [bingStyle, setBingStyle] = useAtom(bingConversationStyleAtom)

const { messages, sendMessage, resetConversation, stopGenerating, setInput, bot, input, generating } = useBing()
const { messages, sendMessage, resetConversation, stopGenerating, setInput, bot,input, generating } = useBing()

useEffect(() => {
window.scrollTo({
Expand Down
38 changes: 38 additions & 0 deletions src/components/ui/voice/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
$duration-list: 474 433 407 458 400 427;

.voice-button {
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
> div {
background: #52467b;
bottom: 1px;
height: 3px;
width: 1px;
margin: 0px 1px;
border-radius: 5px;
animation: sound 0ms -600ms linear infinite alternate;
@for $i from 1 through length($duration-list) {
$duration: nth($duration-list, $i);
&:nth-child(n + #{$i}) {
left: $i * 10px;
animation-duration: #{$duration}ms;
}
}
}
}


@keyframes sound {
0% {
opacity: .35;
height: 3px;
}
100% {
opacity: 1;
height: 20px;
}
}


9 changes: 9 additions & 0 deletions src/components/ui/voice/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import './index.scss'

export default function Voice(props: React.ComponentProps<'div'>) {
return (
<div className="voice-button" {...props}>
{Array.from({ length: 6 }).map(() => <div />)}
</div>
)
}
52 changes: 52 additions & 0 deletions src/components/voice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { useEffect } from 'react'
import 'regenerator-runtime/runtime'
import { useSetAtom } from 'jotai'
import SpeechRecognition, { useSpeechRecognition } from 'react-speech-recognition'
import { useBing } from '@/lib/hooks/use-bing'
import Image from 'next/image'
import VoiceIcon from '@/assets/images/voice.svg'
import VoiceButton from './ui/voice'
import { voiceListenAtom } from '@/state'

const Voice = ({ setInput, sendMessage }: Pick<ReturnType<typeof useBing>, 'setInput' | 'sendMessage'>) => {
const {
transcript,
listening,
resetTranscript,
browserSupportsSpeechRecognition
} = useSpeechRecognition()

if (!browserSupportsSpeechRecognition) {
return null
}

const setListen = useSetAtom(voiceListenAtom)

useEffect(() => {
if (/[ ](||退)?$/.test(transcript)) {
const command = RegExp.$1
if (command === '退出') {
SpeechRecognition.stopListening()
} else if (command === '发送') {
sendMessage(transcript.slice(0, -3))
}

resetTranscript()
return () => {
SpeechRecognition.stopListening()
}
}
setInput(transcript)
}, [transcript])

useEffect(() => {
setListen(listening)
}, [listening])

return listening ? (
<VoiceButton onClick={SpeechRecognition.stopListening} />
) : (
<Image alt="start voice" src={VoiceIcon} width={24} className="-mt-0.5" onClick={() => SpeechRecognition.startListening({ continuous: true, language: 'zh-CN' })} />
)
};
export default Voice;
2 changes: 0 additions & 2 deletions src/pages/api/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const headers = createHeaders(req.cookies)

debug('headers', headers)

const response = await fetch(API_ENDPOINT, { method: 'GET', headers, redirect: 'error', mode: 'cors', credentials: 'include' })
.then((res) => res.text())
const maxAge = 86400 * 30
res.writeHead(200, {
'Content-Type': 'application/json',
})
Expand Down
3 changes: 3 additions & 0 deletions src/state/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BingWebBot } from '@/lib/bots/bing'
import { BingConversationStyle, ChatMessageModel, BotId } from '@/lib/bots/bing/types'
import { nanoid } from '@/lib/utils'
import { atom } from 'jotai'
import { atomWithImmer } from 'jotai-immer'
import { atomWithStorage } from 'jotai/utils'
import { atomFamily } from 'jotai/utils'
Expand Down Expand Up @@ -113,3 +114,5 @@ export const chatFamily = atomFamily(
export const hashAtom = atomWithHash('dialog', '')

export const locationAtom = atomWithLocation()

export const voiceListenAtom = atom(false)

0 comments on commit 5d573f8

Please sign in to comment.