commit
de367a1aa0
@ -1,6 +1,7 @@
|
|||||||
# Page Assist
|
# Page Assist
|
||||||
|
|
||||||
[](https://discord.gg/bu54382uBd)
|
[](https://discord.gg/bu54382uBd)
|
||||||
|
[](https://twitter.com/page_assist)
|
||||||
|
|
||||||
Page Assist is an open-source browser extension that provides a sidebar and web UI for your local AI model. It allows you to interact with your model from any webpage.
|
Page Assist is an open-source browser extension that provides a sidebar and web UI for your local AI model. It allows you to interact with your model from any webpage.
|
||||||
## Installation
|
## Installation
|
||||||
|
@ -7,8 +7,10 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cross-env TARGET=chrome wxt",
|
"dev": "cross-env TARGET=chrome wxt",
|
||||||
"dev:firefox": "cross-env TARGET=firefox wxt -b firefox",
|
"dev:firefox": "cross-env TARGET=firefox wxt -b firefox",
|
||||||
|
"dev:edge": "cross-env TARGET=chrome wxt -b edge",
|
||||||
"build": "cross-env TARGET=chrome wxt build",
|
"build": "cross-env TARGET=chrome wxt build",
|
||||||
"build:firefox": "cross-env TARGET=firefox wxt build -b firefox",
|
"build:firefox": "cross-env TARGET=firefox wxt build -b firefox",
|
||||||
|
"build:edge": "cross-env TARGET=chrome wxt build -b edge",
|
||||||
"zip": "cross-env TARGET=chrome wxt zip",
|
"zip": "cross-env TARGET=chrome wxt zip",
|
||||||
"zip:firefox": "cross-env TARGET=firefox wxt zip -b firefox",
|
"zip:firefox": "cross-env TARGET=firefox wxt zip -b firefox",
|
||||||
"compile": "tsc --noEmit",
|
"compile": "tsc --noEmit",
|
||||||
|
@ -119,6 +119,9 @@
|
|||||||
},
|
},
|
||||||
"ssmlEnabled": {
|
"ssmlEnabled": {
|
||||||
"label": "تمكين SSML (لغة ترميز توليف الكلام)"
|
"label": "تمكين SSML (لغة ترميز توليف الكلام)"
|
||||||
|
},
|
||||||
|
"removeReasoningTagTTS": {
|
||||||
|
"label": "إزالة علامة التفكير من تحويل النص إلى كلام"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -113,6 +113,9 @@
|
|||||||
},
|
},
|
||||||
"ssmlEnabled": {
|
"ssmlEnabled": {
|
||||||
"label": "Aktiver SSML (Speech Synthesis Markup Language)"
|
"label": "Aktiver SSML (Speech Synthesis Markup Language)"
|
||||||
|
},
|
||||||
|
"removeReasoningTagTTS": {
|
||||||
|
"label": "Fjern Ræsonnement Tag fra TTS"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -116,6 +116,9 @@
|
|||||||
},
|
},
|
||||||
"ssmlEnabled": {
|
"ssmlEnabled": {
|
||||||
"label": "SSML (Speech Synthesis Markup Language) aktivieren"
|
"label": "SSML (Speech Synthesis Markup Language) aktivieren"
|
||||||
|
},
|
||||||
|
"removeReasoningTagTTS": {
|
||||||
|
"label": "Reasoning-Tag aus Text-zu-Sprache entfernen"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -122,6 +122,9 @@
|
|||||||
},
|
},
|
||||||
"responseSplitting": {
|
"responseSplitting": {
|
||||||
"label": "Response Splitting"
|
"label": "Response Splitting"
|
||||||
|
},
|
||||||
|
"removeReasoningTagTTS": {
|
||||||
|
"label": "Remove Reasoning Tag from TTS"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -116,8 +116,10 @@
|
|||||||
},
|
},
|
||||||
"ssmlEnabled": {
|
"ssmlEnabled": {
|
||||||
"label": "Habilitar SSML (Speech Synthesis Markup Language)"
|
"label": "Habilitar SSML (Speech Synthesis Markup Language)"
|
||||||
}
|
},
|
||||||
}
|
"removeReasoningTagTTS": {
|
||||||
|
"label": "Eliminar Etiqueta de Razonamiento del TTS"
|
||||||
|
} }
|
||||||
},
|
},
|
||||||
"manageModels": {
|
"manageModels": {
|
||||||
"title": "Administar de Modelos",
|
"title": "Administar de Modelos",
|
||||||
|
@ -113,6 +113,9 @@
|
|||||||
},
|
},
|
||||||
"ssmlEnabled": {
|
"ssmlEnabled": {
|
||||||
"label": "فعال کردن SSML (Speech Synthesis Markup Language)"
|
"label": "فعال کردن SSML (Speech Synthesis Markup Language)"
|
||||||
|
},
|
||||||
|
"removeReasoningTagTTS": {
|
||||||
|
"label": "حذف برچسب استدلال از تبدیل متن به گفتار"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -116,6 +116,9 @@
|
|||||||
},
|
},
|
||||||
"ssmlEnabled": {
|
"ssmlEnabled": {
|
||||||
"label": "Activer SSML (langage de balisage de synthèse vocale)"
|
"label": "Activer SSML (langage de balisage de synthèse vocale)"
|
||||||
|
},
|
||||||
|
"removeReasoningTagTTS": {
|
||||||
|
"label": "Supprimer la balise de raisonnement de la synthèse vocale"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -116,6 +116,9 @@
|
|||||||
},
|
},
|
||||||
"ssmlEnabled": {
|
"ssmlEnabled": {
|
||||||
"label": "Abilita SSML (Speech Synthesis Markup Language)"
|
"label": "Abilita SSML (Speech Synthesis Markup Language)"
|
||||||
|
},
|
||||||
|
"removeReasoningTagTTS": {
|
||||||
|
"label": "Rimuovi Tag di Ragionamento dal TTS"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -119,6 +119,9 @@
|
|||||||
},
|
},
|
||||||
"ssmlEnabled": {
|
"ssmlEnabled": {
|
||||||
"label": "SSML (Speech Synthesis Markup Language) を有効にする"
|
"label": "SSML (Speech Synthesis Markup Language) を有効にする"
|
||||||
|
},
|
||||||
|
"removeReasoningTagTTS": {
|
||||||
|
"label": "テキスト読み上げから推論タグを削除"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -119,6 +119,9 @@
|
|||||||
},
|
},
|
||||||
"ssmlEnabled": {
|
"ssmlEnabled": {
|
||||||
"label": "SSML (Speech Synthesis Markup Language) 활성화"
|
"label": "SSML (Speech Synthesis Markup Language) 활성화"
|
||||||
|
},
|
||||||
|
"removeReasoningTagTTS": {
|
||||||
|
"label": "TTS에서 추론 태그 제거"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -119,6 +119,9 @@
|
|||||||
},
|
},
|
||||||
"ssmlEnabled": {
|
"ssmlEnabled": {
|
||||||
"label": "SSML (സ്പീച്ച് സിന്തസിസ് മാർക്കപ്പ് ലാംഗ്വേജ്) പ്രവർത്തനക്ഷമമാക്കുക"
|
"label": "SSML (സ്പീച്ച് സിന്തസിസ് മാർക്കപ്പ് ലാംഗ്വേജ്) പ്രവർത്തനക്ഷമമാക്കുക"
|
||||||
|
},
|
||||||
|
"removeReasoningTagTTS": {
|
||||||
|
"label": "ടിടിഎസിൽ നിന്ന് റീസണിംഗ് ടാഗ് നീക്കം ചെയ്യുക"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -116,6 +116,9 @@
|
|||||||
},
|
},
|
||||||
"ssmlEnabled": {
|
"ssmlEnabled": {
|
||||||
"label": "Aktiver SSML (Speech Synthesis Markup Language)"
|
"label": "Aktiver SSML (Speech Synthesis Markup Language)"
|
||||||
|
},
|
||||||
|
"removeReasoningTagTTS": {
|
||||||
|
"label": "Fjern Resonneringsmerke fra TTS"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -116,6 +116,9 @@
|
|||||||
},
|
},
|
||||||
"ssmlEnabled": {
|
"ssmlEnabled": {
|
||||||
"label": "Ativar SSML (Linguagem de Marcação de Síntese de Fala)"
|
"label": "Ativar SSML (Linguagem de Marcação de Síntese de Fala)"
|
||||||
|
},
|
||||||
|
"removeReasoningTagTTS": {
|
||||||
|
"label": "Remover Tag de Raciocínio do TTS"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -117,6 +117,9 @@
|
|||||||
},
|
},
|
||||||
"ssmlEnabled": {
|
"ssmlEnabled": {
|
||||||
"label": "Включить SSML (язык разметки синтеза речи)"
|
"label": "Включить SSML (язык разметки синтеза речи)"
|
||||||
|
},
|
||||||
|
"removeReasoningTagTTS": {
|
||||||
|
"label": "Удалить тег рассуждения из TTS"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -116,6 +116,9 @@
|
|||||||
},
|
},
|
||||||
"ssmlEnabled": {
|
"ssmlEnabled": {
|
||||||
"label": "Aktivera SSML (Speech Synthesis Markup Language)"
|
"label": "Aktivera SSML (Speech Synthesis Markup Language)"
|
||||||
|
},
|
||||||
|
"removeReasoningTagTTS": {
|
||||||
|
"label": "Ta bort resonemangstagg från Text till Tal"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -116,6 +116,9 @@
|
|||||||
},
|
},
|
||||||
"ssmlEnabled": {
|
"ssmlEnabled": {
|
||||||
"label": "Ввімкнути SSML (Мова Розмітки для Синтезу Голосу)"
|
"label": "Ввімкнути SSML (Мова Розмітки для Синтезу Голосу)"
|
||||||
|
},
|
||||||
|
"removeReasoningTagTTS": {
|
||||||
|
"label": "Видалити тег міркування з TTS"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -119,6 +119,9 @@
|
|||||||
},
|
},
|
||||||
"ssmlEnabled": {
|
"ssmlEnabled": {
|
||||||
"label": "启用SSML(语音合成标记语言)"
|
"label": "启用SSML(语音合成标记语言)"
|
||||||
|
},
|
||||||
|
"removeReasoningTagTTS": {
|
||||||
|
"label": "从语音合成中移除推理标签"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -13,6 +13,12 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
@layer utilities {
|
||||||
|
.mask-bottom-fade {
|
||||||
|
mask-image: linear-gradient(0deg, transparent 0, #000 160px);
|
||||||
|
-webkit-mask-image: linear-gradient(0deg, transparent 0, #000 160px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,10 +42,36 @@ export const CodeBlock: FC<Props> = ({ language, value }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="code relative text-base font-sans codeblock bg-zinc-950 rounded-md overflow-hidden">
|
<div className="not-prose">
|
||||||
<div className="flex bg-gray-800 items-center justify-between py-1.5 px-4">
|
<div className=" [&_div+div]:!mt-0 my-4 bg-zinc-950 rounded-xl">
|
||||||
<span className="text-xs lowercase text-gray-200">{language}</span>
|
<div className="flex flex-row px-4 py-2 rounded-t-xl bg-gray-800 ">
|
||||||
|
<span className="font-mono text-xs">{language || "text"}</span>
|
||||||
|
</div>
|
||||||
|
<div className="sticky top-9 md:top-[5.75rem]">
|
||||||
|
<div className="absolute bottom-0 right-2 flex h-9 items-center">
|
||||||
|
<Tooltip title={t("downloadCode")}>
|
||||||
|
<button
|
||||||
|
onClick={handleDownload}
|
||||||
|
className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-gray-200 hover:bg-gray-700 hover:text-gray-100 focus:outline-none">
|
||||||
|
<DownloadIcon className="size-4" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title={t("copyToClipboard")}>
|
||||||
|
<button
|
||||||
|
onClick={handleCopy}
|
||||||
|
className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-gray-200 hover:bg-gray-700 hover:text-gray-100 focus:outline-none">
|
||||||
|
{!isBtnPressed ? (
|
||||||
|
<ClipboardIcon className="size-4" />
|
||||||
|
) : (
|
||||||
|
<CheckIcon className="size-4 text-green-400" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <div className="flex sticky bg-gray-800 items-center justify-between py-1.5 px-4">
|
||||||
|
<span className="text-xs lowercase text-gray-200">{language}</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Tooltip title={t("downloadCode")}>
|
<Tooltip title={t("downloadCode")}>
|
||||||
<button
|
<button
|
||||||
@ -66,28 +92,29 @@ export const CodeBlock: FC<Props> = ({ language, value }) => {
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
</div> */}
|
||||||
|
<SyntaxHighlighter
|
||||||
|
language={language}
|
||||||
|
style={coldarkDark}
|
||||||
|
PreTag="div"
|
||||||
|
customStyle={{
|
||||||
|
margin: 0,
|
||||||
|
width: "100%",
|
||||||
|
background: "transparent",
|
||||||
|
padding: "1.5rem 1rem"
|
||||||
|
}}
|
||||||
|
lineNumberStyle={{
|
||||||
|
userSelect: "none"
|
||||||
|
}}
|
||||||
|
codeTagProps={{
|
||||||
|
style: {
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
fontFamily: "var(--font-mono)"
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{value}
|
||||||
|
</SyntaxHighlighter>
|
||||||
</div>
|
</div>
|
||||||
<SyntaxHighlighter
|
|
||||||
language={language}
|
|
||||||
style={coldarkDark}
|
|
||||||
PreTag="div"
|
|
||||||
customStyle={{
|
|
||||||
margin: 0,
|
|
||||||
width: "100%",
|
|
||||||
background: "transparent",
|
|
||||||
padding: "1.5rem 1rem"
|
|
||||||
}}
|
|
||||||
lineNumberStyle={{
|
|
||||||
userSelect: "none"
|
|
||||||
}}
|
|
||||||
codeTagProps={{
|
|
||||||
style: {
|
|
||||||
fontSize: "0.9rem",
|
|
||||||
fontFamily: "var(--font-mono)"
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
{value}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
</div>
|
</div>
|
||||||
{previewVisible && (
|
{previewVisible && (
|
||||||
<Modal
|
<Modal
|
||||||
|
@ -25,6 +25,9 @@ function Markdown({
|
|||||||
remarkPlugins={[remarkGfm, remarkMath]}
|
remarkPlugins={[remarkGfm, remarkMath]}
|
||||||
rehypePlugins={[rehypeKatex]}
|
rehypePlugins={[rehypeKatex]}
|
||||||
components={{
|
components={{
|
||||||
|
pre({ children }) {
|
||||||
|
return children
|
||||||
|
},
|
||||||
code({ node, inline, className, children, ...props }) {
|
code({ node, inline, className, children, ...props }) {
|
||||||
const match = /language-(\w+)/.exec(className || "")
|
const match = /language-(\w+)/.exec(className || "")
|
||||||
return !inline ? (
|
return !inline ? (
|
||||||
|
@ -18,7 +18,7 @@ import { useTTS } from "@/hooks/useTTS"
|
|||||||
import { tagColors } from "@/utils/color"
|
import { tagColors } from "@/utils/color"
|
||||||
import { removeModelSuffix } from "@/db/models"
|
import { removeModelSuffix } from "@/db/models"
|
||||||
import { GenerationInfo } from "./GenerationInfo"
|
import { GenerationInfo } from "./GenerationInfo"
|
||||||
import { parseReasoning, removeReasoning } from "@/libs/reasoning"
|
import { parseReasoning, } from "@/libs/reasoning"
|
||||||
import { humanizeMilliseconds } from "@/utils/humanize-milliseconds"
|
import { humanizeMilliseconds } from "@/utils/humanize-milliseconds"
|
||||||
type Props = {
|
type Props = {
|
||||||
message: string
|
message: string
|
||||||
@ -52,247 +52,250 @@ export const PlaygroundMessage = (props: Props) => {
|
|||||||
const { t } = useTranslation("common")
|
const { t } = useTranslation("common")
|
||||||
const { cancel, isSpeaking, speak } = useTTS()
|
const { cancel, isSpeaking, speak } = useTTS()
|
||||||
return (
|
return (
|
||||||
<div className="group w-full text-gray-800 dark:text-gray-100">
|
<div className="group relative flex w-full max-w-3xl flex-col items-end justify-center pb-2 md:px-4 lg:w-4/5 text-gray-800 dark:text-gray-100">
|
||||||
<div className="text-base md:max-w-2xl lg:max-w-xl xl:max-w-3xl flex lg:px-0 m-auto w-full">
|
{/* <div className="text-base md:max-w-2xl lg:max-w-xl xl:max-w-3xl flex lg:px-0 m-auto w-full"> */}
|
||||||
<div className="flex flex-row gap-4 md:gap-6 p-4 m-auto w-full">
|
<div className="flex flex-row gap-4 md:gap-6 my-2 m-auto w-full">
|
||||||
<div className="w-8 flex flex-col relative items-end">
|
<div className="w-8 flex flex-col relative items-end">
|
||||||
<div className="relative h-7 w-7 p-1 rounded-sm text-white flex items-center justify-center text-opacity-100r">
|
<div className="relative h-7 w-7 p-1 rounded-sm text-white flex items-center justify-center text-opacity-100r">
|
||||||
{props.isBot ? (
|
{props.isBot ? (
|
||||||
!props.botAvatar ? (
|
!props.botAvatar ? (
|
||||||
<div className="absolute h-8 w-8 rounded-full bg-gradient-to-r from-green-300 to-purple-400"></div>
|
<div className="absolute h-8 w-8 rounded-full bg-gradient-to-r from-green-300 to-purple-400"></div>
|
||||||
) : (
|
|
||||||
props.botAvatar
|
|
||||||
)
|
|
||||||
) : !props.userAvatar ? (
|
|
||||||
<div className="absolute h-8 w-8 rounded-full from-blue-400 to-blue-600 bg-gradient-to-r"></div>
|
|
||||||
) : (
|
) : (
|
||||||
props.userAvatar
|
props.botAvatar
|
||||||
)}
|
)
|
||||||
</div>
|
) : !props.userAvatar ? (
|
||||||
</div>
|
<div className="absolute h-8 w-8 rounded-full from-blue-400 to-blue-600 bg-gradient-to-r"></div>
|
||||||
<div className="flex w-[calc(100%-50px)] flex-col gap-2 lg:w-[calc(100%-115px)]">
|
) : (
|
||||||
<span className="text-xs font-bold text-gray-800 dark:text-white">
|
props.userAvatar
|
||||||
{props.isBot
|
|
||||||
? props.name === "chrome::gemini-nano::page-assist"
|
|
||||||
? "Gemini Nano"
|
|
||||||
: removeModelSuffix(
|
|
||||||
props.name?.replaceAll(/accounts\/[^\/]+\/models\//g, "")
|
|
||||||
)
|
|
||||||
: "You"}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{props.isBot &&
|
|
||||||
props.isSearchingInternet &&
|
|
||||||
props.currentMessageIndex === props.totalMessages - 1 ? (
|
|
||||||
<WebSearch />
|
|
||||||
) : null}
|
|
||||||
<div>
|
|
||||||
{props?.message_type && (
|
|
||||||
<Tag color={tagColors[props?.message_type] || "default"}>
|
|
||||||
{t(`copilot.${props?.message_type}`)}
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-grow flex-col">
|
|
||||||
{!editMode ? (
|
|
||||||
props.isBot ? (
|
|
||||||
<>
|
|
||||||
{parseReasoning(props.message).map((e, i) => {
|
|
||||||
if (e.type === "reasoning") {
|
|
||||||
return (
|
|
||||||
<Collapse
|
|
||||||
key={i}
|
|
||||||
className="border-none !mb-3"
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
key: "reasoning",
|
|
||||||
label:
|
|
||||||
props.isStreaming && e?.reasoning_running ? (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="italic">
|
|
||||||
{t("reasoning.thinking")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
t("reasoning.thought", {
|
|
||||||
time: humanizeMilliseconds(
|
|
||||||
props.reasoningTimeTaken
|
|
||||||
)
|
|
||||||
})
|
|
||||||
),
|
|
||||||
children: <Markdown message={e.content} />
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Markdown key={i} message={e.content} />
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p
|
|
||||||
className={`prose dark:prose-invert whitespace-pre-line prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark ${
|
|
||||||
props.message_type &&
|
|
||||||
"italic text-gray-500 dark:text-gray-400 text-sm"
|
|
||||||
}`}>
|
|
||||||
{props.message}
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<EditMessageForm
|
|
||||||
value={props.message}
|
|
||||||
onSumbit={props.onEditFormSubmit}
|
|
||||||
onClose={() => setEditMode(false)}
|
|
||||||
isBot={props.isBot}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{/* source if available */}
|
|
||||||
{props.images &&
|
|
||||||
props.images.filter((img) => img.length > 0).length > 0 && (
|
|
||||||
<div className="flex md:max-w-2xl lg:max-w-xl xl:max-w-3xl mt-4 m-auto w-full">
|
|
||||||
{props.images
|
|
||||||
.filter((image) => image.length > 0)
|
|
||||||
.map((image, index) => (
|
|
||||||
<Image
|
|
||||||
key={index}
|
|
||||||
src={image}
|
|
||||||
alt="Uploaded Image"
|
|
||||||
width={180}
|
|
||||||
className="rounded-md relative"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{props.isBot && props?.sources && props?.sources.length > 0 && (
|
|
||||||
<Collapse
|
|
||||||
className="mt-6"
|
|
||||||
ghost
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
key: "1",
|
|
||||||
label: (
|
|
||||||
<div className="italic text-gray-500 dark:text-gray-400">
|
|
||||||
{t("citations")}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
children: (
|
|
||||||
<div className="mb-3 flex flex-wrap gap-2">
|
|
||||||
{props?.sources?.map((source, index) => (
|
|
||||||
<MessageSource
|
|
||||||
onSourceClick={props.onSourceClick}
|
|
||||||
key={index}
|
|
||||||
source={source}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!props.isProcessing && !editMode && (
|
|
||||||
<div
|
|
||||||
className={`space-x-2 gap-2 mt-3 flex ${
|
|
||||||
props.currentMessageIndex !== props.totalMessages - 1
|
|
||||||
// there is few style issue so i am commenting this out for v1.4.5 release
|
|
||||||
// next release we will fix this
|
|
||||||
// ? "invisible group-hover:visible"
|
|
||||||
? "hidden group-hover:flex"
|
|
||||||
// ""
|
|
||||||
: "flex"
|
|
||||||
}`}>
|
|
||||||
{props.isTTSEnabled && (
|
|
||||||
<Tooltip title={t("tts")}>
|
|
||||||
<button
|
|
||||||
aria-label={t("tts")}
|
|
||||||
onClick={() => {
|
|
||||||
if (isSpeaking) {
|
|
||||||
cancel()
|
|
||||||
} else {
|
|
||||||
speak({
|
|
||||||
utterance: removeReasoning(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")}>
|
|
||||||
<button
|
|
||||||
aria-label={t("copyToClipboard")}
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(props.message)
|
|
||||||
setIsBtnPressed(true)
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsBtnPressed(false)
|
|
||||||
}, 2000)
|
|
||||||
}}
|
|
||||||
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">
|
|
||||||
{!isBtnPressed ? (
|
|
||||||
<ClipboardIcon className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
|
||||||
) : (
|
|
||||||
<CheckIcon className="w-3 h-3 text-green-400 group-hover:text-green-500" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{props.generationInfo && (
|
|
||||||
<Popover
|
|
||||||
content={
|
|
||||||
<GenerationInfo
|
|
||||||
generationInfo={props.generationInfo}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={t("generationInfo")}>
|
|
||||||
<button
|
|
||||||
aria-label={t("generationInfo")}
|
|
||||||
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">
|
|
||||||
<InfoIcon className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
|
||||||
</button>
|
|
||||||
</Popover>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!props.hideEditAndRegenerate &&
|
|
||||||
props.currentMessageIndex === props.totalMessages - 1 && (
|
|
||||||
<Tooltip title={t("regenerate")}>
|
|
||||||
<button
|
|
||||||
aria-label={t("regenerate")}
|
|
||||||
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">
|
|
||||||
<RotateCcw className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!props.hideEditAndRegenerate && (
|
|
||||||
<Tooltip title={t("edit")}>
|
|
||||||
<button
|
|
||||||
onClick={() => setEditMode(true)}
|
|
||||||
aria-label={t("edit")}
|
|
||||||
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">
|
|
||||||
<Pen className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex w-[calc(100%-50px)] flex-col gap-2 lg:w-[calc(100%-115px)]">
|
||||||
|
<span className="text-xs font-bold text-gray-800 dark:text-white">
|
||||||
|
{props.isBot
|
||||||
|
? props.name === "chrome::gemini-nano::page-assist"
|
||||||
|
? "Gemini Nano"
|
||||||
|
: removeModelSuffix(
|
||||||
|
props.name?.replaceAll(/accounts\/[^\/]+\/models\//g, "")
|
||||||
|
)
|
||||||
|
: "You"}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{props.isBot &&
|
||||||
|
props.isSearchingInternet &&
|
||||||
|
props.currentMessageIndex === props.totalMessages - 1 ? (
|
||||||
|
<WebSearch />
|
||||||
|
) : null}
|
||||||
|
<div>
|
||||||
|
{props?.message_type && (
|
||||||
|
<Tag color={tagColors[props?.message_type] || "default"}>
|
||||||
|
{t(`copilot.${props?.message_type}`)}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-grow flex-col">
|
||||||
|
{!editMode ? (
|
||||||
|
props.isBot ? (
|
||||||
|
<>
|
||||||
|
{parseReasoning(props.message).map((e, i) => {
|
||||||
|
if (e.type === "reasoning") {
|
||||||
|
return (
|
||||||
|
<Collapse
|
||||||
|
key={i}
|
||||||
|
className="border-none !mb-3"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: "reasoning",
|
||||||
|
label:
|
||||||
|
props.isStreaming && e?.reasoning_running ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="italic">
|
||||||
|
{t("reasoning.thinking")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
t("reasoning.thought", {
|
||||||
|
time: humanizeMilliseconds(
|
||||||
|
props.reasoningTimeTaken
|
||||||
|
)
|
||||||
|
})
|
||||||
|
),
|
||||||
|
children: <Markdown message={e.content} />
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Markdown key={i} message={e.content} />
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p
|
||||||
|
className={`prose dark:prose-invert whitespace-pre-line prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark ${
|
||||||
|
props.message_type &&
|
||||||
|
"italic text-gray-500 dark:text-gray-400 text-sm"
|
||||||
|
}`}>
|
||||||
|
{props.message}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<EditMessageForm
|
||||||
|
value={props.message}
|
||||||
|
onSumbit={props.onEditFormSubmit}
|
||||||
|
onClose={() => setEditMode(false)}
|
||||||
|
isBot={props.isBot}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* source if available */}
|
||||||
|
{props.images &&
|
||||||
|
props.images.filter((img) => img.length > 0).length > 0 && (
|
||||||
|
<div>
|
||||||
|
{props.images
|
||||||
|
.filter((image) => image.length > 0)
|
||||||
|
.map((image, index) => (
|
||||||
|
<Image
|
||||||
|
key={index}
|
||||||
|
src={image}
|
||||||
|
alt="Uploaded Image"
|
||||||
|
width={180}
|
||||||
|
className="rounded-md relative"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{props.isBot && props?.sources && props?.sources.length > 0 && (
|
||||||
|
<Collapse
|
||||||
|
className="mt-6"
|
||||||
|
ghost
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: "1",
|
||||||
|
label: (
|
||||||
|
<div className="italic text-gray-500 dark:text-gray-400">
|
||||||
|
{t("citations")}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
children: (
|
||||||
|
<div className="mb-3 flex flex-wrap gap-2">
|
||||||
|
{props?.sources?.map((source, index) => (
|
||||||
|
<MessageSource
|
||||||
|
onSourceClick={props.onSourceClick}
|
||||||
|
key={index}
|
||||||
|
source={source}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!props.isProcessing && !editMode ? (
|
||||||
|
<div
|
||||||
|
className={`space-x-2 gap-2 flex ${
|
||||||
|
props.currentMessageIndex !== props.totalMessages - 1
|
||||||
|
? // there is few style issue so i am commenting this out for v1.4.5 release
|
||||||
|
// next release we will fix this
|
||||||
|
"invisible group-hover:visible"
|
||||||
|
: // ? "hidden group-hover:flex"
|
||||||
|
""
|
||||||
|
// : "flex"
|
||||||
|
}`}>
|
||||||
|
{props.isTTSEnabled && (
|
||||||
|
<Tooltip title={t("tts")}>
|
||||||
|
<button
|
||||||
|
aria-label={t("tts")}
|
||||||
|
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")}>
|
||||||
|
<button
|
||||||
|
aria-label={t("copyToClipboard")}
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(props.message)
|
||||||
|
setIsBtnPressed(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsBtnPressed(false)
|
||||||
|
}, 2000)
|
||||||
|
}}
|
||||||
|
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">
|
||||||
|
{!isBtnPressed ? (
|
||||||
|
<ClipboardIcon className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
||||||
|
) : (
|
||||||
|
<CheckIcon className="w-3 h-3 text-green-400 group-hover:text-green-500" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{props.generationInfo && (
|
||||||
|
<Popover
|
||||||
|
content={
|
||||||
|
<GenerationInfo generationInfo={props.generationInfo} />
|
||||||
|
}
|
||||||
|
title={t("generationInfo")}>
|
||||||
|
<button
|
||||||
|
aria-label={t("generationInfo")}
|
||||||
|
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">
|
||||||
|
<InfoIcon className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!props.hideEditAndRegenerate &&
|
||||||
|
props.currentMessageIndex === props.totalMessages - 1 && (
|
||||||
|
<Tooltip title={t("regenerate")}>
|
||||||
|
<button
|
||||||
|
aria-label={t("regenerate")}
|
||||||
|
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">
|
||||||
|
<RotateCcw className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!props.hideEditAndRegenerate && (
|
||||||
|
<Tooltip title={t("edit")}>
|
||||||
|
<button
|
||||||
|
onClick={() => setEditMode(true)}
|
||||||
|
aria-label={t("edit")}
|
||||||
|
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">
|
||||||
|
<Pen className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// add invisible div to prevent layout shift
|
||||||
|
<div className="invisible">
|
||||||
|
<div 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"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* </div> */}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -91,7 +91,7 @@ export const Header: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`sticky top-0 z-[999] flex h-16 p-3 bg-gray-50 border-b dark:bg-[#171717] dark:border-gray-600 ${
|
className={`absolute top-0 z-10 flex h-14 w-full flex-row items-center justify-center p-3 overflow-x-auto lg:overflow-x-visible bg-gray-50 border-b dark:bg-[#171717] dark:border-gray-600 ${
|
||||||
temporaryChat && "!bg-gray-200 dark:!bg-black"
|
temporaryChat && "!bg-gray-200 dark:!bg-black"
|
||||||
}`}>
|
}`}>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
@ -209,12 +209,6 @@ export const Header: React.FC<Props> = ({
|
|||||||
<div className="flex flex-1 justify-end px-4">
|
<div className="flex flex-1 justify-end px-4">
|
||||||
<div className="ml-4 flex items-center md:ml-6">
|
<div className="ml-4 flex items-center md:ml-6">
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
{/* {pathname === "/" &&
|
|
||||||
messages.length > 0 &&
|
|
||||||
!streaming &&
|
|
||||||
shareModeEnabled && (
|
|
||||||
<ShareBtn historyId={historyId} messages={messages} />
|
|
||||||
)} */}
|
|
||||||
{messages.length > 0 && !streaming && (
|
{messages.length > 0 && !streaming && (
|
||||||
<MoreOptions
|
<MoreOptions
|
||||||
shareModeEnabled={shareModeEnabled}
|
shareModeEnabled={shareModeEnabled}
|
||||||
@ -246,7 +240,7 @@ export const Header: React.FC<Props> = ({
|
|||||||
<CogIcon className="w-6 h-6" />
|
<CogIcon className="w-6 h-6" />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,72 +36,75 @@ export default function OptionLayout({
|
|||||||
const { setSystemPrompt } = useStoreChatModelSettings()
|
const { setSystemPrompt } = useStoreChatModelSettings()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex h-full w-full">
|
||||||
<div className="flex flex-col min-h-screen">
|
<main className="relative h-dvh w-full">
|
||||||
<Header
|
<div className="relative z-10 w-full">
|
||||||
setSidebarOpen={setSidebarOpen}
|
<Header
|
||||||
setOpenModelSettings={setOpenModelSettings}
|
setSidebarOpen={setSidebarOpen}
|
||||||
/>
|
setOpenModelSettings={setOpenModelSettings}
|
||||||
<main className="flex-1">{children}</main>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/* <div className="relative flex h-full flex-col items-center"> */}
|
||||||
|
{children}
|
||||||
|
{/* </div> */}
|
||||||
|
<Drawer
|
||||||
|
title={
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
{t("sidebarTitle")}
|
||||||
|
|
||||||
<Drawer
|
<Tooltip
|
||||||
title={
|
title={t(
|
||||||
<div className="flex items-center justify-between">
|
"settings:generalSettings.system.deleteChatHistory.label"
|
||||||
{t("sidebarTitle")}
|
)}
|
||||||
|
placement="right">
|
||||||
<Tooltip
|
<button
|
||||||
title={t(
|
onClick={async () => {
|
||||||
"settings:generalSettings.system.deleteChatHistory.label"
|
const confirm = window.confirm(
|
||||||
)}
|
t(
|
||||||
placement="right">
|
"settings:generalSettings.system.deleteChatHistory.confirm"
|
||||||
<button
|
)
|
||||||
onClick={async () => {
|
|
||||||
const confirm = window.confirm(
|
|
||||||
t(
|
|
||||||
"settings:generalSettings.system.deleteChatHistory.confirm"
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
const db = new PageAssitDatabase()
|
const db = new PageAssitDatabase()
|
||||||
await db.deleteAllChatHistory()
|
await db.deleteAllChatHistory()
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: ["fetchChatHistory"]
|
queryKey: ["fetchChatHistory"]
|
||||||
})
|
})
|
||||||
clearChat()
|
clearChat()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-gray-100">
|
className="text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-gray-100">
|
||||||
<EraserIcon className="size-5" />
|
<EraserIcon className="size-5" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
placement="left"
|
placement="left"
|
||||||
closeIcon={null}
|
closeIcon={null}
|
||||||
onClose={() => setSidebarOpen(false)}
|
|
||||||
open={sidebarOpen}>
|
|
||||||
<Sidebar
|
|
||||||
onClose={() => setSidebarOpen(false)}
|
onClose={() => setSidebarOpen(false)}
|
||||||
setMessages={setMessages}
|
open={sidebarOpen}>
|
||||||
setHistory={setHistory}
|
<Sidebar
|
||||||
setHistoryId={setHistoryId}
|
onClose={() => setSidebarOpen(false)}
|
||||||
setSelectedModel={setSelectedModel}
|
setMessages={setMessages}
|
||||||
setSelectedSystemPrompt={setSelectedSystemPrompt}
|
setHistory={setHistory}
|
||||||
clearChat={clearChat}
|
setHistoryId={setHistoryId}
|
||||||
historyId={historyId}
|
setSelectedModel={setSelectedModel}
|
||||||
setSystemPrompt={setSystemPrompt}
|
setSelectedSystemPrompt={setSelectedSystemPrompt}
|
||||||
temporaryChat={temporaryChat}
|
clearChat={clearChat}
|
||||||
history={history}
|
historyId={historyId}
|
||||||
/>
|
setSystemPrompt={setSystemPrompt}
|
||||||
</Drawer>
|
temporaryChat={temporaryChat}
|
||||||
|
history={history}
|
||||||
|
/>
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
<CurrentChatModelSettings
|
<CurrentChatModelSettings
|
||||||
open={openModelSettings}
|
open={openModelSettings}
|
||||||
setOpen={setOpenModelSettings}
|
setOpen={setOpenModelSettings}
|
||||||
useDrawer
|
useDrawer
|
||||||
/>
|
/>
|
||||||
</>
|
</main>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -20,9 +20,7 @@ export const NewChat: React.FC<Props> = ({ clearChat }) => {
|
|||||||
<label className="flex items-center gap-6 justify-between px-1 py-0.5 cursor-pointer w-full">
|
<label className="flex items-center gap-6 justify-between px-1 py-0.5 cursor-pointer w-full">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<TimerReset className="h-4 w-4 text-gray-600" />
|
<TimerReset className="h-4 w-4 text-gray-600" />
|
||||||
<span>
|
<span>{t("temporaryChat")}</span>
|
||||||
{t("temporaryChat")}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={temporaryChat}
|
checked={temporaryChat}
|
||||||
@ -44,12 +42,12 @@ export const NewChat: React.FC<Props> = ({ clearChat }) => {
|
|||||||
<button
|
<button
|
||||||
onClick={clearChat}
|
onClick={clearChat}
|
||||||
className="inline-flex dark:bg-transparent bg-white items-center rounded-s-lg rounded-e-none border dark:border-gray-700 bg-transparent px-3 py-2.5 pe-6 text-xs lg:text-sm font-medium leading-4 text-gray-800 dark:text-white disabled:opacity-50 ease-in-out transition-colors duration-200 hover:bg-gray-100 dark:hover:bg-gray-800 dark:hover:text-white">
|
className="inline-flex dark:bg-transparent bg-white items-center rounded-s-lg rounded-e-none border dark:border-gray-700 bg-transparent px-3 py-2.5 pe-6 text-xs lg:text-sm font-medium leading-4 text-gray-800 dark:text-white disabled:opacity-50 ease-in-out transition-colors duration-200 hover:bg-gray-100 dark:hover:bg-gray-800 dark:hover:text-white">
|
||||||
<SquarePen className="h-5 w-5" />
|
<SquarePen className="size-4 sm:size-5" />
|
||||||
<span className="truncate ms-3">{t("newChat")}</span>
|
<span className="truncate ms-3 hidden sm:inline">{t("newChat")}</span>
|
||||||
</button>
|
</button>{" "}
|
||||||
<Dropdown menu={{ items }} trigger={["click"]}>
|
<Dropdown menu={{ items }} trigger={["click"]}>
|
||||||
<button className="inline-flex dark:bg-transparent bg-white items-center rounded-lg border-s-0 rounded-s-none border dark:border-gray-700 bg-transparent px-3 py-2.5 text-xs lg:text-sm font-medium leading-4 text-gray-800 dark:text-white disabled:opacity-50 ease-in-out transition-colors duration-200 hover:bg-gray-100 dark:hover:bg-gray-800 dark:hover:text-white">
|
<button className="inline-flex dark:bg-transparent bg-white items-center rounded-lg border-s-0 rounded-s-none border dark:border-gray-700 bg-transparent px-3 py-2.5 text-xs lg:text-sm font-medium leading-4 text-gray-800 dark:text-white disabled:opacity-50 ease-in-out transition-colors duration-200 hover:bg-gray-100 dark:hover:bg-gray-800 dark:hover:text-white">
|
||||||
<MoreHorizontal className="h-5 w-5 text-gray-600 dark:text-gray-400" />
|
<MoreHorizontal className="size-4 sm:size-5 text-gray-600 dark:text-gray-400" />
|
||||||
</button>
|
</button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,93 +54,95 @@ const LinkComponent = (item: {
|
|||||||
export const SettingsLayout = ({ children }: { children: React.ReactNode }) => {
|
export const SettingsLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const { t } = useTranslation(["settings", "common", "openai"])
|
const { t } = useTranslation(["settings", "common", "openai"])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex min-h-screen -z-10 w-full flex-col">
|
||||||
<div className="mx-auto max-w-7xl lg:flex lg:gap-x-16 lg:px-8">
|
<main className="relative w-full flex-1">
|
||||||
<aside className="flex lg:rounded-md bg-white lg:p-4 lg:mt-20 overflow-x-auto lg:border-0 border-b py-4 lg:block lg:w-80 lg:flex-none dark:bg-[#171717] dark:border-gray-600">
|
<div className="mx-auto w-full h-full custom-scrollbar overflow-y-auto">
|
||||||
<nav className="flex-none px-4 sm:px-6 lg:px-0">
|
<div className="flex flex-col lg:flex-row lg:gap-x-16 lg:px-24">
|
||||||
<ul
|
<aside className="sticky lg:mt-0 mt-14 top-0 bg-white dark:bg-[#171717] border-b dark:border-gray-600 lg:border-0 lg:bg-transparent lg:dark:bg-transparent">
|
||||||
role="list"
|
<nav className="w-full overflow-x-auto px-4 py-4 sm:px-6 lg:px-0 lg:py-0 lg:mt-20">
|
||||||
className="flex gap-x-3 gap-y-1 whitespace-nowrap lg:flex-col">
|
<ul
|
||||||
<LinkComponent
|
role="list"
|
||||||
href="/settings"
|
className="flex flex-row lg:flex-col gap-x-3 gap-y-1 min-w-max lg:min-w-0">
|
||||||
name={t("generalSettings.title")}
|
<LinkComponent
|
||||||
icon={OrbitIcon}
|
href="/settings"
|
||||||
current={location.pathname}
|
name={t("generalSettings.title")}
|
||||||
/>
|
icon={OrbitIcon}
|
||||||
<LinkComponent
|
current={location.pathname}
|
||||||
href="/settings/rag"
|
/>
|
||||||
name={t("rag.title")}
|
<LinkComponent
|
||||||
icon={CombineIcon}
|
href="/settings/rag"
|
||||||
current={location.pathname}
|
name={t("rag.title")}
|
||||||
/>
|
icon={CombineIcon}
|
||||||
<LinkComponent
|
current={location.pathname}
|
||||||
href="/settings/ollama"
|
/>
|
||||||
name={t("ollamaSettings.title")}
|
<LinkComponent
|
||||||
icon={OllamaIcon}
|
href="/settings/ollama"
|
||||||
current={location.pathname}
|
name={t("ollamaSettings.title")}
|
||||||
/>
|
icon={OllamaIcon}
|
||||||
{import.meta.env.BROWSER === "chrome" && (
|
current={location.pathname}
|
||||||
<LinkComponent
|
/>
|
||||||
href="/settings/chrome"
|
{import.meta.env.BROWSER === "chrome" && (
|
||||||
name={t("chromeAiSettings.title")}
|
<LinkComponent
|
||||||
icon={ChromeIcon}
|
href="/settings/chrome"
|
||||||
current={location.pathname}
|
name={t("chromeAiSettings.title")}
|
||||||
beta
|
icon={ChromeIcon}
|
||||||
/>
|
current={location.pathname}
|
||||||
)}
|
beta
|
||||||
<LinkComponent
|
/>
|
||||||
href="/settings/openai"
|
)}
|
||||||
name={t("openai:settings")}
|
<LinkComponent
|
||||||
icon={CpuIcon}
|
href="/settings/openai"
|
||||||
current={location.pathname}
|
name={t("openai:settings")}
|
||||||
beta
|
icon={CpuIcon}
|
||||||
/>
|
current={location.pathname}
|
||||||
<LinkComponent
|
beta
|
||||||
href="/settings/model"
|
/>
|
||||||
name={t("manageModels.title")}
|
<LinkComponent
|
||||||
current={location.pathname}
|
href="/settings/model"
|
||||||
icon={BrainCircuitIcon}
|
name={t("manageModels.title")}
|
||||||
/>
|
current={location.pathname}
|
||||||
<LinkComponent
|
icon={BrainCircuitIcon}
|
||||||
href="/settings/knowledge"
|
/>
|
||||||
name={
|
<LinkComponent
|
||||||
<div className="inline-flex items-center gap-2">
|
href="/settings/knowledge"
|
||||||
{t("manageKnowledge.title")}
|
name={
|
||||||
</div>
|
<div className="inline-flex items-center gap-2">
|
||||||
}
|
{t("manageKnowledge.title")}
|
||||||
icon={BlocksIcon}
|
</div>
|
||||||
current={location.pathname}
|
}
|
||||||
/>
|
icon={BlocksIcon}
|
||||||
<LinkComponent
|
current={location.pathname}
|
||||||
href="/settings/prompt"
|
/>
|
||||||
name={t("managePrompts.title")}
|
<LinkComponent
|
||||||
icon={BookIcon}
|
href="/settings/prompt"
|
||||||
current={location.pathname}
|
name={t("managePrompts.title")}
|
||||||
/>
|
icon={BookIcon}
|
||||||
<LinkComponent
|
current={location.pathname}
|
||||||
href="/settings/share"
|
/>
|
||||||
name={t("manageShare.title")}
|
<LinkComponent
|
||||||
icon={ShareIcon}
|
href="/settings/share"
|
||||||
current={location.pathname}
|
name={t("manageShare.title")}
|
||||||
/>
|
icon={ShareIcon}
|
||||||
<LinkComponent
|
current={location.pathname}
|
||||||
href="/settings/about"
|
/>
|
||||||
name={t("about.title")}
|
<LinkComponent
|
||||||
icon={InfoIcon}
|
href="/settings/about"
|
||||||
current={location.pathname}
|
name={t("about.title")}
|
||||||
/>
|
icon={InfoIcon}
|
||||||
</ul>
|
current={location.pathname}
|
||||||
</nav>
|
/>
|
||||||
</aside>
|
</ul>
|
||||||
|
</nav>
|
||||||
<main className={"px-4 py-16 sm:px-6 lg:flex-auto lg:px-0 lg:py-20"}>
|
</aside>
|
||||||
<div className="mx-auto max-w-2xl space-y-16 sm:space-y-10 lg:mx-0 lg:max-w-none">
|
<main className="flex-1 px-4 py-8 sm:px-6 lg:px-0 lg:py-20">
|
||||||
{children}
|
<div className="mx-auto max-w-4xl space-y-8 sm:space-y-10">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,8 @@ import {
|
|||||||
} from "@/db"
|
} from "@/db"
|
||||||
import { getLastUsedChatSystemPrompt } from "@/services/model-settings"
|
import { getLastUsedChatSystemPrompt } from "@/services/model-settings"
|
||||||
import { useStoreChatModelSettings } from "@/store/model"
|
import { useStoreChatModelSettings } from "@/store/model"
|
||||||
|
import { useSmartScroll } from "@/hooks/useSmartScroll"
|
||||||
|
import { ChevronDown } from "lucide-react"
|
||||||
|
|
||||||
export const Playground = () => {
|
export const Playground = () => {
|
||||||
const drop = React.useRef<HTMLDivElement>(null)
|
const drop = React.useRef<HTMLDivElement>(null)
|
||||||
@ -21,9 +23,14 @@ export const Playground = () => {
|
|||||||
setHistoryId,
|
setHistoryId,
|
||||||
setHistory,
|
setHistory,
|
||||||
setMessages,
|
setMessages,
|
||||||
setSelectedSystemPrompt
|
setSelectedSystemPrompt,
|
||||||
|
streaming
|
||||||
} = useMessageOption()
|
} = useMessageOption()
|
||||||
const { setSystemPrompt } = useStoreChatModelSettings()
|
const { setSystemPrompt } = useStoreChatModelSettings()
|
||||||
|
const { containerRef, isAtBottom, scrollToBottom } = useSmartScroll(
|
||||||
|
messages,
|
||||||
|
streaming
|
||||||
|
)
|
||||||
|
|
||||||
const [dropState, setDropState] = React.useState<
|
const [dropState, setDropState] = React.useState<
|
||||||
"idle" | "dragging" | "error"
|
"idle" | "dragging" | "error"
|
||||||
@ -125,23 +132,25 @@ export const Playground = () => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={drop}
|
ref={drop}
|
||||||
className={`${
|
className={`relative flex h-full flex-col items-center ${
|
||||||
dropState === "dragging" ? "bg-gray-100 dark:bg-gray-800 z-10" : ""
|
dropState === "dragging" ? "bg-gray-100 dark:bg-gray-800" : ""
|
||||||
} bg-white dark:bg-[#171717]`}>
|
} bg-white dark:bg-[#171717]`}>
|
||||||
<PlaygroundChat />
|
<div
|
||||||
|
ref={containerRef}
|
||||||
<div className="flex flex-col items-center">
|
className="custom-scrollbar bg-bottom-mask-light dark:bg-bottom-mask-dark mask-bottom-fade will-change-mask flex h-full w-full flex-col items-center overflow-x-hidden overflow-y-auto px-5">
|
||||||
<div className="flex-grow">
|
<PlaygroundChat />
|
||||||
<div className="w-full flex justify-center">
|
</div>
|
||||||
<div className="bottom-0 w-full bg-transparent border-0 fixed pt-2">
|
<div className="absolute bottom-0 w-full">
|
||||||
<div className="stretch mx-2 flex flex-row gap-3 md:mx-4 lg:mx-auto lg:max-w-2xl xl:max-w-3xl justify-center items-center">
|
{!isAtBottom && (
|
||||||
<div className="relative h-full flex-1 items-center justify-center md:flex-col">
|
<div className="fixed bottom-36 z-20 left-0 right-0 flex justify-center">
|
||||||
<PlaygroundForm dropedFile={dropedFile} />
|
<button
|
||||||
</div>
|
onClick={scrollToBottom}
|
||||||
</div>
|
className="bg-gray-50 shadow border border-gray-200 dark:border-none dark:bg-white/20 p-1.5 rounded-full pointer-events-auto">
|
||||||
</div>
|
<ChevronDown className="size-4 text-gray-600 dark:text-gray-300" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
<PlaygroundForm dropedFile={dropedFile} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -3,8 +3,6 @@ import { useMessageOption } from "~/hooks/useMessageOption"
|
|||||||
import { PlaygroundEmpty } from "./PlaygroundEmpty"
|
import { PlaygroundEmpty } from "./PlaygroundEmpty"
|
||||||
import { PlaygroundMessage } from "~/components/Common/Playground/Message"
|
import { PlaygroundMessage } from "~/components/Common/Playground/Message"
|
||||||
import { MessageSourcePopup } from "@/components/Common/Playground/MessageSourcePopup"
|
import { MessageSourcePopup } from "@/components/Common/Playground/MessageSourcePopup"
|
||||||
import { useSmartScroll } from "~/hooks/useSmartScroll"
|
|
||||||
import { ChevronDown } from "lucide-react"
|
|
||||||
|
|
||||||
export const PlaygroundChat = () => {
|
export const PlaygroundChat = () => {
|
||||||
const {
|
const {
|
||||||
@ -18,18 +16,11 @@ export const PlaygroundChat = () => {
|
|||||||
const [isSourceOpen, setIsSourceOpen] = React.useState(false)
|
const [isSourceOpen, setIsSourceOpen] = React.useState(false)
|
||||||
const [source, setSource] = React.useState<any>(null)
|
const [source, setSource] = React.useState<any>(null)
|
||||||
|
|
||||||
const { containerRef, isAtBottom, scrollToBottom } = useSmartScroll(
|
|
||||||
messages,
|
|
||||||
streaming
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div className="relative flex w-full flex-col items-center pt-16 pb-4">
|
||||||
ref={containerRef}
|
|
||||||
className="custom-scrollbar grow flex flex-col md:translate-x-0 transition-transform duration-300 ease-in-out overflow-y-auto h-[calc(100vh-160px)]">
|
|
||||||
{messages.length === 0 && (
|
{messages.length === 0 && (
|
||||||
<div className="mt-32">
|
<div className="mt-32 w-full">
|
||||||
<PlaygroundEmpty />
|
<PlaygroundEmpty />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -59,19 +50,9 @@ export const PlaygroundChat = () => {
|
|||||||
reasoningTimeTaken={message?.reasoning_time_taken}
|
reasoningTimeTaken={message?.reasoning_time_taken}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{messages.length > 0 && (
|
|
||||||
<div className="w-full h-10 flex-shrink-0"></div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{!isAtBottom && (
|
<div className="w-full pb-[157px]"></div>
|
||||||
<div className="fixed bottom-36 z-20 left-0 right-0 flex justify-center">
|
|
||||||
<button
|
|
||||||
onClick={scrollToBottom}
|
|
||||||
className="bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full pointer-events-auto">
|
|
||||||
<ChevronDown className="size-4 text-gray-600 dark:text-gray-300" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<MessageSourcePopup
|
<MessageSourcePopup
|
||||||
open={isSourceOpen}
|
open={isSourceOpen}
|
||||||
setOpen={setIsSourceOpen}
|
setOpen={setIsSourceOpen}
|
||||||
|
@ -205,241 +205,251 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex w-full flex-col items-center p-2 pt-1 pb-4">
|
||||||
className={`px-3 pt-3 bg-gray-100 dark:bg-[#262626] border rounded-t-xl dark:border-gray-600
|
<div className="relative z-10 flex w-full flex-col items-center justify-center gap-2 text-base">
|
||||||
${temporaryChat && "!bg-gray-200 dark:!bg-black "}
|
<div className="relative flex w-full flex-row justify-center gap-2 lg:w-4/5">
|
||||||
`}>
|
<div
|
||||||
<div
|
className={` bg-neutral-50 dark:bg-[#262626] relative w-full max-w-[48rem] p-1 backdrop-blur-lg duration-100 border border-gray-300 rounded-xl dark:border-gray-600
|
||||||
className={`h-full rounded-md shadow relative ${
|
${temporaryChat ? "!bg-gray-200 dark:!bg-black " : ""}
|
||||||
form.values.image.length === 0 ? "hidden" : "block"
|
`}>
|
||||||
}`}>
|
<div
|
||||||
<div className="relative">
|
className={`border-b border-gray-200 dark:border-gray-600 relative ${
|
||||||
<Image
|
form.values.image.length === 0 ? "hidden" : "block"
|
||||||
src={form.values.image}
|
}`}>
|
||||||
alt="Uploaded Image"
|
<button
|
||||||
width={180}
|
type="button"
|
||||||
preview={false}
|
onClick={() => {
|
||||||
className="rounded-md"
|
form.setFieldValue("image", "")
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
form.setFieldValue("image", "")
|
|
||||||
}}
|
|
||||||
className="flex items-center justify-center absolute top-0 m-2 bg-white dark:bg-[#262626] p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-600 text-black dark:text-gray-100">
|
|
||||||
<X className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className={`flex rounded-t-xl bg-white dark:bg-transparent ${
|
|
||||||
temporaryChat && "!bg-gray-100 dark:!bg-black"
|
|
||||||
}`}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.onSubmit(async (value) => {
|
|
||||||
stopListening()
|
|
||||||
if (!selectedModel || selectedModel.length === 0) {
|
|
||||||
form.setFieldError("message", t("formError.noModel"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (webSearch) {
|
|
||||||
const defaultEM = await defaultEmbeddingModelForRag()
|
|
||||||
if (!defaultEM) {
|
|
||||||
form.setFieldError("message", t("formError.noEmbeddingModel"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
value.message.trim().length === 0 &&
|
|
||||||
value.image.length === 0
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
form.reset()
|
|
||||||
textAreaFocus()
|
|
||||||
await sendMessage({
|
|
||||||
image: value.image,
|
|
||||||
message: value.message.trim()
|
|
||||||
})
|
|
||||||
})}
|
|
||||||
className="shrink-0 flex-grow flex flex-col items-center ">
|
|
||||||
<input
|
|
||||||
id="file-upload"
|
|
||||||
name="file-upload"
|
|
||||||
type="file"
|
|
||||||
className="sr-only"
|
|
||||||
ref={inputRef}
|
|
||||||
accept="image/*"
|
|
||||||
multiple={false}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
<div className="w-full border-x border-t flex flex-col dark:border-gray-600 rounded-t-xl p-2">
|
|
||||||
<textarea
|
|
||||||
onCompositionStart={() => {
|
|
||||||
if (import.meta.env.BROWSER !== "firefox") {
|
|
||||||
setTyping(true)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
onCompositionEnd={() => {
|
className="absolute top-1 left-1 flex items-center justify-center z-10 bg-white dark:bg-[#262626] p-0.5 rounded-full hover:bg-gray-100 dark:hover:bg-gray-600 text-black dark:text-gray-100">
|
||||||
if (import.meta.env.BROWSER !== "firefox") {
|
<X className="h-4 w-4" />
|
||||||
setTyping(false)
|
</button>{" "}
|
||||||
}
|
<Image
|
||||||
}}
|
src={form.values.image}
|
||||||
onKeyDown={(e) => handleKeyDown(e)}
|
alt="Uploaded Image"
|
||||||
ref={textareaRef}
|
preview={false}
|
||||||
className="px-2 py-2 w-full resize-none bg-transparent focus-within:outline-none focus:ring-0 focus-visible:ring-0 ring-0 dark:ring-0 border-0 dark:text-gray-100"
|
className="rounded-md max-h-32"
|
||||||
onPaste={handlePaste}
|
|
||||||
rows={1}
|
|
||||||
style={{ minHeight: "30px" }}
|
|
||||||
tabIndex={0}
|
|
||||||
placeholder={t("form.textarea.placeholder")}
|
|
||||||
{...form.getInputProps("message")}
|
|
||||||
/>
|
/>
|
||||||
<div className="mt-2 flex justify-between items-center">
|
|
||||||
<div className="flex">
|
|
||||||
{!selectedKnowledge && (
|
|
||||||
<Tooltip title={t("tooltip.searchInternet")}>
|
|
||||||
<div className="inline-flex items-center gap-2">
|
|
||||||
<PiGlobe className={`h-5 w-5 dark:text-gray-300 `} />
|
|
||||||
<Switch
|
|
||||||
value={webSearch}
|
|
||||||
onChange={(e) => setWebSearch(e)}
|
|
||||||
checkedChildren={t("form.webSearch.on")}
|
|
||||||
unCheckedChildren={t("form.webSearch.off")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex !justify-end gap-3">
|
|
||||||
{!selectedKnowledge && (
|
|
||||||
<Tooltip title={t("tooltip.uploadImage")}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
inputRef.current?.click()
|
|
||||||
}}
|
|
||||||
className={`flex items-center justify-center dark:text-gray-300 ${
|
|
||||||
chatMode === "rag" ? "hidden" : "block"
|
|
||||||
}`}>
|
|
||||||
<ImageIcon className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{browserSupportsSpeechRecognition && (
|
|
||||||
<Tooltip title={t("tooltip.speechToText")}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={async () => {
|
|
||||||
if (isListening) {
|
|
||||||
stopSpeechRecognition()
|
|
||||||
} else {
|
|
||||||
resetTranscript()
|
|
||||||
startListening({
|
|
||||||
continuous: true,
|
|
||||||
lang: speechToTextLanguage
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={`flex items-center justify-center dark:text-gray-300`}>
|
|
||||||
{!isListening ? (
|
|
||||||
<MicIcon className="h-5 w-5" />
|
|
||||||
) : (
|
|
||||||
<div className="relative">
|
|
||||||
<span className="animate-ping absolute inline-flex h-3 w-3 rounded-full bg-red-400 opacity-75"></span>
|
|
||||||
<MicIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
<KnowledgeSelect />
|
|
||||||
|
|
||||||
{!isSending ? (
|
|
||||||
<Dropdown.Button
|
|
||||||
htmlType="submit"
|
|
||||||
disabled={isSending}
|
|
||||||
className="!justify-end !w-auto"
|
|
||||||
icon={
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="w-5 h-5">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="m19.5 8.25-7.5 7.5-7.5-7.5"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
menu={{
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
key: 1,
|
|
||||||
label: (
|
|
||||||
<Checkbox
|
|
||||||
checked={sendWhenEnter}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSendWhenEnter(e.target.checked)
|
|
||||||
}>
|
|
||||||
{t("sendWhenEnter")}
|
|
||||||
</Checkbox>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 2,
|
|
||||||
label: (
|
|
||||||
<Checkbox
|
|
||||||
checked={useOCR}
|
|
||||||
onChange={(e) => setUseOCR(e.target.checked)}>
|
|
||||||
{t("useOCR")}
|
|
||||||
</Checkbox>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}}>
|
|
||||||
<div className="inline-flex gap-2">
|
|
||||||
{sendWhenEnter ? (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
|
||||||
className="h-5 w-5"
|
|
||||||
viewBox="0 0 24 24">
|
|
||||||
<path d="M9 10L4 15 9 20"></path>
|
|
||||||
<path d="M20 4v7a4 4 0 01-4 4H4"></path>
|
|
||||||
</svg>
|
|
||||||
) : null}
|
|
||||||
{t("common:submit")}
|
|
||||||
</div>
|
|
||||||
</Dropdown.Button>
|
|
||||||
) : (
|
|
||||||
<Tooltip title={t("tooltip.stopStreaming")}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={stopStreamingRequest}
|
|
||||||
className="text-gray-800 dark:text-gray-300">
|
|
||||||
<StopCircleIcon className="h-6 w-6" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<div>
|
||||||
</div>
|
<div
|
||||||
{form.errors.message && (
|
className={`flex bg-transparent `}>
|
||||||
<div className="text-red-500 text-center text-sm mt-1">
|
<form
|
||||||
{form.errors.message}
|
onSubmit={form.onSubmit(async (value) => {
|
||||||
|
stopListening()
|
||||||
|
if (!selectedModel || selectedModel.length === 0) {
|
||||||
|
form.setFieldError("message", t("formError.noModel"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (webSearch) {
|
||||||
|
const defaultEM = await defaultEmbeddingModelForRag()
|
||||||
|
const simpleSearch = await getIsSimpleInternetSearch()
|
||||||
|
if (!defaultEM && !simpleSearch) {
|
||||||
|
form.setFieldError(
|
||||||
|
"message",
|
||||||
|
t("formError.noEmbeddingModel")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
value.message.trim().length === 0 &&
|
||||||
|
value.image.length === 0
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
form.reset()
|
||||||
|
textAreaFocus()
|
||||||
|
await sendMessage({
|
||||||
|
image: value.image,
|
||||||
|
message: value.message.trim()
|
||||||
|
})
|
||||||
|
})}
|
||||||
|
className="shrink-0 flex-grow flex flex-col items-center ">
|
||||||
|
<input
|
||||||
|
id="file-upload"
|
||||||
|
name="file-upload"
|
||||||
|
type="file"
|
||||||
|
className="sr-only"
|
||||||
|
ref={inputRef}
|
||||||
|
accept="image/*"
|
||||||
|
multiple={false}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
<div className="w-full flex flex-col dark:border-gray-600 p-2">
|
||||||
|
<textarea
|
||||||
|
onCompositionStart={() => {
|
||||||
|
if (import.meta.env.BROWSER !== "firefox") {
|
||||||
|
setTyping(true)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCompositionEnd={() => {
|
||||||
|
if (import.meta.env.BROWSER !== "firefox") {
|
||||||
|
setTyping(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => handleKeyDown(e)}
|
||||||
|
ref={textareaRef}
|
||||||
|
className="px-2 py-2 w-full resize-none bg-transparent focus-within:outline-none focus:ring-0 focus-visible:ring-0 ring-0 dark:ring-0 border-0 dark:text-gray-100"
|
||||||
|
onPaste={handlePaste}
|
||||||
|
rows={1}
|
||||||
|
style={{ minHeight: "35px" }}
|
||||||
|
tabIndex={0}
|
||||||
|
placeholder={t("form.textarea.placeholder")}
|
||||||
|
{...form.getInputProps("message")}
|
||||||
|
/>
|
||||||
|
<div className="mt-2 flex justify-between items-center">
|
||||||
|
<div className="flex">
|
||||||
|
{!selectedKnowledge && (
|
||||||
|
<Tooltip title={t("tooltip.searchInternet")}>
|
||||||
|
<div className="inline-flex items-center gap-2">
|
||||||
|
<PiGlobe
|
||||||
|
className={`h-5 w-5 dark:text-gray-300 `}
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
value={webSearch}
|
||||||
|
onChange={(e) => setWebSearch(e)}
|
||||||
|
checkedChildren={t("form.webSearch.on")}
|
||||||
|
unCheckedChildren={t("form.webSearch.off")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex !justify-end gap-3">
|
||||||
|
{!selectedKnowledge && (
|
||||||
|
<Tooltip title={t("tooltip.uploadImage")}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
inputRef.current?.click()
|
||||||
|
}}
|
||||||
|
className={`flex items-center justify-center dark:text-gray-300 ${
|
||||||
|
chatMode === "rag" ? "hidden" : "block"
|
||||||
|
}`}>
|
||||||
|
<ImageIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{browserSupportsSpeechRecognition && (
|
||||||
|
<Tooltip title={t("tooltip.speechToText")}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
if (isListening) {
|
||||||
|
stopSpeechRecognition()
|
||||||
|
} else {
|
||||||
|
resetTranscript()
|
||||||
|
startListening({
|
||||||
|
continuous: true,
|
||||||
|
lang: speechToTextLanguage
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`flex items-center justify-center dark:text-gray-300`}>
|
||||||
|
{!isListening ? (
|
||||||
|
<MicIcon className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<div className="relative">
|
||||||
|
<span className="animate-ping absolute inline-flex h-3 w-3 rounded-full bg-red-400 opacity-75"></span>
|
||||||
|
<MicIcon className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<KnowledgeSelect />
|
||||||
|
|
||||||
|
{!isSending ? (
|
||||||
|
<Dropdown.Button
|
||||||
|
htmlType="submit"
|
||||||
|
disabled={isSending}
|
||||||
|
className="!justify-end !w-auto"
|
||||||
|
icon={
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-5 h-5">
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="m19.5 8.25-7.5 7.5-7.5-7.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
menu={{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 1,
|
||||||
|
label: (
|
||||||
|
<Checkbox
|
||||||
|
checked={sendWhenEnter}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSendWhenEnter(e.target.checked)
|
||||||
|
}>
|
||||||
|
{t("sendWhenEnter")}
|
||||||
|
</Checkbox>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 2,
|
||||||
|
label: (
|
||||||
|
<Checkbox
|
||||||
|
checked={useOCR}
|
||||||
|
onChange={(e) =>
|
||||||
|
setUseOCR(e.target.checked)
|
||||||
|
}>
|
||||||
|
{t("useOCR")}
|
||||||
|
</Checkbox>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}}>
|
||||||
|
<div className="inline-flex gap-2">
|
||||||
|
{sendWhenEnter ? (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
className="h-5 w-5"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path d="M9 10L4 15 9 20"></path>
|
||||||
|
<path d="M20 4v7a4 4 0 01-4 4H4"></path>
|
||||||
|
</svg>
|
||||||
|
) : null}
|
||||||
|
{t("common:submit")}
|
||||||
|
</div>
|
||||||
|
</Dropdown.Button>
|
||||||
|
) : (
|
||||||
|
<Tooltip title={t("tooltip.stopStreaming")}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={stopStreamingRequest}
|
||||||
|
className="text-gray-800 dark:text-gray-300">
|
||||||
|
<StopCircleIcon className="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{form.errors.message && (
|
||||||
|
<div className="text-red-500 text-center text-sm mt-1">
|
||||||
|
{form.errors.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -17,6 +17,7 @@ export const TTSModeSettings = ({ hideBorder }: { hideBorder?: boolean }) => {
|
|||||||
ttsProvider: "",
|
ttsProvider: "",
|
||||||
voice: "",
|
voice: "",
|
||||||
ssmlEnabled: false,
|
ssmlEnabled: false,
|
||||||
|
removeReasoningTagTTS: true,
|
||||||
elevenLabsApiKey: "",
|
elevenLabsApiKey: "",
|
||||||
elevenLabsVoiceId: "",
|
elevenLabsVoiceId: "",
|
||||||
elevenLabsModel: "",
|
elevenLabsModel: "",
|
||||||
@ -209,6 +210,20 @@ export const TTSModeSettings = ({ hideBorder }: { hideBorder?: boolean }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between">
|
||||||
|
<span className="text-gray-700 dark:text-neutral-50 ">
|
||||||
|
{t("generalSettings.tts.removeReasoningTagTTS.label")}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
className="mt-4 sm:mt-0"
|
||||||
|
{...form.getInputProps("removeReasoningTagTTS", {
|
||||||
|
type: "checkbox"
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<SaveButton btnType="submit" />
|
<SaveButton btnType="submit" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,42 +23,45 @@ export const SidePanelBody = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<div className="grow flex flex-col md:translate-x-0 transition-transform duration-300 ease-in-out">
|
<>
|
||||||
{messages.length === 0 && <EmptySidePanel />}
|
<div className="relative flex w-full flex-col items-center pt-16 pb-4">
|
||||||
{messages.map((message, index) => (
|
{messages.length === 0 && <EmptySidePanel />}
|
||||||
<PlaygroundMessage
|
{messages.map((message, index) => (
|
||||||
key={index}
|
<PlaygroundMessage
|
||||||
isBot={message.isBot}
|
key={index}
|
||||||
message={message.message}
|
isBot={message.isBot}
|
||||||
name={message.name}
|
message={message.message}
|
||||||
images={message.images || []}
|
name={message.name}
|
||||||
currentMessageIndex={index}
|
images={message.images || []}
|
||||||
totalMessages={messages.length}
|
currentMessageIndex={index}
|
||||||
onRengerate={regenerateLastMessage}
|
totalMessages={messages.length}
|
||||||
message_type={message.messageType}
|
onRengerate={regenerateLastMessage}
|
||||||
isProcessing={streaming}
|
message_type={message.messageType}
|
||||||
isSearchingInternet={isSearchingInternet}
|
isProcessing={streaming}
|
||||||
sources={message.sources}
|
isSearchingInternet={isSearchingInternet}
|
||||||
onEditFormSubmit={(value) => {
|
sources={message.sources}
|
||||||
editMessage(index, value, !message.isBot)
|
onEditFormSubmit={(value) => {
|
||||||
}}
|
editMessage(index, value, !message.isBot)
|
||||||
onSourceClick={(data) => {
|
}}
|
||||||
setSource(data)
|
onSourceClick={(data) => {
|
||||||
setIsSourceOpen(true)
|
setSource(data)
|
||||||
}}
|
setIsSourceOpen(true)
|
||||||
isTTSEnabled={ttsEnabled}
|
}}
|
||||||
generationInfo={message?.generationInfo}
|
isTTSEnabled={ttsEnabled}
|
||||||
isStreaming={streaming}
|
generationInfo={message?.generationInfo}
|
||||||
reasoningTimeTaken={message?.reasoning_time_taken}
|
isStreaming={streaming}
|
||||||
/>
|
reasoningTimeTaken={message?.reasoning_time_taken}
|
||||||
))}
|
/>
|
||||||
<div className="w-full h-48 flex-shrink-0"></div>
|
))}
|
||||||
<div ref={divRef} />
|
<div ref={divRef} />
|
||||||
|
</div>
|
||||||
|
<div className="w-full pb-[157px]"></div>
|
||||||
|
|
||||||
<MessageSourcePopup
|
<MessageSourcePopup
|
||||||
open={isSourceOpen}
|
open={isSourceOpen}
|
||||||
setOpen={setIsSourceOpen}
|
setOpen={setIsSourceOpen}
|
||||||
source={source}
|
source={source}
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -134,8 +134,8 @@ export const EmptySidePanel = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto sm:max-w-md px-4 mt-10">
|
<div className="mx-auto sm:max-w-lg px-4 mt-10">
|
||||||
<div className="rounded-lg justify-center items-center flex flex-col border dark:border-gray-700 p-8 bg-white dark:bg-[#262626] shadow-sm">
|
<div className="rounded-lg justify-center items-center flex flex-col border border-gray-300 dark:border-gray-700 p-8 bg-white dark:bg-[#262626] shadow-sm">
|
||||||
{(ollamaStatus === "pending" || isRefetching) && (
|
{(ollamaStatus === "pending" || isRefetching) && (
|
||||||
<div className="inline-flex items-center space-x-2">
|
<div className="inline-flex items-center space-x-2">
|
||||||
<div className="w-3 h-3 bg-blue-500 rounded-full animate-bounce"></div>
|
<div className="w-3 h-3 bg-blue-500 rounded-full animate-bounce"></div>
|
||||||
|
@ -226,271 +226,290 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
|
|||||||
}, [defaultChatWithWebsite])
|
}, [defaultChatWithWebsite])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-3 pt-3 md:px-6 md:pt-6 bg-white dark:bg-[#262626] border rounded-t-xl border-gray-300 dark:border-gray-600">
|
<div className="flex w-full flex-col items-center p-2 pt-1 pb-4">
|
||||||
<div
|
<div className="relative z-10 flex w-full flex-col items-center justify-center gap-2 text-base">
|
||||||
className={`h-full rounded-md shadow relative ${
|
<div className="relative flex w-full flex-row justify-center gap-2 lg:w-4/5">
|
||||||
form.values.image.length === 0 ? "hidden" : "block"
|
<div
|
||||||
}`}>
|
className={` bg-neutral-50 dark:bg-[#262626] relative w-full max-w-[48rem] p-1 backdrop-blur-lg duration-100 border border-gray-300 rounded-xl dark:border-gray-600
|
||||||
<div className="relative">
|
`}>
|
||||||
<Image
|
<div
|
||||||
src={form.values.image}
|
className={`h-full shadow relative ${
|
||||||
alt="Uploaded Image"
|
form.values.image.length === 0 ? "hidden" : "block"
|
||||||
width={180}
|
}`}>
|
||||||
preview={false}
|
<div className="relative">
|
||||||
className="rounded-md"
|
<Image
|
||||||
/>
|
src={form.values.image}
|
||||||
<button
|
alt="Uploaded Image"
|
||||||
onClick={() => {
|
width={180}
|
||||||
form.setFieldValue("image", "")
|
preview={false}
|
||||||
}}
|
className="rounded-md"
|
||||||
className="flex items-center justify-center absolute top-0 m-2 bg-white dark:bg-[#262626] p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-600 text-black dark:text-gray-100">
|
/>
|
||||||
<X className="h-5 w-5" />
|
<button
|
||||||
</button>
|
onClick={() => {
|
||||||
</div>
|
form.setFieldValue("image", "")
|
||||||
</div>
|
}}
|
||||||
<div>
|
className="flex items-center justify-center absolute top-0 m-2 bg-white dark:bg-[#262626] p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-600 text-black dark:text-gray-100">
|
||||||
<div className="flex">
|
<X className="h-5 w-5" />
|
||||||
<form
|
</button>
|
||||||
onSubmit={form.onSubmit(async (value) => {
|
|
||||||
if (!selectedModel || selectedModel.length === 0) {
|
|
||||||
form.setFieldError("message", t("formError.noModel"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (chatMode === "rag") {
|
|
||||||
const defaultEM = await defaultEmbeddingModelForRag()
|
|
||||||
if (!defaultEM) {
|
|
||||||
form.setFieldError("message", t("formError.noEmbeddingModel"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (webSearch) {
|
|
||||||
const defaultEM = await defaultEmbeddingModelForRag()
|
|
||||||
if (!defaultEM) {
|
|
||||||
form.setFieldError("message", t("formError.noEmbeddingModel"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await stopListening()
|
|
||||||
if (
|
|
||||||
value.message.trim().length === 0 &&
|
|
||||||
value.image.length === 0
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
form.reset()
|
|
||||||
textAreaFocus()
|
|
||||||
await sendMessage({
|
|
||||||
image: value.image,
|
|
||||||
message: value.message.trim()
|
|
||||||
})
|
|
||||||
})}
|
|
||||||
className="shrink-0 flex-grow flex flex-col items-center ">
|
|
||||||
<input
|
|
||||||
id="file-upload"
|
|
||||||
name="file-upload"
|
|
||||||
type="file"
|
|
||||||
className="sr-only"
|
|
||||||
ref={inputRef}
|
|
||||||
accept="image/*"
|
|
||||||
multiple={false}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
<div className="w-full border-x border-t border-gray-300 flex flex-col dark:border-gray-600 rounded-t-xl p-2">
|
|
||||||
<textarea
|
|
||||||
onKeyDown={(e) => handleKeyDown(e)}
|
|
||||||
ref={textareaRef}
|
|
||||||
className="px-2 py-2 w-full resize-none bg-transparent focus-within:outline-none focus:ring-0 focus-visible:ring-0 ring-0 dark:ring-0 border-0 dark:text-gray-100"
|
|
||||||
onPaste={handlePaste}
|
|
||||||
rows={1}
|
|
||||||
style={{ minHeight: "60px" }}
|
|
||||||
tabIndex={0}
|
|
||||||
onCompositionStart={() => {
|
|
||||||
if (import.meta.env.BROWSER !== "firefox") {
|
|
||||||
setTyping(true)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onCompositionEnd={() => {
|
|
||||||
if (import.meta.env.BROWSER !== "firefox") {
|
|
||||||
setTyping(false)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder={t("form.textarea.placeholder")}
|
|
||||||
{...form.getInputProps("message")}
|
|
||||||
/>
|
|
||||||
<div className="flex mt-4 justify-end gap-3">
|
|
||||||
{chatMode !== "vision" && (
|
|
||||||
<Tooltip title={t("tooltip.searchInternet")}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setWebSearch(!webSearch)}
|
|
||||||
className={`inline-flex items-center gap-2 ${
|
|
||||||
chatMode === "rag" ? "hidden" : "block"
|
|
||||||
}`}>
|
|
||||||
{webSearch ? (
|
|
||||||
<PiGlobe className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
|
||||||
) : (
|
|
||||||
<PiGlobeX className="h-5 w-5 text-gray-600 dark:text-gray-400" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
<ModelSelect />
|
|
||||||
{browserSupportsSpeechRecognition && (
|
|
||||||
<Tooltip title={t("tooltip.speechToText")}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={async () => {
|
|
||||||
if (isListening) {
|
|
||||||
stopListening()
|
|
||||||
} else {
|
|
||||||
resetTranscript()
|
|
||||||
startListening({
|
|
||||||
continuous: true,
|
|
||||||
lang: speechToTextLanguage
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={`flex items-center justify-center dark:text-gray-300`}>
|
|
||||||
{!isListening ? (
|
|
||||||
<MicIcon className="h-5 w-5" />
|
|
||||||
) : (
|
|
||||||
<div className="relative">
|
|
||||||
<span className="animate-ping absolute inline-flex h-3 w-3 rounded-full bg-red-400 opacity-75"></span>
|
|
||||||
<MicIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
<Tooltip title={t("tooltip.vision")}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
if (chatMode === "vision") {
|
|
||||||
setChatMode("normal")
|
|
||||||
} else {
|
|
||||||
setChatMode("vision")
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={chatMode === "rag"}
|
|
||||||
className={`flex items-center justify-center dark:text-gray-300 ${
|
|
||||||
chatMode === "rag" ? "hidden" : "block"
|
|
||||||
} disabled:opacity-50`}>
|
|
||||||
{chatMode === "vision" ? (
|
|
||||||
<EyeIcon className="h-5 w-5" />
|
|
||||||
) : (
|
|
||||||
<EyeOffIcon className="h-5 w-5" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title={t("tooltip.uploadImage")}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
inputRef.current?.click()
|
|
||||||
}}
|
|
||||||
disabled={chatMode === "vision"}
|
|
||||||
className={`flex items-center justify-center disabled:opacity-50 dark:text-gray-300 ${
|
|
||||||
chatMode === "rag" ? "hidden" : "block"
|
|
||||||
}`}>
|
|
||||||
<ImageIcon className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
{!streaming ? (
|
|
||||||
<Dropdown.Button
|
|
||||||
htmlType="submit"
|
|
||||||
disabled={isSending}
|
|
||||||
className="!justify-end !w-auto"
|
|
||||||
icon={
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="w-5 h-5">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="m19.5 8.25-7.5 7.5-7.5-7.5"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
menu={{
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
key: 1,
|
|
||||||
label: (
|
|
||||||
<Checkbox
|
|
||||||
checked={sendWhenEnter}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSendWhenEnter(e.target.checked)
|
|
||||||
}>
|
|
||||||
{t("sendWhenEnter")}
|
|
||||||
</Checkbox>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 2,
|
|
||||||
label: (
|
|
||||||
<Checkbox
|
|
||||||
checked={chatMode === "rag"}
|
|
||||||
onChange={(e) => {
|
|
||||||
setChatMode(e.target.checked ? "rag" : "normal")
|
|
||||||
}}>
|
|
||||||
{t("common:chatWithCurrentPage")}
|
|
||||||
</Checkbox>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 3,
|
|
||||||
label: (
|
|
||||||
<Checkbox
|
|
||||||
checked={useOCR}
|
|
||||||
onChange={(e) => setUseOCR(e.target.checked)}>
|
|
||||||
{t("useOCR")}
|
|
||||||
</Checkbox>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}}>
|
|
||||||
<div className="inline-flex gap-2">
|
|
||||||
{sendWhenEnter ? (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
|
||||||
className="h-5 w-5"
|
|
||||||
viewBox="0 0 24 24">
|
|
||||||
<path d="M9 10L4 15 9 20"></path>
|
|
||||||
<path d="M20 4v7a4 4 0 01-4 4H4"></path>
|
|
||||||
</svg>
|
|
||||||
) : null}
|
|
||||||
{t("common:submit")}
|
|
||||||
</div>
|
|
||||||
</Dropdown.Button>
|
|
||||||
) : (
|
|
||||||
<Tooltip title={t("tooltip.stopStreaming")}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={stopStreamingRequest}
|
|
||||||
className="text-gray-800 dark:text-gray-300">
|
|
||||||
<StopCircleIcon className="h-6 w-6" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<div>
|
||||||
</div>
|
<div className="flex">
|
||||||
{form.errors.message && (
|
<form
|
||||||
<div className="text-red-500 text-center text-sm mt-1">
|
onSubmit={form.onSubmit(async (value) => {
|
||||||
{form.errors.message}
|
if (!selectedModel || selectedModel.length === 0) {
|
||||||
|
form.setFieldError("message", t("formError.noModel"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (chatMode === "rag") {
|
||||||
|
const defaultEM = await defaultEmbeddingModelForRag()
|
||||||
|
if (!defaultEM) {
|
||||||
|
form.setFieldError(
|
||||||
|
"message",
|
||||||
|
t("formError.noEmbeddingModel")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (webSearch) {
|
||||||
|
const defaultEM = await defaultEmbeddingModelForRag()
|
||||||
|
const simpleSearch = await getIsSimpleInternetSearch()
|
||||||
|
if (!defaultEM && !simpleSearch) {
|
||||||
|
form.setFieldError(
|
||||||
|
"message",
|
||||||
|
t("formError.noEmbeddingModel")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await stopListening()
|
||||||
|
if (
|
||||||
|
value.message.trim().length === 0 &&
|
||||||
|
value.image.length === 0
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
form.reset()
|
||||||
|
textAreaFocus()
|
||||||
|
await sendMessage({
|
||||||
|
image: value.image,
|
||||||
|
message: value.message.trim()
|
||||||
|
})
|
||||||
|
})}
|
||||||
|
className="shrink-0 flex-grow flex flex-col items-center ">
|
||||||
|
<input
|
||||||
|
id="file-upload"
|
||||||
|
name="file-upload"
|
||||||
|
type="file"
|
||||||
|
className="sr-only"
|
||||||
|
ref={inputRef}
|
||||||
|
accept="image/*"
|
||||||
|
multiple={false}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
<div className="w-full flex flex-col p-1">
|
||||||
|
<textarea
|
||||||
|
onKeyDown={(e) => handleKeyDown(e)}
|
||||||
|
ref={textareaRef}
|
||||||
|
className="px-2 py-2 w-full resize-none bg-transparent focus-within:outline-none focus:ring-0 focus-visible:ring-0 ring-0 dark:ring-0 border-0 dark:text-gray-100"
|
||||||
|
onPaste={handlePaste}
|
||||||
|
rows={1}
|
||||||
|
style={{ minHeight: "60px" }}
|
||||||
|
tabIndex={0}
|
||||||
|
onCompositionStart={() => {
|
||||||
|
if (import.meta.env.BROWSER !== "firefox") {
|
||||||
|
setTyping(true)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCompositionEnd={() => {
|
||||||
|
if (import.meta.env.BROWSER !== "firefox") {
|
||||||
|
setTyping(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={t("form.textarea.placeholder")}
|
||||||
|
{...form.getInputProps("message")}
|
||||||
|
/>
|
||||||
|
<div className="flex mt-4 justify-end gap-3">
|
||||||
|
{chatMode !== "vision" && (
|
||||||
|
<Tooltip title={t("tooltip.searchInternet")}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setWebSearch(!webSearch)}
|
||||||
|
className={`inline-flex items-center gap-2 ${
|
||||||
|
chatMode === "rag" ? "hidden" : "block"
|
||||||
|
}`}>
|
||||||
|
{webSearch ? (
|
||||||
|
<PiGlobe className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||||
|
) : (
|
||||||
|
<PiGlobeX className="h-5 w-5 text-gray-600 dark:text-gray-400" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<ModelSelect />
|
||||||
|
{browserSupportsSpeechRecognition && (
|
||||||
|
<Tooltip title={t("tooltip.speechToText")}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
if (isListening) {
|
||||||
|
stopListening()
|
||||||
|
} else {
|
||||||
|
resetTranscript()
|
||||||
|
startListening({
|
||||||
|
continuous: true,
|
||||||
|
lang: speechToTextLanguage
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`flex items-center justify-center dark:text-gray-300`}>
|
||||||
|
{!isListening ? (
|
||||||
|
<MicIcon className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<div className="relative">
|
||||||
|
<span className="animate-ping absolute inline-flex h-3 w-3 rounded-full bg-red-400 opacity-75"></span>
|
||||||
|
<MicIcon className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<Tooltip title={t("tooltip.vision")}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (chatMode === "vision") {
|
||||||
|
setChatMode("normal")
|
||||||
|
} else {
|
||||||
|
setChatMode("vision")
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={chatMode === "rag"}
|
||||||
|
className={`flex items-center justify-center dark:text-gray-300 ${
|
||||||
|
chatMode === "rag" ? "hidden" : "block"
|
||||||
|
} disabled:opacity-50`}>
|
||||||
|
{chatMode === "vision" ? (
|
||||||
|
<EyeIcon className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<EyeOffIcon className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title={t("tooltip.uploadImage")}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
inputRef.current?.click()
|
||||||
|
}}
|
||||||
|
disabled={chatMode === "vision"}
|
||||||
|
className={`flex items-center justify-center disabled:opacity-50 dark:text-gray-300 ${
|
||||||
|
chatMode === "rag" ? "hidden" : "block"
|
||||||
|
}`}>
|
||||||
|
<ImageIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
{!streaming ? (
|
||||||
|
<Dropdown.Button
|
||||||
|
htmlType="submit"
|
||||||
|
disabled={isSending}
|
||||||
|
className="!justify-end !w-auto"
|
||||||
|
icon={
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-5 h-5">
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="m19.5 8.25-7.5 7.5-7.5-7.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
menu={{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 1,
|
||||||
|
label: (
|
||||||
|
<Checkbox
|
||||||
|
checked={sendWhenEnter}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSendWhenEnter(e.target.checked)
|
||||||
|
}>
|
||||||
|
{t("sendWhenEnter")}
|
||||||
|
</Checkbox>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 2,
|
||||||
|
label: (
|
||||||
|
<Checkbox
|
||||||
|
checked={chatMode === "rag"}
|
||||||
|
onChange={(e) => {
|
||||||
|
setChatMode(
|
||||||
|
e.target.checked ? "rag" : "normal"
|
||||||
|
)
|
||||||
|
}}>
|
||||||
|
{t("common:chatWithCurrentPage")}
|
||||||
|
</Checkbox>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 3,
|
||||||
|
label: (
|
||||||
|
<Checkbox
|
||||||
|
checked={useOCR}
|
||||||
|
onChange={(e) =>
|
||||||
|
setUseOCR(e.target.checked)
|
||||||
|
}>
|
||||||
|
{t("useOCR")}
|
||||||
|
</Checkbox>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}}>
|
||||||
|
<div className="inline-flex gap-2">
|
||||||
|
{sendWhenEnter ? (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
className="h-5 w-5"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path d="M9 10L4 15 9 20"></path>
|
||||||
|
<path d="M20 4v7a4 4 0 01-4 4H4"></path>
|
||||||
|
</svg>
|
||||||
|
) : null}
|
||||||
|
{t("common:submit")}
|
||||||
|
</div>
|
||||||
|
</Dropdown.Button>
|
||||||
|
) : (
|
||||||
|
<Tooltip title={t("tooltip.stopStreaming")}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={stopStreamingRequest}
|
||||||
|
className="text-gray-800 dark:text-gray-300">
|
||||||
|
<StopCircleIcon className="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{form.errors.message && (
|
||||||
|
<div className="text-red-500 text-center text-sm mt-1">
|
||||||
|
{form.errors.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -40,7 +40,7 @@ export const SidepanelHeader = () => {
|
|||||||
const [sidebarOpen, setSidebarOpen] = React.useState(false)
|
const [sidebarOpen, setSidebarOpen] = React.useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex px-3 justify-between bg-white dark:bg-[#171717] border-b border-gray-300 dark:border-gray-700 py-4 items-center">
|
<div className=" px-3 justify-between bg-white dark:bg-[#171717] border-b border-gray-300 dark:border-gray-700 py-4 items-center absolute top-0 z-10 flex h-14 w-full">
|
||||||
<div className="focus:outline-none focus-visible:ring-2 focus-visible:ring-pink-700 flex items-center dark:text-white">
|
<div className="focus:outline-none focus-visible:ring-2 focus-visible:ring-pink-700 flex items-center dark:text-white">
|
||||||
<img
|
<img
|
||||||
className="h-6 w-auto"
|
className="h-6 w-auto"
|
||||||
|
@ -35,15 +35,9 @@ export default defineBackground({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (import.meta.env.BROWSER === "chrome") {
|
chrome.action.onClicked.addListener((tab) => {
|
||||||
chrome.action.onClicked.addListener((tab) => {
|
chrome.tabs.create({ url: chrome.runtime.getURL("/options.html") })
|
||||||
chrome.tabs.create({ url: chrome.runtime.getURL("/options.html") })
|
})
|
||||||
})
|
|
||||||
} else {
|
|
||||||
browser.browserAction.onClicked.addListener((tab) => {
|
|
||||||
browser.tabs.create({ url: browser.runtime.getURL("/options.html") })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const contextMenuTitle = {
|
const contextMenuTitle = {
|
||||||
webUi: browser.i18n.getMessage("openOptionToChat"),
|
webUi: browser.i18n.getMessage("openOptionToChat"),
|
||||||
@ -91,176 +85,98 @@ export default defineBackground({
|
|||||||
contexts: ["selection"]
|
contexts: ["selection"]
|
||||||
})
|
})
|
||||||
|
|
||||||
if (import.meta.env.BROWSER === "chrome") {
|
browser.contextMenus.onClicked.addListener(async (info, tab) => {
|
||||||
browser.contextMenus.onClicked.addListener(async (info, tab) => {
|
if (info.menuItemId === "open-side-panel-pa") {
|
||||||
if (info.menuItemId === "open-side-panel-pa") {
|
chrome.sidePanel.open({
|
||||||
chrome.sidePanel.open({
|
tabId: tab.id!
|
||||||
tabId: tab.id!
|
})
|
||||||
|
} else if (info.menuItemId === "open-web-ui-pa") {
|
||||||
|
browser.tabs.create({
|
||||||
|
url: browser.runtime.getURL("/options.html")
|
||||||
|
})
|
||||||
|
} else if (info.menuItemId === "summarize-pa") {
|
||||||
|
chrome.sidePanel.open({
|
||||||
|
tabId: tab.id!
|
||||||
|
})
|
||||||
|
// this is a bad method hope somone can fix it :)
|
||||||
|
setTimeout(async () => {
|
||||||
|
await browser.runtime.sendMessage({
|
||||||
|
from: "background",
|
||||||
|
type: "summary",
|
||||||
|
text: info.selectionText
|
||||||
})
|
})
|
||||||
} else if (info.menuItemId === "open-web-ui-pa") {
|
}, isCopilotRunning ? 0 : 5000)
|
||||||
browser.tabs.create({
|
|
||||||
url: browser.runtime.getURL("/options.html")
|
} else if (info.menuItemId === "rephrase-pa") {
|
||||||
|
chrome.sidePanel.open({
|
||||||
|
tabId: tab.id!
|
||||||
|
})
|
||||||
|
setTimeout(async () => {
|
||||||
|
|
||||||
|
await browser.runtime.sendMessage({
|
||||||
|
type: "rephrase",
|
||||||
|
from: "background",
|
||||||
|
text: info.selectionText
|
||||||
})
|
})
|
||||||
} else if (info.menuItemId === "summarize-pa") {
|
}, isCopilotRunning ? 0 : 5000)
|
||||||
chrome.sidePanel.open({
|
|
||||||
tabId: tab.id!
|
} else if (info.menuItemId === "translate-pg") {
|
||||||
|
chrome.sidePanel.open({
|
||||||
|
tabId: tab.id!
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
await browser.runtime.sendMessage({
|
||||||
|
type: "translate",
|
||||||
|
from: "background",
|
||||||
|
text: info.selectionText
|
||||||
})
|
})
|
||||||
// this is a bad method hope somone can fix it :)
|
}, isCopilotRunning ? 0 : 5000)
|
||||||
setTimeout(async () => {
|
} else if (info.menuItemId === "explain-pa") {
|
||||||
await browser.runtime.sendMessage({
|
chrome.sidePanel.open({
|
||||||
from: "background",
|
tabId: tab.id!
|
||||||
type: "summary",
|
})
|
||||||
text: info.selectionText
|
|
||||||
})
|
|
||||||
}, isCopilotRunning ? 0 : 5000)
|
|
||||||
|
|
||||||
} else if (info.menuItemId === "rephrase-pa") {
|
setTimeout(async () => {
|
||||||
chrome.sidePanel.open({
|
await browser.runtime.sendMessage({
|
||||||
tabId: tab.id!
|
type: "explain",
|
||||||
|
from: "background",
|
||||||
|
text: info.selectionText
|
||||||
})
|
})
|
||||||
setTimeout(async () => {
|
}, isCopilotRunning ? 0 : 5000)
|
||||||
|
} else if (info.menuItemId === "custom-pg") {
|
||||||
|
chrome.sidePanel.open({
|
||||||
|
tabId: tab.id!
|
||||||
|
})
|
||||||
|
|
||||||
await browser.runtime.sendMessage({
|
setTimeout(async () => {
|
||||||
type: "rephrase",
|
await browser.runtime.sendMessage({
|
||||||
from: "background",
|
type: "custom",
|
||||||
text: info.selectionText
|
from: "background",
|
||||||
})
|
text: info.selectionText
|
||||||
}, isCopilotRunning ? 0 : 5000)
|
|
||||||
|
|
||||||
} else if (info.menuItemId === "translate-pg") {
|
|
||||||
chrome.sidePanel.open({
|
|
||||||
tabId: tab.id!
|
|
||||||
})
|
})
|
||||||
|
}, isCopilotRunning ? 0 : 5000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
setTimeout(async () => {
|
browser.commands.onCommand.addListener((command) => {
|
||||||
await browser.runtime.sendMessage({
|
switch (command) {
|
||||||
type: "translate",
|
case "execute_side_panel":
|
||||||
from: "background",
|
chrome.tabs.query(
|
||||||
text: info.selectionText
|
{ active: true, currentWindow: true },
|
||||||
})
|
async (tabs) => {
|
||||||
}, isCopilotRunning ? 0 : 5000)
|
const tab = tabs[0]
|
||||||
} else if (info.menuItemId === "explain-pa") {
|
chrome.sidePanel.open({
|
||||||
chrome.sidePanel.open({
|
tabId: tab.id!
|
||||||
tabId: tab.id!
|
})
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
setTimeout(async () => {
|
|
||||||
await browser.runtime.sendMessage({
|
|
||||||
type: "explain",
|
|
||||||
from: "background",
|
|
||||||
text: info.selectionText
|
|
||||||
})
|
|
||||||
}, isCopilotRunning ? 0 : 5000)
|
|
||||||
} else if (info.menuItemId === "custom-pg") {
|
|
||||||
chrome.sidePanel.open({
|
|
||||||
tabId: tab.id!
|
|
||||||
})
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
|
||||||
await browser.runtime.sendMessage({
|
|
||||||
type: "custom",
|
|
||||||
from: "background",
|
|
||||||
text: info.selectionText
|
|
||||||
})
|
|
||||||
}, isCopilotRunning ? 0 : 5000)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
browser.commands.onCommand.addListener((command) => {
|
|
||||||
switch (command) {
|
|
||||||
case "execute_side_panel":
|
|
||||||
chrome.tabs.query(
|
|
||||||
{ active: true, currentWindow: true },
|
|
||||||
async (tabs) => {
|
|
||||||
const tab = tabs[0]
|
|
||||||
chrome.sidePanel.open({
|
|
||||||
tabId: tab.id!
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (import.meta.env.BROWSER === "firefox") {
|
|
||||||
browser.contextMenus.onClicked.addListener((info, tab) => {
|
|
||||||
if (info.menuItemId === "open-side-panel-pa") {
|
|
||||||
browser.sidebarAction.toggle()
|
|
||||||
} else if (info.menuItemId === "open-web-ui-pa") {
|
|
||||||
browser.tabs.create({
|
|
||||||
url: browser.runtime.getURL("/options.html")
|
|
||||||
})
|
|
||||||
} else if (info.menuItemId === "summarize-pa") {
|
|
||||||
if (!isCopilotRunning) {
|
|
||||||
browser.sidebarAction.toggle()
|
|
||||||
}
|
|
||||||
setTimeout(async () => {
|
|
||||||
await browser.runtime.sendMessage({
|
|
||||||
from: "background",
|
|
||||||
type: "summary",
|
|
||||||
text: info.selectionText
|
|
||||||
})
|
|
||||||
}, isCopilotRunning ? 0 : 5000)
|
|
||||||
} else if (info.menuItemId === "rephrase-pa") {
|
|
||||||
if (!isCopilotRunning) {
|
|
||||||
browser.sidebarAction.toggle()
|
|
||||||
}
|
|
||||||
setTimeout(async () => {
|
|
||||||
await browser.runtime.sendMessage({
|
|
||||||
type: "rephrase",
|
|
||||||
from: "background",
|
|
||||||
text: info.selectionText
|
|
||||||
})
|
|
||||||
}, isCopilotRunning ? 0 : 5000)
|
|
||||||
} else if (info.menuItemId === "translate-pg") {
|
|
||||||
if (!isCopilotRunning) {
|
|
||||||
browser.sidebarAction.toggle()
|
|
||||||
}
|
|
||||||
setTimeout(async () => {
|
|
||||||
await browser.runtime.sendMessage({
|
|
||||||
type: "translate",
|
|
||||||
from: "background",
|
|
||||||
text: info.selectionText
|
|
||||||
})
|
|
||||||
}, isCopilotRunning ? 0 : 5000)
|
|
||||||
} else if (info.menuItemId === "explain-pa") {
|
|
||||||
if (!isCopilotRunning) {
|
|
||||||
browser.sidebarAction.toggle()
|
|
||||||
}
|
|
||||||
setTimeout(async () => {
|
|
||||||
await browser.runtime.sendMessage({
|
|
||||||
type: "explain",
|
|
||||||
from: "background",
|
|
||||||
text: info.selectionText
|
|
||||||
})
|
|
||||||
}, isCopilotRunning ? 0 : 5000)
|
|
||||||
} else if (info.menuItemId === "custom-pg") {
|
|
||||||
if (!isCopilotRunning) {
|
|
||||||
browser.sidebarAction.toggle()
|
|
||||||
}
|
|
||||||
setTimeout(async () => {
|
|
||||||
await browser.runtime.sendMessage({
|
|
||||||
type: "custom",
|
|
||||||
from: "background",
|
|
||||||
text: info.selectionText
|
|
||||||
})
|
|
||||||
}, isCopilotRunning ? 0 : 5000)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
browser.commands.onCommand.addListener((command) => {
|
|
||||||
switch (command) {
|
|
||||||
case "execute_side_panel":
|
|
||||||
browser.sidebarAction.toggle()
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
persistent: true
|
persistent: true
|
||||||
})
|
})
|
||||||
|
@ -36,7 +36,12 @@ import { humanMessageFormatter } from "@/utils/human-message"
|
|||||||
import { pageAssistEmbeddingModel } from "@/models/embedding"
|
import { pageAssistEmbeddingModel } from "@/models/embedding"
|
||||||
import { PAMemoryVectorStore } from "@/libs/PAMemoryVectorStore"
|
import { PAMemoryVectorStore } from "@/libs/PAMemoryVectorStore"
|
||||||
import { getScreenshotFromCurrentTab } from "@/libs/get-screenshot"
|
import { getScreenshotFromCurrentTab } from "@/libs/get-screenshot"
|
||||||
import { isReasoningEnded, isReasoningStarted, removeReasoning } from "@/libs/reasoning"
|
import {
|
||||||
|
isReasoningEnded,
|
||||||
|
isReasoningStarted,
|
||||||
|
mergeReasoningContent,
|
||||||
|
removeReasoning
|
||||||
|
} from "@/libs/reasoning"
|
||||||
|
|
||||||
export const useMessage = () => {
|
export const useMessage = () => {
|
||||||
const {
|
const {
|
||||||
@ -413,7 +418,24 @@ export const useMessage = () => {
|
|||||||
let reasoningStartTime: Date | null = null
|
let reasoningStartTime: Date | null = null
|
||||||
let reasoningEndTime: Date | null = null
|
let reasoningEndTime: Date | null = null
|
||||||
let timetaken = 0
|
let timetaken = 0
|
||||||
|
let apiReasoning = false
|
||||||
for await (const chunk of chunks) {
|
for await (const chunk of chunks) {
|
||||||
|
if (chunk?.additional_kwargs?.reasoning_content) {
|
||||||
|
const reasoningContent = mergeReasoningContent(
|
||||||
|
fullText,
|
||||||
|
chunk?.additional_kwargs?.reasoning_content || ""
|
||||||
|
)
|
||||||
|
contentToSave = reasoningContent
|
||||||
|
fullText = reasoningContent
|
||||||
|
apiReasoning = true
|
||||||
|
} else {
|
||||||
|
if (apiReasoning) {
|
||||||
|
fullText += "</think>"
|
||||||
|
contentToSave += "</think>"
|
||||||
|
apiReasoning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
contentToSave += chunk?.content
|
contentToSave += chunk?.content
|
||||||
fullText += chunk?.content
|
fullText += chunk?.content
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
@ -680,7 +702,24 @@ export const useMessage = () => {
|
|||||||
let reasoningStartTime: Date | undefined = undefined
|
let reasoningStartTime: Date | undefined = undefined
|
||||||
let reasoningEndTime: Date | undefined = undefined
|
let reasoningEndTime: Date | undefined = undefined
|
||||||
let timetaken = 0
|
let timetaken = 0
|
||||||
|
let apiReasoning = false
|
||||||
for await (const chunk of chunks) {
|
for await (const chunk of chunks) {
|
||||||
|
if (chunk?.additional_kwargs?.reasoning_content) {
|
||||||
|
const reasoningContent = mergeReasoningContent(
|
||||||
|
fullText,
|
||||||
|
chunk?.additional_kwargs?.reasoning_content || ""
|
||||||
|
)
|
||||||
|
contentToSave = reasoningContent
|
||||||
|
fullText = reasoningContent
|
||||||
|
apiReasoning = true
|
||||||
|
} else {
|
||||||
|
if (apiReasoning) {
|
||||||
|
fullText += "</think>"
|
||||||
|
contentToSave += "</think>"
|
||||||
|
apiReasoning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
contentToSave += chunk?.content
|
contentToSave += chunk?.content
|
||||||
fullText += chunk?.content
|
fullText += chunk?.content
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
@ -950,8 +989,25 @@ export const useMessage = () => {
|
|||||||
let reasoningStartTime: Date | null = null
|
let reasoningStartTime: Date | null = null
|
||||||
let reasoningEndTime: Date | null = null
|
let reasoningEndTime: Date | null = null
|
||||||
let timetaken = 0
|
let timetaken = 0
|
||||||
|
let apiReasoning = false
|
||||||
|
|
||||||
for await (const chunk of chunks) {
|
for await (const chunk of chunks) {
|
||||||
|
if (chunk?.additional_kwargs?.reasoning_content) {
|
||||||
|
const reasoningContent = mergeReasoningContent(
|
||||||
|
fullText,
|
||||||
|
chunk?.additional_kwargs?.reasoning_content || ""
|
||||||
|
)
|
||||||
|
contentToSave = reasoningContent
|
||||||
|
fullText = reasoningContent
|
||||||
|
apiReasoning = true
|
||||||
|
} else {
|
||||||
|
if (apiReasoning) {
|
||||||
|
fullText += "</think>"
|
||||||
|
contentToSave += "</think>"
|
||||||
|
apiReasoning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
contentToSave += chunk?.content
|
contentToSave += chunk?.content
|
||||||
fullText += chunk?.content
|
fullText += chunk?.content
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
@ -1279,7 +1335,24 @@ export const useMessage = () => {
|
|||||||
let timetaken = 0
|
let timetaken = 0
|
||||||
let reasoningStartTime: Date | undefined = undefined
|
let reasoningStartTime: Date | undefined = undefined
|
||||||
let reasoningEndTime: Date | undefined = undefined
|
let reasoningEndTime: Date | undefined = undefined
|
||||||
|
let apiReasoning = false
|
||||||
for await (const chunk of chunks) {
|
for await (const chunk of chunks) {
|
||||||
|
if (chunk?.additional_kwargs?.reasoning_content) {
|
||||||
|
const reasoningContent = mergeReasoningContent(
|
||||||
|
fullText,
|
||||||
|
chunk?.additional_kwargs?.reasoning_content || ""
|
||||||
|
)
|
||||||
|
contentToSave = reasoningContent
|
||||||
|
fullText = reasoningContent
|
||||||
|
apiReasoning = true
|
||||||
|
} else {
|
||||||
|
if (apiReasoning) {
|
||||||
|
fullText += "</think>"
|
||||||
|
contentToSave += "</think>"
|
||||||
|
apiReasoning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
contentToSave += chunk?.content
|
contentToSave += chunk?.content
|
||||||
fullText += chunk?.content
|
fullText += chunk?.content
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
@ -1527,7 +1600,24 @@ export const useMessage = () => {
|
|||||||
let reasoningStartTime: Date | null = null
|
let reasoningStartTime: Date | null = null
|
||||||
let reasoningEndTime: Date | null = null
|
let reasoningEndTime: Date | null = null
|
||||||
let timetaken = 0
|
let timetaken = 0
|
||||||
|
let apiReasoning = false
|
||||||
for await (const chunk of chunks) {
|
for await (const chunk of chunks) {
|
||||||
|
if (chunk?.additional_kwargs?.reasoning_content) {
|
||||||
|
const reasoningContent = mergeReasoningContent(
|
||||||
|
fullText,
|
||||||
|
chunk?.additional_kwargs?.reasoning_content || ""
|
||||||
|
)
|
||||||
|
contentToSave = reasoningContent
|
||||||
|
fullText = reasoningContent
|
||||||
|
apiReasoning = true
|
||||||
|
} else {
|
||||||
|
if (apiReasoning) {
|
||||||
|
fullText += "</think>"
|
||||||
|
contentToSave += "</think>"
|
||||||
|
apiReasoning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
contentToSave += chunk?.content
|
contentToSave += chunk?.content
|
||||||
fullText += chunk?.content
|
fullText += chunk?.content
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
|
@ -40,6 +40,7 @@ import { pageAssistEmbeddingModel } from "@/models/embedding"
|
|||||||
import {
|
import {
|
||||||
isReasoningEnded,
|
isReasoningEnded,
|
||||||
isReasoningStarted,
|
isReasoningStarted,
|
||||||
|
mergeReasoningContent,
|
||||||
removeReasoning
|
removeReasoning
|
||||||
} from "@/libs/reasoning"
|
} from "@/libs/reasoning"
|
||||||
|
|
||||||
@ -331,7 +332,24 @@ export const useMessageOption = () => {
|
|||||||
let count = 0
|
let count = 0
|
||||||
let reasoningStartTime: Date | undefined = undefined
|
let reasoningStartTime: Date | undefined = undefined
|
||||||
let reasoningEndTime: Date | undefined = undefined
|
let reasoningEndTime: Date | undefined = undefined
|
||||||
|
let apiReasoning = false
|
||||||
for await (const chunk of chunks) {
|
for await (const chunk of chunks) {
|
||||||
|
if (chunk?.additional_kwargs?.reasoning_content) {
|
||||||
|
const reasoningContent = mergeReasoningContent(
|
||||||
|
fullText,
|
||||||
|
chunk?.additional_kwargs?.reasoning_content || ""
|
||||||
|
)
|
||||||
|
contentToSave = reasoningContent
|
||||||
|
fullText = reasoningContent
|
||||||
|
apiReasoning = true
|
||||||
|
} else {
|
||||||
|
if (apiReasoning) {
|
||||||
|
fullText += "</think>"
|
||||||
|
contentToSave += "</think>"
|
||||||
|
apiReasoning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
contentToSave += chunk?.content
|
contentToSave += chunk?.content
|
||||||
fullText += chunk?.content
|
fullText += chunk?.content
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
@ -648,8 +666,25 @@ export const useMessageOption = () => {
|
|||||||
let count = 0
|
let count = 0
|
||||||
let reasoningStartTime: Date | null = null
|
let reasoningStartTime: Date | null = null
|
||||||
let reasoningEndTime: Date | null = null
|
let reasoningEndTime: Date | null = null
|
||||||
|
let apiReasoning: boolean = false
|
||||||
|
|
||||||
for await (const chunk of chunks) {
|
for await (const chunk of chunks) {
|
||||||
|
if (chunk?.additional_kwargs?.reasoning_content) {
|
||||||
|
const reasoningContent = mergeReasoningContent(
|
||||||
|
fullText,
|
||||||
|
chunk?.additional_kwargs?.reasoning_content || ""
|
||||||
|
)
|
||||||
|
contentToSave = reasoningContent
|
||||||
|
fullText = reasoningContent
|
||||||
|
apiReasoning = true
|
||||||
|
} else {
|
||||||
|
if (apiReasoning) {
|
||||||
|
fullText += "</think>"
|
||||||
|
contentToSave += "</think>"
|
||||||
|
apiReasoning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
contentToSave += chunk?.content
|
contentToSave += chunk?.content
|
||||||
fullText += chunk?.content
|
fullText += chunk?.content
|
||||||
|
|
||||||
@ -982,8 +1017,25 @@ export const useMessageOption = () => {
|
|||||||
let count = 0
|
let count = 0
|
||||||
let reasoningStartTime: Date | undefined = undefined
|
let reasoningStartTime: Date | undefined = undefined
|
||||||
let reasoningEndTime: Date | undefined = undefined
|
let reasoningEndTime: Date | undefined = undefined
|
||||||
|
let apiReasoning = false
|
||||||
|
|
||||||
for await (const chunk of chunks) {
|
for await (const chunk of chunks) {
|
||||||
|
if (chunk?.additional_kwargs?.reasoning_content) {
|
||||||
|
const reasoningContent = mergeReasoningContent(
|
||||||
|
fullText,
|
||||||
|
chunk?.additional_kwargs?.reasoning_content || ""
|
||||||
|
)
|
||||||
|
contentToSave = reasoningContent
|
||||||
|
fullText = reasoningContent
|
||||||
|
apiReasoning = true
|
||||||
|
} else {
|
||||||
|
if (apiReasoning) {
|
||||||
|
fullText += "</think>"
|
||||||
|
contentToSave += "</think>"
|
||||||
|
apiReasoning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
contentToSave += chunk?.content
|
contentToSave += chunk?.content
|
||||||
fullText += chunk?.content
|
fullText += chunk?.content
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
getElevenLabsApiKey,
|
getElevenLabsApiKey,
|
||||||
getElevenLabsModel,
|
getElevenLabsModel,
|
||||||
getElevenLabsVoiceId,
|
getElevenLabsVoiceId,
|
||||||
|
getRemoveReasoningTagTTS,
|
||||||
getTTSProvider,
|
getTTSProvider,
|
||||||
getVoice,
|
getVoice,
|
||||||
isSSMLEnabled
|
isSSMLEnabled
|
||||||
@ -11,6 +12,7 @@ import {
|
|||||||
import { markdownToSSML } from "@/utils/markdown-to-ssml"
|
import { markdownToSSML } from "@/utils/markdown-to-ssml"
|
||||||
import { generateSpeech } from "@/services/elevenlabs"
|
import { generateSpeech } from "@/services/elevenlabs"
|
||||||
import { splitMessageContent } from "@/utils/tts"
|
import { splitMessageContent } from "@/utils/tts"
|
||||||
|
import { removeReasoning } from "@/libs/reasoning"
|
||||||
|
|
||||||
export interface VoiceOptions {
|
export interface VoiceOptions {
|
||||||
utterance: string
|
utterance: string
|
||||||
@ -26,13 +28,21 @@ export const useTTS = () => {
|
|||||||
try {
|
try {
|
||||||
const voice = await getVoice()
|
const voice = await getVoice()
|
||||||
const provider = await getTTSProvider()
|
const provider = await getTTSProvider()
|
||||||
|
const isRemoveReasoning = await getRemoveReasoningTagTTS()
|
||||||
|
|
||||||
|
if (isRemoveReasoning) {
|
||||||
|
utterance = removeReasoning(utterance)
|
||||||
|
}
|
||||||
|
|
||||||
if (provider === "browser") {
|
if (provider === "browser") {
|
||||||
const isSSML = await isSSMLEnabled()
|
const isSSML = await isSSMLEnabled()
|
||||||
if (isSSML) {
|
if (isSSML) {
|
||||||
utterance = markdownToSSML(utterance)
|
utterance = markdownToSSML(utterance)
|
||||||
}
|
}
|
||||||
if (import.meta.env.BROWSER === "chrome") {
|
if (
|
||||||
|
import.meta.env.BROWSER === "chrome" ||
|
||||||
|
import.meta.env.BROWSER === "edge"
|
||||||
|
) {
|
||||||
chrome.tts.speak(utterance, {
|
chrome.tts.speak(utterance, {
|
||||||
voiceName: voice,
|
voiceName: voice,
|
||||||
onEvent(event) {
|
onEvent(event) {
|
||||||
@ -112,7 +122,10 @@ export const useTTS = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.env.BROWSER === "chrome") {
|
if (
|
||||||
|
import.meta.env.BROWSER === "chrome" ||
|
||||||
|
import.meta.env.BROWSER === "edge"
|
||||||
|
) {
|
||||||
chrome.tts.stop()
|
chrome.tts.stop()
|
||||||
} else {
|
} else {
|
||||||
window.speechSynthesis.cancel()
|
window.speechSynthesis.cancel()
|
||||||
|
@ -24,7 +24,7 @@ const _getHtml = () => {
|
|||||||
|
|
||||||
export const getDataFromCurrentTab = async () => {
|
export const getDataFromCurrentTab = async () => {
|
||||||
const result = new Promise((resolve) => {
|
const result = new Promise((resolve) => {
|
||||||
if (import.meta.env.BROWSER === "chrome") {
|
if (import.meta.env.BROWSER === "chrome" || import.meta.env.BROWSER === "edge") {
|
||||||
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
|
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
|
||||||
const tab = tabs[0]
|
const tab = tabs[0]
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const captureVisibleTab = () => {
|
const captureVisibleTab = () => {
|
||||||
const result = new Promise<string>((resolve) => {
|
const result = new Promise<string>((resolve) => {
|
||||||
if (import.meta.env.BROWSER === "chrome") {
|
if (import.meta.env.BROWSER === "chrome" || import.meta.env.BROWSER === "edge") {
|
||||||
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
|
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
|
||||||
const tab = tabs[0]
|
const tab = tabs[0]
|
||||||
chrome.tabs.captureVisibleTab(null, { format: "png" }, (dataUrl) => {
|
chrome.tabs.captureVisibleTab(null, { format: "png" }, (dataUrl) => {
|
||||||
|
@ -25,7 +25,7 @@ export const getAllOpenAIModels = async (baseUrl: string, apiKey?: string) => {
|
|||||||
clearTimeout(timeoutId)
|
clearTimeout(timeoutId)
|
||||||
|
|
||||||
// if Google API fails to return models, try another approach
|
// if Google API fails to return models, try another approach
|
||||||
if (res.status === 401 && res.url == 'https://generativelanguage.googleapis.com/v1beta/openai/models') {
|
if (res.url == 'https://generativelanguage.googleapis.com/v1beta/openai/models') {
|
||||||
const urlGoogle = `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`
|
const urlGoogle = `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`
|
||||||
const resGoogle = await fetch(urlGoogle, {
|
const resGoogle = await fetch(urlGoogle, {
|
||||||
signal: controller.signal
|
signal: controller.signal
|
||||||
|
@ -1,73 +1,100 @@
|
|||||||
const tags = ["think", "reason", "reasoning", "thought"];
|
const tags = ["think", "reason", "reasoning", "thought"]
|
||||||
export function parseReasoning(text: string): { type: 'reasoning' | 'text', content: string, reasoning_running?: boolean }[] {
|
export function parseReasoning(text: string): {
|
||||||
try {
|
type: "reasoning" | "text"
|
||||||
const result: { type: 'reasoning' | 'text', content: string, reasoning_running?: boolean }[] = []
|
content: string
|
||||||
const tagPattern = new RegExp(`<(${tags.join('|')})>`, 'i')
|
reasoning_running?: boolean
|
||||||
const closeTagPattern = new RegExp(`</(${tags.join('|')})>`, 'i')
|
}[] {
|
||||||
|
try {
|
||||||
|
const result: {
|
||||||
|
type: "reasoning" | "text"
|
||||||
|
content: string
|
||||||
|
reasoning_running?: boolean
|
||||||
|
}[] = []
|
||||||
|
const tagPattern = new RegExp(`<(${tags.join("|")})>`, "i")
|
||||||
|
const closeTagPattern = new RegExp(`</(${tags.join("|")})>`, "i")
|
||||||
|
|
||||||
let currentIndex = 0
|
let currentIndex = 0
|
||||||
let isReasoning = false
|
let isReasoning = false
|
||||||
|
|
||||||
while (currentIndex < text.length) {
|
while (currentIndex < text.length) {
|
||||||
const openTagMatch = text.slice(currentIndex).match(tagPattern)
|
const openTagMatch = text.slice(currentIndex).match(tagPattern)
|
||||||
const closeTagMatch = text.slice(currentIndex).match(closeTagPattern)
|
const closeTagMatch = text.slice(currentIndex).match(closeTagPattern)
|
||||||
|
|
||||||
if (!isReasoning && openTagMatch) {
|
if (!isReasoning && openTagMatch) {
|
||||||
const beforeText = text.slice(currentIndex, currentIndex + openTagMatch.index)
|
const beforeText = text.slice(
|
||||||
if (beforeText.trim()) {
|
currentIndex,
|
||||||
result.push({ type: 'text', content: beforeText.trim() })
|
currentIndex + openTagMatch.index
|
||||||
}
|
)
|
||||||
|
if (beforeText.trim()) {
|
||||||
isReasoning = true
|
result.push({ type: "text", content: beforeText.trim() })
|
||||||
currentIndex += openTagMatch.index! + openTagMatch[0].length
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isReasoning && closeTagMatch) {
|
|
||||||
const reasoningContent = text.slice(currentIndex, currentIndex + closeTagMatch.index)
|
|
||||||
if (reasoningContent.trim()) {
|
|
||||||
result.push({ type: 'reasoning', content: reasoningContent.trim() })
|
|
||||||
}
|
|
||||||
|
|
||||||
isReasoning = false
|
|
||||||
currentIndex += closeTagMatch.index! + closeTagMatch[0].length
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentIndex < text.length) {
|
|
||||||
const remainingText = text.slice(currentIndex)
|
|
||||||
result.push({
|
|
||||||
type: isReasoning ? 'reasoning' : 'text',
|
|
||||||
content: remainingText.trim(),
|
|
||||||
reasoning_running: isReasoning
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
isReasoning = true
|
||||||
} catch (e) {
|
currentIndex += openTagMatch.index! + openTagMatch[0].length
|
||||||
console.error(`Error parsing reasoning: ${e}`)
|
continue
|
||||||
return [
|
}
|
||||||
{
|
|
||||||
type: 'text',
|
if (isReasoning && closeTagMatch) {
|
||||||
content: text
|
const reasoningContent = text.slice(
|
||||||
}
|
currentIndex,
|
||||||
]
|
currentIndex + closeTagMatch.index
|
||||||
|
)
|
||||||
|
if (reasoningContent.trim()) {
|
||||||
|
result.push({ type: "reasoning", content: reasoningContent.trim() })
|
||||||
|
}
|
||||||
|
|
||||||
|
isReasoning = false
|
||||||
|
currentIndex += closeTagMatch.index! + closeTagMatch[0].length
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex < text.length) {
|
||||||
|
const remainingText = text.slice(currentIndex)
|
||||||
|
result.push({
|
||||||
|
type: isReasoning ? "reasoning" : "text",
|
||||||
|
content: remainingText.trim(),
|
||||||
|
reasoning_running: isReasoning
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error parsing reasoning: ${e}`)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
content: text
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isReasoningStarted(text: string): boolean {
|
export function isReasoningStarted(text: string): boolean {
|
||||||
const tagPattern = new RegExp(`<(${tags.join('|')})>`, 'i')
|
const tagPattern = new RegExp(`<(${tags.join("|")})>`, "i")
|
||||||
return tagPattern.test(text)
|
return tagPattern.test(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isReasoningEnded(text: string): boolean {
|
export function isReasoningEnded(text: string): boolean {
|
||||||
const closeTagPattern = new RegExp(`</(${tags.join('|')})>`, 'i')
|
const closeTagPattern = new RegExp(`</(${tags.join("|")})>`, "i")
|
||||||
return closeTagPattern.test(text)
|
return closeTagPattern.test(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeReasoning(text: string): string {
|
export function removeReasoning(text: string): string {
|
||||||
const tagPattern = new RegExp(`<(${tags.join('|')})>.*?</(${tags.join('|')})>`, 'gis')
|
const tagPattern = new RegExp(
|
||||||
return text.replace(tagPattern, '').trim()
|
`<(${tags.join("|")})>.*?</(${tags.join("|")})>`,
|
||||||
|
"gis"
|
||||||
|
)
|
||||||
|
return text.replace(tagPattern, "").trim()
|
||||||
|
}
|
||||||
|
export function mergeReasoningContent(
|
||||||
|
originalText: string,
|
||||||
|
reasoning: string
|
||||||
|
): string {
|
||||||
|
const reasoningTag = "<think>"
|
||||||
|
|
||||||
|
originalText = originalText.replace(reasoningTag, "")
|
||||||
|
|
||||||
|
return `${reasoningTag}${originalText + reasoning}`
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ export const urlRewriteRuntime = async function (
|
|||||||
) {
|
) {
|
||||||
if (browser.runtime && browser.runtime.id) {
|
if (browser.runtime && browser.runtime.id) {
|
||||||
const { isEnableRewriteUrl, rewriteUrl } = await getAdvancedOllamaSettings()
|
const { isEnableRewriteUrl, rewriteUrl } = await getAdvancedOllamaSettings()
|
||||||
if (import.meta.env.BROWSER === "chrome") {
|
if (import.meta.env.BROWSER === "chrome" || import.meta.env.BROWSER === "edge") {
|
||||||
const url = new URL(domain)
|
const url = new URL(domain)
|
||||||
const domains = [url.hostname]
|
const domains = [url.hostname]
|
||||||
let origin = `${url.protocol}//${url.hostname}`
|
let origin = `${url.protocol}//${url.hostname}`
|
||||||
|
@ -37,7 +37,9 @@ export class ChatOllama
|
|||||||
|
|
||||||
baseUrl = "http://localhost:11434";
|
baseUrl = "http://localhost:11434";
|
||||||
|
|
||||||
keepAlive = "5m";
|
// keepAlive = "5m";
|
||||||
|
|
||||||
|
keepAlive?: string;
|
||||||
|
|
||||||
embeddingOnly?: boolean;
|
embeddingOnly?: boolean;
|
||||||
|
|
||||||
@ -117,7 +119,7 @@ export class ChatOllama
|
|||||||
this.baseUrl = fields.baseUrl?.endsWith("/")
|
this.baseUrl = fields.baseUrl?.endsWith("/")
|
||||||
? fields.baseUrl.slice(0, -1)
|
? fields.baseUrl.slice(0, -1)
|
||||||
: fields.baseUrl ?? this.baseUrl;
|
: fields.baseUrl ?? this.baseUrl;
|
||||||
this.keepAlive = parseKeepAlive(fields.keepAlive) ?? this.keepAlive;
|
this.keepAlive = parseKeepAlive(fields.keepAlive);
|
||||||
this.embeddingOnly = fields.embeddingOnly;
|
this.embeddingOnly = fields.embeddingOnly;
|
||||||
this.f16KV = fields.f16KV;
|
this.f16KV = fields.f16KV;
|
||||||
this.frequencyPenalty = fields.frequencyPenalty;
|
this.frequencyPenalty = fields.frequencyPenalty;
|
||||||
|
78
src/models/CustomAIMessageChunk.ts
Normal file
78
src/models/CustomAIMessageChunk.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
interface BaseMessageFields {
|
||||||
|
content: string;
|
||||||
|
name?: string;
|
||||||
|
additional_kwargs?: {
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CustomAIMessageChunk {
|
||||||
|
/** The text of the message. */
|
||||||
|
content: string;
|
||||||
|
|
||||||
|
/** The name of the message sender in a multi-user chat. */
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
/** Additional keyword arguments */
|
||||||
|
additional_kwargs: NonNullable<BaseMessageFields["additional_kwargs"]>;
|
||||||
|
|
||||||
|
constructor(fields: BaseMessageFields) {
|
||||||
|
// Make sure the default value for additional_kwargs is passed into super() for serialization
|
||||||
|
if (!fields.additional_kwargs) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
fields.additional_kwargs = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.name = fields.name;
|
||||||
|
this.content = fields.content;
|
||||||
|
this.additional_kwargs = fields.additional_kwargs;
|
||||||
|
}
|
||||||
|
|
||||||
|
static _mergeAdditionalKwargs(
|
||||||
|
left: NonNullable<BaseMessageFields["additional_kwargs"]>,
|
||||||
|
right: NonNullable<BaseMessageFields["additional_kwargs"]>
|
||||||
|
): NonNullable<BaseMessageFields["additional_kwargs"]> {
|
||||||
|
const merged = { ...left };
|
||||||
|
for (const [key, value] of Object.entries(right)) {
|
||||||
|
if (merged[key] === undefined) {
|
||||||
|
merged[key] = value;
|
||||||
|
}else if (typeof merged[key] === "string") {
|
||||||
|
merged[key] = (merged[key] as string) + value;
|
||||||
|
} else if (
|
||||||
|
!Array.isArray(merged[key]) &&
|
||||||
|
typeof merged[key] === "object"
|
||||||
|
) {
|
||||||
|
merged[key] = this._mergeAdditionalKwargs(
|
||||||
|
merged[key] as NonNullable<BaseMessageFields["additional_kwargs"]>,
|
||||||
|
value as NonNullable<BaseMessageFields["additional_kwargs"]>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`additional_kwargs[${key}] already exists in this message chunk.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
concat(chunk: CustomAIMessageChunk) {
|
||||||
|
return new CustomAIMessageChunk({
|
||||||
|
content: this.content + chunk.content,
|
||||||
|
additional_kwargs: CustomAIMessageChunk._mergeAdditionalKwargs(
|
||||||
|
this.additional_kwargs,
|
||||||
|
chunk.additional_kwargs
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAiMessageChunkFields(value: unknown): value is BaseMessageFields {
|
||||||
|
if (typeof value !== "object" || value == null) return false;
|
||||||
|
return "content" in value && typeof value["content"] === "string";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAiMessageChunkFieldsList(
|
||||||
|
value: unknown[]
|
||||||
|
): value is BaseMessageFields[] {
|
||||||
|
return value.length > 0 && value.every((x) => isAiMessageChunkFields(x));
|
||||||
|
}
|
915
src/models/CustomChatOpenAI.ts
Normal file
915
src/models/CustomChatOpenAI.ts
Normal file
@ -0,0 +1,915 @@
|
|||||||
|
import { type ClientOptions, OpenAI as OpenAIClient } from "openai"
|
||||||
|
import {
|
||||||
|
AIMessage,
|
||||||
|
AIMessageChunk,
|
||||||
|
BaseMessage,
|
||||||
|
ChatMessage,
|
||||||
|
ChatMessageChunk,
|
||||||
|
FunctionMessageChunk,
|
||||||
|
HumanMessageChunk,
|
||||||
|
SystemMessageChunk,
|
||||||
|
ToolMessageChunk
|
||||||
|
} from "@langchain/core/messages"
|
||||||
|
import { ChatGenerationChunk, ChatResult } from "@langchain/core/outputs"
|
||||||
|
import { getEnvironmentVariable } from "@langchain/core/utils/env"
|
||||||
|
import {
|
||||||
|
BaseChatModel,
|
||||||
|
BaseChatModelParams
|
||||||
|
} from "@langchain/core/language_models/chat_models"
|
||||||
|
import { convertToOpenAITool } from "@langchain/core/utils/function_calling"
|
||||||
|
import {
|
||||||
|
RunnablePassthrough,
|
||||||
|
RunnableSequence
|
||||||
|
} from "@langchain/core/runnables"
|
||||||
|
import {
|
||||||
|
JsonOutputParser,
|
||||||
|
StructuredOutputParser
|
||||||
|
} from "@langchain/core/output_parsers"
|
||||||
|
import { JsonOutputKeyToolsParser } from "@langchain/core/output_parsers/openai_tools"
|
||||||
|
import { wrapOpenAIClientError } from "./utils/openai.js"
|
||||||
|
import {
|
||||||
|
ChatOpenAICallOptions,
|
||||||
|
getEndpoint,
|
||||||
|
OpenAIChatInput,
|
||||||
|
OpenAICoreRequestOptions
|
||||||
|
} from "@langchain/openai"
|
||||||
|
import { CallbackManagerForLLMRun } from "@langchain/core/callbacks/manager"
|
||||||
|
import { TokenUsage } from "@langchain/core/language_models/base"
|
||||||
|
import { LegacyOpenAIInput } from "./types.js"
|
||||||
|
import { CustomAIMessageChunk } from "./CustomAIMessageChunk.js"
|
||||||
|
|
||||||
|
type OpenAIRoleEnum = "system" | "assistant" | "user" | "function" | "tool"
|
||||||
|
|
||||||
|
function extractGenericMessageCustomRole(message: ChatMessage) {
|
||||||
|
if (
|
||||||
|
message.role !== "system" &&
|
||||||
|
message.role !== "assistant" &&
|
||||||
|
message.role !== "user" &&
|
||||||
|
message.role !== "function" &&
|
||||||
|
message.role !== "tool"
|
||||||
|
) {
|
||||||
|
console.warn(`Unknown message role: ${message.role}`)
|
||||||
|
}
|
||||||
|
return message.role
|
||||||
|
}
|
||||||
|
export function messageToOpenAIRole(message: BaseMessage): OpenAIRoleEnum {
|
||||||
|
const type = message._getType()
|
||||||
|
switch (type) {
|
||||||
|
case "system":
|
||||||
|
return "system"
|
||||||
|
case "ai":
|
||||||
|
return "assistant"
|
||||||
|
case "human":
|
||||||
|
return "user"
|
||||||
|
case "function":
|
||||||
|
return "function"
|
||||||
|
case "tool":
|
||||||
|
return "tool"
|
||||||
|
case "generic": {
|
||||||
|
if (!ChatMessage.isInstance(message))
|
||||||
|
throw new Error("Invalid generic chat message")
|
||||||
|
return extractGenericMessageCustomRole(message) as OpenAIRoleEnum
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function openAIResponseToChatMessage(
|
||||||
|
message: OpenAIClient.Chat.Completions.ChatCompletionMessage
|
||||||
|
) {
|
||||||
|
switch (message.role) {
|
||||||
|
case "assistant":
|
||||||
|
return new AIMessage(message.content || "", {
|
||||||
|
// function_call: message.function_call,
|
||||||
|
// tool_calls: message.tool_calls
|
||||||
|
// reasoning_content: message?.reasoning_content || null
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
return new ChatMessage(message.content || "", message.role ?? "unknown")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function _convertDeltaToMessageChunk(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
delta: Record<string, any>,
|
||||||
|
defaultRole?: OpenAIRoleEnum
|
||||||
|
) {
|
||||||
|
const role = delta.role ?? defaultRole
|
||||||
|
const content = delta.content ?? ""
|
||||||
|
const reasoning_content: string | undefined | null =
|
||||||
|
delta?.reasoning_content ?? undefined
|
||||||
|
let additional_kwargs
|
||||||
|
if (delta.function_call) {
|
||||||
|
additional_kwargs = {
|
||||||
|
function_call: delta.function_call
|
||||||
|
}
|
||||||
|
} else if (delta.tool_calls) {
|
||||||
|
additional_kwargs = {
|
||||||
|
tool_calls: delta.tool_calls
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
additional_kwargs = {}
|
||||||
|
}
|
||||||
|
if (role === "user") {
|
||||||
|
return new HumanMessageChunk({ content })
|
||||||
|
} else if (role === "assistant") {
|
||||||
|
return new CustomAIMessageChunk({
|
||||||
|
content,
|
||||||
|
additional_kwargs: {
|
||||||
|
...additional_kwargs,
|
||||||
|
reasoning_content
|
||||||
|
}
|
||||||
|
}) as any
|
||||||
|
} else if (role === "system") {
|
||||||
|
return new SystemMessageChunk({ content })
|
||||||
|
} else if (role === "function") {
|
||||||
|
return new FunctionMessageChunk({
|
||||||
|
content,
|
||||||
|
additional_kwargs,
|
||||||
|
name: delta.name
|
||||||
|
})
|
||||||
|
} else if (role === "tool") {
|
||||||
|
return new ToolMessageChunk({
|
||||||
|
content,
|
||||||
|
additional_kwargs,
|
||||||
|
tool_call_id: delta.tool_call_id
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return new ChatMessageChunk({ content, role })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function convertMessagesToOpenAIParams(messages: any[]) {
|
||||||
|
// TODO: Function messages do not support array content, fix cast
|
||||||
|
return messages.map((message) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const completionParam: { role: string; content: string; name?: string } = {
|
||||||
|
role: messageToOpenAIRole(message),
|
||||||
|
content: message.content
|
||||||
|
}
|
||||||
|
if (message.name != null) {
|
||||||
|
completionParam.name = message.name
|
||||||
|
}
|
||||||
|
|
||||||
|
return completionParam
|
||||||
|
})
|
||||||
|
}
|
||||||
|
export class CustomChatOpenAI<
|
||||||
|
CallOptions extends ChatOpenAICallOptions = ChatOpenAICallOptions
|
||||||
|
>
|
||||||
|
extends BaseChatModel<CallOptions>
|
||||||
|
implements OpenAIChatInput {
|
||||||
|
temperature = 1
|
||||||
|
|
||||||
|
topP = 1
|
||||||
|
|
||||||
|
frequencyPenalty = 0
|
||||||
|
|
||||||
|
presencePenalty = 0
|
||||||
|
|
||||||
|
n = 1
|
||||||
|
|
||||||
|
logitBias?: Record<string, number>
|
||||||
|
|
||||||
|
modelName = "gpt-3.5-turbo"
|
||||||
|
|
||||||
|
model = "gpt-3.5-turbo"
|
||||||
|
|
||||||
|
modelKwargs?: OpenAIChatInput["modelKwargs"]
|
||||||
|
|
||||||
|
stop?: string[]
|
||||||
|
|
||||||
|
stopSequences?: string[]
|
||||||
|
|
||||||
|
user?: string
|
||||||
|
|
||||||
|
timeout?: number
|
||||||
|
|
||||||
|
streaming = false
|
||||||
|
|
||||||
|
streamUsage = true
|
||||||
|
|
||||||
|
maxTokens?: number
|
||||||
|
|
||||||
|
logprobs?: boolean
|
||||||
|
|
||||||
|
topLogprobs?: number
|
||||||
|
|
||||||
|
openAIApiKey?: string
|
||||||
|
|
||||||
|
apiKey?: string
|
||||||
|
|
||||||
|
azureOpenAIApiVersion?: string
|
||||||
|
|
||||||
|
azureOpenAIApiKey?: string
|
||||||
|
|
||||||
|
azureADTokenProvider?: () => Promise<string>
|
||||||
|
|
||||||
|
azureOpenAIApiInstanceName?: string
|
||||||
|
|
||||||
|
azureOpenAIApiDeploymentName?: string
|
||||||
|
|
||||||
|
azureOpenAIBasePath?: string
|
||||||
|
|
||||||
|
organization?: string
|
||||||
|
|
||||||
|
protected client: OpenAIClient
|
||||||
|
|
||||||
|
protected clientConfig: ClientOptions
|
||||||
|
static lc_name() {
|
||||||
|
return "ChatOpenAI"
|
||||||
|
}
|
||||||
|
get callKeys() {
|
||||||
|
return [
|
||||||
|
...super.callKeys,
|
||||||
|
"options",
|
||||||
|
"function_call",
|
||||||
|
"functions",
|
||||||
|
"tools",
|
||||||
|
"tool_choice",
|
||||||
|
"promptIndex",
|
||||||
|
"response_format",
|
||||||
|
"seed"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
get lc_secrets() {
|
||||||
|
return {
|
||||||
|
openAIApiKey: "OPENAI_API_KEY",
|
||||||
|
azureOpenAIApiKey: "AZURE_OPENAI_API_KEY",
|
||||||
|
organization: "OPENAI_ORGANIZATION"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get lc_aliases() {
|
||||||
|
return {
|
||||||
|
modelName: "model",
|
||||||
|
openAIApiKey: "openai_api_key",
|
||||||
|
azureOpenAIApiVersion: "azure_openai_api_version",
|
||||||
|
azureOpenAIApiKey: "azure_openai_api_key",
|
||||||
|
azureOpenAIApiInstanceName: "azure_openai_api_instance_name",
|
||||||
|
azureOpenAIApiDeploymentName: "azure_openai_api_deployment_name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
constructor(
|
||||||
|
fields?: Partial<OpenAIChatInput> &
|
||||||
|
BaseChatModelParams & {
|
||||||
|
configuration?: ClientOptions & LegacyOpenAIInput
|
||||||
|
},
|
||||||
|
/** @deprecated */
|
||||||
|
configuration?: ClientOptions & LegacyOpenAIInput
|
||||||
|
) {
|
||||||
|
super(fields ?? {})
|
||||||
|
Object.defineProperty(this, "lc_serializable", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: true
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "temperature", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: 1
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "topP", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: 1
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "frequencyPenalty", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: 0
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "presencePenalty", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: 0
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "n", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: 1
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "logitBias", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "modelName", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: "gpt-3.5-turbo"
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "modelKwargs", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "stop", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "user", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "timeout", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "streaming", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: false
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "maxTokens", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "logprobs", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "topLogprobs", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "openAIApiKey", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "azureOpenAIApiVersion", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "azureOpenAIApiKey", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "azureOpenAIApiInstanceName", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "azureOpenAIApiDeploymentName", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "azureOpenAIBasePath", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "organization", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "client", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
})
|
||||||
|
Object.defineProperty(this, "clientConfig", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
})
|
||||||
|
this.openAIApiKey =
|
||||||
|
fields?.openAIApiKey ?? getEnvironmentVariable("OPENAI_API_KEY")
|
||||||
|
|
||||||
|
this.modelName = fields?.modelName ?? this.modelName
|
||||||
|
this.modelKwargs = fields?.modelKwargs ?? {}
|
||||||
|
this.timeout = fields?.timeout
|
||||||
|
this.temperature = fields?.temperature ?? this.temperature
|
||||||
|
this.topP = fields?.topP ?? this.topP
|
||||||
|
this.frequencyPenalty = fields?.frequencyPenalty ?? this.frequencyPenalty
|
||||||
|
this.presencePenalty = fields?.presencePenalty ?? this.presencePenalty
|
||||||
|
this.maxTokens = fields?.maxTokens
|
||||||
|
this.logprobs = fields?.logprobs
|
||||||
|
this.topLogprobs = fields?.topLogprobs
|
||||||
|
this.n = fields?.n ?? this.n
|
||||||
|
this.logitBias = fields?.logitBias
|
||||||
|
this.stop = fields?.stop
|
||||||
|
this.user = fields?.user
|
||||||
|
this.streaming = fields?.streaming ?? false
|
||||||
|
this.clientConfig = {
|
||||||
|
apiKey: this.openAIApiKey,
|
||||||
|
organization: this.organization,
|
||||||
|
baseURL: configuration?.basePath ?? fields?.configuration?.basePath,
|
||||||
|
dangerouslyAllowBrowser: true,
|
||||||
|
defaultHeaders:
|
||||||
|
configuration?.baseOptions?.headers ??
|
||||||
|
fields?.configuration?.baseOptions?.headers,
|
||||||
|
defaultQuery:
|
||||||
|
configuration?.baseOptions?.params ??
|
||||||
|
fields?.configuration?.baseOptions?.params,
|
||||||
|
...configuration,
|
||||||
|
...fields?.configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get the parameters used to invoke the model
|
||||||
|
*/
|
||||||
|
invocationParams(options) {
|
||||||
|
function isStructuredToolArray(tools) {
|
||||||
|
return (
|
||||||
|
tools !== undefined &&
|
||||||
|
tools.every((tool) => Array.isArray(tool.lc_namespace))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const params = {
|
||||||
|
model: this.modelName,
|
||||||
|
temperature: this.temperature,
|
||||||
|
top_p: this.topP,
|
||||||
|
frequency_penalty: this.frequencyPenalty,
|
||||||
|
presence_penalty: this.presencePenalty,
|
||||||
|
max_tokens: this.maxTokens === -1 ? undefined : this.maxTokens,
|
||||||
|
logprobs: this.logprobs,
|
||||||
|
top_logprobs: this.topLogprobs,
|
||||||
|
n: this.n,
|
||||||
|
logit_bias: this.logitBias,
|
||||||
|
stop: options?.stop ?? this.stop,
|
||||||
|
user: this.user,
|
||||||
|
stream: this.streaming,
|
||||||
|
functions: options?.functions,
|
||||||
|
function_call: options?.function_call,
|
||||||
|
tools: isStructuredToolArray(options?.tools)
|
||||||
|
? options?.tools.map(convertToOpenAITool)
|
||||||
|
: options?.tools,
|
||||||
|
tool_choice: options?.tool_choice,
|
||||||
|
response_format: options?.response_format,
|
||||||
|
seed: options?.seed,
|
||||||
|
...this.modelKwargs
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
/** @ignore */
|
||||||
|
_identifyingParams() {
|
||||||
|
return {
|
||||||
|
model_name: this.modelName,
|
||||||
|
//@ts-ignore
|
||||||
|
...this?.invocationParams(),
|
||||||
|
...this.clientConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async *_streamResponseChunks(
|
||||||
|
messages: BaseMessage[],
|
||||||
|
options: this["ParsedCallOptions"],
|
||||||
|
runManager?: CallbackManagerForLLMRun
|
||||||
|
): AsyncGenerator<ChatGenerationChunk> {
|
||||||
|
const messagesMapped = convertMessagesToOpenAIParams(messages)
|
||||||
|
const params = {
|
||||||
|
...this.invocationParams(options),
|
||||||
|
messages: messagesMapped,
|
||||||
|
stream: true
|
||||||
|
}
|
||||||
|
let defaultRole
|
||||||
|
//@ts-ignore
|
||||||
|
const streamIterable = await this.completionWithRetry(params, options)
|
||||||
|
for await (const data of streamIterable) {
|
||||||
|
const choice = data?.choices[0]
|
||||||
|
if (!choice) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const { delta } = choice
|
||||||
|
if (!delta) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const chunk = _convertDeltaToMessageChunk(delta, defaultRole)
|
||||||
|
defaultRole = delta.role ?? defaultRole
|
||||||
|
const newTokenIndices = {
|
||||||
|
//@ts-ignore
|
||||||
|
prompt: options?.promptIndex ?? 0,
|
||||||
|
completion: choice.index ?? 0
|
||||||
|
}
|
||||||
|
if (typeof chunk.content !== "string") {
|
||||||
|
console.log(
|
||||||
|
"[WARNING]: Received non-string content from OpenAI. This is currently not supported."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const generationInfo = { ...newTokenIndices } as any
|
||||||
|
if (choice.finish_reason !== undefined) {
|
||||||
|
generationInfo.finish_reason = choice.finish_reason
|
||||||
|
}
|
||||||
|
if (this.logprobs) {
|
||||||
|
generationInfo.logprobs = choice.logprobs
|
||||||
|
}
|
||||||
|
const generationChunk = new ChatGenerationChunk({
|
||||||
|
message: chunk,
|
||||||
|
text: chunk.content,
|
||||||
|
generationInfo
|
||||||
|
})
|
||||||
|
yield generationChunk
|
||||||
|
// eslint-disable-next-line no-void
|
||||||
|
void runManager?.handleLLMNewToken(
|
||||||
|
generationChunk.text ?? "",
|
||||||
|
newTokenIndices,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{ chunk: generationChunk }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (options.signal?.aborted) {
|
||||||
|
throw new Error("AbortError")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get the identifying parameters for the model
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
identifyingParams() {
|
||||||
|
return this._identifyingParams()
|
||||||
|
}
|
||||||
|
/** @ignore */
|
||||||
|
async _generate(
|
||||||
|
messages: BaseMessage[],
|
||||||
|
options: this["ParsedCallOptions"],
|
||||||
|
runManager?: CallbackManagerForLLMRun
|
||||||
|
): Promise<ChatResult> {
|
||||||
|
const tokenUsage: TokenUsage = {}
|
||||||
|
const params = this.invocationParams(options)
|
||||||
|
const messagesMapped: any[] = convertMessagesToOpenAIParams(messages)
|
||||||
|
if (params.stream) {
|
||||||
|
const stream = this._streamResponseChunks(messages, options, runManager)
|
||||||
|
const finalChunks: Record<number, ChatGenerationChunk> = {}
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
//@ts-ignore
|
||||||
|
chunk.message.response_metadata = {
|
||||||
|
...chunk.generationInfo,
|
||||||
|
//@ts-ignore
|
||||||
|
...chunk.message.response_metadata
|
||||||
|
}
|
||||||
|
const index = chunk.generationInfo?.completion ?? 0
|
||||||
|
if (finalChunks[index] === undefined) {
|
||||||
|
finalChunks[index] = chunk
|
||||||
|
} else {
|
||||||
|
finalChunks[index] = finalChunks[index].concat(chunk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const generations = Object.entries(finalChunks)
|
||||||
|
.sort(([aKey], [bKey]) => parseInt(aKey, 10) - parseInt(bKey, 10))
|
||||||
|
.map(([_, value]) => value)
|
||||||
|
const { functions, function_call } = this.invocationParams(options)
|
||||||
|
// OpenAI does not support token usage report under stream mode,
|
||||||
|
// fallback to estimation.
|
||||||
|
const promptTokenUsage = await this.getEstimatedTokenCountFromPrompt(
|
||||||
|
messages,
|
||||||
|
functions,
|
||||||
|
function_call
|
||||||
|
)
|
||||||
|
const completionTokenUsage =
|
||||||
|
await this.getNumTokensFromGenerations(generations)
|
||||||
|
tokenUsage.promptTokens = promptTokenUsage
|
||||||
|
tokenUsage.completionTokens = completionTokenUsage
|
||||||
|
tokenUsage.totalTokens = promptTokenUsage + completionTokenUsage
|
||||||
|
return { generations, llmOutput: { estimatedTokenUsage: tokenUsage } }
|
||||||
|
} else {
|
||||||
|
const data = await this.completionWithRetry(
|
||||||
|
{
|
||||||
|
...params,
|
||||||
|
//@ts-ignore
|
||||||
|
stream: false,
|
||||||
|
messages: messagesMapped
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal: options?.signal,
|
||||||
|
//@ts-ignore
|
||||||
|
...options?.options
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const {
|
||||||
|
completion_tokens: completionTokens,
|
||||||
|
prompt_tokens: promptTokens,
|
||||||
|
total_tokens: totalTokens
|
||||||
|
//@ts-ignore
|
||||||
|
} = data?.usage ?? {}
|
||||||
|
if (completionTokens) {
|
||||||
|
tokenUsage.completionTokens =
|
||||||
|
(tokenUsage.completionTokens ?? 0) + completionTokens
|
||||||
|
}
|
||||||
|
if (promptTokens) {
|
||||||
|
tokenUsage.promptTokens = (tokenUsage.promptTokens ?? 0) + promptTokens
|
||||||
|
}
|
||||||
|
if (totalTokens) {
|
||||||
|
tokenUsage.totalTokens = (tokenUsage.totalTokens ?? 0) + totalTokens
|
||||||
|
}
|
||||||
|
const generations = []
|
||||||
|
//@ts-ignore
|
||||||
|
for (const part of data?.choices ?? []) {
|
||||||
|
const text = part.message?.content ?? ""
|
||||||
|
const generation = {
|
||||||
|
text,
|
||||||
|
message: openAIResponseToChatMessage(
|
||||||
|
part.message ?? { role: "assistant" }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
//@ts-ignore
|
||||||
|
generation.generationInfo = {
|
||||||
|
...(part.finish_reason ? { finish_reason: part.finish_reason } : {}),
|
||||||
|
...(part.logprobs ? { logprobs: part.logprobs } : {})
|
||||||
|
}
|
||||||
|
generations.push(generation)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
generations,
|
||||||
|
llmOutput: { tokenUsage }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Estimate the number of tokens a prompt will use.
|
||||||
|
* Modified from: https://github.com/hmarr/openai-chat-tokens/blob/main/src/index.ts
|
||||||
|
*/
|
||||||
|
async getEstimatedTokenCountFromPrompt(messages, functions, function_call) {
|
||||||
|
let tokens = (await this.getNumTokensFromMessages(messages)).totalCount
|
||||||
|
if (functions && messages.find((m) => m._getType() === "system")) {
|
||||||
|
tokens -= 4
|
||||||
|
}
|
||||||
|
if (function_call === "none") {
|
||||||
|
tokens += 1
|
||||||
|
} else if (typeof function_call === "object") {
|
||||||
|
tokens += (await this.getNumTokens(function_call.name)) + 4
|
||||||
|
}
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Estimate the number of tokens an array of generations have used.
|
||||||
|
*/
|
||||||
|
async getNumTokensFromGenerations(generations) {
|
||||||
|
const generationUsages = await Promise.all(
|
||||||
|
generations.map(async (generation) => {
|
||||||
|
if (generation.message.additional_kwargs?.function_call) {
|
||||||
|
return (await this.getNumTokensFromMessages([generation.message]))
|
||||||
|
.countPerMessage[0]
|
||||||
|
} else {
|
||||||
|
return await this.getNumTokens(generation.message.content)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return generationUsages.reduce((a, b) => a + b, 0)
|
||||||
|
}
|
||||||
|
async getNumTokensFromMessages(messages) {
|
||||||
|
let totalCount = 0
|
||||||
|
let tokensPerMessage = 0
|
||||||
|
let tokensPerName = 0
|
||||||
|
// From: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_format_inputs_to_ChatGPT_models.ipynb
|
||||||
|
if (this.modelName === "gpt-3.5-turbo-0301") {
|
||||||
|
tokensPerMessage = 4
|
||||||
|
tokensPerName = -1
|
||||||
|
} else {
|
||||||
|
tokensPerMessage = 3
|
||||||
|
tokensPerName = 1
|
||||||
|
}
|
||||||
|
const countPerMessage = await Promise.all(
|
||||||
|
messages.map(async (message) => {
|
||||||
|
const textCount = await this.getNumTokens(message.content)
|
||||||
|
const roleCount = await this.getNumTokens(messageToOpenAIRole(message))
|
||||||
|
const nameCount =
|
||||||
|
message.name !== undefined
|
||||||
|
? tokensPerName + (await this.getNumTokens(message.name))
|
||||||
|
: 0
|
||||||
|
let count = textCount + tokensPerMessage + roleCount + nameCount
|
||||||
|
// From: https://github.com/hmarr/openai-chat-tokens/blob/main/src/index.ts messageTokenEstimate
|
||||||
|
const openAIMessage = message
|
||||||
|
if (openAIMessage._getType() === "function") {
|
||||||
|
count -= 2
|
||||||
|
}
|
||||||
|
if (openAIMessage.additional_kwargs?.function_call) {
|
||||||
|
count += 3
|
||||||
|
}
|
||||||
|
if (openAIMessage?.additional_kwargs.function_call?.name) {
|
||||||
|
count += await this.getNumTokens(
|
||||||
|
openAIMessage.additional_kwargs.function_call?.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (openAIMessage.additional_kwargs.function_call?.arguments) {
|
||||||
|
try {
|
||||||
|
count += await this.getNumTokens(
|
||||||
|
// Remove newlines and spaces
|
||||||
|
JSON.stringify(
|
||||||
|
JSON.parse(
|
||||||
|
openAIMessage.additional_kwargs.function_call?.arguments
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Error parsing function arguments",
|
||||||
|
error,
|
||||||
|
JSON.stringify(openAIMessage.additional_kwargs.function_call)
|
||||||
|
)
|
||||||
|
count += await this.getNumTokens(
|
||||||
|
openAIMessage.additional_kwargs.function_call?.arguments
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totalCount += count
|
||||||
|
return count
|
||||||
|
})
|
||||||
|
)
|
||||||
|
totalCount += 3 // every reply is primed with <|start|>assistant<|message|>
|
||||||
|
return { totalCount, countPerMessage }
|
||||||
|
}
|
||||||
|
async completionWithRetry(
|
||||||
|
request: OpenAIClient.Chat.ChatCompletionCreateParamsStreaming,
|
||||||
|
options?: OpenAICoreRequestOptions
|
||||||
|
) {
|
||||||
|
const requestOptions = this._getClientOptions(options)
|
||||||
|
return this.caller.call(async () => {
|
||||||
|
try {
|
||||||
|
const res = await this.client.chat.completions.create(
|
||||||
|
request,
|
||||||
|
requestOptions
|
||||||
|
)
|
||||||
|
return res
|
||||||
|
} catch (e) {
|
||||||
|
const error = wrapOpenAIClientError(e)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_getClientOptions(options) {
|
||||||
|
if (!this.client) {
|
||||||
|
const openAIEndpointConfig = {
|
||||||
|
azureOpenAIApiDeploymentName: this.azureOpenAIApiDeploymentName,
|
||||||
|
azureOpenAIApiInstanceName: this.azureOpenAIApiInstanceName,
|
||||||
|
azureOpenAIApiKey: this.azureOpenAIApiKey,
|
||||||
|
azureOpenAIBasePath: this.azureOpenAIBasePath,
|
||||||
|
baseURL: this.clientConfig.baseURL
|
||||||
|
}
|
||||||
|
const endpoint = getEndpoint(openAIEndpointConfig)
|
||||||
|
const params = {
|
||||||
|
...this.clientConfig,
|
||||||
|
baseURL: endpoint,
|
||||||
|
timeout: this.timeout,
|
||||||
|
maxRetries: 0
|
||||||
|
}
|
||||||
|
if (!params.baseURL) {
|
||||||
|
delete params.baseURL
|
||||||
|
}
|
||||||
|
this.client = new OpenAIClient(params)
|
||||||
|
}
|
||||||
|
const requestOptions = {
|
||||||
|
...this.clientConfig,
|
||||||
|
...options
|
||||||
|
}
|
||||||
|
if (this.azureOpenAIApiKey) {
|
||||||
|
requestOptions.headers = {
|
||||||
|
"api-key": this.azureOpenAIApiKey,
|
||||||
|
...requestOptions.headers
|
||||||
|
}
|
||||||
|
requestOptions.query = {
|
||||||
|
"api-version": this.azureOpenAIApiVersion,
|
||||||
|
...requestOptions.query
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requestOptions
|
||||||
|
}
|
||||||
|
_llmType() {
|
||||||
|
return "openai"
|
||||||
|
}
|
||||||
|
/** @ignore */
|
||||||
|
_combineLLMOutput(...llmOutputs) {
|
||||||
|
return llmOutputs.reduce(
|
||||||
|
(acc, llmOutput) => {
|
||||||
|
if (llmOutput && llmOutput.tokenUsage) {
|
||||||
|
acc.tokenUsage.completionTokens +=
|
||||||
|
llmOutput.tokenUsage.completionTokens ?? 0
|
||||||
|
acc.tokenUsage.promptTokens += llmOutput.tokenUsage.promptTokens ?? 0
|
||||||
|
acc.tokenUsage.totalTokens += llmOutput.tokenUsage.totalTokens ?? 0
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tokenUsage: {
|
||||||
|
completionTokens: 0,
|
||||||
|
promptTokens: 0,
|
||||||
|
totalTokens: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
withStructuredOutput(outputSchema, config) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
let schema
|
||||||
|
let name
|
||||||
|
let method
|
||||||
|
let includeRaw
|
||||||
|
if (isStructuredOutputMethodParams(outputSchema)) {
|
||||||
|
schema = outputSchema.schema
|
||||||
|
name = outputSchema.name
|
||||||
|
method = outputSchema.method
|
||||||
|
includeRaw = outputSchema.includeRaw
|
||||||
|
} else {
|
||||||
|
schema = outputSchema
|
||||||
|
name = config?.name
|
||||||
|
method = config?.method
|
||||||
|
includeRaw = config?.includeRaw
|
||||||
|
}
|
||||||
|
let llm
|
||||||
|
let outputParser
|
||||||
|
if (method === "jsonMode") {
|
||||||
|
llm = this.bind({})
|
||||||
|
if (isZodSchema(schema)) {
|
||||||
|
outputParser = StructuredOutputParser.fromZodSchema(schema)
|
||||||
|
} else {
|
||||||
|
outputParser = new JsonOutputParser()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let functionName = name ?? "extract"
|
||||||
|
// Is function calling
|
||||||
|
|
||||||
|
let openAIFunctionDefinition
|
||||||
|
if (
|
||||||
|
typeof schema.name === "string" &&
|
||||||
|
typeof schema.parameters === "object" &&
|
||||||
|
schema.parameters != null
|
||||||
|
) {
|
||||||
|
openAIFunctionDefinition = schema
|
||||||
|
functionName = schema.name
|
||||||
|
} else {
|
||||||
|
openAIFunctionDefinition = {
|
||||||
|
name: schema.title ?? functionName,
|
||||||
|
description: schema.description ?? "",
|
||||||
|
parameters: schema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
llm = this.bind({})
|
||||||
|
outputParser = new JsonOutputKeyToolsParser({
|
||||||
|
returnSingle: true,
|
||||||
|
keyName: functionName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!includeRaw) {
|
||||||
|
return llm.pipe(outputParser)
|
||||||
|
}
|
||||||
|
const parserAssign = RunnablePassthrough.assign({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
parsed: (input, config) => outputParser.invoke(input.raw, config)
|
||||||
|
})
|
||||||
|
const parserNone = RunnablePassthrough.assign({
|
||||||
|
parsed: () => null
|
||||||
|
})
|
||||||
|
const parsedWithFallback = parserAssign.withFallbacks({
|
||||||
|
fallbacks: [parserNone]
|
||||||
|
})
|
||||||
|
return RunnableSequence.from([
|
||||||
|
{
|
||||||
|
raw: llm
|
||||||
|
},
|
||||||
|
parsedWithFallback
|
||||||
|
] as any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function isZodSchema(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
input
|
||||||
|
) {
|
||||||
|
// Check for a characteristic method of Zod schemas
|
||||||
|
return typeof input?.parse === "function"
|
||||||
|
}
|
||||||
|
function isStructuredOutputMethodParams(
|
||||||
|
x
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
x !== undefined &&
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
typeof x.schema === "object"
|
||||||
|
)
|
||||||
|
}
|
@ -118,7 +118,7 @@ export class OllamaEmbeddingsPageAssist extends Embeddings {
|
|||||||
|
|
||||||
headers?: Record<string, string>
|
headers?: Record<string, string>
|
||||||
|
|
||||||
keepAlive = "5m"
|
keepAlive?: string
|
||||||
|
|
||||||
requestOptions?: OllamaRequestParams["options"]
|
requestOptions?: OllamaRequestParams["options"]
|
||||||
|
|
||||||
|
@ -2,9 +2,9 @@ import { getModelInfo, isCustomModel, isOllamaModel } from "@/db/models"
|
|||||||
import { ChatChromeAI } from "./ChatChromeAi"
|
import { ChatChromeAI } from "./ChatChromeAi"
|
||||||
import { ChatOllama } from "./ChatOllama"
|
import { ChatOllama } from "./ChatOllama"
|
||||||
import { getOpenAIConfigById } from "@/db/openai"
|
import { getOpenAIConfigById } from "@/db/openai"
|
||||||
import { ChatOpenAI } from "@langchain/openai"
|
|
||||||
import { urlRewriteRuntime } from "@/libs/runtime"
|
import { urlRewriteRuntime } from "@/libs/runtime"
|
||||||
import { ChatGoogleAI } from "./ChatGoogleAI"
|
import { ChatGoogleAI } from "./ChatGoogleAI"
|
||||||
|
import { CustomChatOpenAI } from "./CustomChatOpenAI"
|
||||||
|
|
||||||
export const pageAssistModel = async ({
|
export const pageAssistModel = async ({
|
||||||
model,
|
model,
|
||||||
@ -76,7 +76,7 @@ export const pageAssistModel = async ({
|
|||||||
}) as any
|
}) as any
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ChatOpenAI({
|
return new CustomChatOpenAI({
|
||||||
modelName: modelInfo.model_id,
|
modelName: modelInfo.model_id,
|
||||||
openAIApiKey: providerInfo.apiKey || "temp",
|
openAIApiKey: providerInfo.apiKey || "temp",
|
||||||
temperature,
|
temperature,
|
||||||
|
@ -95,7 +95,7 @@ const getGoogleDocs = () => {
|
|||||||
|
|
||||||
export const parseGoogleDocs = async () => {
|
export const parseGoogleDocs = async () => {
|
||||||
const result = new Promise((resolve) => {
|
const result = new Promise((resolve) => {
|
||||||
if (import.meta.env.BROWSER === "chrome") {
|
if (import.meta.env.BROWSER === "chrome" || import.meta.env.BROWSER === "edge") {
|
||||||
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
|
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
|
||||||
const tab = tabs[0]
|
const tab = tabs[0]
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ export const isTwitterTimeline = (url: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const isTwitterProfile = (url: string) => {
|
export const isTwitterProfile = (url: string) => {
|
||||||
const PROFILE_REGEX = /twitter\.com\/[a-zA-Z0-9_]+/g
|
const PROFILE_REGEX = /x\.com\/[a-zA-Z0-9_]+/g
|
||||||
const X_REGEX = /x\.com\/[a-zA-Z0-9_]+/g
|
const X_REGEX = /x\.com\/[a-zA-Z0-9_]+/g
|
||||||
return PROFILE_REGEX.test(url) || X_REGEX.test(url)
|
return PROFILE_REGEX.test(url) || X_REGEX.test(url)
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import OptionLayout from "~/components/Layouts/Layout"
|
import OptionLayout from "~/components/Layouts/Layout"
|
||||||
import { Playground } from "~/components/Option/Playground/Playground"
|
import { Playground } from "~/components/Option/Playground/Playground"
|
||||||
|
|
||||||
const OptionIndex = () => {
|
const OptionIndex = () => {
|
||||||
return (
|
return (
|
||||||
<OptionLayout>
|
<OptionLayout>
|
||||||
<Playground />
|
<Playground />
|
||||||
</OptionLayout>
|
</OptionLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -126,25 +126,25 @@ const SidepanelChat = () => {
|
|||||||
}, [bgMsg])
|
}, [bgMsg])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex h-full w-full">
|
||||||
ref={drop}
|
<main className="relative h-dvh w-full">
|
||||||
className={`flex ${
|
<div className="relative z-10 w-full">
|
||||||
dropState === "dragging" && chatMode === "normal"
|
<SidepanelHeader />
|
||||||
? "bg-neutral-200 dark:bg-gray-800 z-10"
|
</div>
|
||||||
: "bg-neutral-50 dark:bg-[#171717]"
|
<div
|
||||||
} flex-col min-h-screen mx-auto max-w-7xl`}>
|
ref={drop}
|
||||||
<div className="sticky top-0 z-10">
|
className={`relative flex h-full flex-col items-center ${
|
||||||
<SidepanelHeader />
|
dropState === "dragging" ? "bg-gray-100 dark:bg-gray-800" : ""
|
||||||
</div>
|
} bg-white dark:bg-[#171717]`}>
|
||||||
<SidePanelBody />
|
<div className="custom-scrollbar bg-bottom-mask-light dark:bg-bottom-mask-dark mask-bottom-fade will-change-mask flex h-full w-full flex-col items-center overflow-x-hidden overflow-y-auto px-5">
|
||||||
|
<SidePanelBody />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="bottom-0 w-full bg-transparent border-0 fixed pt-2">
|
<div className="absolute bottom-0 w-full">
|
||||||
<div className="stretch mx-2 flex flex-row gap-3 md:mx-4 lg:mx-auto lg:max-w-2xl xl:max-w-3xl">
|
|
||||||
<div className="relative flex flex-col h-full flex-1 items-stretch md:flex-col">
|
|
||||||
<SidepanelForm dropedFile={dropedFile} />
|
<SidepanelForm dropedFile={dropedFile} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -75,9 +75,9 @@ export const getAllModelSettings = async () => {
|
|||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const value = await storage.get(key)
|
const value = await storage.get(key)
|
||||||
settings[key] = value
|
settings[key] = value
|
||||||
if (!value && key === "keepAlive") {
|
// if (!value && key === "keepAlive") {
|
||||||
settings[key] = "5m"
|
// settings[key] = "5m"
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
return settings
|
return settings
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -98,9 +98,9 @@ export const getAllDefaultModelSettings = async (): Promise<ModelSettings> => {
|
|||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const value = await storage.get(key)
|
const value = await storage.get(key)
|
||||||
settings[key] = value
|
settings[key] = value
|
||||||
if (!value && key === "keepAlive") {
|
// if (!value && key === "keepAlive") {
|
||||||
settings[key] = "5m"
|
// settings[key] = "5m"
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
return settings
|
return settings
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { Storage } from "@plasmohq/storage"
|
import { Storage } from "@plasmohq/storage"
|
||||||
|
|
||||||
const storage = new Storage()
|
const storage = new Storage()
|
||||||
|
const storage2 = new Storage({
|
||||||
|
area: "local"
|
||||||
|
})
|
||||||
|
|
||||||
const DEFAULT_TTS_PROVIDER = "browser"
|
const DEFAULT_TTS_PROVIDER = "browser"
|
||||||
|
|
||||||
@ -21,7 +24,7 @@ export const setTTSProvider = async (ttsProvider: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getBrowserTTSVoices = async () => {
|
export const getBrowserTTSVoices = async () => {
|
||||||
if (import.meta.env.BROWSER === "chrome") {
|
if (import.meta.env.BROWSER === "chrome" || import.meta.env.BROWSER === "edge") {
|
||||||
const tts = await chrome.tts.getVoices()
|
const tts = await chrome.tts.getVoices()
|
||||||
return tts
|
return tts
|
||||||
} else {
|
} else {
|
||||||
@ -98,10 +101,22 @@ export const getResponseSplitting = async () => {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getRemoveReasoningTagTTS = async () => {
|
||||||
|
const data = await storage2.get("removeReasoningTagTTS")
|
||||||
|
if (!data || data.length === 0 || data === "") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return data === "true"
|
||||||
|
}
|
||||||
|
|
||||||
export const setResponseSplitting = async (responseSplitting: string) => {
|
export const setResponseSplitting = async (responseSplitting: string) => {
|
||||||
await storage.set("ttsResponseSplitting", responseSplitting)
|
await storage.set("ttsResponseSplitting", responseSplitting)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const setRemoveReasoningTagTTS = async (removeReasoningTagTTS: boolean) => {
|
||||||
|
await storage2.set("removeReasoningTagTTS", removeReasoningTagTTS.toString())
|
||||||
|
}
|
||||||
|
|
||||||
export const getTTSSettings = async () => {
|
export const getTTSSettings = async () => {
|
||||||
const [
|
const [
|
||||||
ttsEnabled,
|
ttsEnabled,
|
||||||
@ -112,7 +127,8 @@ export const getTTSSettings = async () => {
|
|||||||
elevenLabsApiKey,
|
elevenLabsApiKey,
|
||||||
elevenLabsVoiceId,
|
elevenLabsVoiceId,
|
||||||
elevenLabsModel,
|
elevenLabsModel,
|
||||||
responseSplitting
|
responseSplitting,
|
||||||
|
removeReasoningTagTTS
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
isTTSEnabled(),
|
isTTSEnabled(),
|
||||||
getTTSProvider(),
|
getTTSProvider(),
|
||||||
@ -122,7 +138,8 @@ export const getTTSSettings = async () => {
|
|||||||
getElevenLabsApiKey(),
|
getElevenLabsApiKey(),
|
||||||
getElevenLabsVoiceId(),
|
getElevenLabsVoiceId(),
|
||||||
getElevenLabsModel(),
|
getElevenLabsModel(),
|
||||||
getResponseSplitting()
|
getResponseSplitting(),
|
||||||
|
getRemoveReasoningTagTTS()
|
||||||
])
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -134,7 +151,8 @@ export const getTTSSettings = async () => {
|
|||||||
elevenLabsApiKey,
|
elevenLabsApiKey,
|
||||||
elevenLabsVoiceId,
|
elevenLabsVoiceId,
|
||||||
elevenLabsModel,
|
elevenLabsModel,
|
||||||
responseSplitting
|
responseSplitting,
|
||||||
|
removeReasoningTagTTS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,7 +164,8 @@ export const setTTSSettings = async ({
|
|||||||
elevenLabsApiKey,
|
elevenLabsApiKey,
|
||||||
elevenLabsVoiceId,
|
elevenLabsVoiceId,
|
||||||
elevenLabsModel,
|
elevenLabsModel,
|
||||||
responseSplitting
|
responseSplitting,
|
||||||
|
removeReasoningTagTTS
|
||||||
}: {
|
}: {
|
||||||
ttsEnabled: boolean
|
ttsEnabled: boolean
|
||||||
ttsProvider: string
|
ttsProvider: string
|
||||||
@ -156,6 +175,7 @@ export const setTTSSettings = async ({
|
|||||||
elevenLabsVoiceId: string
|
elevenLabsVoiceId: string
|
||||||
elevenLabsModel: string
|
elevenLabsModel: string
|
||||||
responseSplitting: string
|
responseSplitting: string
|
||||||
|
removeReasoningTagTTS: boolean
|
||||||
}) => {
|
}) => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
setTTSEnabled(ttsEnabled),
|
setTTSEnabled(ttsEnabled),
|
||||||
@ -165,6 +185,7 @@ export const setTTSSettings = async ({
|
|||||||
setElevenLabsApiKey(elevenLabsApiKey),
|
setElevenLabsApiKey(elevenLabsApiKey),
|
||||||
setElevenLabsVoiceId(elevenLabsVoiceId),
|
setElevenLabsVoiceId(elevenLabsVoiceId),
|
||||||
setElevenLabsModel(elevenLabsModel),
|
setElevenLabsModel(elevenLabsModel),
|
||||||
setResponseSplitting(responseSplitting)
|
setResponseSplitting(responseSplitting),
|
||||||
|
setRemoveReasoningTagTTS(removeReasoningTagTTS)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { browser } from "wxt/browser"
|
import { browser } from "wxt/browser"
|
||||||
|
|
||||||
export const setTitle = ({ title }: { title: string }) => {
|
export const setTitle = ({ title }: { title: string }) => {
|
||||||
if (import.meta.env.BROWSER === "chrome") {
|
if (import.meta.env.BROWSER === "chrome" || import.meta.env.BROWSER === "edge") {
|
||||||
chrome.action.setTitle({ title })
|
chrome.action.setTitle({ title })
|
||||||
} else {
|
} else {
|
||||||
browser.browserAction.setTitle({ title })
|
browser.browserAction.setTitle({ title })
|
||||||
@ -9,7 +9,7 @@ export const setTitle = ({ title }: { title: string }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const setBadgeBackgroundColor = ({ color }: { color: string }) => {
|
export const setBadgeBackgroundColor = ({ color }: { color: string }) => {
|
||||||
if (import.meta.env.BROWSER === "chrome") {
|
if (import.meta.env.BROWSER === "chrome" || import.meta.env.BROWSER === "edge") {
|
||||||
chrome.action.setBadgeBackgroundColor({ color })
|
chrome.action.setBadgeBackgroundColor({ color })
|
||||||
} else {
|
} else {
|
||||||
browser.browserAction.setBadgeBackgroundColor({ color })
|
browser.browserAction.setBadgeBackgroundColor({ color })
|
||||||
@ -17,7 +17,7 @@ export const setBadgeBackgroundColor = ({ color }: { color: string }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const setBadgeText = ({ text }: { text: string }) => {
|
export const setBadgeText = ({ text }: { text: string }) => {
|
||||||
if (import.meta.env.BROWSER === "chrome") {
|
if (import.meta.env.BROWSER === "chrome" || import.meta.env.BROWSER === "edge") {
|
||||||
chrome.action.setBadgeText({ text })
|
chrome.action.setBadgeText({ text })
|
||||||
} else {
|
} else {
|
||||||
browser.browserAction.setBadgeText({ text })
|
browser.browserAction.setBadgeText({ text })
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { isCustomModel } from "@/db/models"
|
import { isCustomModel } from "@/db/models"
|
||||||
|
import { removeReasoning } from "@/libs/reasoning"
|
||||||
import {
|
import {
|
||||||
HumanMessage,
|
HumanMessage,
|
||||||
AIMessage,
|
AIMessage,
|
||||||
@ -51,11 +52,11 @@ export const generateHistory = (
|
|||||||
history.push(
|
history.push(
|
||||||
new AIMessage({
|
new AIMessage({
|
||||||
content: isCustom
|
content: isCustom
|
||||||
? message.content
|
? removeReasoning(message.content)
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: message.content
|
text: removeReasoning(message.content)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
import { createWorker } from 'tesseract.js';
|
import { createWorker } from "tesseract.js"
|
||||||
|
|
||||||
export async function processImageForOCR(imageData: string): Promise<string> {
|
export async function processImageForOCR(imageData: string): Promise<string> {
|
||||||
const worker = await createWorker('eng-fast', undefined, {
|
try {
|
||||||
workerPath: "/ocr/worker.min.js",
|
const isOCROffline = import.meta.env.BROWSER === "edge"
|
||||||
workerBlobURL: false,
|
const worker = await createWorker(!isOCROffline ? "eng-fast" : "eng", undefined, {
|
||||||
corePath: "/ocr/tesseract-core-simd.js",
|
workerPath: "/ocr/worker.min.js",
|
||||||
errorHandler: e => console.error(e),
|
workerBlobURL: false,
|
||||||
langPath: "/ocr/lang"
|
corePath: "/ocr/tesseract-core-simd.js",
|
||||||
});
|
errorHandler: (e) => console.error(e),
|
||||||
|
langPath: !isOCROffline ? "/ocr/lang" : undefined
|
||||||
|
})
|
||||||
|
|
||||||
const result = await worker.recognize(imageData);
|
const result = await worker.recognize(imageData)
|
||||||
|
|
||||||
await worker.terminate();
|
await worker.terminate()
|
||||||
|
|
||||||
return result.data.text;
|
return result.data.text
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing image for OCR:", error)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,5 +3,16 @@ module.exports = {
|
|||||||
mode: "jit",
|
mode: "jit",
|
||||||
darkMode: "class",
|
darkMode: "class",
|
||||||
content: ["./src/**/*.tsx"],
|
content: ["./src/**/*.tsx"],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
backgroundImage: {
|
||||||
|
'bottom-mask-light': 'linear-gradient(0deg, transparent 0, #ffffff 160px)',
|
||||||
|
'bottom-mask-dark': 'linear-gradient(0deg, transparent 0, #171717 160px)',
|
||||||
|
},
|
||||||
|
maskImage: {
|
||||||
|
'bottom-fade': 'linear-gradient(0deg, transparent 0, #000 160px)',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")]
|
plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")]
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ export default defineConfig({
|
|||||||
outDir: "build",
|
outDir: "build",
|
||||||
|
|
||||||
manifest: {
|
manifest: {
|
||||||
version: "1.4.5",
|
version: "1.5.0",
|
||||||
name:
|
name:
|
||||||
process.env.TARGET === "firefox"
|
process.env.TARGET === "firefox"
|
||||||
? "Page Assist - A Web UI for Local AI Models"
|
? "Page Assist - A Web UI for Local AI Models"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user