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}` | ||||||
|  | } | ||||||
| @ -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