Merge pull request #70 from n4ze3m/next

v1.1.8
This commit is contained in:
Muhammed Nazeem 2024-05-18 23:19:16 +05:30 committed by GitHub
commit dfaffa0faa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 391 additions and 96 deletions

View File

@ -118,8 +118,9 @@ This will start a development server and watch for changes in the source files.
| Chrome | ✅ | ✅ | ✅ | | Chrome | ✅ | ✅ | ✅ |
| Brave | ✅ | ✅ | ✅ | | Brave | ✅ | ✅ | ✅ |
| Firefox | ✅ | ✅ | ✅ | | Firefox | ✅ | ✅ | ✅ |
| Vivaldi | ✅ | ✅ | ✅ |
| Edge | ✅ | ❌ | ✅ | | Edge | ✅ | ❌ | ✅ |
| Opera GX | ❌ | ❌ | ✅ | | Opera | ❌ | ❌ | ✅ |
| Arc | ❌ | ❌ | ✅ | | Arc | ❌ | ❌ | ✅ |
## Local AI Provider ## Local AI Provider
@ -134,6 +135,13 @@ This will start a development server and watch for changes in the source files.
- [ ] More Customization Options - [ ] More Customization Options
- [ ] Better UI/UX - [ ] Better UI/UX
## Privacy
Page Assist does not collect any personal data. The only time the extension communicates with the server is when you are using the share feature, which can be disabled from the settings.
All the data is stored locally in the browser storage. You can view the source code and verify it yourself.
## Contributing ## Contributing
Contributions are welcome. If you have any feature requests, bug reports, or questions, feel free to create an issue. Contributions are welcome. If you have any feature requests, bug reports, or questions, feel free to create an issue.
@ -146,6 +154,17 @@ If you like the project and want to support it, you can buy me a coffee. It will
or you can sponsor me on GitHub. or you can sponsor me on GitHub.
## Blogs and Videos About Page Assist
This are some of the blogs and videos about Page Assist. If you have written a blog or made a video about Page Assist, feel free to create a PR and add it here.
- [OllamaをChromeAddonのPage Assistで簡単操作](https://note.com/lucas_san/n/nf00d01a02c3a) by [LucasChatGPT](https://twitter.com/LucasChatGPT)
- [This Chrome Extension Surprised Me](https://www.youtube.com/watch?v=IvLTlDy9G8c) by [Matt Williams](https://www.youtube.com/@technovangelist)
- [Ollama With 1 Click](https://www.youtube.com/watch?v=61uN5jtj2wo) by [Yaron Been From EcomXFactor](https://www.youtube.com/@ecomxfactor-YaronBeen)
## License ## License
MIT MIT

View File

@ -195,7 +195,7 @@
"delete": "Are you sure you want to delete this share? This action cannot be undone." "delete": "Are you sure you want to delete this share? This action cannot be undone."
}, },
"label": "Manage Page Share", "label": "Manage Page Share",
"description": "Enable or disable the page share feature. By default, the page share feature is enabled." "description": "Enable or disable the page share feature"
}, },
"notification": { "notification": {
"pageShareSuccess": "Page Share URL updated successfully", "pageShareSuccess": "Page Share URL updated successfully",

View File

@ -198,7 +198,7 @@
"delete": "本当にこの共有を削除しますか?この操作は元に戻せません。" "delete": "本当にこの共有を削除しますか?この操作は元に戻せません。"
}, },
"label": "ページ共有を管理する", "label": "ページ共有を管理する",
"description": "ページ共有機能を有効または無効にします。デフォルトでは、ページ共有機能は有効になっています。" "description": "ページ共有機能を有効または無効にする"
}, },
"notification": { "notification": {
"pageShareSuccess": "ページ共有URLが正常に更新されました", "pageShareSuccess": "ページ共有URLが正常に更新されました",

View File

@ -198,7 +198,7 @@
"delete": "ഈ പങ്കിടല്‍ ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ? ഈ പ്രവർത്തനം പിന്നീട് പിൻവലിക്കാനാകില്ല." "delete": "ഈ പങ്കിടല്‍ ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ? ഈ പ്രവർത്തനം പിന്നീട് പിൻവലിക്കാനാകില്ല."
}, },
"label": "പേജ് ഷെയർ നിയന്ത്രിക്കുക", "label": "പേജ് ഷെയർ നിയന്ത്രിക്കുക",
"description": "പേജ് ഷെയർ സവിശേഷത സജീവമാക്കുകയോ അക്ഷമമാക്കുകയോ ചെയ്യുക. സ്ഥിരംമായി, പേജ് ഷെയർ സവിശേഷത സജീവമാക്കപ്പെടുന്നു." "description": "പേജ് ഷെയർ സാങ്കേതികത സജ്ജീകരിക്കുക അല്ലെങ്കിൽ നിലവിളിക്കുക ."
}, },
"notification": { "notification": {
"pageShareSuccess": "പേജ് പങ്കിടാനുള്ള URL വിജയകരമായി അപ്ഡേറ്റ് ചെയ്തു", "pageShareSuccess": "പേജ് പങ്കിടാനുള്ള URL വിജയകരമായി അപ്ഡേറ്റ് ചെയ്തു",

View File

@ -195,7 +195,7 @@
"delete": "Вы уверены, что хотите удалить этот обмен? Это действие нельзя отменить." "delete": "Вы уверены, что хотите удалить этот обмен? Это действие нельзя отменить."
}, },
"label": "Управление общим доступом к странице", "label": "Управление общим доступом к странице",
"description": "Включите или отключите функцию общего доступа к странице. По умолчанию функция общего доступа к странице включена." "description": "Включить или отключить функцию обмена страницей"
}, },
"notification": { "notification": {
"pageShareSuccess": "URL обмена страницей успешно обновлен", "pageShareSuccess": "URL обмена страницей успешно обновлен",

View File

@ -199,7 +199,7 @@
"delete": "您确定要删除此对话共享吗?这个操作不能撤销。" "delete": "您确定要删除此对话共享吗?这个操作不能撤销。"
}, },
"label": "管理页面分享", "label": "管理页面分享",
"description": "启用或禁用页面分享功能。默认情况下,页面分享功能已启用。" "description": "启用或禁用页面分享功能 "
}, },
"notification": { "notification": {
"pageShareSuccess": "对话共享服务 URL 已成功更新", "pageShareSuccess": "对话共享服务 URL 已成功更新",

View File

@ -0,0 +1,90 @@
import { useQuery } from "@tanstack/react-query"
import { Dropdown, Empty, Tooltip } from "antd"
import { BookIcon, ComputerIcon, ZapIcon } from "lucide-react"
import React from "react"
import { useTranslation } from "react-i18next"
import { getAllPrompts } from "@/db"
import { useMessageOption } from "@/hooks/useMessageOption"
export const PromptSelect: React.FC = () => {
const { t } = useTranslation("option")
const {
selectedSystemPrompt,
setSelectedQuickPrompt,
setSelectedSystemPrompt
} = useMessageOption()
const { data } = useQuery({
queryKey: ["getAllPromptsForSelect"],
queryFn: getAllPrompts
})
const handlePromptChange = (value?: string) => {
if (!value) {
setSelectedSystemPrompt(undefined)
setSelectedQuickPrompt(undefined)
return
}
const prompt = data?.find((prompt) => prompt.id === value)
if (prompt?.is_system) {
setSelectedSystemPrompt(prompt.id)
} else {
setSelectedSystemPrompt(undefined)
setSelectedQuickPrompt(prompt!.content)
}
}
return (
<>
{data && (
<Dropdown
menu={{
items:
data.length > 0
? data?.map((prompt) => ({
key: prompt.id,
label: (
<div className="w-52 gap-2 text-lg truncate inline-flex line-clamp-3 items-center dark:border-gray-700">
<span
key={prompt.title}
className="flex flex-row gap-3 items-center">
{prompt.is_system ? (
<ComputerIcon className="w-4 h-4" />
) : (
<ZapIcon className="w-4 h-4" />
)}
{prompt.title}
</span>
</div>
),
onClick: () => {
if (selectedSystemPrompt === prompt.id) {
setSelectedSystemPrompt(undefined)
} else {
handlePromptChange(prompt.id)
}
}
}))
: [
{
key: "empty",
label: <Empty />
}
],
style: {
maxHeight: 500,
overflowY: "scroll"
},
className: "no-scrollbar",
activeKey: selectedSystemPrompt
}}
placement={"topLeft"}
trigger={["click"]}>
<Tooltip title={t("selectAPrompt")}>
<button type="button" className="dark:text-gray-300">
<BookIcon className="h-5 w-5" />
</button>
</Tooltip>
</Dropdown>
)}
</>
)
}

View File

@ -12,6 +12,7 @@ import {
ComputerIcon, ComputerIcon,
GithubIcon, GithubIcon,
PanelLeftIcon, PanelLeftIcon,
SlashIcon,
SquarePen, SquarePen,
ZapIcon ZapIcon
} from "lucide-react" } from "lucide-react"
@ -21,6 +22,8 @@ import { useTranslation } from "react-i18next"
import { OllamaIcon } from "../Icons/Ollama" import { OllamaIcon } from "../Icons/Ollama"
import { SelectedKnowledge } from "../Option/Knowledge/SelectedKnwledge" import { SelectedKnowledge } from "../Option/Knowledge/SelectedKnwledge"
import { useStorage } from "@plasmohq/storage/hook" import { useStorage } from "@plasmohq/storage/hook"
import { ModelSelect } from "../Common/ModelSelect"
import { PromptSelect } from "../Common/PromptSelect"
export default function OptionLayout({ export default function OptionLayout({
children children
@ -29,7 +32,7 @@ export default function OptionLayout({
}) { }) {
const [sidebarOpen, setSidebarOpen] = useState(false) const [sidebarOpen, setSidebarOpen] = useState(false)
const { t } = useTranslation(["option", "common"]) const { t } = useTranslation(["option", "common"])
const [shareModeEnabled] = useStorage("shareMode", true) const [shareModeEnabled] = useStorage("shareMode", false)
const { const {
selectedModel, selectedModel,
@ -89,7 +92,7 @@ export default function OptionLayout({
<NavLink <NavLink
to="/" to="/"
className="text-gray-500 items-center dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"> className="text-gray-500 items-center dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<ChevronLeft className="w-6 h-6" /> <ChevronLeft className="w-4 h-4" />
</NavLink> </NavLink>
</div> </div>
)} )}
@ -103,15 +106,17 @@ export default function OptionLayout({
<div> <div>
<button <button
onClick={clearChat} onClick={clearChat}
className="inline-flex dark:bg-transparent bg-white items-center rounded-lg border dark:border-gray-700 bg-transparent px-3 py-3 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-lg 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">
<SquarePen className="h-4 w-4 mr-3" /> <SquarePen className="h-5 w-5 " />
{t("newChat")} <span className=" truncate ml-3">
{t("newChat")}
</span>
</button> </button>
</div> </div>
<span className="text-lg font-thin text-zinc-300 dark:text-zinc-600"> <span className="text-lg font-thin text-zinc-300 dark:text-zinc-600">
{"/"} {"/"}
</span> </span>
<div> <div className="hidden lg:block">
<Select <Select
value={selectedModel} value={selectedModel}
onChange={(e) => { onChange={(e) => {
@ -141,10 +146,13 @@ export default function OptionLayout({
}))} }))}
/> />
</div> </div>
<div className="lg:hidden">
<ModelSelect />
</div>
<span className="text-lg font-thin text-zinc-300 dark:text-zinc-600"> <span className="text-lg font-thin text-zinc-300 dark:text-zinc-600">
{"/"} {"/"}
</span> </span>
<div> <div className="hidden lg:block">
<Select <Select
size="large" size="large"
loading={isPromptLoading} loading={isPromptLoading}
@ -177,6 +185,9 @@ export default function OptionLayout({
}))} }))}
/> />
</div> </div>
<div className="lg:hidden">
<PromptSelect />
</div>
<SelectedKnowledge /> <SelectedKnowledge />
</div> </div>
<div className="flex flex-1 justify-end px-4"> <div className="flex flex-1 justify-end px-4">
@ -190,7 +201,7 @@ export default function OptionLayout({
<a <a
href="https://github.com/n4ze3m/page-assist" href="https://github.com/n4ze3m/page-assist"
target="_blank" target="_blank"
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"> className="!text-gray-500 hidden lg:block dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<GithubIcon className="w-6 h-6" /> <GithubIcon className="w-6 h-6" />
</a> </a>
</Tooltip> </Tooltip>

View File

@ -1,5 +1,6 @@
import { Blocks, XIcon } from "lucide-react" import { Blocks, XIcon } from "lucide-react"
import { useMessageOption } from "@/hooks/useMessageOption" import { useMessageOption } from "@/hooks/useMessageOption"
import { Tooltip } from "antd"
export const SelectedKnowledge = () => { export const SelectedKnowledge = () => {
const { selectedKnowledge: knowledge, setSelectedKnowledge } = const { selectedKnowledge: knowledge, setSelectedKnowledge } =
@ -8,17 +9,21 @@ export const SelectedKnowledge = () => {
if (!knowledge) return <></> if (!knowledge) return <></>
return ( return (
<div className="flex flex-row items-center gap-3"> <div className="flex flex-row items-center gap-3">
<span className="text-lg font-thin text-zinc-300 dark:text-zinc-600"> <span className="text-lg font-thin text-zinc-300 dark:text-zinc-600">
{"/"} {"/"}
</span> </span>
<div className="border flex justify-between items-center rounded-full px-2 py-1 gap-2 bg-gray-100 dark:bg-slate-800 dark:border-slate-700"> <div className="border flex justify-between items-center rounded-full px-2 py-1 gap-2 bg-gray-100 dark:bg-slate-800 dark:border-slate-700">
<div className="inline-flex items-center gap-2"> <Tooltip
<Blocks className="h-5 w-5 text-gray-400" /> title={knowledge.title}
<span className="text-xs font-semibold dark:text-gray-100"> >
<div className="inline-flex truncate items-center gap-2">
<Blocks className="h-5 w-5 text-gray-400" />
<span className="text-xs hidden lg:inline-block font-semibold dark:text-gray-100">
{knowledge.title} {knowledge.title}
</span> </span>
</div> </div>
</Tooltip>
<div> <div>
<button <button
onClick={() => setSelectedKnowledge(null)} onClick={() => setSelectedKnowledge(null)}

View File

@ -91,7 +91,10 @@ export const ModelsBody = () => {
{status === "pending" && <Skeleton paragraph={{ rows: 8 }} />} {status === "pending" && <Skeleton paragraph={{ rows: 8 }} />}
{status === "success" && ( {status === "success" && (
<Table <div
className="overflow-x-auto"
>
<Table
columns={[ columns={[
{ {
title: t("manageModels.columns.name"), title: t("manageModels.columns.name"),
@ -207,6 +210,7 @@ export const ModelsBody = () => {
dataSource={data} dataSource={data}
rowKey={(record) => `${record.model}-${record.digest}`} rowKey={(record) => `${record.model}-${record.digest}`}
/> />
</div>
)} )}
</div> </div>

View File

@ -71,7 +71,11 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
} }
} }
} }
const handlePaste = (e: React.ClipboardEvent) => {
if (e.clipboardData.files.length > 0) {
onInputChange(e.clipboardData.files[0])
}
}
React.useEffect(() => { React.useEffect(() => {
if (dropedFile) { if (dropedFile) {
onInputChange(dropedFile) onInputChange(dropedFile)
@ -219,6 +223,7 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
ref={textareaRef} 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" 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"
required required
onPaste={handlePaste}
rows={1} rows={1}
style={{ minHeight: "60px" }} style={{ minHeight: "60px" }}
tabIndex={0} tabIndex={0}

View File

@ -116,7 +116,7 @@ export const SettingOther = () => {
if (confirm) { if (confirm) {
const db = new PageAssitDatabase() const db = new PageAssitDatabase()
await db.deleteChatHistory() await db.deleteAllChatHistory()
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["fetchChatHistory"] queryKey: ["fetchChatHistory"]
}) })

View File

@ -11,7 +11,10 @@ import { useStorage } from "@plasmohq/storage/hook"
export const OptionShareBody = () => { export const OptionShareBody = () => {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { t } = useTranslation(["settings"]) const { t } = useTranslation(["settings"])
const [shareModeEnabled, setShareModelEnabled] = useStorage("shareMode", true) const [shareModeEnabled, setShareModelEnabled] = useStorage(
"shareMode",
false
)
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ["fetchShareInfo"], queryKey: ["fetchShareInfo"],
@ -25,8 +28,12 @@ export const OptionShareBody = () => {
}) })
const onSubmit = async (values: { url: string }) => { const onSubmit = async (values: { url: string }) => {
const isOk = await verifyPageShareURL(values.url) if (shareModeEnabled) {
if (isOk) { const isOk = await verifyPageShareURL(values.url)
if (isOk) {
await setPageShareUrl(values.url)
}
} else {
await setPageShareUrl(values.url) await setPageShareUrl(values.url)
} }
} }

View File

@ -5,7 +5,8 @@ import { EmptySidePanel } from "../Chat/empty"
import { useWebUI } from "@/store/webui" import { useWebUI } from "@/store/webui"
export const SidePanelBody = () => { export const SidePanelBody = () => {
const { messages, streaming } = useMessage() const { messages, streaming, regenerateLastMessage, editMessage } =
useMessage()
const divRef = React.useRef<HTMLDivElement>(null) const divRef = React.useRef<HTMLDivElement>(null)
const { ttsEnabled } = useWebUI() const { ttsEnabled } = useWebUI()
React.useEffect(() => { React.useEffect(() => {
@ -18,7 +19,6 @@ export const SidePanelBody = () => {
{messages.length === 0 && <EmptySidePanel />} {messages.length === 0 && <EmptySidePanel />}
{messages.map((message, index) => ( {messages.map((message, index) => (
<PlaygroundMessage <PlaygroundMessage
onEditFormSubmit={(value) => {}}
key={index} key={index}
isBot={message.isBot} isBot={message.isBot}
message={message.message} message={message.message}
@ -26,13 +26,19 @@ export const SidePanelBody = () => {
images={message.images || []} images={message.images || []}
currentMessageIndex={index} currentMessageIndex={index}
totalMessages={messages.length} totalMessages={messages.length}
onRengerate={() => {}} onRengerate={regenerateLastMessage}
onEditFormSubmit={(value) => {
editMessage(index, value, !message.isBot)
}}
isProcessing={streaming} isProcessing={streaming}
hideEditAndRegenerate
isTTSEnabled={ttsEnabled} isTTSEnabled={ttsEnabled}
/> />
))} ))}
<div className="w-full h-32 md:h-48 flex-shrink-0"></div> {import.meta.env.BROWSER === "chrome" ? (
<div className="w-full h-32 md:h-48 flex-shrink-0"></div>
) : (
<div className="w-full h-48 flex-shrink-0"></div>
)}
<div ref={divRef} /> <div ref={divRef} />
</div> </div>
) )

View File

@ -23,11 +23,6 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
const [typing, setTyping] = React.useState<boolean>(false) const [typing, setTyping] = React.useState<boolean>(false)
const { t } = useTranslation(["playground", "common"]) const { t } = useTranslation(["playground", "common"])
const textAreaFocus = () => {
if (textareaRef.current) {
textareaRef.current.focus()
}
}
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
message: "", message: "",
@ -48,39 +43,11 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
} }
} }
} }
const textAreaFocus = () => {
React.useEffect(() => { if (textareaRef.current) {
if (dropedFile) { textareaRef.current.focus()
onInputChange(dropedFile)
} }
}, [dropedFile]) }
useDynamicTextareaSize(textareaRef, form.values.message, 120)
const {
onSubmit,
selectedModel,
chatMode,
speechToTextLanguage,
stopStreamingRequest
} = useMessage()
const { isListening, start, stop, transcript } = useSpeechRecognition()
React.useEffect(() => {
if (isListening) {
form.setFieldValue("message", transcript)
}
}, [transcript])
const { mutateAsync: sendMessage, isPending: isSending } = useMutation({
mutationFn: onSubmit,
onSuccess: () => {
textAreaFocus()
},
onError: (error) => {
textAreaFocus()
}
})
const handleKeyDown = (e: React.KeyboardEvent) => { const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Process" || e.key === "229") return if (e.key === "Process" || e.key === "229") return
if ( if (
@ -116,8 +83,47 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
} }
} }
const handlePaste = (e: React.ClipboardEvent) => {
if (e.clipboardData.files.length > 0) {
onInputChange(e.clipboardData.files[0])
}
}
const {
onSubmit,
selectedModel,
chatMode,
speechToTextLanguage,
stopStreamingRequest,
streaming
} = useMessage()
const { isListening, start, stop, transcript } = useSpeechRecognition()
React.useEffect(() => {
if (dropedFile) {
onInputChange(dropedFile)
}
}, [dropedFile])
useDynamicTextareaSize(textareaRef, form.values.message, 120)
React.useEffect(() => {
if (isListening) {
form.setFieldValue("message", transcript)
}
}, [transcript])
const { mutateAsync: sendMessage, isPending: isSending } = useMutation({
mutationFn: onSubmit,
onSuccess: () => {
textAreaFocus()
},
onError: (error) => {
textAreaFocus()
}
})
return ( return (
<div className="px-3 pt-3 md:px-6 md:pt-6 md:bg-white dark:bg-[#262626] border rounded-t-xl border-black/10 dark:border-gray-600"> <div className="px-3 pt-3 md:px-6 md:pt-6 bg-gray-50 dark:bg-[#262626] border rounded-t-xl border-black/10 dark:border-gray-600">
<div <div
className={`h-full rounded-md shadow relative ${ className={`h-full rounded-md shadow relative ${
form.values.image.length === 0 ? "hidden" : "block" form.values.image.length === 0 ? "hidden" : "block"
@ -178,6 +184,7 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
ref={textareaRef} 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" 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"
required required
onPaste={handlePaste}
rows={1} rows={1}
style={{ minHeight: "60px" }} style={{ minHeight: "60px" }}
tabIndex={0} tabIndex={0}
@ -224,7 +231,7 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
<ImageIcon className="h-5 w-5" /> <ImageIcon className="h-5 w-5" />
</button> </button>
</Tooltip> </Tooltip>
{!isSending ? ( {!streaming ? (
<Dropdown.Button <Dropdown.Button
htmlType="submit" htmlType="submit"
disabled={isSending} disabled={isSending}

View File

@ -14,15 +14,16 @@ import {
saveForRag saveForRag
} from "~/services/ollama" } from "~/services/ollama"
import { Skeleton, Radio, Select, Form, InputNumber } from "antd" import { Skeleton, Radio, Select, Form, InputNumber, Collapse } from "antd"
import { useDarkMode } from "~/hooks/useDarkmode" import { useDarkMode } from "~/hooks/useDarkmode"
import { SaveButton } from "~/components/Common/SaveButton" import { SaveButton } from "~/components/Common/SaveButton"
import { SUPPORTED_LANGUAGES } from "~/utils/supporetd-languages" import { SUPPORTED_LANGUAGES } from "~/utils/supporetd-languages"
import { useMessage } from "~/hooks/useMessage" import { useMessage } from "~/hooks/useMessage"
import { MoonIcon, SunIcon } from "lucide-react" import { MoonIcon, SunIcon } from "lucide-react"
import { useTranslation } from "react-i18next" import { Trans, useTranslation } from "react-i18next"
import { useI18n } from "@/hooks/useI18n" import { useI18n } from "@/hooks/useI18n"
import { TTSModeSettings } from "@/components/Option/Settings/tts-mode" import { TTSModeSettings } from "@/components/Option/Settings/tts-mode"
import { AdvanceOllamaSettings } from "@/components/Common/AdvanceOllamaSettings"
export const SettingsBody = () => { export const SettingsBody = () => {
const { t } = useTranslation("settings") const { t } = useTranslation("settings")
@ -180,8 +181,8 @@ export const SettingsBody = () => {
)} )}
</div> </div>
<div className="border border-gray-300 dark:border-gray-700 rounded p-4 bg-white dark:bg-[#171717]"> <div className="border flex flex-col gap-4 border-gray-300 dark:border-gray-700 rounded p-4 bg-white dark:bg-[#171717]">
<h2 className="text-md mb-4 font-semibold dark:text-white"> <h2 className="text-md font-semibold dark:text-white">
{t("ollamaSettings.heading")} {t("ollamaSettings.heading")}
</h2> </h2>
<input <input
@ -191,6 +192,37 @@ export const SettingsBody = () => {
onChange={(e) => setOllamaURL(e.target.value)} onChange={(e) => setOllamaURL(e.target.value)}
placeholder={t("ollamaSettings.settings.ollamaUrl.placeholder")} placeholder={t("ollamaSettings.settings.ollamaUrl.placeholder")}
/> />
<Collapse
size="small"
items={[
{
key: "1",
label: (
<div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
{t("ollamaSettings.settings.advanced.label")}
</h2>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
<Trans
i18nKey="settings:ollamaSettings.settings.advanced.help"
components={{
anchor: (
<a
href="https://github.com/n4ze3m/page-assist/blob/main/docs/connection-issue.md#solutions"
target="__blank"
className="text-blue-600 dark:text-blue-400"></a>
)
}}
/>
</p>
</div>
),
children: <AdvanceOllamaSettings />
}
]}
/>
<div className="flex justify-end"> <div className="flex justify-end">
<SaveButton <SaveButton
onClick={() => { onClick={() => {

View File

@ -89,7 +89,7 @@ export class PageAssitDatabase {
const history_id = message.history_id const history_id = message.history_id
const chatHistory = await this.getChatHistory(history_id) const chatHistory = await this.getChatHistory(history_id)
const newChatHistory = [message, ...chatHistory] const newChatHistory = [message, ...chatHistory]
this.db.set({ [history_id]: newChatHistory }) await this.db.set({ [history_id]: newChatHistory })
} }
async removeChatHistory(id: string) { async removeChatHistory(id: string) {
@ -112,12 +112,21 @@ export class PageAssitDatabase {
this.db.clear() this.db.clear()
} }
async deleteChatHistory() { async deleteChatHistory(id: string) {
const chatHistories = await this.getChatHistories() const chatHistories = await this.getChatHistories()
for (const history of chatHistories) { const newChatHistories = chatHistories.filter(
(history) => history.id !== id
)
this.db.set({ chatHistories: newChatHistories })
this.db.remove(id)
}
async deleteAllChatHistory() {
const chatHistories = await this.getChatHistories()
chatHistories.forEach((history) => {
this.db.remove(history.id) this.db.remove(history.id)
} })
this.db.remove("chatHistories") this.db.set({ chatHistories: [] })
} }
async deleteMessage(history_id: string) { async deleteMessage(history_id: string) {
@ -455,4 +464,4 @@ export const importPrompts = async (prompts: Prompts) => {
for (const prompt of prompts) { for (const prompt of prompts) {
await db.addPrompt(prompt) await db.addPrompt(prompt)
} }
} }

View File

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="manifest.type" content="browser_action" /> <meta name="manifest.type" content="browser_action" />
<meta name="manifest.open_at_install" content="false" /> <meta name="manifest.open_at_install" content="false" />
<meta name="manifest.browser_style" content="false" />
<link href="~/assets/tailwind.css" rel="stylesheet" /> <link href="~/assets/tailwind.css" rel="stylesheet" />
<meta charset="utf-8" /> <meta charset="utf-8" />
</head> </head>

View File

@ -11,11 +11,15 @@ import { useStoreMessage } from "~/store"
import { ChatOllama } from "@langchain/community/chat_models/ollama" import { ChatOllama } from "@langchain/community/chat_models/ollama"
import { HumanMessage, SystemMessage } from "@langchain/core/messages" import { HumanMessage, SystemMessage } from "@langchain/core/messages"
import { getDataFromCurrentTab } from "~/libs/get-html" import { getDataFromCurrentTab } from "~/libs/get-html"
import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama"
import { MemoryVectorStore } from "langchain/vectorstores/memory" import { MemoryVectorStore } from "langchain/vectorstores/memory"
import { memoryEmbedding } from "@/utils/memory-embeddings" import { memoryEmbedding } from "@/utils/memory-embeddings"
import { ChatHistory } from "@/store/option" import { ChatHistory } from "@/store/option"
import { generateID } from "@/db" import {
deleteChatForEdit,
generateID,
removeMessageUsingHistoryId,
updateMessageByIndex
} from "@/db"
import { saveMessageOnError, saveMessageOnSuccess } from "./chat-helper" import { saveMessageOnError, saveMessageOnSuccess } from "./chat-helper"
import { notification } from "antd" import { notification } from "antd"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
@ -139,8 +143,20 @@ export const useMessage = () => {
setCurrentURL(url) setCurrentURL(url)
isAlreadyExistEmbedding = keepTrackOfEmbedding[currentURL] isAlreadyExistEmbedding = keepTrackOfEmbedding[currentURL]
} else { } else {
isAlreadyExistEmbedding = keepTrackOfEmbedding[currentURL] const { content: html, url, type, pdf } = await getDataFromCurrentTab()
embedURL = currentURL if (currentURL !== url) {
embedHTML = html
embedURL = url
embedType = type
embedPDF = pdf
setCurrentURL(url)
} else {
embedHTML = html
embedURL = currentURL
embedType = type
embedPDF = pdf
}
isAlreadyExistEmbedding = keepTrackOfEmbedding[url]
} }
setMessages(newMessage) setMessages(newMessage)
@ -157,6 +173,7 @@ export const useMessage = () => {
try { try {
if (isAlreadyExistEmbedding) { if (isAlreadyExistEmbedding) {
vectorstore = isAlreadyExistEmbedding vectorstore = isAlreadyExistEmbedding
console.log("Embedding already exist")
} else { } else {
vectorstore = await memoryEmbedding({ vectorstore = await memoryEmbedding({
html: embedHTML, html: embedHTML,
@ -168,6 +185,8 @@ export const useMessage = () => {
type: embedType, type: embedType,
url: embedURL url: embedURL
}) })
console.log("Embedding created")
} }
let query = message let query = message
const { ragPrompt: systemPrompt, ragQuestionPrompt: questionPrompt } = const { ragPrompt: systemPrompt, ragQuestionPrompt: questionPrompt } =
@ -202,7 +221,7 @@ export const useMessage = () => {
url: "" url: ""
} }
}) })
// message = message.trim().replaceAll("\n", " ") // message = message.trim().replaceAll("\n", " ")
let humanMessage = new HumanMessage({ let humanMessage = new HumanMessage({
content: [ content: [
@ -478,8 +497,6 @@ export const useMessage = () => {
setIsProcessing(false) setIsProcessing(false)
setStreaming(false) setStreaming(false)
setIsProcessing(false)
setStreaming(false)
} catch (e) { } catch (e) {
const errorSave = await saveMessageOnError({ const errorSave = await saveMessageOnError({
e, e,
@ -509,17 +526,38 @@ export const useMessage = () => {
const onSubmit = async ({ const onSubmit = async ({
message, message,
image image,
isRegenerate,
controller,
memory,
messages: chatHistory
}: { }: {
message: string message: string
image: string image: string
isRegenerate?: boolean
messages?: Message[]
memory?: ChatHistory
controller?: AbortController
}) => { }) => {
const newController = new AbortController() let signal: AbortSignal
let signal = newController.signal if (!controller) {
setAbortController(newController) const newController = new AbortController()
signal = newController.signal
setAbortController(newController)
} else {
setAbortController(controller)
signal = controller.signal
}
if (chatMode === "normal") { if (chatMode === "normal") {
await normalChatMode(message, image, false, messages, history, signal) await normalChatMode(
message,
image,
isRegenerate,
chatHistory || messages,
memory || history,
signal
)
} else { } else {
const newEmbeddingController = new AbortController() const newEmbeddingController = new AbortController()
let embeddingSignal = newEmbeddingController.signal let embeddingSignal = newEmbeddingController.signal
@ -527,9 +565,9 @@ export const useMessage = () => {
await chatWithWebsiteMode( await chatWithWebsiteMode(
message, message,
image, image,
false, isRegenerate,
messages, chatHistory || messages,
history, memory || history,
signal, signal,
embeddingSignal embeddingSignal
) )
@ -548,9 +586,68 @@ export const useMessage = () => {
setAbortController(null) setAbortController(null)
} }
} }
const editMessage = async (
index: number,
message: string,
isHuman: boolean
) => {
let newMessages = messages
let newHistory = history
if (isHuman) {
const currentHumanMessage = newMessages[index]
newMessages[index].message = message
const previousMessages = newMessages.slice(0, index + 1)
setMessages(previousMessages)
const previousHistory = newHistory.slice(0, index)
setHistory(previousHistory)
await updateMessageByIndex(historyId, index, message)
await deleteChatForEdit(historyId, index)
const abortController = new AbortController()
await onSubmit({
message: message,
image: currentHumanMessage.images[0] || "",
isRegenerate: true,
messages: previousMessages,
memory: previousHistory,
controller: abortController
})
} else {
newMessages[index].message = message
setMessages(newMessages)
newHistory[index].content = message
setHistory(newHistory)
await updateMessageByIndex(historyId, index, message)
}
}
const regenerateLastMessage = async () => {
if (history.length > 0) {
const lastMessage = history[history.length - 2]
let newHistory = history.slice(0, -2)
let mewMessages = messages
mewMessages.pop()
setHistory(newHistory)
setMessages(mewMessages)
await removeMessageUsingHistoryId(historyId)
if (lastMessage.role === "user") {
const newController = new AbortController()
await onSubmit({
message: lastMessage.content,
image: lastMessage.image || "",
isRegenerate: true,
memory: newHistory,
controller: newController
})
}
}
}
return { return {
messages, messages,
setMessages, setMessages,
editMessage,
onSubmit, onSubmit,
setStreaming, setStreaming,
streaming, streaming,
@ -569,6 +666,7 @@ export const useMessage = () => {
setChatMode, setChatMode,
isEmbedding, isEmbedding,
speechToTextLanguage, speechToTextLanguage,
setSpeechToTextLanguage setSpeechToTextLanguage,
regenerateLastMessage
} }
} }

View File

@ -48,7 +48,7 @@ export default defineConfig({
outDir: "build", outDir: "build",
manifest: { manifest: {
version: "1.1.7", version: "1.1.8",
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"
@ -71,6 +71,7 @@ export default defineConfig({
: undefined, : undefined,
commands: { commands: {
_execute_action: { _execute_action: {
description: "Open the Web UI",
suggested_key: { suggested_key: {
default: "Ctrl+Shift+L" default: "Ctrl+Shift+L"
} }