Skip to content

Commit 6bb5a81

Browse files
PetaflopsjosStorer
authored andcommitted
feat: added a ReadButton to convert the text output from GPT into voice and read it out loud
1 parent efb7cc9 commit 6bb5a81

File tree

3 files changed

+63
-0
lines changed

3 files changed

+63
-0
lines changed

src/components/ConversationItem/index.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState } from 'react'
22
import { ChevronDownIcon, XCircleIcon, SyncIcon } from '@primer/octicons-react'
33
import CopyButton from '../CopyButton'
4+
import ReadButton from '../ReadButton'
45
import PropTypes from 'prop-types'
56
import MarkdownRender from '../MarkdownRender/markdown.jsx'
67
import { useTranslation } from 'react-i18next'
@@ -73,6 +74,9 @@ export function ConversationItem({ type, content, session, done, port, onRetry }
7374
{session && (
7475
<CopyButton contentFn={() => content.replace(/\n<hr\/>$/, '')} size={14} />
7576
)}
77+
{session && (
78+
<ReadButton contentFn={() => content.replace(/\n<hr\/>$/, '')} size={14} />
79+
)}
7680
{!collapsed ? (
7781
<span
7882
title={t('Collapse')}

src/components/ReadButton/index.jsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { useState } from 'react'
2+
import { UnmuteIcon, MuteIcon } from '@primer/octicons-react'
3+
import PropTypes from 'prop-types'
4+
import { useTranslation } from 'react-i18next'
5+
6+
ReadButton.propTypes = {
7+
contentFn: PropTypes.func.isRequired,
8+
size: PropTypes.number.isRequired,
9+
className: PropTypes.string,
10+
}
11+
12+
function ReadButton({ className, contentFn, size }) {
13+
const { t } = useTranslation()
14+
const [speaking, setSpeaking] = useState(false)
15+
16+
const startSpeak = () => {
17+
speechSynthesis.cancel()
18+
19+
let text = contentFn()
20+
21+
const utterance = new SpeechSynthesisUtterance(text)
22+
Object.assign(utterance, {
23+
// lang: 'zh-CN',
24+
rate: 0.9,
25+
volume: 1,
26+
onend: () => setSpeaking(false),
27+
onerror: () => setSpeaking(false),
28+
})
29+
30+
let supportedVoices = speechSynthesis.getVoices()
31+
for (let i = 0; i < supportedVoices.length; i++) {
32+
if (supportedVoices[i].lang.indexOf(text[0]) >= 0) {
33+
utterance.voice = supportedVoices[i]
34+
break
35+
}
36+
}
37+
38+
speechSynthesis.speak(utterance)
39+
setSpeaking(true)
40+
}
41+
42+
const stopSpeak = () => {
43+
speechSynthesis.cancel()
44+
setSpeaking(false)
45+
}
46+
47+
return (
48+
<span
49+
title={t('Read')}
50+
className={`gpt-util-icon ${className ? className : ''}`}
51+
onClick={speaking ? stopSpeak : startSpeak}
52+
>
53+
{speaking ? <MuteIcon size={size} /> : <UnmuteIcon size={size} />}
54+
</span>
55+
)
56+
}
57+
58+
export default ReadButton

src/components/index.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from './DeleteButton'
77
export * from './FeedbackForChatGPTWeb'
88
export * from './FloatingToolbar'
99
export * from './InputBox'
10+
export * from './ReadButton'

0 commit comments

Comments
 (0)