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 | ✅ | ✅ | ✅ |
| 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

View File

@ -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",

View File

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

View File

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

View File

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

View File

@ -199,7 +199,7 @@
"delete": "您确定要删除此对话共享吗?这个操作不能撤销。"
},
"label": "管理页面分享",
"description": "启用或禁用页面分享功能。默认情况下,页面分享功能已启用。"
"description": "启用或禁用页面分享功能 "
},
"notification": {
"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,
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" />
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>

View File

@ -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 } =
@ -13,12 +14,16 @@ export const SelectedKnowledge = () => {
{"/"}
</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">
<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 font-semibold dark:text-gray-100">
<span className="text-xs hidden lg:inline-block font-semibold dark:text-gray-100">
{knowledge.title}
</span>
</div>
</Tooltip>
<div>
<button
onClick={() => setSelectedKnowledge(null)}

View File

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

View File

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

View File

@ -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,10 +28,14 @@ export const OptionShareBody = () => {
})
const onSubmit = async (values: { url: string }) => {
if (shareModeEnabled) {
const isOk = await verifyPageShareURL(values.url)
if (isOk) {
await setPageShareUrl(values.url)
}
} else {
await setPageShareUrl(values.url)
}
}
const onDelete = async ({

View File

@ -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}
/>
))}
{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>
)

View File

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

View File

@ -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={() => {

View File

@ -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) {
this.db.remove(history.id)
const newChatHistories = chatHistories.filter(
(history) => history.id !== id
)
this.db.set({ chatHistories: newChatHistories })
this.db.remove(id)
}
this.db.remove("chatHistories")
async deleteAllChatHistory() {
const chatHistories = await this.getChatHistories()
chatHistories.forEach((history) => {
this.db.remove(history.id)
})
this.db.set({ chatHistories: [] })
}
async deleteMessage(history_id: string) {

View File

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

View File

@ -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]
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 } =
@ -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
}) => {
let signal: AbortSignal
if (!controller) {
const newController = new AbortController()
let signal = newController.signal
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
}
}

View File

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