Update import statement in local-duckduckgo.ts, prompt.tsx, wxt.config.ts, webui.tsx, PlaygroundChat.tsx, other.tsx, Markdown.tsx, and useMessageOption.tsx
This commit is contained in:
parent
291f7392c2
commit
a3810cd534
@ -1,7 +1,6 @@
|
|||||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||||
import remarkGfm from "remark-gfm"
|
import remarkGfm from "remark-gfm"
|
||||||
import { nightOwl } from "react-syntax-highlighter/dist/cjs/styles/prism"
|
import { nightOwl } from "react-syntax-highlighter/dist/cjs/styles/prism"
|
||||||
import rehypeMathjax from "rehype-mathjax"
|
|
||||||
import remarkMath from "remark-math"
|
import remarkMath from "remark-math"
|
||||||
import ReactMarkdown from "react-markdown"
|
import ReactMarkdown from "react-markdown"
|
||||||
import "property-information"
|
import "property-information"
|
||||||
@ -19,7 +18,6 @@ export default function Markdown({ message }: { message: string }) {
|
|||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
className="prose break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark"
|
className="prose break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark"
|
||||||
remarkPlugins={[remarkGfm, remarkMath]}
|
remarkPlugins={[remarkGfm, remarkMath]}
|
||||||
rehypePlugins={[rehypeMathjax]}
|
|
||||||
components={{
|
components={{
|
||||||
code({ node, inline, className, children, ...props }) {
|
code({ node, inline, className, children, ...props }) {
|
||||||
const match = /language-(\w+)/.exec(className || "")
|
const match = /language-(\w+)/.exec(className || "")
|
||||||
|
@ -2,10 +2,18 @@ import Markdown from "../../Common/Markdown"
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { Image, Tooltip } from "antd"
|
import { Image, Tooltip } from "antd"
|
||||||
import { WebSearch } from "./WebSearch"
|
import { WebSearch } from "./WebSearch"
|
||||||
import { CheckIcon, ClipboardIcon, Pen, RotateCcw } from "lucide-react"
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
ClipboardIcon,
|
||||||
|
Pen,
|
||||||
|
PlayIcon,
|
||||||
|
RotateCcw,
|
||||||
|
Square
|
||||||
|
} from "lucide-react"
|
||||||
import { EditMessageForm } from "./EditMessageForm"
|
import { EditMessageForm } from "./EditMessageForm"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { MessageSource } from "./MessageSource"
|
import { MessageSource } from "./MessageSource"
|
||||||
|
import { useTTS } from "@/hooks/useTTS"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
message: string
|
message: string
|
||||||
@ -25,6 +33,7 @@ type Props = {
|
|||||||
sources?: any[]
|
sources?: any[]
|
||||||
hideEditAndRegenerate?: boolean
|
hideEditAndRegenerate?: boolean
|
||||||
onSourceClick?: (source: any) => void
|
onSourceClick?: (source: any) => void
|
||||||
|
isTTSEnabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlaygroundMessage = (props: Props) => {
|
export const PlaygroundMessage = (props: Props) => {
|
||||||
@ -32,6 +41,7 @@ export const PlaygroundMessage = (props: Props) => {
|
|||||||
const [editMode, setEditMode] = React.useState(false)
|
const [editMode, setEditMode] = React.useState(false)
|
||||||
|
|
||||||
const { t } = useTranslation("common")
|
const { t } = useTranslation("common")
|
||||||
|
const { cancel, isSpeaking, speak } = useTTS()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group w-full text-gray-800 dark:text-gray-100">
|
<div className="group w-full text-gray-800 dark:text-gray-100">
|
||||||
@ -98,8 +108,10 @@ export const PlaygroundMessage = (props: Props) => {
|
|||||||
<div className="mb-3 flex flex-wrap gap-2">
|
<div className="mb-3 flex flex-wrap gap-2">
|
||||||
{props?.sources?.map((source, index) => (
|
{props?.sources?.map((source, index) => (
|
||||||
<MessageSource
|
<MessageSource
|
||||||
onSourceClick={props.onSourceClick}
|
onSourceClick={props.onSourceClick}
|
||||||
key={index} source={source} />
|
key={index}
|
||||||
|
source={source}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -110,11 +122,31 @@ export const PlaygroundMessage = (props: Props) => {
|
|||||||
? "hidden group-hover:flex"
|
? "hidden group-hover:flex"
|
||||||
: "flex"
|
: "flex"
|
||||||
}`}>
|
}`}>
|
||||||
|
{props.isTTSEnabled && (
|
||||||
|
<Tooltip title={t("tts")}>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (isSpeaking) {
|
||||||
|
cancel()
|
||||||
|
} else {
|
||||||
|
speak({
|
||||||
|
utterance: props.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
||||||
|
{!isSpeaking ? (
|
||||||
|
<PlayIcon className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
||||||
|
) : (
|
||||||
|
<Square className="w-3 h-3 text-red-400 group-hover:text-red-500" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
{props.isBot && (
|
{props.isBot && (
|
||||||
<>
|
<>
|
||||||
{!props.hideCopy && (
|
{!props.hideCopy && (
|
||||||
<Tooltip title={t("copyToClipboard")}
|
<Tooltip title={t("copyToClipboard")}>
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(props.message)
|
navigator.clipboard.writeText(props.message)
|
||||||
@ -135,8 +167,7 @@ export const PlaygroundMessage = (props: Props) => {
|
|||||||
|
|
||||||
{!props.hideEditAndRegenerate &&
|
{!props.hideEditAndRegenerate &&
|
||||||
props.currentMessageIndex === props.totalMessages - 1 && (
|
props.currentMessageIndex === props.totalMessages - 1 && (
|
||||||
<Tooltip title={t("regenerate")}
|
<Tooltip title={t("regenerate")}>
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
onClick={props.onRengerate}
|
onClick={props.onRengerate}
|
||||||
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
||||||
@ -147,8 +178,7 @@ export const PlaygroundMessage = (props: Props) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!props.hideEditAndRegenerate && (
|
{!props.hideEditAndRegenerate && (
|
||||||
<Tooltip title={t("edit")}
|
<Tooltip title={t("edit")}>
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setEditMode(true)}
|
onClick={() => setEditMode(true)}
|
||||||
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
||||||
|
@ -10,7 +10,8 @@ export const PlaygroundChat = () => {
|
|||||||
streaming,
|
streaming,
|
||||||
regenerateLastMessage,
|
regenerateLastMessage,
|
||||||
isSearchingInternet,
|
isSearchingInternet,
|
||||||
editMessage
|
editMessage,
|
||||||
|
ttsEnabled
|
||||||
} = useMessageOption()
|
} = useMessageOption()
|
||||||
const divRef = React.useRef<HTMLDivElement>(null)
|
const divRef = React.useRef<HTMLDivElement>(null)
|
||||||
const [isSourceOpen, setIsSourceOpen] = React.useState(false)
|
const [isSourceOpen, setIsSourceOpen] = React.useState(false)
|
||||||
@ -50,6 +51,7 @@ export const PlaygroundChat = () => {
|
|||||||
setSource(data)
|
setSource(data)
|
||||||
setIsSourceOpen(true)
|
setIsSourceOpen(true)
|
||||||
}}
|
}}
|
||||||
|
isTTSEnabled={ttsEnabled}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{messages.length > 0 && (
|
{messages.length > 0 && (
|
||||||
|
@ -8,6 +8,7 @@ import { MoonIcon, SunIcon } from "lucide-react"
|
|||||||
import { SearchModeSettings } from "./search-mode"
|
import { SearchModeSettings } from "./search-mode"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useI18n } from "@/hooks/useI18n"
|
import { useI18n } from "@/hooks/useI18n"
|
||||||
|
import { TTSModeSettings } from "./tts-mode"
|
||||||
|
|
||||||
export const SettingOther = () => {
|
export const SettingOther = () => {
|
||||||
const { clearChat, speechToTextLanguage, setSpeechToTextLanguage } =
|
const { clearChat, speechToTextLanguage, setSpeechToTextLanguage } =
|
||||||
@ -89,6 +90,7 @@ export const SettingOther = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<SearchModeSettings />
|
<SearchModeSettings />
|
||||||
|
<TTSModeSettings />
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
|
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
|
||||||
|
@ -5,8 +5,6 @@ import { useTranslation } from "react-i18next"
|
|||||||
import { SaveButton } from "~/components/Common/SaveButton"
|
import { SaveButton } from "~/components/Common/SaveButton"
|
||||||
import {
|
import {
|
||||||
getWebSearchPrompt,
|
getWebSearchPrompt,
|
||||||
setSystemPromptForNonRagOption,
|
|
||||||
systemPromptForNonRagOption,
|
|
||||||
geWebSearchFollowUpPrompt,
|
geWebSearchFollowUpPrompt,
|
||||||
setWebPrompts,
|
setWebPrompts,
|
||||||
promptForRag,
|
promptForRag,
|
||||||
|
@ -44,43 +44,50 @@ export const SearchModeSettings = () => {
|
|||||||
await setSearchSettings(values)
|
await setSearchSettings(values)
|
||||||
})}
|
})}
|
||||||
className="space-y-4">
|
className="space-y-4">
|
||||||
<div className="flex flex-row justify-between">
|
<div className="flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between">
|
||||||
<span className="text-gray-500 dark:text-neutral-50 ">
|
<span className="text-gray-500 dark:text-neutral-50 ">
|
||||||
{t("generalSettings.webSearch.provider.label")}
|
{t("generalSettings.webSearch.provider.label")}
|
||||||
</span>
|
</span>
|
||||||
<Select
|
<div>
|
||||||
placeholder={t("generalSettings.webSearch.provider.placeholder")}
|
<Select
|
||||||
showSearch
|
placeholder={t("generalSettings.webSearch.provider.placeholder")}
|
||||||
style={{ width: "200px" }}
|
showSearch
|
||||||
options={SUPPORTED_SERACH_PROVIDERS}
|
className="w-full mt-4 sm:mt-0 sm:w-[200px]"
|
||||||
filterOption={(input, option) =>
|
options={SUPPORTED_SERACH_PROVIDERS}
|
||||||
option!.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 ||
|
filterOption={(input, option) =>
|
||||||
option!.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
option!.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 ||
|
||||||
}
|
option!.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||||
{...form.getInputProps("searchProvider")}
|
}
|
||||||
/>
|
{...form.getInputProps("searchProvider")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row justify-between">
|
<div className="flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between">
|
||||||
<span className="text-gray-500 dark:text-neutral-50 ">
|
<span className="text-gray-500 dark:text-neutral-50 ">
|
||||||
{t("generalSettings.webSearch.searchMode.label")}
|
{t("generalSettings.webSearch.searchMode.label")}
|
||||||
</span>
|
</span>
|
||||||
<Switch
|
<div>
|
||||||
{...form.getInputProps("isSimpleInternetSearch", {
|
<Switch
|
||||||
type: "checkbox"
|
className="mt-4 sm:mt-0"
|
||||||
})}
|
{...form.getInputProps("isSimpleInternetSearch", {
|
||||||
/>
|
type: "checkbox"
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row justify-between">
|
<div className="flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between">
|
||||||
<span className="text-gray-500 dark:text-neutral-50 ">
|
<span className="text-gray-500 dark:text-neutral-50 ">
|
||||||
{t("generalSettings.webSearch.totalSearchResults.label")}
|
{t("generalSettings.webSearch.totalSearchResults.label")}
|
||||||
</span>
|
</span>
|
||||||
<InputNumber
|
<div>
|
||||||
placeholder={t(
|
<InputNumber
|
||||||
"generalSettings.webSearch.totalSearchResults.placeholder"
|
placeholder={t(
|
||||||
)}
|
"generalSettings.webSearch.totalSearchResults.placeholder"
|
||||||
{...form.getInputProps("totalSearchResults")}
|
)}
|
||||||
style={{ width: "200px" }}
|
{...form.getInputProps("totalSearchResults")}
|
||||||
/>
|
className="!w-full mt-4 sm:mt-0 sm:w-[200px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
|
116
src/components/Option/Settings/tts-mode.tsx
Normal file
116
src/components/Option/Settings/tts-mode.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { SaveButton } from "@/components/Common/SaveButton"
|
||||||
|
import { getSearchSettings, setSearchSettings } from "@/services/search"
|
||||||
|
import { getTTSSettings, setTTSSettings } from "@/services/tts"
|
||||||
|
import { SUPPORTED_SERACH_PROVIDERS } from "@/utils/search-provider"
|
||||||
|
import { useForm } from "@mantine/form"
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
|
import { Select, Skeleton, Switch, InputNumber } from "antd"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
export const TTSModeSettings = ({ hideTitle }: { hideTitle?: boolean }) => {
|
||||||
|
const { t } = useTranslation("settings")
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
ttsEnabled: false,
|
||||||
|
ttsProvider: "",
|
||||||
|
voice: "",
|
||||||
|
ssmlEnabled: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { status, data } = useQuery({
|
||||||
|
queryKey: ["fetchTTSSettings"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const data = await getTTSSettings()
|
||||||
|
form.setValues(data)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (status === "pending" || status === "error") {
|
||||||
|
return <Skeleton active />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{!hideTitle && (
|
||||||
|
<div className="mb-5">
|
||||||
|
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
|
||||||
|
{t("generalSettings.tts.heading")}
|
||||||
|
</h2>
|
||||||
|
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit(async (values) => {
|
||||||
|
await setTTSSettings(values)
|
||||||
|
})}
|
||||||
|
className="space-y-4">
|
||||||
|
<div className="flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between">
|
||||||
|
<span className="text-gray-500 dark:text-neutral-50 ">
|
||||||
|
{t("generalSettings.tts.ttsEnabled.label")}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
className="mt-4 sm:mt-0"
|
||||||
|
{...form.getInputProps("ttsEnabled", {
|
||||||
|
type: "checkbox"
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between">
|
||||||
|
<span className="text-gray-500 dark:text-neutral-50 ">
|
||||||
|
{t("generalSettings.tts.ttsProvider.label")}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
placeholder={t("generalSettings.tts.ttsProvider.placeholder")}
|
||||||
|
className="w-full mt-4 sm:mt-0 sm:w-[200px]"
|
||||||
|
options={[{ label: "Browser TTS", value: "browser" }]}
|
||||||
|
{...form.getInputProps("ttsProvider")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between">
|
||||||
|
<span className="text-gray-500 dark:text-neutral-50 ">
|
||||||
|
{t("generalSettings.tts.ttsVoice.label")}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
placeholder={t("generalSettings.tts.ttsVoice.placeholder")}
|
||||||
|
className="w-full mt-4 sm:mt-0 sm:w-[200px]"
|
||||||
|
options={data?.browserTTSVoices?.map(
|
||||||
|
(voice) =>
|
||||||
|
({
|
||||||
|
label: `${voice.voiceName} - ${voice.lang}`.trim(),
|
||||||
|
value: voice.voiceName
|
||||||
|
}) || []
|
||||||
|
)}
|
||||||
|
{...form.getInputProps("voice")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between">
|
||||||
|
<span className="text-gray-500 dark:text-neutral-50 ">
|
||||||
|
{t("generalSettings.tts.ssmlEnabled.label")}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
className="mt-4 sm:mt-0"
|
||||||
|
{...form.getInputProps("ssmlEnabled", {
|
||||||
|
type: "checkbox"
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<SaveButton btnType="submit" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -28,6 +28,8 @@ import { usePageAssist } from "@/context"
|
|||||||
import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama"
|
import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama"
|
||||||
import { PageAssistVectorStore } from "@/libs/PageAssistVectorStore"
|
import { PageAssistVectorStore } from "@/libs/PageAssistVectorStore"
|
||||||
import { formatDocs } from "@/chain/chat-with-x"
|
import { formatDocs } from "@/chain/chat-with-x"
|
||||||
|
import { useWebUI } from "@/store/webui"
|
||||||
|
import { isTTSEnabled } from "@/services/tts"
|
||||||
|
|
||||||
export const useMessageOption = () => {
|
export const useMessageOption = () => {
|
||||||
const {
|
const {
|
||||||
@ -66,11 +68,22 @@ export const useMessageOption = () => {
|
|||||||
setSelectedKnowledge
|
setSelectedKnowledge
|
||||||
} = useStoreMessageOption()
|
} = useStoreMessageOption()
|
||||||
|
|
||||||
|
const { ttsEnabled, setTTSEnabled } = useWebUI()
|
||||||
|
|
||||||
const { t } = useTranslation("option")
|
const { t } = useTranslation("option")
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const textareaRef = React.useRef<HTMLTextAreaElement>(null)
|
const textareaRef = React.useRef<HTMLTextAreaElement>(null)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const checkTTSEnabled = async () => {
|
||||||
|
const tts = await isTTSEnabled()
|
||||||
|
setTTSEnabled(tts)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkTTSEnabled()
|
||||||
|
}, [])
|
||||||
|
|
||||||
const clearChat = () => {
|
const clearChat = () => {
|
||||||
navigate("/")
|
navigate("/")
|
||||||
setMessages([])
|
setMessages([])
|
||||||
@ -835,7 +848,7 @@ export const useMessageOption = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentHumanMessage = newMessages[index]
|
const currentHumanMessage = newMessages[index]
|
||||||
|
newMessages[index].message = message
|
||||||
const previousMessages = newMessages.slice(0, index + 1)
|
const previousMessages = newMessages.slice(0, index + 1)
|
||||||
setMessages(previousMessages)
|
setMessages(previousMessages)
|
||||||
const previousHistory = newHistory.slice(0, index)
|
const previousHistory = newHistory.slice(0, index)
|
||||||
@ -893,6 +906,7 @@ export const useMessageOption = () => {
|
|||||||
setSelectedSystemPrompt,
|
setSelectedSystemPrompt,
|
||||||
textareaRef,
|
textareaRef,
|
||||||
selectedKnowledge,
|
selectedKnowledge,
|
||||||
setSelectedKnowledge
|
setSelectedKnowledge,
|
||||||
|
ttsEnabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
54
src/hooks/useTTS.tsx
Normal file
54
src/hooks/useTTS.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { notification } from "antd"
|
||||||
|
import { getVoice, isSSMLEnabled } from "@/services/tts"
|
||||||
|
import { markdownToSSML } from "@/utils/markdown-to-ssml"
|
||||||
|
|
||||||
|
type VoiceOptions = {
|
||||||
|
utterance: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTTS = () => {
|
||||||
|
const [isSpeaking, setIsSpeaking] = useState(false)
|
||||||
|
|
||||||
|
const speak = async ({ utterance }: VoiceOptions) => {
|
||||||
|
try {
|
||||||
|
const voice = await getVoice()
|
||||||
|
const isSSML = await isSSMLEnabled()
|
||||||
|
if (isSSML) {
|
||||||
|
utterance = markdownToSSML(utterance)
|
||||||
|
}
|
||||||
|
chrome.tts.speak(utterance, {
|
||||||
|
voiceName: voice,
|
||||||
|
onEvent(event) {
|
||||||
|
if (event.type === "start") {
|
||||||
|
setIsSpeaking(true)
|
||||||
|
} else if (event.type === "end") {
|
||||||
|
setIsSpeaking(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
notification.error({
|
||||||
|
message: "Error",
|
||||||
|
description: "Something went wrong while trying to play the audio"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
chrome.tts.stop()
|
||||||
|
setIsSpeaking(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
speak,
|
||||||
|
cancel,
|
||||||
|
isSpeaking
|
||||||
|
}
|
||||||
|
}
|
91
src/services/tts.ts
Normal file
91
src/services/tts.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { Storage } from "@plasmohq/storage"
|
||||||
|
|
||||||
|
const storage = new Storage()
|
||||||
|
|
||||||
|
const DEFAULT_TTS_PROVIDER = "browser"
|
||||||
|
|
||||||
|
const AVAILABLE_TTS_PROVIDERS = ["browser"] as const
|
||||||
|
|
||||||
|
export const getTTSProvider = async (): Promise<
|
||||||
|
(typeof AVAILABLE_TTS_PROVIDERS)[number]
|
||||||
|
> => {
|
||||||
|
const ttsProvider = await storage.get("ttsProvider")
|
||||||
|
if (!ttsProvider || ttsProvider.length === 0) {
|
||||||
|
return DEFAULT_TTS_PROVIDER
|
||||||
|
}
|
||||||
|
return ttsProvider as (typeof AVAILABLE_TTS_PROVIDERS)[number]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setTTSProvider = async (ttsProvider: string) => {
|
||||||
|
await storage.set("ttsProvider", ttsProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getBrowserTTSVoices = async () => {
|
||||||
|
const tts = await chrome.tts.getVoices()
|
||||||
|
return tts
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getVoice = async () => {
|
||||||
|
const voice = await storage.get("voice")
|
||||||
|
return voice
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setVoice = async (voice: string) => {
|
||||||
|
await storage.set("voice", voice)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isTTSEnabled = async () => {
|
||||||
|
const data = await storage.get("isTTSEnabled")
|
||||||
|
return data === "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setTTSEnabled = async (isTTSEnabled: boolean) => {
|
||||||
|
await storage.set("isTTSEnabled", isTTSEnabled.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isSSMLEnabled = async () => {
|
||||||
|
const data = await storage.get("isSSMLEnabled")
|
||||||
|
return data === "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setSSMLEnabled = async (isSSMLEnabled: boolean) => {
|
||||||
|
await storage.set("isSSMLEnabled", isSSMLEnabled.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTTSSettings = async () => {
|
||||||
|
const [ttsEnabled, ttsProvider, browserTTSVoices, voice, ssmlEnabled] =
|
||||||
|
await Promise.all([
|
||||||
|
isTTSEnabled(),
|
||||||
|
getTTSProvider(),
|
||||||
|
getBrowserTTSVoices(),
|
||||||
|
getVoice(),
|
||||||
|
isSSMLEnabled()
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
ttsEnabled,
|
||||||
|
ttsProvider,
|
||||||
|
browserTTSVoices,
|
||||||
|
voice,
|
||||||
|
ssmlEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setTTSSettings = async ({
|
||||||
|
ttsEnabled,
|
||||||
|
ttsProvider,
|
||||||
|
voice,
|
||||||
|
ssmlEnabled
|
||||||
|
}: {
|
||||||
|
ttsEnabled: boolean
|
||||||
|
ttsProvider: string
|
||||||
|
voice: string
|
||||||
|
ssmlEnabled: boolean
|
||||||
|
}) => {
|
||||||
|
await Promise.all([
|
||||||
|
setTTSEnabled(ttsEnabled),
|
||||||
|
setTTSProvider(ttsProvider),
|
||||||
|
setVoice(voice),
|
||||||
|
setSSMLEnabled(ssmlEnabled)
|
||||||
|
])
|
||||||
|
}
|
@ -3,9 +3,15 @@ import { create } from "zustand"
|
|||||||
type State = {
|
type State = {
|
||||||
sendWhenEnter: boolean
|
sendWhenEnter: boolean
|
||||||
setSendWhenEnter: (sendWhenEnter: boolean) => void
|
setSendWhenEnter: (sendWhenEnter: boolean) => void
|
||||||
|
|
||||||
|
ttsEnabled: boolean
|
||||||
|
setTTSEnabled: (isTTSEnabled: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useWebUI = create<State>((set) => ({
|
export const useWebUI = create<State>((set) => ({
|
||||||
sendWhenEnter: true,
|
sendWhenEnter: true,
|
||||||
setSendWhenEnter: (sendWhenEnter) => set({ sendWhenEnter })
|
setSendWhenEnter: (sendWhenEnter) => set({ sendWhenEnter }),
|
||||||
|
|
||||||
|
ttsEnabled: false,
|
||||||
|
setTTSEnabled: (ttsEnabled) => set({ ttsEnabled })
|
||||||
}))
|
}))
|
||||||
|
20
src/utils/markdown-to-ssml.ts
Normal file
20
src/utils/markdown-to-ssml.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export function markdownToSSML(markdown: string): string {
|
||||||
|
let ssml = markdown.replace(/\\n/g, "<break/>")
|
||||||
|
|
||||||
|
ssml = ssml.replace(
|
||||||
|
/^(#{1,6}) (.*?)(?=\r?\n\s*?(?:\r?\n|$))/gm,
|
||||||
|
(match, hashes, heading) => {
|
||||||
|
const level = hashes.length
|
||||||
|
const rate = (level - 1) * 10 + 100
|
||||||
|
return `<prosody rate="${rate}%">${heading}</prosody>`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ssml = ssml.replace(/\\\*\\\*(.\*?)\\\*\\\*/g, "<emphasis>$1</emphasis>")
|
||||||
|
ssml = ssml.replace(
|
||||||
|
/\\\*(.\*?)\\\*/g,
|
||||||
|
'<amazon:effect name="whispered">$1</amazon:effect>'
|
||||||
|
)
|
||||||
|
ssml = `<speak>${ssml}</speak>`
|
||||||
|
return `<?xml version="1.0"?>${ssml}`
|
||||||
|
}
|
@ -93,7 +93,7 @@ export const webDuckDuckGoSearch = async (query: string) => {
|
|||||||
const textSplitter = new RecursiveCharacterTextSplitter({
|
const textSplitter = new RecursiveCharacterTextSplitter({
|
||||||
chunkSize,
|
chunkSize,
|
||||||
chunkOverlap
|
chunkOverlap
|
||||||
})
|
})
|
||||||
|
|
||||||
const chunks = await textSplitter.splitDocuments(docs)
|
const chunks = await textSplitter.splitDocuments(docs)
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ export default defineConfig({
|
|||||||
srcDir: "src",
|
srcDir: "src",
|
||||||
outDir: "build",
|
outDir: "build",
|
||||||
manifest: {
|
manifest: {
|
||||||
version: "1.1.2",
|
version: "1.1.3",
|
||||||
name: '__MSG_extName__',
|
name: '__MSG_extName__',
|
||||||
description: '__MSG_extDescription__',
|
description: '__MSG_extDescription__',
|
||||||
default_locale: 'en',
|
default_locale: 'en',
|
||||||
@ -52,7 +52,8 @@ export default defineConfig({
|
|||||||
"declarativeNetRequest",
|
"declarativeNetRequest",
|
||||||
"action",
|
"action",
|
||||||
"unlimitedStorage",
|
"unlimitedStorage",
|
||||||
"contextMenus"
|
"contextMenus",
|
||||||
|
"tts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user