commit
dfaffa0faa
21
README.md
21
README.md
@ -118,8 +118,9 @@ This will start a development server and watch for changes in the source files.
|
||||
| Chrome | ✅ | ✅ | ✅ |
|
||||
| Brave | ✅ | ✅ | ✅ |
|
||||
| Firefox | ✅ | ✅ | ✅ |
|
||||
| Vivaldi | ✅ | ✅ | ✅ |
|
||||
| Edge | ✅ | ❌ | ✅ |
|
||||
| Opera GX | ❌ | ❌ | ✅ |
|
||||
| Opera | ❌ | ❌ | ✅ |
|
||||
| Arc | ❌ | ❌ | ✅ |
|
||||
|
||||
## Local AI Provider
|
||||
@ -134,6 +135,13 @@ This will start a development server and watch for changes in the source files.
|
||||
- [ ] More Customization Options
|
||||
- [ ] 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
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
MIT
|
||||
|
@ -195,7 +195,7 @@
|
||||
"delete": "Are you sure you want to delete this share? This action cannot be undone."
|
||||
},
|
||||
"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": {
|
||||
"pageShareSuccess": "Page Share URL updated successfully",
|
||||
|
@ -198,7 +198,7 @@
|
||||
"delete": "本当にこの共有を削除しますか?この操作は元に戻せません。"
|
||||
},
|
||||
"label": "ページ共有を管理する",
|
||||
"description": "ページ共有機能を有効または無効にします。デフォルトでは、ページ共有機能は有効になっています。"
|
||||
"description": "ページ共有機能を有効または無効にする"
|
||||
},
|
||||
"notification": {
|
||||
"pageShareSuccess": "ページ共有URLが正常に更新されました",
|
||||
|
@ -198,7 +198,7 @@
|
||||
"delete": "ഈ പങ്കിടല് ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ? ഈ പ്രവർത്തനം പിന്നീട് പിൻവലിക്കാനാകില്ല."
|
||||
},
|
||||
"label": "പേജ് ഷെയർ നിയന്ത്രിക്കുക",
|
||||
"description": "പേജ് ഷെയർ സവിശേഷത സജീവമാക്കുകയോ അക്ഷമമാക്കുകയോ ചെയ്യുക. സ്ഥിരംമായി, പേജ് ഷെയർ സവിശേഷത സജീവമാക്കപ്പെടുന്നു."
|
||||
"description": "പേജ് ഷെയർ സാങ്കേതികത സജ്ജീകരിക്കുക അല്ലെങ്കിൽ നിലവിളിക്കുക ."
|
||||
},
|
||||
"notification": {
|
||||
"pageShareSuccess": "പേജ് പങ്കിടാനുള്ള URL വിജയകരമായി അപ്ഡേറ്റ് ചെയ്തു",
|
||||
|
@ -195,7 +195,7 @@
|
||||
"delete": "Вы уверены, что хотите удалить этот обмен? Это действие нельзя отменить."
|
||||
},
|
||||
"label": "Управление общим доступом к странице",
|
||||
"description": "Включите или отключите функцию общего доступа к странице. По умолчанию функция общего доступа к странице включена."
|
||||
"description": "Включить или отключить функцию обмена страницей"
|
||||
},
|
||||
"notification": {
|
||||
"pageShareSuccess": "URL обмена страницей успешно обновлен",
|
||||
|
@ -199,7 +199,7 @@
|
||||
"delete": "您确定要删除此对话共享吗?这个操作不能撤销。"
|
||||
},
|
||||
"label": "管理页面分享",
|
||||
"description": "启用或禁用页面分享功能。默认情况下,页面分享功能已启用。"
|
||||
"description": "启用或禁用页面分享功能 "
|
||||
},
|
||||
"notification": {
|
||||
"pageShareSuccess": "对话共享服务 URL 已成功更新",
|
||||
|
90
src/components/Common/PromptSelect.tsx
Normal file
90
src/components/Common/PromptSelect.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -12,6 +12,7 @@ import {
|
||||
ComputerIcon,
|
||||
GithubIcon,
|
||||
PanelLeftIcon,
|
||||
SlashIcon,
|
||||
SquarePen,
|
||||
ZapIcon
|
||||
} from "lucide-react"
|
||||
@ -21,6 +22,8 @@ import { useTranslation } from "react-i18next"
|
||||
import { OllamaIcon } from "../Icons/Ollama"
|
||||
import { SelectedKnowledge } from "../Option/Knowledge/SelectedKnwledge"
|
||||
import { useStorage } from "@plasmohq/storage/hook"
|
||||
import { ModelSelect } from "../Common/ModelSelect"
|
||||
import { PromptSelect } from "../Common/PromptSelect"
|
||||
|
||||
export default function OptionLayout({
|
||||
children
|
||||
@ -29,7 +32,7 @@ export default function OptionLayout({
|
||||
}) {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
const { t } = useTranslation(["option", "common"])
|
||||
const [shareModeEnabled] = useStorage("shareMode", true)
|
||||
const [shareModeEnabled] = useStorage("shareMode", false)
|
||||
|
||||
const {
|
||||
selectedModel,
|
||||
@ -89,7 +92,7 @@ export default function OptionLayout({
|
||||
<NavLink
|
||||
to="/"
|
||||
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>
|
||||
</div>
|
||||
)}
|
||||
@ -103,15 +106,17 @@ export default function OptionLayout({
|
||||
<div>
|
||||
<button
|
||||
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">
|
||||
<SquarePen className="h-4 w-4 mr-3" />
|
||||
{t("newChat")}
|
||||
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-5 w-5 " />
|
||||
<span className=" truncate ml-3">
|
||||
{t("newChat")}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-lg font-thin text-zinc-300 dark:text-zinc-600">
|
||||
{"/"}
|
||||
</span>
|
||||
<div>
|
||||
<div className="hidden lg:block">
|
||||
<Select
|
||||
value={selectedModel}
|
||||
onChange={(e) => {
|
||||
@ -141,10 +146,13 @@ export default function OptionLayout({
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div className="lg:hidden">
|
||||
<ModelSelect />
|
||||
</div>
|
||||
<span className="text-lg font-thin text-zinc-300 dark:text-zinc-600">
|
||||
{"/"}
|
||||
</span>
|
||||
<div>
|
||||
<div className="hidden lg:block">
|
||||
<Select
|
||||
size="large"
|
||||
loading={isPromptLoading}
|
||||
@ -177,6 +185,9 @@ export default function OptionLayout({
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div className="lg:hidden">
|
||||
<PromptSelect />
|
||||
</div>
|
||||
<SelectedKnowledge />
|
||||
</div>
|
||||
<div className="flex flex-1 justify-end px-4">
|
||||
@ -190,7 +201,7 @@ export default function OptionLayout({
|
||||
<a
|
||||
href="https://github.com/n4ze3m/page-assist"
|
||||
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" />
|
||||
</a>
|
||||
</Tooltip>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Blocks, XIcon } from "lucide-react"
|
||||
import { useMessageOption } from "@/hooks/useMessageOption"
|
||||
import { Tooltip } from "antd"
|
||||
|
||||
export const SelectedKnowledge = () => {
|
||||
const { selectedKnowledge: knowledge, setSelectedKnowledge } =
|
||||
@ -8,17 +9,21 @@ export const SelectedKnowledge = () => {
|
||||
if (!knowledge) 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>
|
||||
<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">
|
||||
<Blocks className="h-5 w-5 text-gray-400" />
|
||||
<span className="text-xs font-semibold dark:text-gray-100">
|
||||
<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">
|
||||
<Tooltip
|
||||
title={knowledge.title}
|
||||
>
|
||||
<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}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setSelectedKnowledge(null)}
|
||||
|
@ -91,7 +91,10 @@ export const ModelsBody = () => {
|
||||
{status === "pending" && <Skeleton paragraph={{ rows: 8 }} />}
|
||||
|
||||
{status === "success" && (
|
||||
<Table
|
||||
<div
|
||||
className="overflow-x-auto"
|
||||
>
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
title: t("manageModels.columns.name"),
|
||||
@ -207,6 +210,7 @@ export const ModelsBody = () => {
|
||||
dataSource={data}
|
||||
rowKey={(record) => `${record.model}-${record.digest}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
@ -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(() => {
|
||||
if (dropedFile) {
|
||||
onInputChange(dropedFile)
|
||||
@ -219,6 +223,7 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
|
||||
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"
|
||||
required
|
||||
onPaste={handlePaste}
|
||||
rows={1}
|
||||
style={{ minHeight: "60px" }}
|
||||
tabIndex={0}
|
||||
|
@ -116,7 +116,7 @@ export const SettingOther = () => {
|
||||
|
||||
if (confirm) {
|
||||
const db = new PageAssitDatabase()
|
||||
await db.deleteChatHistory()
|
||||
await db.deleteAllChatHistory()
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["fetchChatHistory"]
|
||||
})
|
||||
|
@ -11,7 +11,10 @@ import { useStorage } from "@plasmohq/storage/hook"
|
||||
export const OptionShareBody = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const { t } = useTranslation(["settings"])
|
||||
const [shareModeEnabled, setShareModelEnabled] = useStorage("shareMode", true)
|
||||
const [shareModeEnabled, setShareModelEnabled] = useStorage(
|
||||
"shareMode",
|
||||
false
|
||||
)
|
||||
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ["fetchShareInfo"],
|
||||
@ -25,8 +28,12 @@ export const OptionShareBody = () => {
|
||||
})
|
||||
|
||||
const onSubmit = async (values: { url: string }) => {
|
||||
const isOk = await verifyPageShareURL(values.url)
|
||||
if (isOk) {
|
||||
if (shareModeEnabled) {
|
||||
const isOk = await verifyPageShareURL(values.url)
|
||||
if (isOk) {
|
||||
await setPageShareUrl(values.url)
|
||||
}
|
||||
} else {
|
||||
await setPageShareUrl(values.url)
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,8 @@ import { EmptySidePanel } from "../Chat/empty"
|
||||
import { useWebUI } from "@/store/webui"
|
||||
|
||||
export const SidePanelBody = () => {
|
||||
const { messages, streaming } = useMessage()
|
||||
const { messages, streaming, regenerateLastMessage, editMessage } =
|
||||
useMessage()
|
||||
const divRef = React.useRef<HTMLDivElement>(null)
|
||||
const { ttsEnabled } = useWebUI()
|
||||
React.useEffect(() => {
|
||||
@ -18,7 +19,6 @@ export const SidePanelBody = () => {
|
||||
{messages.length === 0 && <EmptySidePanel />}
|
||||
{messages.map((message, index) => (
|
||||
<PlaygroundMessage
|
||||
onEditFormSubmit={(value) => {}}
|
||||
key={index}
|
||||
isBot={message.isBot}
|
||||
message={message.message}
|
||||
@ -26,13 +26,19 @@ export const SidePanelBody = () => {
|
||||
images={message.images || []}
|
||||
currentMessageIndex={index}
|
||||
totalMessages={messages.length}
|
||||
onRengerate={() => {}}
|
||||
onRengerate={regenerateLastMessage}
|
||||
onEditFormSubmit={(value) => {
|
||||
editMessage(index, value, !message.isBot)
|
||||
}}
|
||||
isProcessing={streaming}
|
||||
hideEditAndRegenerate
|
||||
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>
|
||||
)
|
||||
|
@ -23,11 +23,6 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
|
||||
const [typing, setTyping] = React.useState<boolean>(false)
|
||||
const { t } = useTranslation(["playground", "common"])
|
||||
|
||||
const textAreaFocus = () => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.focus()
|
||||
}
|
||||
}
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
message: "",
|
||||
@ -48,39 +43,11 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (dropedFile) {
|
||||
onInputChange(dropedFile)
|
||||
const textAreaFocus = () => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.focus()
|
||||
}
|
||||
}, [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) => {
|
||||
if (e.key === "Process" || e.key === "229") return
|
||||
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 (
|
||||
<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
|
||||
className={`h-full rounded-md shadow relative ${
|
||||
form.values.image.length === 0 ? "hidden" : "block"
|
||||
@ -178,6 +184,7 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
|
||||
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"
|
||||
required
|
||||
onPaste={handlePaste}
|
||||
rows={1}
|
||||
style={{ minHeight: "60px" }}
|
||||
tabIndex={0}
|
||||
@ -224,7 +231,7 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
|
||||
<ImageIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{!isSending ? (
|
||||
{!streaming ? (
|
||||
<Dropdown.Button
|
||||
htmlType="submit"
|
||||
disabled={isSending}
|
||||
|
@ -14,15 +14,16 @@ import {
|
||||
saveForRag
|
||||
} 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 { SaveButton } from "~/components/Common/SaveButton"
|
||||
import { SUPPORTED_LANGUAGES } from "~/utils/supporetd-languages"
|
||||
import { useMessage } from "~/hooks/useMessage"
|
||||
import { MoonIcon, SunIcon } from "lucide-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
import { useI18n } from "@/hooks/useI18n"
|
||||
import { TTSModeSettings } from "@/components/Option/Settings/tts-mode"
|
||||
import { AdvanceOllamaSettings } from "@/components/Common/AdvanceOllamaSettings"
|
||||
|
||||
export const SettingsBody = () => {
|
||||
const { t } = useTranslation("settings")
|
||||
@ -180,8 +181,8 @@ export const SettingsBody = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border 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">
|
||||
<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 font-semibold dark:text-white">
|
||||
{t("ollamaSettings.heading")}
|
||||
</h2>
|
||||
<input
|
||||
@ -191,6 +192,37 @@ export const SettingsBody = () => {
|
||||
onChange={(e) => setOllamaURL(e.target.value)}
|
||||
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">
|
||||
<SaveButton
|
||||
onClick={() => {
|
||||
|
@ -89,7 +89,7 @@ export class PageAssitDatabase {
|
||||
const history_id = message.history_id
|
||||
const chatHistory = await this.getChatHistory(history_id)
|
||||
const newChatHistory = [message, ...chatHistory]
|
||||
this.db.set({ [history_id]: newChatHistory })
|
||||
await this.db.set({ [history_id]: newChatHistory })
|
||||
}
|
||||
|
||||
async removeChatHistory(id: string) {
|
||||
@ -112,12 +112,21 @@ export class PageAssitDatabase {
|
||||
this.db.clear()
|
||||
}
|
||||
|
||||
async deleteChatHistory() {
|
||||
async deleteChatHistory(id: string) {
|
||||
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("chatHistories")
|
||||
})
|
||||
this.db.set({ chatHistories: [] })
|
||||
}
|
||||
|
||||
async deleteMessage(history_id: string) {
|
||||
|
@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="manifest.type" content="browser_action" />
|
||||
<meta name="manifest.open_at_install" content="false" />
|
||||
<meta name="manifest.browser_style" content="false" />
|
||||
<link href="~/assets/tailwind.css" rel="stylesheet" />
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
@ -11,11 +11,15 @@ import { useStoreMessage } from "~/store"
|
||||
import { ChatOllama } from "@langchain/community/chat_models/ollama"
|
||||
import { HumanMessage, SystemMessage } from "@langchain/core/messages"
|
||||
import { getDataFromCurrentTab } from "~/libs/get-html"
|
||||
import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama"
|
||||
import { MemoryVectorStore } from "langchain/vectorstores/memory"
|
||||
import { memoryEmbedding } from "@/utils/memory-embeddings"
|
||||
import { ChatHistory } from "@/store/option"
|
||||
import { generateID } from "@/db"
|
||||
import {
|
||||
deleteChatForEdit,
|
||||
generateID,
|
||||
removeMessageUsingHistoryId,
|
||||
updateMessageByIndex
|
||||
} from "@/db"
|
||||
import { saveMessageOnError, saveMessageOnSuccess } from "./chat-helper"
|
||||
import { notification } from "antd"
|
||||
import { useTranslation } from "react-i18next"
|
||||
@ -139,8 +143,20 @@ export const useMessage = () => {
|
||||
setCurrentURL(url)
|
||||
isAlreadyExistEmbedding = keepTrackOfEmbedding[currentURL]
|
||||
} else {
|
||||
isAlreadyExistEmbedding = keepTrackOfEmbedding[currentURL]
|
||||
embedURL = currentURL
|
||||
const { content: html, url, type, pdf } = await getDataFromCurrentTab()
|
||||
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)
|
||||
@ -157,6 +173,7 @@ export const useMessage = () => {
|
||||
try {
|
||||
if (isAlreadyExistEmbedding) {
|
||||
vectorstore = isAlreadyExistEmbedding
|
||||
console.log("Embedding already exist")
|
||||
} else {
|
||||
vectorstore = await memoryEmbedding({
|
||||
html: embedHTML,
|
||||
@ -168,6 +185,8 @@ export const useMessage = () => {
|
||||
type: embedType,
|
||||
url: embedURL
|
||||
})
|
||||
|
||||
console.log("Embedding created")
|
||||
}
|
||||
let query = message
|
||||
const { ragPrompt: systemPrompt, ragQuestionPrompt: questionPrompt } =
|
||||
@ -202,7 +221,7 @@ export const useMessage = () => {
|
||||
url: ""
|
||||
}
|
||||
})
|
||||
// message = message.trim().replaceAll("\n", " ")
|
||||
// message = message.trim().replaceAll("\n", " ")
|
||||
|
||||
let humanMessage = new HumanMessage({
|
||||
content: [
|
||||
@ -478,8 +497,6 @@ export const useMessage = () => {
|
||||
|
||||
setIsProcessing(false)
|
||||
setStreaming(false)
|
||||
setIsProcessing(false)
|
||||
setStreaming(false)
|
||||
} catch (e) {
|
||||
const errorSave = await saveMessageOnError({
|
||||
e,
|
||||
@ -509,17 +526,38 @@ export const useMessage = () => {
|
||||
|
||||
const onSubmit = async ({
|
||||
message,
|
||||
image
|
||||
image,
|
||||
isRegenerate,
|
||||
controller,
|
||||
memory,
|
||||
messages: chatHistory
|
||||
}: {
|
||||
message: string
|
||||
image: string
|
||||
isRegenerate?: boolean
|
||||
messages?: Message[]
|
||||
memory?: ChatHistory
|
||||
controller?: AbortController
|
||||
}) => {
|
||||
const newController = new AbortController()
|
||||
let signal = newController.signal
|
||||
setAbortController(newController)
|
||||
let signal: AbortSignal
|
||||
if (!controller) {
|
||||
const newController = new AbortController()
|
||||
signal = newController.signal
|
||||
setAbortController(newController)
|
||||
} else {
|
||||
setAbortController(controller)
|
||||
signal = controller.signal
|
||||
}
|
||||
|
||||
if (chatMode === "normal") {
|
||||
await normalChatMode(message, image, false, messages, history, signal)
|
||||
await normalChatMode(
|
||||
message,
|
||||
image,
|
||||
isRegenerate,
|
||||
chatHistory || messages,
|
||||
memory || history,
|
||||
signal
|
||||
)
|
||||
} else {
|
||||
const newEmbeddingController = new AbortController()
|
||||
let embeddingSignal = newEmbeddingController.signal
|
||||
@ -527,9 +565,9 @@ export const useMessage = () => {
|
||||
await chatWithWebsiteMode(
|
||||
message,
|
||||
image,
|
||||
false,
|
||||
messages,
|
||||
history,
|
||||
isRegenerate,
|
||||
chatHistory || messages,
|
||||
memory || history,
|
||||
signal,
|
||||
embeddingSignal
|
||||
)
|
||||
@ -548,9 +586,68 @@ export const useMessage = () => {
|
||||
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 {
|
||||
messages,
|
||||
setMessages,
|
||||
editMessage,
|
||||
onSubmit,
|
||||
setStreaming,
|
||||
streaming,
|
||||
@ -569,6 +666,7 @@ export const useMessage = () => {
|
||||
setChatMode,
|
||||
isEmbedding,
|
||||
speechToTextLanguage,
|
||||
setSpeechToTextLanguage
|
||||
setSpeechToTextLanguage,
|
||||
regenerateLastMessage
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ export default defineConfig({
|
||||
outDir: "build",
|
||||
|
||||
manifest: {
|
||||
version: "1.1.7",
|
||||
version: "1.1.8",
|
||||
name:
|
||||
process.env.TARGET === "firefox"
|
||||
? "Page Assist - A Web UI for Local AI Models"
|
||||
@ -71,6 +71,7 @@ export default defineConfig({
|
||||
: undefined,
|
||||
commands: {
|
||||
_execute_action: {
|
||||
description: "Open the Web UI",
|
||||
suggested_key: {
|
||||
default: "Ctrl+Shift+L"
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user