Merge pull request #22 from n4ze3m/wxt

Migrated to WXT
This commit is contained in:
Muhammed Nazeem 2024-03-23 14:47:34 +05:30 committed by GitHub
commit 4055231bbc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 2904 additions and 388 deletions

1
.gitignore vendored
View File

@ -40,3 +40,4 @@ keys.json
# typescript # typescript
.tsbuildinfo .tsbuildinfo
.wxt

View File

@ -5,9 +5,14 @@
"description": "Use your locally running AI models to assist you in your web browsing.", "description": "Use your locally running AI models to assist you in your web browsing.",
"author": "n4ze3m", "author": "n4ze3m",
"scripts": { "scripts": {
"dev": "plasmo dev", "dev": "wxt",
"build": "plasmo build", "dev:firefox": "wxt -b firefox",
"package": "plasmo package" "build": "wxt build",
"build:firefox": "wxt build -b firefox",
"zip": "wxt zip",
"zip:firefox": "wxt zip -b firefox",
"compile": "tsc --noEmit",
"postinstall": "wxt prepare"
}, },
"dependencies": { "dependencies": {
"@ant-design/cssinjs": "^1.18.4", "@ant-design/cssinjs": "^1.18.4",
@ -21,6 +26,7 @@
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",
"@tanstack/react-query": "^5.17.19", "@tanstack/react-query": "^5.17.19",
"@vitejs/plugin-react": "^4.2.1",
"antd": "^5.13.3", "antd": "^5.13.3",
"axios": "^1.6.7", "axios": "^1.6.7",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
@ -53,34 +59,7 @@
"postcss": "^8.4.33", "postcss": "^8.4.33",
"prettier": "3.2.4", "prettier": "3.2.4",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "5.3.3" "typescript": "5.3.3",
}, "wxt": "^0.17.7"
"manifest": {
"host_permissions": [
"http://*/*",
"https://*/*"
],
"commands": {
"_execute_action": {
"suggested_key": {
"default": "Ctrl+Shift+L"
}
},
"execute_side_panel": {
"description": "Open the side panel",
"suggested_key": {
"default": "Ctrl+Shift+P"
}
}
},
"permissions": [
"storage",
"activeTab",
"scripting",
"declarativeNetRequest",
"action",
"unlimitedStorage",
"contextMenus"
]
} }
} }

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -1,138 +0,0 @@
import { getOllamaURL, isOllamaRunning } from "~services/ollama"
export {}
const progressHuman = (completed: number, total: number) => {
return ((completed / total) * 100).toFixed(0) + "%"
}
const clearBadge = () => {
chrome.action.setBadgeText({ text: "" })
chrome.action.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) {
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) {
chrome.action.setBadgeText({
text: progressHuman(json.completed, json.total)
})
chrome.action.setBadgeBackgroundColor({ color: "#0000FF" })
} else {
chrome.action.setBadgeText({ text: "🏋️‍♂️" })
chrome.action.setBadgeBackgroundColor({ color: "#FFFFFF" })
}
chrome.action.setTitle({ title: json.status })
if (json.status === "success") {
isSuccess = true
}
} catch (e) {
console.error(e)
}
}
if (isSuccess) {
chrome.action.setBadgeText({ text: "✅" })
chrome.action.setBadgeBackgroundColor({ color: "#00FF00" })
chrome.action.setTitle({ title: "Model pulled successfully" })
} else {
chrome.action.setBadgeText({ text: "❌" })
chrome.action.setBadgeBackgroundColor({ color: "#FF0000" })
chrome.action.setTitle({ title: "Model pull failed" })
}
setTimeout(() => {
clearBadge()
}, 5000)
}
chrome.runtime.onMessage.addListener(async (message) => {
if (message.type === "sidepanel") {
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
const tab = tabs[0]
await chrome.sidePanel.open({
tabId: tab.id
})
})
} else if (message.type === "pull_model") {
const ollamaURL = await getOllamaURL()
const isRunning = await isOllamaRunning()
if (!isRunning) {
chrome.action.setBadgeText({ text: "E" })
chrome.action.setBadgeBackgroundColor({ color: "#FF0000" })
chrome.action.setTitle({ title: "Ollama is not running" })
setTimeout(() => {
clearBadge()
}, 5000)
}
await streamDownload(ollamaURL, message.modelName)
}
})
chrome.action.onClicked.addListener((tab) => {
chrome.tabs.create({ url: chrome.runtime.getURL("options.html") })
})
chrome.commands.onCommand.addListener((command) => {
switch (command) {
case "execute_side_panel":
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
const tab = tabs[0]
await chrome.sidePanel.open({
tabId: tab.id
})
})
break
default:
break
}
})
chrome.contextMenus.create({
id: "open-side-panel-pa",
title: "Open Side Panel to Chat",
contexts: ["all"]
})
chrome.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === "open-side-panel-pa") {
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
const tab = tabs[0]
await chrome.sidePanel.open({
tabId: tab.id
})
})
}
})

View File

@ -14,7 +14,7 @@ import {
RunnableMap, RunnableMap,
RunnableSequence, RunnableSequence,
} from "langchain/schema/runnable"; } from "langchain/schema/runnable";
import type { ChatHistory } from "~store"; import type { ChatHistory } from "~/store";
type RetrievalChainInput = { type RetrievalChainInput = {
chat_history: string; chat_history: string;
question: string; question: string;

View File

@ -1,6 +1,6 @@
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import React from "react" import React from "react"
import useDynamicTextareaSize from "~hooks/useDynamicTextareaSize" import useDynamicTextareaSize from "~/hooks/useDynamicTextareaSize"
type Props = { type Props = {
value: string value: string

View File

@ -1,13 +1,13 @@
import { Form, Image, Input, Modal, Tooltip, message } from "antd" import { Form, Image, Input, Modal, Tooltip, message } from "antd"
import { Share } from "lucide-react" import { Share } from "lucide-react"
import { useState } from "react" import { useState } from "react"
import type { Message } from "~store/option" import type { Message } from "~/store/option"
import Markdown from "./Markdown" import Markdown from "./Markdown"
import React from "react" import React from "react"
import { useMutation } from "@tanstack/react-query" import { useMutation } from "@tanstack/react-query"
import { getPageShareUrl } from "~services/ollama" import { getPageShareUrl } from "~/services/ollama"
import { cleanUrl } from "~libs/clean-url" import { cleanUrl } from "~/libs/clean-url"
import { getUserId, saveWebshare } from "~libs/db" import { getUserId, saveWebshare } from "~/libs/db"
type Props = { type Props = {
messages: Message[] messages: Message[]

View File

@ -4,8 +4,8 @@ import { useLocation, NavLink } from "react-router-dom"
import { Sidebar } from "../Option/Sidebar" import { Sidebar } from "../Option/Sidebar"
import { Drawer, Select, Tooltip } from "antd" import { Drawer, Select, Tooltip } from "antd"
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { getAllModels } from "~services/ollama" import { getAllModels } from "~/services/ollama"
import { useMessageOption } from "~hooks/useMessageOption" import { useMessageOption } from "~/hooks/useMessageOption"
import { import {
ChevronLeft, ChevronLeft,
CogIcon, CogIcon,
@ -15,8 +15,8 @@ import {
SquarePen, SquarePen,
ZapIcon ZapIcon
} from "lucide-react" } from "lucide-react"
import { getAllPrompts } from "~libs/db" import { getAllPrompts } from "~/libs/db"
import { ShareBtn } from "~components/Common/ShareBtn" import { ShareBtn } from "~/components/Common/ShareBtn"
export default function OptionLayout({ export default function OptionLayout({
children children

View File

@ -1,7 +1,7 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { Skeleton, Table, Tag, Tooltip, notification, Modal, Input } from "antd" import { Skeleton, Table, Tag, Tooltip, notification, Modal, Input } from "antd"
import { bytePerSecondFormatter } from "~libs/byte-formater" import { bytePerSecondFormatter } from "~/libs/byte-formater"
import { deleteModel, getAllModels } from "~services/ollama" import { deleteModel, getAllModels } from "~/services/ollama"
import dayjs from "dayjs" import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime" import relativeTime from "dayjs/plugin/relativeTime"
import { useState } from "react" import { useState } from "react"

View File

@ -1,7 +1,7 @@
import React from "react" import React from "react"
import { useMessageOption } from "~hooks/useMessageOption" import { useMessageOption } from "~/hooks/useMessageOption"
import { PlaygroundEmpty } from "./PlaygroundEmpty" import { PlaygroundEmpty } from "./PlaygroundEmpty"
import { PlaygroundMessage } from "~components/Common/Playground/Message" import { PlaygroundMessage } from "~/components/Common/Playground/Message"
export const PlaygroundChat = () => { export const PlaygroundChat = () => {
const { const {

View File

@ -5,7 +5,7 @@ import {
getOllamaURL, getOllamaURL,
isOllamaRunning, isOllamaRunning,
setOllamaURL as saveOllamaURL setOllamaURL as saveOllamaURL
} from "~services/ollama" } from "~/services/ollama"
export const PlaygroundEmpty = () => { export const PlaygroundEmpty = () => {
const [ollamaURL, setOllamaURL] = useState<string>("") const [ollamaURL, setOllamaURL] = useState<string>("")

View File

@ -1,16 +1,16 @@
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { useMutation, useQueryClient } from "@tanstack/react-query" import { useMutation, useQueryClient } from "@tanstack/react-query"
import React from "react" import React from "react"
import useDynamicTextareaSize from "~hooks/useDynamicTextareaSize" import useDynamicTextareaSize from "~/hooks/useDynamicTextareaSize"
import { toBase64 } from "~libs/to-base64" import { toBase64 } from "~/libs/to-base64"
import { useMessageOption } from "~hooks/useMessageOption" import { useMessageOption } from "~/hooks/useMessageOption"
import { Checkbox, Dropdown, Switch, Tooltip } from "antd" import { Checkbox, Dropdown, Switch, Tooltip } from "antd"
import { Image } from "antd" import { Image } from "antd"
import { useSpeechRecognition } from "~hooks/useSpeechRecognition" import { useSpeechRecognition } from "~/hooks/useSpeechRecognition"
import { useWebUI } from "~store/webui" import { useWebUI } from "~/store/webui"
import { defaultEmbeddingModelForRag } from "~services/ollama" import { defaultEmbeddingModelForRag } from "~/services/ollama"
import { ImageIcon, MicIcon, StopCircleIcon, X } from "lucide-react" import { ImageIcon, MicIcon, StopCircleIcon, X } from "lucide-react"
import { getVariable } from "~utils/select-varaible" import { getVariable } from "~/utils/select-varaible"
type Props = { type Props = {
dropedFile: File | undefined dropedFile: File | undefined

View File

@ -16,7 +16,7 @@ import {
getAllPrompts, getAllPrompts,
savePrompt, savePrompt,
updatePrompt updatePrompt
} from "~libs/db" } from "~/libs/db"
export const PromptBody = () => { export const PromptBody = () => {
const queryClient = useQueryClient() const queryClient = useQueryClient()

View File

@ -1,7 +1,7 @@
import { useMutation, useQuery } from "@tanstack/react-query" import { useMutation, useQuery } from "@tanstack/react-query"
import { Form, InputNumber, Select, Skeleton } from "antd" import { Form, InputNumber, Select, Skeleton } from "antd"
import { useState } from "react" import { useState } from "react"
import { SaveButton } from "~components/Common/SaveButton" import { SaveButton } from "~/components/Common/SaveButton"
import { import {
defaultEmbeddingChunkOverlap, defaultEmbeddingChunkOverlap,
defaultEmbeddingChunkSize, defaultEmbeddingChunkSize,
@ -10,7 +10,7 @@ import {
getOllamaURL, getOllamaURL,
saveForRag, saveForRag,
setOllamaURL as saveOllamaURL setOllamaURL as saveOllamaURL
} from "~services/ollama" } from "~/services/ollama"
import { SettingPrompt } from "./prompt" import { SettingPrompt } from "./prompt"
export const SettingsOllama = () => { export const SettingsOllama = () => {

View File

@ -1,9 +1,9 @@
import { useQueryClient } from "@tanstack/react-query" import { useQueryClient } from "@tanstack/react-query"
import { useDarkMode } from "~hooks/useDarkmode" import { useDarkMode } from "~/hooks/useDarkmode"
import { useMessageOption } from "~hooks/useMessageOption" import { useMessageOption } from "~/hooks/useMessageOption"
import { PageAssitDatabase } from "~libs/db" import { PageAssitDatabase } from "~/libs/db"
import { Select } from "antd" import { Select } from "antd"
import { SUPPORTED_LANGUAGES } from "~utils/supporetd-languages" import { SUPPORTED_LANGUAGES } from "~/utils/supporetd-languages"
import { MoonIcon, SunIcon } from "lucide-react" import { MoonIcon, SunIcon } from "lucide-react"
import { SearchModeSettings } from "./search-mode" import { SearchModeSettings } from "./search-mode"

View File

@ -1,14 +1,14 @@
import { useQuery, useQueryClient } from "@tanstack/react-query" import { useQuery, useQueryClient } from "@tanstack/react-query"
import { Skeleton, Radio, Form, Alert } from "antd" import { Skeleton, Radio, Form, Alert } from "antd"
import React from "react" import React from "react"
import { SaveButton } from "~components/Common/SaveButton" import { SaveButton } from "~/components/Common/SaveButton"
import { import {
getWebSearchPrompt, getWebSearchPrompt,
setSystemPromptForNonRagOption, setSystemPromptForNonRagOption,
systemPromptForNonRagOption, systemPromptForNonRagOption,
geWebSearchFollowUpPrompt, geWebSearchFollowUpPrompt,
setWebPrompts setWebPrompts
} from "~services/ollama" } from "~/services/ollama"
export const SettingPrompt = () => { export const SettingPrompt = () => {
const [selectedValue, setSelectedValue] = React.useState<"normal" | "web">( const [selectedValue, setSelectedValue] = React.useState<"normal" | "web">(

View File

@ -3,7 +3,7 @@ import { Skeleton, Switch } from "antd"
import { import {
getIsSimpleInternetSearch, getIsSimpleInternetSearch,
setIsSimpleInternetSearch setIsSimpleInternetSearch
} from "~services/ollama" } from "~/services/ollama"
export const SearchModeSettings = () => { export const SearchModeSettings = () => {
const { data, status } = useQuery({ const { data, status } = useQuery({

View File

@ -1,10 +1,10 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { Form, Input, Skeleton, Table, Tooltip, message } from "antd" import { Form, Input, Skeleton, Table, Tooltip, message } from "antd"
import { Trash2 } from "lucide-react" import { Trash2 } from "lucide-react"
import { SaveButton } from "~components/Common/SaveButton" import { SaveButton } from "~/components/Common/SaveButton"
import { deleteWebshare, getAllWebshares, getUserId } from "~libs/db" import { deleteWebshare, getAllWebshares, getUserId } from "~/libs/db"
import { getPageShareUrl, setPageShareUrl } from "~services/ollama" import { getPageShareUrl, setPageShareUrl } from "~/services/ollama"
import { verifyPageShareURL } from "~utils/verify-page-share" import { verifyPageShareURL } from "~/utils/verify-page-share"
export const OptionShareBody = () => { export const OptionShareBody = () => {
const queryClient = useQueryClient() const queryClient = useQueryClient()

View File

@ -5,9 +5,9 @@ import {
formatToMessage, formatToMessage,
deleteByHistoryId, deleteByHistoryId,
updateHistory updateHistory
} from "~libs/db" } from "~/libs/db"
import { Empty, Skeleton } from "antd" import { Empty, Skeleton } from "antd"
import { useMessageOption } from "~hooks/useMessageOption" import { useMessageOption } from "~/hooks/useMessageOption"
import { useState } from "react" import { useState } from "react"
import { PencilIcon, Trash2 } from "lucide-react" import { PencilIcon, Trash2 } from "lucide-react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"

View File

@ -1,6 +1,6 @@
import React from "react" import React from "react"
import { PlaygroundMessage } from "~components/Common/Playground/Message" import { PlaygroundMessage } from "~/components/Common/Playground/Message"
import { useMessage } from "~hooks/useMessage" import { useMessage } from "~/hooks/useMessage"
import { EmptySidePanel } from "../Chat/empty" import { EmptySidePanel } from "../Chat/empty"
export const SidePanelBody = () => { export const SidePanelBody = () => {

View File

@ -2,13 +2,13 @@ import { useQuery } from "@tanstack/react-query"
import { Select } from "antd" import { Select } from "antd"
import { RotateCcw } from "lucide-react" import { RotateCcw } from "lucide-react"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useMessage } from "~hooks/useMessage" import { useMessage } from "~/hooks/useMessage"
import { import {
getAllModels, getAllModels,
getOllamaURL, getOllamaURL,
isOllamaRunning, isOllamaRunning,
setOllamaURL as saveOllamaURL setOllamaURL as saveOllamaURL
} from "~services/ollama" } from "~/services/ollama"
export const EmptySidePanel = () => { export const EmptySidePanel = () => {
const [ollamaURL, setOllamaURL] = useState<string>("") const [ollamaURL, setOllamaURL] = useState<string>("")

View File

@ -1,13 +1,13 @@
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { useMutation } from "@tanstack/react-query" import { useMutation } from "@tanstack/react-query"
import React from "react" import React from "react"
import useDynamicTextareaSize from "~hooks/useDynamicTextareaSize" import useDynamicTextareaSize from "~/hooks/useDynamicTextareaSize"
import { useMessage } from "~hooks/useMessage" import { useMessage } from "~/hooks/useMessage"
import { toBase64 } from "~libs/to-base64" import { toBase64 } from "~/libs/to-base64"
import { Checkbox, Dropdown, Image, Tooltip } from "antd" import { Checkbox, Dropdown, Image, Tooltip } from "antd"
import { useSpeechRecognition } from "~hooks/useSpeechRecognition" import { useSpeechRecognition } from "~/hooks/useSpeechRecognition"
import { useWebUI } from "~store/webui" import { useWebUI } from "~/store/webui"
import { defaultEmbeddingModelForRag } from "~services/ollama" import { defaultEmbeddingModelForRag } from "~/services/ollama"
import { ImageIcon, MicIcon, X } from "lucide-react" import { ImageIcon, MicIcon, X } from "lucide-react"
type Props = { type Props = {

View File

@ -1,5 +1,5 @@
import logoImage from "data-base64:~assets/icon.png" import logoImage from "~/assets/icon.png"
import { useMessage } from "~hooks/useMessage" import { useMessage } from "~/hooks/useMessage"
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import { Tooltip } from "antd" import { Tooltip } from "antd"
import { BoxesIcon, CogIcon, RefreshCcw } from "lucide-react" import { BoxesIcon, CogIcon, RefreshCcw } from "lucide-react"

View File

@ -12,13 +12,13 @@ import {
defaultEmbeddingChunkSize, defaultEmbeddingChunkSize,
defaultEmbeddingModelForRag, defaultEmbeddingModelForRag,
saveForRag saveForRag
} from "~services/ollama" } from "~/services/ollama"
import { Skeleton, Radio, Select, Form, InputNumber } from "antd" import { Skeleton, Radio, Select, Form, InputNumber } 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"
export const SettingsBody = () => { export const SettingsBody = () => {

View File

@ -1,6 +1,7 @@
import logoImage from "data-base64:~assets/icon.png"
import { ChevronLeft } from "lucide-react" import { ChevronLeft } from "lucide-react"
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import logoImage from "~/assets/icon.png"
export const SidepanelSettingsHeader = () => { export const SidepanelSettingsHeader = () => {
return ( return (
<div className="flex px-3 justify-start gap-3 bg-white dark:bg-[#171717] border-b border-gray-300 dark:border-gray-700 py-4 items-center"> <div className="flex px-3 justify-start gap-3 bg-white dark:bg-[#171717] border-b border-gray-300 dark:border-gray-700 py-4 items-center">

View File

@ -1,54 +0,0 @@
import type { PlasmoCSConfig } from "plasmo"
export const config: PlasmoCSConfig = {
matches: ["*://ollama.com/library/*"],
all_frames: true
}
const downloadModel = async (modelName: string) => {
const ok = confirm(
`[Page Assist Extension] Do you want to pull ${modelName} model? This has nothing to do with Ollama.com website. The model will be pulled locally once you confirm.`
)
if (ok) {
alert(
`[Page Assist Extension] Pulling ${modelName} model. For more details, check the extension icon.`
)
await chrome.runtime.sendMessage({
type: "pull_model",
modelName
})
return true
}
return false
}
const downloadSVG = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-5 w-5 pageasssist-icon">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
`
const codeDiv = document.querySelectorAll("div.language-none")
for (let i = 0; i < codeDiv.length; i++) {
const button = codeDiv[i].querySelector("button")
const command = codeDiv[i].querySelector("input")
if (button && command) {
const newButton = document.createElement("button")
newButton.innerHTML = downloadSVG
newButton.className = `border-l ${button.className}`
newButton.id = `download-${i}-pageassist`
const modelName = command?.value
.replace("ollama run", "")
.replace("ollama pull", "")
.trim()
newButton.addEventListener("click", () => {
downloadModel(modelName)
})
const span = document.createElement("span")
span.title = "Download model via Page Assist"
span.appendChild(newButton)
button.parentNode.appendChild(span)
}
}

142
src/entries/background.ts Normal file
View File

@ -0,0 +1,142 @@
import { getOllamaURL, isOllamaRunning } from "../services/ollama"
const progressHuman = (completed: number, total: number) => {
return ((completed / total) * 100).toFixed(0) + "%"
}
const clearBadge = () => {
chrome.action.setBadgeText({ text: "" })
chrome.action.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) {
chrome.action.setBadgeText({
text: progressHuman(json.completed, json.total)
})
chrome.action.setBadgeBackgroundColor({ color: "#0000FF" })
} else {
chrome.action.setBadgeText({ text: "🏋️‍♂️" })
chrome.action.setBadgeBackgroundColor({ color: "#FFFFFF" })
}
chrome.action.setTitle({ title: json.status })
if (json.status === "success") {
isSuccess = true
}
} catch (e) {
console.error(e)
}
}
if (isSuccess) {
chrome.action.setBadgeText({ text: "✅" })
chrome.action.setBadgeBackgroundColor({ color: "#00FF00" })
chrome.action.setTitle({ title: "Model pulled successfully" })
} else {
chrome.action.setBadgeText({ text: "❌" })
chrome.action.setBadgeBackgroundColor({ color: "#FF0000" })
chrome.action.setTitle({ title: "Model pull failed" })
}
setTimeout(() => {
clearBadge()
}, 5000)
}
export default defineBackground({
main() {
chrome.runtime.onMessage.addListener(async (message) => {
if (message.type === "sidepanel") {
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
const tab = tabs[0]
chrome.sidePanel.open({
tabId: tab.id!
})
})
} else if (message.type === "pull_model") {
const ollamaURL = await getOllamaURL()
const isRunning = await isOllamaRunning()
if (!isRunning) {
chrome.action.setBadgeText({ text: "E" })
chrome.action.setBadgeBackgroundColor({ color: "#FF0000" })
chrome.action.setTitle({ title: "Ollama is not running" })
setTimeout(() => {
clearBadge()
}, 5000)
}
await streamDownload(ollamaURL, message.modelName)
}
})
chrome.action.onClicked.addListener((tab) => {
chrome.tabs.create({ url: chrome.runtime.getURL("options.html") })
})
chrome.commands.onCommand.addListener((command) => {
switch (command) {
case "execute_side_panel":
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
const tab = tabs[0]
chrome.sidePanel.open({
tabId: tab.id!
})
})
break
default:
break
}
})
chrome.contextMenus.create({
id: "open-side-panel-pa",
title: "Open Side Panel to Chat",
contexts: ["all"]
})
chrome.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === "open-side-panel-pa") {
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
const tab = tabs[0]
await chrome.sidePanel.open({
tabId: tab.id!
})
})
}
})
},
persistent: true
})

View File

@ -0,0 +1,56 @@
export default defineContentScript({
main(ctx) {
const downloadModel = async (modelName: string) => {
const ok = confirm(
`[Page Assist Extension] Do you want to pull ${modelName} model? This has nothing to do with Ollama.com website. The model will be pulled locally once you confirm.`
)
if (ok) {
alert(
`[Page Assist Extension] Pulling ${modelName} model. For more details, check the extension icon.`
)
await chrome.runtime.sendMessage({
type: "pull_model",
modelName
})
return true
}
return false
}
const downloadSVG = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-5 w-5 pageasssist-icon">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
`
const codeDiv = document.querySelectorAll("div.language-none")
for (let i = 0; i < codeDiv.length; i++) {
const button = codeDiv[i].querySelector("button")
const command = codeDiv[i].querySelector("input")
if (button && command) {
const newButton = document.createElement("button")
newButton.innerHTML = downloadSVG
newButton.className = `border-l ${button.className}`
newButton.id = `download-${i}-pageassist`
const modelName = command?.value
.replace("ollama run", "")
.replace("ollama pull", "")
.trim()
newButton.addEventListener("click", () => {
downloadModel(modelName)
})
const span = document.createElement("span")
span.title = "Download model via Page Assist"
span.appendChild(newButton)
if (button.parentNode) {
button.parentNode.appendChild(span)
}
}
}
},
allFrames: true,
matches: ["*://ollama.com/library/*"]
})

View File

@ -3,11 +3,10 @@ import { MemoryRouter } from "react-router-dom"
import { ToastContainer } from "react-toastify" import { ToastContainer } from "react-toastify"
import "react-toastify/dist/ReactToastify.css" import "react-toastify/dist/ReactToastify.css"
const queryClient = new QueryClient() const queryClient = new QueryClient()
import "./css/tailwind.css"
import { ConfigProvider, theme } from "antd" import { ConfigProvider, theme } from "antd"
import { StyleProvider } from "@ant-design/cssinjs" import { StyleProvider } from "@ant-design/cssinjs"
import { useDarkMode } from "~hooks/useDarkmode" import { useDarkMode } from "~/hooks/useDarkmode"
import { OptionRouting } from "~routes" import { OptionRouting } from "~/routes"
function IndexOption() { function IndexOption() {
const { mode } = useDarkMode() const { mode } = useDarkMode()
return ( return (

View File

@ -0,0 +1,14 @@
<!doctype html>
<html>
<head>
<title>Page Assist - Web UI</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="manifest.type" content="browser_action" />
<link href="~/assets/tailwind.css" rel="stylesheet" />
<meta charset="utf-8" />
</head>
<body class="bg-white dark:bg-[#171717]">
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import IndexOption from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<IndexOption />
</React.StrictMode>,
);

View File

@ -1,13 +1,12 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { MemoryRouter } from "react-router-dom" import { MemoryRouter } from "react-router-dom"
import { SidepanelRouting } from "~routes" import { SidepanelRouting } from "~/routes"
import { ToastContainer } from "react-toastify" import { ToastContainer } from "react-toastify"
import "react-toastify/dist/ReactToastify.css" import "react-toastify/dist/ReactToastify.css"
const queryClient = new QueryClient() const queryClient = new QueryClient()
import "./css/tailwind.css"
import { ConfigProvider, theme } from "antd" import { ConfigProvider, theme } from "antd"
import { StyleProvider } from "@ant-design/cssinjs" import { StyleProvider } from "@ant-design/cssinjs"
import { useDarkMode } from "~hooks/useDarkmode" import { useDarkMode } from "~/hooks/useDarkmode"
function IndexSidepanel() { function IndexSidepanel() {
const { mode } = useDarkMode() const { mode } = useDarkMode()

View File

@ -0,0 +1,14 @@
<!doctype html>
<html>
<head>
<title>Page Assist - Web UI</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="manifest.type" content="browser_action" />
<link href="~/assets/tailwind.css" rel="stylesheet" />
<meta charset="utf-8" />
</head>
<body class="bg-white dark:bg-[#171717]">
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from "react"
import ReactDOM from "react-dom/client"
import IndexSidepanel from "./App"
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<IndexSidepanel />
</React.StrictMode>
)

View File

@ -1,5 +1,5 @@
import React from "react" import React from "react"
import { cleanUrl } from "~libs/clean-url" import { cleanUrl } from "~/libs/clean-url"
import { import {
defaultEmbeddingChunkOverlap, defaultEmbeddingChunkOverlap,
defaultEmbeddingChunkSize, defaultEmbeddingChunkSize,
@ -7,8 +7,8 @@ import {
getOllamaURL, getOllamaURL,
promptForRag, promptForRag,
systemPromptForNonRag systemPromptForNonRag
} from "~services/ollama" } from "~/services/ollama"
import { useStoreMessage, type ChatHistory, type Message } from "~store" import { useStoreMessage, type ChatHistory, type Message } from "~/store"
import { ChatOllama } from "@langchain/community/chat_models/ollama" import { ChatOllama } from "@langchain/community/chat_models/ollama"
import { import {
HumanMessage, HumanMessage,
@ -16,16 +16,16 @@ import {
type MessageContent, type MessageContent,
SystemMessage SystemMessage
} from "@langchain/core/messages" } from "@langchain/core/messages"
import { getHtmlOfCurrentTab } from "~libs/get-html" import { getHtmlOfCurrentTab } from "~/libs/get-html"
import { PageAssistHtmlLoader } from "~loader/html" import { PageAssistHtmlLoader } from "~/loader/html"
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter" import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"
import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama" import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama"
import { import {
createChatWithWebsiteChain, createChatWithWebsiteChain,
groupMessagesByConversation groupMessagesByConversation
} from "~chain/chat-with-website" } from "~/chain/chat-with-website"
import { MemoryVectorStore } from "langchain/vectorstores/memory" import { MemoryVectorStore } from "langchain/vectorstores/memory"
import { chromeRunTime } from "~libs/runtime" import { chromeRunTime } from "~/libs/runtime"
export type BotResponse = { export type BotResponse = {
bot: { bot: {
text: string text: string

View File

@ -1,11 +1,11 @@
import React from "react" import React from "react"
import { cleanUrl } from "~libs/clean-url" import { cleanUrl } from "~/libs/clean-url"
import { import {
geWebSearchFollowUpPrompt, geWebSearchFollowUpPrompt,
getOllamaURL, getOllamaURL,
systemPromptForNonRagOption systemPromptForNonRagOption
} from "~services/ollama" } from "~/services/ollama"
import { type ChatHistory, type Message } from "~store/option" import { type ChatHistory, type Message } from "~/store/option"
import { ChatOllama } from "@langchain/community/chat_models/ollama" import { ChatOllama } from "@langchain/community/chat_models/ollama"
import { import {
HumanMessage, HumanMessage,
@ -13,7 +13,7 @@ import {
type MessageContent, type MessageContent,
SystemMessage SystemMessage
} from "@langchain/core/messages" } from "@langchain/core/messages"
import { useStoreMessageOption } from "~store/option" import { useStoreMessageOption } from "~/store/option"
import { import {
deleteChatForEdit, deleteChatForEdit,
getPromptById, getPromptById,
@ -21,10 +21,10 @@ import {
saveHistory, saveHistory,
saveMessage, saveMessage,
updateMessageByIndex updateMessageByIndex
} from "~libs/db" } from "~/libs/db"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { notification } from "antd" import { notification } from "antd"
import { getSystemPromptForWeb } from "~web/web" import { getSystemPromptForWeb } from "~/web/web"
export type BotResponse = { export type BotResponse = {
bot: { bot: {

View File

@ -1,7 +1,7 @@
import { import {
type ChatHistory as ChatHistoryType, type ChatHistory as ChatHistoryType,
type Message as MessageType type Message as MessageType
} from "~store/option" } from "~/store/option"
type HistoryInfo = { type HistoryInfo = {
id: string id: string

View File

@ -1,7 +1,7 @@
import { BaseDocumentLoader } from "langchain/document_loaders/base" import { BaseDocumentLoader } from "langchain/document_loaders/base"
import { Document } from "@langchain/core/documents" import { Document } from "@langchain/core/documents"
import { compile } from "html-to-text" import { compile } from "html-to-text"
import { chromeRunTime } from "~libs/runtime" import { chromeRunTime } from "~/libs/runtime"
import { YtTranscript } from "yt-transcript" import { YtTranscript } from "yt-transcript"
const YT_REGEX = const YT_REGEX =

View File

@ -1,8 +0,0 @@
<!doctype html>
<html>
<head>
<title>__plasmo_static_index_title__</title>
<meta charset="utf-8" />
</head>
<body class="bg-white dark:bg-[#171717]"></body>
</html>

BIN
src/public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
src/public/icon/128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
src/public/icon/16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

BIN
src/public/icon/32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 B

BIN
src/public/icon/48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

BIN
src/public/icon/64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,6 +1,6 @@
import { Route, Routes } from "react-router-dom" import { Route, Routes } from "react-router-dom"
import { SidepanelChat } from "./sidepanel-chat" import { SidepanelChat } from "./sidepanel-chat"
import { useDarkMode } from "~hooks/useDarkmode" import { useDarkMode } from "~/hooks/useDarkmode"
import { SidepanelSettings } from "./sidepanel-settings" import { SidepanelSettings } from "./sidepanel-settings"
import { OptionIndex } from "./option-index" import { OptionIndex } from "./option-index"
import { OptionModal } from "./option-settings-model" import { OptionModal } from "./option-settings-model"

View File

@ -1,5 +1,5 @@
import OptionLayout from "~components/Layouts/Layout" import OptionLayout from "~/components/Layouts/Layout"
import { Playground } from "~components/Option/Playground/Playground" import { Playground } from "~/components/Option/Playground/Playground"
export const OptionIndex = () => { export const OptionIndex = () => {
return ( return (

View File

@ -1,6 +1,6 @@
import { SettingsLayout } from "~components/Layouts/SettingsOptionLayout" import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout"
import OptionLayout from "~components/Layouts/Layout" import OptionLayout from "~/components/Layouts/Layout"
import { ModelsBody } from "~components/Option/Models" import { ModelsBody } from "~/components/Option/Models"
export const OptionModal = () => { export const OptionModal = () => {
return ( return (

View File

@ -1,6 +1,6 @@
import { SettingsLayout } from "~components/Layouts/SettingsOptionLayout" import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout"
import OptionLayout from "~components/Layouts/Layout" import OptionLayout from "~/components/Layouts/Layout"
import { PromptBody } from "~components/Option/Prompt" import { PromptBody } from "~/components/Option/Prompt"
export const OptionPrompt = () => { export const OptionPrompt = () => {
return ( return (

View File

@ -1,6 +1,6 @@
import { SettingsLayout } from "~components/Layouts/SettingsOptionLayout" import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout"
import OptionLayout from "~components/Layouts/Layout" import OptionLayout from "~/components/Layouts/Layout"
import { OptionShareBody } from "~components/Option/Share" import { OptionShareBody } from "~/components/Option/Share"
export const OptionShare = () => { export const OptionShare = () => {
return ( return (

View File

@ -1,6 +1,6 @@
import { SettingsLayout } from "~components/Layouts/SettingsOptionLayout" import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout"
import OptionLayout from "~components/Layouts/Layout" import OptionLayout from "~/components/Layouts/Layout"
import { SettingOther } from "~components/Option/Settings/other" import { SettingOther } from "~/components/Option/Settings/other"
export const OptionSettings = () => { export const OptionSettings = () => {
return ( return (

View File

@ -1,6 +1,6 @@
import { SettingsLayout } from "~components/Layouts/SettingsOptionLayout" import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout"
import OptionLayout from "~components/Layouts/Layout" import OptionLayout from "~/components/Layouts/Layout"
import { SettingsOllama } from "~components/Option/Settings/ollama" import { SettingsOllama } from "~/components/Option/Settings/ollama"
export const OptionOllamaSettings = () => { export const OptionOllamaSettings = () => {
return ( return (

View File

@ -1,8 +1,8 @@
import React from "react" import React from "react"
import { SidePanelBody } from "~components/Sidepanel/Chat/body" import { SidePanelBody } from "~/components/Sidepanel/Chat/body"
import { SidepanelForm } from "~components/Sidepanel/Chat/form" import { SidepanelForm } from "~/components/Sidepanel/Chat/form"
import { SidepanelHeader } from "~components/Sidepanel/Chat/header" import { SidepanelHeader } from "~/components/Sidepanel/Chat/header"
import { useMessage } from "~hooks/useMessage" import { useMessage } from "~/hooks/useMessage"
export const SidepanelChat = () => { export const SidepanelChat = () => {
const drop = React.useRef<HTMLDivElement>(null) const drop = React.useRef<HTMLDivElement>(null)

View File

@ -1,5 +1,5 @@
import { SettingsBody } from "~components/Sidepanel/Settings/body" import { SettingsBody } from "~/components/Sidepanel/Settings/body"
import { SidepanelSettingsHeader } from "~components/Sidepanel/Settings/header" import { SidepanelSettingsHeader } from "~/components/Sidepanel/Settings/header"
export const SidepanelSettings = () => { export const SidepanelSettings = () => {
return ( return (

View File

@ -1,6 +1,6 @@
import { Storage } from "@plasmohq/storage" import { Storage } from "@plasmohq/storage"
import { cleanUrl } from "~libs/clean-url" import { cleanUrl } from "../libs/clean-url"
import { chromeRunTime } from "~libs/runtime" import { chromeRunTime } from "../libs/runtime"
const storage = new Storage() const storage = new Storage()

View File

@ -1,4 +1,4 @@
import { cleanUrl } from "~libs/clean-url" import { cleanUrl } from "~/libs/clean-url"
export const verifyPageShareURL = async (url: string) => { export const verifyPageShareURL = async (url: string) => {
const res = await fetch(`${cleanUrl(url)}/api/v1/ping`) const res = await fetch(`${cleanUrl(url)}/api/v1/ping`)

View File

@ -2,10 +2,10 @@ import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama"
import type { Document } from "@langchain/core/documents" import type { Document } from "@langchain/core/documents"
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter" import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"
import { MemoryVectorStore } from "langchain/vectorstores/memory" import { MemoryVectorStore } from "langchain/vectorstores/memory"
import { cleanUrl } from "~libs/clean-url" import { cleanUrl } from "~/libs/clean-url"
import { chromeRunTime } from "~libs/runtime" import { chromeRunTime } from "~/libs/runtime"
import { PageAssistHtmlLoader } from "~loader/html" import { PageAssistHtmlLoader } from "~/loader/html"
import { defaultEmbeddingChunkOverlap, defaultEmbeddingChunkSize, defaultEmbeddingModelForRag, getIsSimpleInternetSearch, getOllamaURL } from "~services/ollama" import { defaultEmbeddingChunkOverlap, defaultEmbeddingChunkSize, defaultEmbeddingModelForRag, getIsSimpleInternetSearch, getOllamaURL } from "~/services/ollama"
const BLOCKED_HOSTS = [ const BLOCKED_HOSTS = [
"google.com", "google.com",

View File

@ -1,4 +1,4 @@
import { getWebSearchPrompt } from "~services/ollama" import { getWebSearchPrompt } from "~/services/ollama"
import { webSearch } from "./local-google" import { webSearch } from "./local-google"
const getHostName = (url: string) => { const getHostName = (url: string) => {

View File

@ -1,11 +1,13 @@
{ {
"extends": "plasmo/templates/tsconfig.base", "extends": "./.wxt/tsconfig.json",
"exclude": ["node_modules"],
"include": [".plasmo/index.d.ts", "./**/*.ts", "./**/*.tsx"],
"compilerOptions": { "compilerOptions": {
"paths": { "noEmit": true,
"~*": ["./src/*"] "allowImportingTsExtensions": true,
}, "allowSyntheticDefaultImports": true,
"baseUrl": "." "esModuleInterop": true,
} "jsx": "react-jsx"
},
"exclude": [
"node_modules"
],
} }

44
wxt.config.ts Normal file
View File

@ -0,0 +1,44 @@
import { defineConfig } from "wxt"
import react from "@vitejs/plugin-react"
// See https://wxt.dev/api/config.html
export default defineConfig({
vite: () => ({
plugins: [react()],
}),
entrypointsDir: "entries",
srcDir: "src",
outDir: "build",
manifest: {
name: "Page Assist - A Web UI for Local AI Models",
version: "1.1.0",
description:
"Use your locally running AI models to assist you in your web browsing.",
action: {},
author: "n4ze3m",
host_permissions: ["http://*/*", "https://*/*"],
commands: {
_execute_action: {
suggested_key: {
default: "Ctrl+Shift+L"
}
},
execute_side_panel: {
description: "Open the side panel",
suggested_key: {
default: "Ctrl+Shift+P"
}
}
},
permissions: [
"storage",
"sidePanel",
"activeTab",
"scripting",
"declarativeNetRequest",
"action",
"unlimitedStorage",
"contextMenus"
]
}
})

2524
yarn.lock

File diff suppressed because it is too large Load Diff