feat: copilot context menu for tool

This commit is contained in:
n4ze3m 2024-08-03 23:00:57 +05:30
parent 9c7ebc8778
commit ac9c9ca887
14 changed files with 696 additions and 132 deletions

View File

@ -1,6 +1,6 @@
import Markdown from "../../Common/Markdown"
import React from "react"
import { Image, Tooltip } from "antd"
import { Tag, Image, Tooltip } from "antd"
import { WebSearch } from "./WebSearch"
import {
CheckIcon,
@ -17,6 +17,7 @@ import { useTTS } from "@/hooks/useTTS"
type Props = {
message: string
message_type?: string
hideCopy?: boolean
botAvatar?: JSX.Element
userAvatar?: JSX.Element
@ -76,13 +77,21 @@ export const PlaygroundMessage = (props: Props) => {
props.currentMessageIndex === props.totalMessages - 1 ? (
<WebSearch />
) : null}
<div>
{props?.message_type && (
<Tag color="blue">{props?.message_type}</Tag>
)}
</div>
<div className="flex flex-grow flex-col">
{!editMode ? (
props.isBot ? (
<Markdown message={props.message} />
) : (
<p className="prose dark:prose-invert whitespace-pre-line prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark">
<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-xs"
}`}>
{props.message}
</p>
)

View File

@ -35,6 +35,7 @@ export const SidePanelBody = () => {
currentMessageIndex={index}
totalMessages={messages.length}
onRengerate={regenerateLastMessage}
message_type={message.messageType}
isProcessing={streaming}
isSearchingInternet={isSearchingInternet}
sources={message.sources}

View File

@ -31,6 +31,7 @@ type Message = {
sources?: string[]
search?: WebSearch
createdAt: number
messageType?: string
}
type Webshare = {
@ -241,7 +242,8 @@ export const saveMessage = async (
content: string,
images: string[],
source?: any[],
time?: number
time?: number,
message_type?: string
) => {
const id = generateID()
let createdAt = Date.now()
@ -256,7 +258,8 @@ export const saveMessage = async (
content,
images,
createdAt,
sources: source
sources: source,
messageType: message_type
}
const db = new PageAssitDatabase()
await db.addMessage(message)

View File

@ -1,84 +1,10 @@
import { getOllamaURL, isOllamaRunning } from "../services/ollama"
import { browser } from "wxt/browser"
import { setBadgeBackgroundColor, setBadgeText, setTitle } from "@/utils/action"
const progressHuman = (completed: number, total: number) => {
return ((completed / total) * 100).toFixed(0) + "%"
}
const clearBadge = () => {
setBadgeText({ text: "" })
setTitle({ title: "" })
}
const streamDownload = async (url: string, model: string) => {
url += "/api/pull"
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ model, stream: true })
})
const reader = response.body?.getReader()
const decoder = new TextDecoder()
let isSuccess = true
while (true) {
if (!reader) {
break
}
const { done, value } = await reader.read()
if (done) {
break
}
const text = decoder.decode(value)
try {
const json = JSON.parse(text.trim()) as {
status: string
total?: number
completed?: number
}
if (json.total && json.completed) {
setBadgeText({
text: progressHuman(json.completed, json.total)
})
setBadgeBackgroundColor({ color: "#0000FF" })
} else {
setBadgeText({ text: "🏋️‍♂️" })
setBadgeBackgroundColor({ color: "#FFFFFF" })
}
setTitle({ title: json.status })
if (json.status === "success") {
isSuccess = true
}
} catch (e) {
console.error(e)
}
}
if (isSuccess) {
setBadgeText({ text: "✅" })
setBadgeBackgroundColor({ color: "#00FF00" })
setTitle({ title: "Model pulled successfully" })
} else {
setBadgeText({ text: "❌" })
setBadgeBackgroundColor({ color: "#FF0000" })
setTitle({ title: "Model pull failed" })
}
setTimeout(() => {
clearBadge()
}, 5000)
}
import { clearBadge, streamDownload } from "@/utils/pull-ollama"
export default defineBackground({
main() {
let isCopilotRunning: boolean = false
browser.runtime.onMessage.addListener(async (message) => {
if (message.type === "sidepanel") {
await browser.sidebarAction.open()
@ -100,6 +26,15 @@ export default defineBackground({
}
})
browser.runtime.onConnect.addListener((port) => {
if (port.name === "pgCopilot") {
isCopilotRunning = true
port.onDisconnect.addListener(() => {
isCopilotRunning = false
})
}
})
if (import.meta.env.BROWSER === "chrome") {
chrome.action.onClicked.addListener((tab) => {
chrome.tabs.create({ url: chrome.runtime.getURL("/options.html") })
@ -124,10 +59,41 @@ export default defineBackground({
browser.contextMenus.create({
id: contextMenuId["sidePanel"],
title: contextMenuTitle["sidePanel"],
contexts: ["all"]
contexts: ["page", "selection"]
})
browser.contextMenus.create({
id: "summarize-pa",
title: "Summarize",
contexts: ["selection"]
})
browser.contextMenus.create({
id: "explain-pa",
title: "Explain",
contexts: ["selection"]
})
browser.contextMenus.create({
id: "rephrase-pa",
title: "Rephrase",
contexts: ["selection"]
})
browser.contextMenus.create({
id: "translate-pg",
title: "Translate",
contexts: ["selection"]
})
// browser.contextMenus.create({
// id: "custom-pg",
// title: "Custom",
// contexts: ["selection"]
// })
if (import.meta.env.BROWSER === "chrome") {
browser.contextMenus.onClicked.addListener((info, tab) => {
browser.contextMenus.onClicked.addListener(async (info, tab) => {
if (info.menuItemId === "open-side-panel-pa") {
chrome.sidePanel.open({
tabId: tab.id!
@ -136,6 +102,68 @@ export default defineBackground({
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
})
}, isCopilotRunning ? 0 : 5000)
} 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
})
}, isCopilotRunning ? 0 : 5000)
} 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
})
}, isCopilotRunning ? 0 : 5000)
} else if (info.menuItemId === "explain-pa") {
chrome.sidePanel.open({
tabId: tab.id!
})
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)
}
})

View File

@ -13,7 +13,8 @@ export const saveMessageOnError = async ({
selectedModel,
setHistoryId,
isRegenerating,
message_source = "web-ui"
message_source = "web-ui",
message_type
}: {
e: any
setHistory: (history: ChatHistory) => void
@ -26,6 +27,7 @@ export const saveMessageOnError = async ({
setHistoryId: (historyId: string) => void
isRegenerating: boolean
message_source?: "copilot" | "web-ui"
message_type?: string
}) => {
if (
e?.name === "AbortError" ||
@ -55,7 +57,8 @@ export const saveMessageOnError = async ({
userMessage,
[image],
[],
1
1,
message_type
)
}
await saveMessage(
@ -65,7 +68,8 @@ export const saveMessageOnError = async ({
botMessage,
[],
[],
2
2,
message_type
)
await setLastUsedChatModel(historyId, selectedModel)
} else {
@ -78,7 +82,8 @@ export const saveMessageOnError = async ({
userMessage,
[image],
[],
1
1,
message_type
)
}
await saveMessage(
@ -88,7 +93,8 @@ export const saveMessageOnError = async ({
botMessage,
[],
[],
2
2,
message_type
)
setHistoryId(newHistoryId.id)
await setLastUsedChatModel(newHistoryId.id, selectedModel)
@ -109,7 +115,8 @@ export const saveMessageOnSuccess = async ({
image,
fullText,
source,
message_source = "web-ui"
message_source = "web-ui",
message_type
}: {
historyId: string | null
setHistoryId: (historyId: string) => void
@ -119,7 +126,8 @@ export const saveMessageOnSuccess = async ({
image: string
fullText: string
source: any[]
message_source?: "copilot" | "web-ui"
message_source?: "copilot" | "web-ui",
message_type?: string
}) => {
if (historyId) {
if (!isRegenerate) {
@ -130,7 +138,8 @@ export const saveMessageOnSuccess = async ({
message,
[image],
[],
1
1,
message_type
)
}
await saveMessage(
@ -140,7 +149,8 @@ export const saveMessageOnSuccess = async ({
fullText,
[],
source,
2
2,
message_type
)
await setLastUsedChatModel(historyId, selectedModel!)
} else {
@ -152,7 +162,8 @@ export const saveMessageOnSuccess = async ({
message,
[image],
[],
1
1,
message_type
)
await saveMessage(
newHistoryId.id,
@ -161,7 +172,8 @@ export const saveMessageOnSuccess = async ({
fullText,
[],
source,
2
2,
message_type
)
setHistoryId(newHistoryId.id)
await setLastUsedChatModel(newHistoryId.id, selectedModel!)

View File

@ -0,0 +1,29 @@
import { useState, useEffect } from "react"
interface Message {
from: string
type: string
text: string
}
function useBackgroundMessage() {
const [message, setMessage] = useState<Message | null>(null)
useEffect(() => {
const messageListener = (request: Message) => {
if (request.from === "background") {
setMessage(request)
}
}
browser.runtime.connect({ name: 'pgCopilot' })
browser.runtime.onMessage.addListener(messageListener)
return () => {
browser.runtime.onMessage.removeListener(messageListener)
}
}, [])
return message
}
export default useBackgroundMessage

View File

@ -31,6 +31,7 @@ import { useStoreChatModelSettings } from "@/store/model"
import { getAllDefaultModelSettings } from "@/services/model-settings"
import { getSystemPromptForWeb } from "@/web/web"
import { pageAssistModel } from "@/models"
import { getPrompt } from "@/services/application"
export const useMessage = () => {
const {
@ -51,8 +52,10 @@ export const useMessage = () => {
isSearchingInternet
} = useStoreMessageOption()
const [chatWithWebsiteEmbedding] = useStorage("chatWithWebsiteEmbedding", true)
const [chatWithWebsiteEmbedding] = useStorage(
"chatWithWebsiteEmbedding",
true
)
const [maxWebsiteContext] = useStorage("maxWebsiteContext", 4028)
const {
@ -857,13 +860,206 @@ export const useMessage = () => {
}
}
const presetChatMode = async (
message: string,
image: string,
isRegenerate: boolean,
messages: Message[],
history: ChatHistory,
signal: AbortSignal,
messageType: string
) => {
setStreaming(true)
const url = await getOllamaURL()
const userDefaultModelSettings = await getAllDefaultModelSettings()
if (image.length > 0) {
image = `data:image/jpeg;base64,${image.split(",")[1]}`
}
const ollama = await pageAssistModel({
model: selectedModel!,
baseUrl: cleanUrl(url),
keepAlive:
currentChatModelSettings?.keepAlive ??
userDefaultModelSettings?.keepAlive,
temperature:
currentChatModelSettings?.temperature ??
userDefaultModelSettings?.temperature,
topK: currentChatModelSettings?.topK ?? userDefaultModelSettings?.topK,
topP: currentChatModelSettings?.topP ?? userDefaultModelSettings?.topP,
numCtx:
currentChatModelSettings?.numCtx ?? userDefaultModelSettings?.numCtx,
seed: currentChatModelSettings?.seed
})
let newMessage: Message[] = []
let generateMessageId = generateID()
if (!isRegenerate) {
newMessage = [
...messages,
{
isBot: false,
name: "You",
message,
sources: [],
images: [image],
messageType: messageType
},
{
isBot: true,
name: selectedModel,
message: "▋",
sources: [],
id: generateMessageId
}
]
} else {
newMessage = [
...messages,
{
isBot: true,
name: selectedModel,
message: "▋",
sources: [],
id: generateMessageId
}
]
}
setMessages(newMessage)
let fullText = ""
let contentToSave = ""
try {
const prompt = await getPrompt(messageType)
let humanMessage = new HumanMessage({
content: [
{
text: prompt.replace("{text}", message),
type: "text"
}
]
})
if (image.length > 0) {
humanMessage = new HumanMessage({
content: [
{
text: prompt.replace("{text}", message),
type: "text"
},
{
image_url: image,
type: "image_url"
}
]
})
}
const chunks = await ollama.stream([humanMessage], {
signal: signal
})
let count = 0
for await (const chunk of chunks) {
contentToSave += chunk.content
fullText += chunk.content
if (count === 0) {
setIsProcessing(true)
}
setMessages((prev) => {
return prev.map((message) => {
if (message.id === generateMessageId) {
return {
...message,
message: fullText + "▋"
}
}
return message
})
})
count++
}
setMessages((prev) => {
return prev.map((message) => {
if (message.id === generateMessageId) {
return {
...message,
message: fullText
}
}
return message
})
})
setHistory([
...history,
{
role: "user",
content: message,
image,
messageType
},
{
role: "assistant",
content: fullText
}
])
await saveMessageOnSuccess({
historyId,
setHistoryId,
isRegenerate,
selectedModel: selectedModel,
message,
image,
fullText,
source: [],
message_source: "copilot",
message_type: messageType
})
setIsProcessing(false)
setStreaming(false)
} catch (e) {
const errorSave = await saveMessageOnError({
e,
botMessage: fullText,
history,
historyId,
image,
selectedModel,
setHistory,
setHistoryId,
userMessage: message,
isRegenerating: isRegenerate,
message_source: "copilot",
message_type: messageType
})
if (!errorSave) {
notification.error({
message: t("error"),
description: e?.message || t("somethingWentWrong")
})
}
setIsProcessing(false)
setStreaming(false)
} finally {
setAbortController(null)
}
}
const onSubmit = async ({
message,
image,
isRegenerate,
controller,
memory,
messages: chatHistory
messages: chatHistory,
messageType
}: {
message: string
image: string
@ -871,6 +1067,7 @@ export const useMessage = () => {
messages?: Message[]
memory?: ChatHistory
controller?: AbortController
messageType?: string
}) => {
let signal: AbortSignal
if (!controller) {
@ -882,39 +1079,52 @@ export const useMessage = () => {
signal = controller.signal
}
if (chatMode === "normal") {
if (webSearch) {
await searchChatMode(
message,
image,
isRegenerate || false,
messages,
memory || history,
signal
)
} else {
await normalChatMode(
message,
image,
isRegenerate,
chatHistory || messages,
memory || history,
signal
)
}
} else {
const newEmbeddingController = new AbortController()
let embeddingSignal = newEmbeddingController.signal
setEmbeddingController(newEmbeddingController)
await chatWithWebsiteMode(
// this means that the user is trying to send something from a selected text on the web
if (messageType) {
await presetChatMode(
message,
image,
isRegenerate,
chatHistory || messages,
memory || history,
signal,
embeddingSignal
messageType
)
} else {
if (chatMode === "normal") {
if (webSearch) {
await searchChatMode(
message,
image,
isRegenerate || false,
messages,
memory || history,
signal
)
} else {
await normalChatMode(
message,
image,
isRegenerate,
chatHistory || messages,
memory || history,
signal
)
}
} else {
const newEmbeddingController = new AbortController()
let embeddingSignal = newEmbeddingController.signal
setEmbeddingController(newEmbeddingController)
await chatWithWebsiteMode(
message,
image,
isRegenerate,
chatHistory || messages,
memory || history,
signal,
embeddingSignal
)
}
}
}
@ -982,7 +1192,8 @@ export const useMessage = () => {
image: lastMessage.image || "",
isRegenerate: true,
memory: newHistory,
controller: newController
controller: newController,
messageType: lastMessage.messageType
})
}
}

View File

@ -3,8 +3,11 @@ import {
formatToMessage,
getRecentChatFromCopilot
} from "@/db"
import useBackgroundMessage from "@/hooks/useBackgroundMessage"
import { copilotResumeLastChat } from "@/services/app"
import { notification } from "antd"
import React from "react"
import { useTranslation } from "react-i18next"
import { SidePanelBody } from "~/components/Sidepanel/Chat/body"
import { SidepanelForm } from "~/components/Sidepanel/Chat/form"
import { SidepanelHeader } from "~/components/Sidepanel/Chat/header"
@ -13,17 +16,27 @@ import { useMessage } from "~/hooks/useMessage"
const SidepanelChat = () => {
const drop = React.useRef<HTMLDivElement>(null)
const [dropedFile, setDropedFile] = React.useState<File | undefined>()
const { t } = useTranslation(["playground"])
const [dropState, setDropState] = React.useState<
"idle" | "dragging" | "error"
>("idle")
const { chatMode, messages, setHistory, setHistoryId, setMessages } =
useMessage()
const {
chatMode,
streaming,
onSubmit,
messages,
setHistory,
setHistoryId,
setMessages,
selectedModel
} = useMessage()
const bgMsg = useBackgroundMessage()
const setRecentMessagesOnLoad = async () => {
const isEnabled = await copilotResumeLastChat();
const isEnabled = await copilotResumeLastChat()
if (!isEnabled) {
return;
return
}
if (messages.length === 0) {
const recentChat = await getRecentChatFromCopilot()
@ -92,11 +105,26 @@ const SidepanelChat = () => {
}
}, [])
React.useEffect(() => {
setRecentMessagesOnLoad()
}, [])
React.useEffect(() => {
if (bgMsg && !streaming) {
if (selectedModel) {
onSubmit({
message: bgMsg.text,
messageType: bgMsg.type,
image: ""
})
} else {
notification.error({
message: t("formError.noModel")
})
}
}
}, [bgMsg])
return (
<div
ref={drop}

162
src/services/application.ts Normal file
View File

@ -0,0 +1,162 @@
import { Storage } from "@plasmohq/storage"
const storage = new Storage()
const DEFAULT_SUMMARY_PROMPT = `Provide a concise summary of the following text, capturing its main ideas and key points:
Text:
---------
{text}
---------
Summarize the text in no more than 3-4 sentences.
Response:`
const DEFAULT_REPHRASE_PROMPT = `Rewrite the following text in a different way, maintaining its original meaning but using alternative vocabulary and sentence structures:
Text:
---------
{text}
---------
Ensure that your rephrased version conveys the same information and intent as the original.
Response:`
const DEFAULT_TRANSLATE_PROMPT = `Translate the following text from its original language into "english"0. Maintain the tone and style of the original text as much as possible:
Text:
---------
{text}
---------
Response:`
const DEFAULT_EXPLAIN_PROMPT = `Provide a detailed explanation of the following text, breaking down its key concepts, implications, and context:
Text:
---------
{text}
---------
Your explanation should:
Clarify any complex terms or ideas
Provide relevant background information
Discuss the significance or implications of the content
Address any potential questions a reader might have
Use examples or analogies to illustrate points when appropriate
Aim for a comprehensive explanation that would help someone with little prior knowledge fully understand the text.
Response:`
const DEFAULT_CUSTOM_PROMPT = `{text}`
export const getSummaryPrompt = async () => {
return (await storage.get("copilotSummaryPrompt")) || DEFAULT_SUMMARY_PROMPT
}
export const setSummaryPrompt = async (prompt: string) => {
await storage.set("copilotSummaryPrompt", prompt)
}
export const getRephrasePrompt = async () => {
return (await storage.get("copilotRephrasePrompt")) || DEFAULT_REPHRASE_PROMPT
}
export const setRephrasePrompt = async (prompt: string) => {
await storage.set("copilotRephrasePrompt", prompt)
}
export const getTranslatePrompt = async () => {
return (
(await storage.get("copilotTranslatePrompt")) || DEFAULT_TRANSLATE_PROMPT
)
}
export const setTranslatePrompt = async (prompt: string) => {
await storage.set("copilotTranslatePrompt", prompt)
}
export const getExplainPrompt = async () => {
return (await storage.get("copilotExplainPrompt")) || DEFAULT_EXPLAIN_PROMPT
}
export const setExplainPrompt = async (prompt: string) => {
await storage.set("copilotExplainPrompt", prompt)
}
export const getCustomPrompt = async () => {
return (await storage.get("copilotCustomPrompt")) || DEFAULT_CUSTOM_PROMPT
}
export const setCustomPrompt = async (prompt: string) => {
await storage.set("copilotCustomPrompt", prompt)
}
export const getAllCopilotPrompts = async () => {
const [
summaryPrompt,
rephrasePrompt,
translatePrompt,
explainPrompt,
customPrompt
] = await Promise.all([
getSummaryPrompt(),
getRephrasePrompt(),
getTranslatePrompt(),
getExplainPrompt(),
getCustomPrompt()
])
return [
{ key: "summary", prompt: summaryPrompt },
{ key: "rephrase", prompt: rephrasePrompt },
{ key: "translate", prompt: translatePrompt },
{ key: "explain", prompt: explainPrompt },
{ key: "custom", prompt: customPrompt }
]
}
export const setAllCopilotPrompts = async (
prompts: { key: string; prompt: string }[]
) => {
for (const { key, prompt } of prompts) {
switch (key) {
case "summary":
await setSummaryPrompt(prompt)
break
case "rephrase":
await setRephrasePrompt(prompt)
break
case "translate":
await setTranslatePrompt(prompt)
break
case "explain":
await setExplainPrompt(prompt)
break
case "custom":
await setCustomPrompt(prompt)
break
}
}
}
export const getPrompt = async (key: string) => {
switch (key) {
case "summary":
return await getSummaryPrompt()
case "rephrase":
return await getRephrasePrompt()
case "translate":
return await getTranslatePrompt()
case "explain":
return await getExplainPrompt()
case "custom":
return await getCustomPrompt()
default:
return ""
}
}

View File

@ -12,6 +12,7 @@ export type ChatHistory = {
role: "user" | "assistant" | "system"
content: string
image?: string
messageType?: string
}[]
type State = {

View File

@ -18,12 +18,14 @@ export type Message = {
images?: string[]
search?: WebSearch
id?: string
messageType?: string
}
export type ChatHistory = {
role: "user" | "assistant" | "system"
content: string
image?: string
image?: string,
messageType?: string
}[]
type State = {

View File

@ -14,5 +14,6 @@ type WebSearch = {
sources: any[]
images?: string[]
search?: WebSearch
messageType?: string
id?: string
}

77
src/utils/pull-ollama.ts Normal file
View File

@ -0,0 +1,77 @@
import { setBadgeBackgroundColor, setBadgeText, setTitle } from "@/utils/action"
export const progressHuman = (completed: number, total: number) => {
return ((completed / total) * 100).toFixed(0) + "%"
}
export const clearBadge = () => {
setBadgeText({ text: "" })
setTitle({ title: "" })
}
export const streamDownload = async (url: string, model: string) => {
url += "/api/pull"
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ model, stream: true })
})
const reader = response.body?.getReader()
const decoder = new TextDecoder()
let isSuccess = true
while (true) {
if (!reader) {
break
}
const { done, value } = await reader.read()
if (done) {
break
}
const text = decoder.decode(value)
try {
const json = JSON.parse(text.trim()) as {
status: string
total?: number
completed?: number
}
if (json.total && json.completed) {
setBadgeText({
text: progressHuman(json.completed, json.total)
})
setBadgeBackgroundColor({ color: "#0000FF" })
} else {
setBadgeText({ text: "🏋️‍♂️" })
setBadgeBackgroundColor({ color: "#FFFFFF" })
}
setTitle({ title: json.status })
if (json.status === "success") {
isSuccess = true
}
} catch (e) {
console.error(e)
}
}
if (isSuccess) {
setBadgeText({ text: "✅" })
setBadgeBackgroundColor({ color: "#00FF00" })
setTitle({ title: "Model pulled successfully" })
} else {
setBadgeText({ text: "❌" })
setBadgeBackgroundColor({ color: "#FF0000" })
setTitle({ title: "Model pull failed" })
}
setTimeout(() => {
clearBadge()
}, 5000)
}

View File

@ -50,7 +50,7 @@ export default defineConfig({
outDir: "build",
manifest: {
version: "1.1.16",
version: "1.2.0",
name:
process.env.TARGET === "firefox"
? "Page Assist - A Web UI for Local AI Models"