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:
n4ze3m 2024-04-11 00:08:20 +05:30
parent 291f7392c2
commit a3810cd534
14 changed files with 385 additions and 46 deletions

View File

@ -1,7 +1,6 @@
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import remarkGfm from "remark-gfm"
import { nightOwl } from "react-syntax-highlighter/dist/cjs/styles/prism"
import rehypeMathjax from "rehype-mathjax"
import remarkMath from "remark-math"
import ReactMarkdown from "react-markdown"
import "property-information"
@ -19,7 +18,6 @@ export default function Markdown({ message }: { message: string }) {
<ReactMarkdown
className="prose break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark"
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeMathjax]}
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || "")

View File

@ -2,10 +2,18 @@ import Markdown from "../../Common/Markdown"
import React from "react"
import { Image, Tooltip } from "antd"
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 { useTranslation } from "react-i18next"
import { MessageSource } from "./MessageSource"
import { useTTS } from "@/hooks/useTTS"
type Props = {
message: string
@ -25,6 +33,7 @@ type Props = {
sources?: any[]
hideEditAndRegenerate?: boolean
onSourceClick?: (source: any) => void
isTTSEnabled?: boolean
}
export const PlaygroundMessage = (props: Props) => {
@ -32,6 +41,7 @@ export const PlaygroundMessage = (props: Props) => {
const [editMode, setEditMode] = React.useState(false)
const { t } = useTranslation("common")
const { cancel, isSpeaking, speak } = useTTS()
return (
<div className="group w-full text-gray-800 dark:text-gray-100">
@ -99,7 +109,9 @@ export const PlaygroundMessage = (props: Props) => {
{props?.sources?.map((source, index) => (
<MessageSource
onSourceClick={props.onSourceClick}
key={index} source={source} />
key={index}
source={source}
/>
))}
</div>
)}
@ -110,11 +122,31 @@ export const PlaygroundMessage = (props: Props) => {
? "hidden group-hover: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.hideCopy && (
<Tooltip title={t("copyToClipboard")}
>
<Tooltip title={t("copyToClipboard")}>
<button
onClick={() => {
navigator.clipboard.writeText(props.message)
@ -135,8 +167,7 @@ export const PlaygroundMessage = (props: Props) => {
{!props.hideEditAndRegenerate &&
props.currentMessageIndex === props.totalMessages - 1 && (
<Tooltip title={t("regenerate")}
>
<Tooltip title={t("regenerate")}>
<button
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">
@ -147,8 +178,7 @@ export const PlaygroundMessage = (props: Props) => {
</>
)}
{!props.hideEditAndRegenerate && (
<Tooltip title={t("edit")}
>
<Tooltip title={t("edit")}>
<button
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">

View File

@ -10,7 +10,8 @@ export const PlaygroundChat = () => {
streaming,
regenerateLastMessage,
isSearchingInternet,
editMessage
editMessage,
ttsEnabled
} = useMessageOption()
const divRef = React.useRef<HTMLDivElement>(null)
const [isSourceOpen, setIsSourceOpen] = React.useState(false)
@ -50,6 +51,7 @@ export const PlaygroundChat = () => {
setSource(data)
setIsSourceOpen(true)
}}
isTTSEnabled={ttsEnabled}
/>
))}
{messages.length > 0 && (

View File

@ -8,6 +8,7 @@ import { MoonIcon, SunIcon } from "lucide-react"
import { SearchModeSettings } from "./search-mode"
import { useTranslation } from "react-i18next"
import { useI18n } from "@/hooks/useI18n"
import { TTSModeSettings } from "./tts-mode"
export const SettingOther = () => {
const { clearChat, speechToTextLanguage, setSpeechToTextLanguage } =
@ -89,6 +90,7 @@ export const SettingOther = () => {
</button>
</div>
<SearchModeSettings />
<TTSModeSettings />
<div>
<div className="mb-5">
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">

View File

@ -5,8 +5,6 @@ import { useTranslation } from "react-i18next"
import { SaveButton } from "~/components/Common/SaveButton"
import {
getWebSearchPrompt,
setSystemPromptForNonRagOption,
systemPromptForNonRagOption,
geWebSearchFollowUpPrompt,
setWebPrompts,
promptForRag,

View File

@ -44,14 +44,15 @@ export const SearchModeSettings = () => {
await setSearchSettings(values)
})}
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 ">
{t("generalSettings.webSearch.provider.label")}
</span>
<div>
<Select
placeholder={t("generalSettings.webSearch.provider.placeholder")}
showSearch
style={{ width: "200px" }}
className="w-full mt-4 sm:mt-0 sm:w-[200px]"
options={SUPPORTED_SERACH_PROVIDERS}
filterOption={(input, option) =>
option!.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 ||
@ -60,28 +61,34 @@ export const SearchModeSettings = () => {
{...form.getInputProps("searchProvider")}
/>
</div>
<div className="flex flex-row justify-between">
</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.webSearch.searchMode.label")}
</span>
<div>
<Switch
className="mt-4 sm:mt-0"
{...form.getInputProps("isSimpleInternetSearch", {
type: "checkbox"
})}
/>
</div>
<div className="flex flex-row justify-between">
</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.webSearch.totalSearchResults.label")}
</span>
<div>
<InputNumber
placeholder={t(
"generalSettings.webSearch.totalSearchResults.placeholder"
)}
{...form.getInputProps("totalSearchResults")}
style={{ width: "200px" }}
className="!w-full mt-4 sm:mt-0 sm:w-[200px]"
/>
</div>
</div>
<div className="flex justify-end">
<SaveButton btnType="submit" />

View 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>
)
}

View File

@ -28,6 +28,8 @@ import { usePageAssist } from "@/context"
import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama"
import { PageAssistVectorStore } from "@/libs/PageAssistVectorStore"
import { formatDocs } from "@/chain/chat-with-x"
import { useWebUI } from "@/store/webui"
import { isTTSEnabled } from "@/services/tts"
export const useMessageOption = () => {
const {
@ -66,11 +68,22 @@ export const useMessageOption = () => {
setSelectedKnowledge
} = useStoreMessageOption()
const { ttsEnabled, setTTSEnabled } = useWebUI()
const { t } = useTranslation("option")
const navigate = useNavigate()
const textareaRef = React.useRef<HTMLTextAreaElement>(null)
React.useEffect(() => {
const checkTTSEnabled = async () => {
const tts = await isTTSEnabled()
setTTSEnabled(tts)
}
checkTTSEnabled()
}, [])
const clearChat = () => {
navigate("/")
setMessages([])
@ -835,7 +848,7 @@ export const useMessageOption = () => {
}
const currentHumanMessage = newMessages[index]
newMessages[index].message = message
const previousMessages = newMessages.slice(0, index + 1)
setMessages(previousMessages)
const previousHistory = newHistory.slice(0, index)
@ -893,6 +906,7 @@ export const useMessageOption = () => {
setSelectedSystemPrompt,
textareaRef,
selectedKnowledge,
setSelectedKnowledge
setSelectedKnowledge,
ttsEnabled
}
}

54
src/hooks/useTTS.tsx Normal file
View 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
View 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)
])
}

View File

@ -3,9 +3,15 @@ import { create } from "zustand"
type State = {
sendWhenEnter: boolean
setSendWhenEnter: (sendWhenEnter: boolean) => void
ttsEnabled: boolean
setTTSEnabled: (isTTSEnabled: boolean) => void
}
export const useWebUI = create<State>((set) => ({
sendWhenEnter: true,
setSendWhenEnter: (sendWhenEnter) => set({ sendWhenEnter })
setSendWhenEnter: (sendWhenEnter) => set({ sendWhenEnter }),
ttsEnabled: false,
setTTSEnabled: (ttsEnabled) => set({ ttsEnabled })
}))

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

View File

@ -24,7 +24,7 @@ export default defineConfig({
srcDir: "src",
outDir: "build",
manifest: {
version: "1.1.2",
version: "1.1.3",
name: '__MSG_extName__',
description: '__MSG_extDescription__',
default_locale: 'en',
@ -52,7 +52,8 @@ export default defineConfig({
"declarativeNetRequest",
"action",
"unlimitedStorage",
"contextMenus"
"contextMenus",
"tts"
]
}
})