Migrated to WXT
This commit is contained in:
142
src/entries/background.ts
Normal file
142
src/entries/background.ts
Normal 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
|
||||
})
|
||||
56
src/entries/ollama-pull.content.ts
Normal file
56
src/entries/ollama-pull.content.ts
Normal 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/*"]
|
||||
})
|
||||
30
src/entries/options/App.tsx
Normal file
30
src/entries/options/App.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
import { MemoryRouter } from "react-router-dom"
|
||||
import { ToastContainer } from "react-toastify"
|
||||
import "react-toastify/dist/ReactToastify.css"
|
||||
const queryClient = new QueryClient()
|
||||
import { ConfigProvider, theme } from "antd"
|
||||
import { StyleProvider } from "@ant-design/cssinjs"
|
||||
import { useDarkMode } from "~/hooks/useDarkmode"
|
||||
import { OptionRouting } from "~/routes"
|
||||
function IndexOption() {
|
||||
const { mode } = useDarkMode()
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
algorithm:
|
||||
mode === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm
|
||||
}}>
|
||||
<StyleProvider hashPriority="high">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<OptionRouting />
|
||||
<ToastContainer />
|
||||
</QueryClientProvider>
|
||||
</StyleProvider>
|
||||
</ConfigProvider>
|
||||
</MemoryRouter>
|
||||
)
|
||||
}
|
||||
|
||||
export default IndexOption
|
||||
14
src/entries/options/index.html
Normal file
14
src/entries/options/index.html
Normal 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>
|
||||
10
src/entries/options/main.tsx
Normal file
10
src/entries/options/main.tsx
Normal 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>,
|
||||
);
|
||||
31
src/entries/sidepanel/App.tsx
Normal file
31
src/entries/sidepanel/App.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
import { MemoryRouter } from "react-router-dom"
|
||||
import { SidepanelRouting } from "~/routes"
|
||||
import { ToastContainer } from "react-toastify"
|
||||
import "react-toastify/dist/ReactToastify.css"
|
||||
const queryClient = new QueryClient()
|
||||
import { ConfigProvider, theme } from "antd"
|
||||
import { StyleProvider } from "@ant-design/cssinjs"
|
||||
import { useDarkMode } from "~/hooks/useDarkmode"
|
||||
function IndexSidepanel() {
|
||||
const { mode } = useDarkMode()
|
||||
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
algorithm:
|
||||
mode === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm
|
||||
}}>
|
||||
<StyleProvider hashPriority="high">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SidepanelRouting />
|
||||
<ToastContainer />
|
||||
</QueryClientProvider>
|
||||
</StyleProvider>
|
||||
</ConfigProvider>
|
||||
</MemoryRouter>
|
||||
)
|
||||
}
|
||||
|
||||
export default IndexSidepanel
|
||||
14
src/entries/sidepanel/index.html
Normal file
14
src/entries/sidepanel/index.html
Normal 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>
|
||||
9
src/entries/sidepanel/main.tsx
Normal file
9
src/entries/sidepanel/main.tsx
Normal 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>
|
||||
)
|
||||
Reference in New Issue
Block a user