chore: Update .gitignore and add .idea folder, update npm dependencies, and improve code logic for chat history and knowledge export/import

This commit is contained in:
n4ze3m 2024-05-11 19:32:36 +05:30
parent 677aa6ef51
commit f9f621c920
18 changed files with 448 additions and 261 deletions

3
.gitignore vendored
View File

@ -42,4 +42,7 @@ keys.json
# typescript # typescript
.tsbuildinfo .tsbuildinfo
# WXT
.wxt .wxt
# WebStorm
.idea

BIN
bun.lockb

Binary file not shown.

View File

@ -5,12 +5,12 @@
"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": "wxt", "dev": "cross-env TARGET=chrome wxt",
"dev:firefox": "wxt -b firefox", "dev:firefox": "cross-env TARGET=firefox wxt -b firefox",
"build": "wxt build", "build": "cross-env TARGET=chrome wxt build",
"build:firefox": "wxt build -b firefox", "build:firefox": "cross-env TARGET=chrome cross-env TARGET=firefox wxt build -b firefox",
"zip": "wxt zip", "zip": "cross-env TARGET=chrome wxt zip",
"zip:firefox": "wxt zip -b firefox", "zip:firefox": "cross-env TARGET=firefox wxt zip -b firefox",
"compile": "tsc --noEmit", "compile": "tsc --noEmit",
"postinstall": "wxt prepare" "postinstall": "wxt prepare"
}, },
@ -66,6 +66,7 @@
"@types/react-syntax-highlighter": "^15.5.11", "@types/react-syntax-highlighter": "^15.5.11",
"@types/turndown": "^5.0.4", "@types/turndown": "^5.0.4",
"autoprefixer": "^10.4.17", "autoprefixer": "^10.4.17",
"cross-env": "^7.0.3",
"postcss": "^8.4.33", "postcss": "^8.4.33",
"prettier": "3.2.4", "prettier": "3.2.4",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",

View File

@ -23,7 +23,7 @@ Click the button below to deploy the code to Railway.
```bash ```bash
git clone https://github.com/n4ze3m/page-share-app.git git clone https://github.com/n4ze3m/page-share-app.git
cd page-assist-app cd page-share-app
``` ```
2. Run the server 2. Run the server

View File

@ -60,7 +60,7 @@ export const ModelsBody = () => {
form.reset() form.reset()
chrome.runtime.sendMessage({ browser.runtime.sendMessage({
type: "pull_model", type: "pull_model",
modelName modelName
}) })

View File

@ -11,7 +11,7 @@ export const AboutApp = () => {
const { data, status } = useQuery({ const { data, status } = useQuery({
queryKey: ["fetchOllamURL"], queryKey: ["fetchOllamURL"],
queryFn: async () => { queryFn: async () => {
const chromeVersion = chrome.runtime.getManifest().version const chromeVersion = browser.runtime.getManifest().version
try { try {
const url = await getOllamaURL() const url = await getOllamaURL()
const req = await fetch(`${cleanUrl(url)}/api/version`) const req = await fetch(`${cleanUrl(url)}/api/version`)

View File

@ -2,6 +2,7 @@ import {
type ChatHistory as ChatHistoryType, type ChatHistory as ChatHistoryType,
type Message as MessageType type Message as MessageType
} from "~/store/option" } from "~/store/option"
import { Storage, browser } from "wxt/browser"
type HistoryInfo = { type HistoryInfo = {
id: string id: string
@ -57,15 +58,18 @@ type ChatHistory = HistoryInfo[]
type Prompts = Prompt[] type Prompts = Prompt[]
export class PageAssitDatabase { export class PageAssitDatabase {
db: chrome.storage.StorageArea db: Storage.LocalStorageArea
constructor() { constructor() {
this.db = chrome.storage.local this.db = browser.storage.local
} }
async getChatHistory(id: string): Promise<MessageHistory> { async getChatHistory(id: string): Promise<MessageHistory> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.get(id, (result) => { // this.db.get(id, (result) => {
// resolve(result[id] || [])
// })
this.db.get(id).then((result) => {
resolve(result[id] || []) resolve(result[id] || [])
}) })
}) })
@ -73,7 +77,10 @@ export class PageAssitDatabase {
async getChatHistories(): Promise<ChatHistory> { async getChatHistories(): Promise<ChatHistory> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.get("chatHistories", (result) => { // this.db.get("chatHistories", (result) => {
// resolve(result.chatHistories || [])
// })
this.db.get("chatHistories").then((result) => {
resolve(result.chatHistories || []) resolve(result.chatHistories || [])
}) })
}) })
@ -126,7 +133,10 @@ export class PageAssitDatabase {
async getAllPrompts(): Promise<Prompts> { async getAllPrompts(): Promise<Prompts> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.get("prompts", (result) => { // this.db.get("prompts", (result) => {
// resolve(result.prompts || [])
// })
this.db.get("prompts").then((result) => {
resolve(result.prompts || []) resolve(result.prompts || [])
}) })
}) })
@ -169,7 +179,10 @@ export class PageAssitDatabase {
async getWebshare(id: string) { async getWebshare(id: string) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.get(id, (result) => { // this.db.get(id, (result) => {
// resolve(result[id] || [])
// })
this.db.get(id).then((result) => {
resolve(result[id] || []) resolve(result[id] || [])
}) })
}) })
@ -177,7 +190,10 @@ export class PageAssitDatabase {
async getAllWebshares(): Promise<Webshare[]> { async getAllWebshares(): Promise<Webshare[]> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.get("webshares", (result) => { // this.db.get("webshares", (result) => {
// resolve(result.webshares || [])
// })
this.db.get("webshares").then((result) => {
resolve(result.webshares || []) resolve(result.webshares || [])
}) })
}) })
@ -197,7 +213,10 @@ export class PageAssitDatabase {
async getUserID() { async getUserID() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.get("user_id", (result) => { // this.db.get("user_id", (result) => {
// resolve(result.user_id || "")
// })
this.db.get("user_id").then((result) => {
resolve(result.user_id || "") resolve(result.user_id || "")
}) })
}) })

View File

@ -1,3 +1,4 @@
import { Storage, browser } from "wxt/browser"
import { deleteVector, deleteVectorByFileId } from "./vector" import { deleteVector, deleteVectorByFileId } from "./vector"
export type Source = { export type Source = {
@ -24,89 +25,105 @@ export const generateID = () => {
}) })
} }
export class PageAssistKnowledge { export class PageAssistKnowledge {
db: chrome.storage.StorageArea db: Storage.LocalStorageArea
constructor() { constructor() {
this.db = chrome.storage.local this.db = browser.storage.local
} }
getAll = async (): Promise<Knowledge[]> => { getAll = async (): Promise<Knowledge[]> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.get(null, (result) => { // this.db.get(null, (result) => {
if (chrome.runtime.lastError) { // if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError) // reject(chrome.runtime.lastError)
} else { // } else {
// const data = Object.keys(result).map((key) => result[key])
// resolve(data)
// }
// })
this.db.get(null).then((result) => {
const data = Object.keys(result).map((key) => result[key]) const data = Object.keys(result).map((key) => result[key])
resolve(data) resolve(data)
}
}) })
}) })
} }
getById = async (id: string): Promise<Knowledge> => { getById = async (id: string): Promise<Knowledge> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.get(id, (result) => { this.db.get(id).then((result) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError)
} else {
resolve(result[id]) resolve(result[id])
}
}) })
}) })
}
}
create = async (knowledge: Knowledge): Promise<void> => { create = async (knowledge: Knowledge): Promise<void> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.set({ [knowledge.id]: knowledge }, () => { // this.db.set({ [knowledge.id]: knowledge }, () => {
if (chrome.runtime.lastError) { // if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError) // reject(chrome.runtime.lastError)
} else { // } else {
// resolve()
// }
// })
this.db.set({ [knowledge.id]: knowledge }).then(() => {
resolve() resolve()
}
}) })
}) })
} }
update = async (knowledge: Knowledge): Promise<void> => { update = async (knowledge: Knowledge): Promise<void> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.set({ [knowledge.id]: knowledge }, () => { // this.db.set({ [knowledge.id]: knowledge }, () => {
if (chrome.runtime.lastError) { // if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError) // reject(chrome.runtime.lastError)
} else { // } else {
// resolve()
// }
// })
this.db.set({ [knowledge.id]: knowledge }).then(() => {
resolve() resolve()
}
}) })
}) })
} }
delete = async (id: string): Promise<void> => { delete = async (id: string): Promise<void> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.remove(id, () => { // this.db.remove(id, () => {
if (chrome.runtime.lastError) { // if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError) // reject(chrome.runtime.lastError)
} else { // } else {
// resolve()
// }
// })
this.db.remove(id).then(() => {
resolve() resolve()
}
}) })
}) })
} }
deleteSource = async (id: string, source_id: string): Promise<void> => { deleteSource = async (id: string, source_id: string): Promise<void> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.get(id, (result) => { // this.db.get(id, (result) => {
if (chrome.runtime.lastError) { // if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError) // reject(chrome.runtime.lastError)
} else { // } else {
// const data = result[id] as Knowledge
// data.source = data.source.filter((s) => s.source_id !== source_id)
// this.db.set({ [id]: data }, () => {
// if (chrome.runtime.lastError) {
// reject(chrome.runtime.lastError)
// } else {
// resolve()
// }
// })
// }
// })
this.db.get(id).then((result) => {
const data = result[id] as Knowledge const data = result[id] as Knowledge
data.source = data.source.filter((s) => s.source_id !== source_id) data.source = data.source.filter((s) => s.source_id !== source_id)
this.db.set({ [id]: data }, () => { this.db.set({ [id]: data }).then(() => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError)
} else {
resolve() resolve()
}
}) })
}
}) })
}) })
} }

View File

@ -1,3 +1,5 @@
import { Storage, browser } from "wxt/browser"
interface PageAssistVector { interface PageAssistVector {
file_id: string file_id: string
content: string content: string
@ -11,10 +13,10 @@ export type VectorData = {
} }
export class PageAssistVectorDb { export class PageAssistVectorDb {
db: chrome.storage.StorageArea db: Storage.LocalStorageArea
constructor() { constructor() {
this.db = chrome.storage.local this.db = browser.storage.local
} }
insertVector = async ( insertVector = async (
@ -22,36 +24,55 @@ export class PageAssistVectorDb {
vector: PageAssistVector[] vector: PageAssistVector[]
): Promise<void> => { ): Promise<void> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.get(id, (result) => { // this.db.get(id, (result) => {
if (chrome.runtime.lastError) { // if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError) // reject(chrome.runtime.lastError)
} else { // } else {
// const data = result[id] as VectorData
// if (!data) {
// this.db.set({ [id]: { id, vectors: vector } }, () => {
// if (chrome.runtime.lastError) {
// reject(chrome.runtime.lastError)
// } else {
// resolve()
// }
// })
// } else {
// this.db.set(
// {
// [id]: {
// ...data,
// vectors: data.vectors.concat(vector)
// }
// },
// () => {
// if (chrome.runtime.lastError) {
// reject(chrome.runtime.lastError)
// } else {
// resolve()
// }
// }
// )
// }
// }
// })
this.db.get(id).then((result) => {
const data = result[id] as VectorData const data = result[id] as VectorData
if (!data) { if (!data) {
this.db.set({ [id]: { id, vectors: vector } }, () => { this.db.set({ [id]: { id, vectors: vector } }).then(() => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError)
} else {
resolve() resolve()
}
}) })
} else { } else {
this.db.set( this.db
{ .set({
[id]: { [id]: {
...data, ...data,
vectors: data.vectors.concat(vector) vectors: data.vectors.concat(vector)
} }
}, })
() => { .then(() => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError)
} else {
resolve() resolve()
} })
}
)
}
} }
}) })
}) })
@ -59,56 +80,72 @@ export class PageAssistVectorDb {
deleteVector = async (id: string): Promise<void> => { deleteVector = async (id: string): Promise<void> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.remove(id, () => { // this.db.remove(id, () => {
if (chrome.runtime.lastError) { // if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError) // reject(chrome.runtime.lastError)
} else { // } else {
// resolve()
// }
// })
this.db.remove(id).then(() => {
resolve() resolve()
}
}) })
}) })
} }
deleteVectorByFileId = async (id: string, file_id: string): Promise<void> => { deleteVectorByFileId = async (id: string, file_id: string): Promise<void> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.get(id, (result) => { // this.db.get(id, (result) => {
if (chrome.runtime.lastError) { // if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError) // reject(chrome.runtime.lastError)
} else { // } else {
// const data = result[id] as VectorData
// data.vectors = data.vectors.filter((v) => v.file_id !== file_id)
// this.db.set({ [id]: data }, () => {
// if (chrome.runtime.lastError) {
// reject(chrome.runtime.lastError)
// } else {
// resolve()
// }
// })
// }
// })
this.db.get(id).then((result) => {
const data = result[id] as VectorData const data = result[id] as VectorData
data.vectors = data.vectors.filter((v) => v.file_id !== file_id) data.vectors = data.vectors.filter((v) => v.file_id !== file_id)
this.db.set({ [id]: data }, () => { this.db.set({ [id]: data }).then(() => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError)
} else {
resolve() resolve()
}
}) })
}
}) })
}) })
} }
getVector = async (id: string): Promise<VectorData> => { getVector = async (id: string): Promise<VectorData> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.get(id, (result) => { // this.db.get(id, (result) => {
if (chrome.runtime.lastError) { // if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError) // reject(chrome.runtime.lastError)
} else { // } else {
// resolve(result[id] as VectorData)
// }
// })
this.db.get(id).then((result) => {
resolve(result[id] as VectorData) resolve(result[id] as VectorData)
}
}) })
}) })
} }
getAll = async (): Promise<VectorData[]> => { getAll = async (): Promise<VectorData[]> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.get(null, (result) => { // this.db.get(null, (result) => {
if (chrome.runtime.lastError) { // if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError) // reject(chrome.runtime.lastError)
} else { // } else {
// resolve(Object.values(result))
// }
// })
this.db.get(null).then((result) => {
resolve(Object.values(result)) resolve(Object.values(result))
}
}) })
}) })
} }
@ -119,12 +156,15 @@ export class PageAssistVectorDb {
data.forEach((d) => { data.forEach((d) => {
obj[d.id] = d obj[d.id] = d
}) })
this.db.set(obj, () => { // this.db.set(obj, () => {
if (chrome.runtime.lastError) { // if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError) // reject(chrome.runtime.lastError)
} else { // } else {
// resolve()
// }
// })
this.db.set(obj).then(() => {
resolve() resolve()
}
}) })
}) })
} }

View File

@ -1,13 +1,13 @@
import { getOllamaURL, isOllamaRunning } from "../services/ollama" import { getOllamaURL, isOllamaRunning } from "../services/ollama"
import { Storage } from "@plasmohq/storage" import { browser } from "wxt/browser"
const progressHuman = (completed: number, total: number) => { const progressHuman = (completed: number, total: number) => {
return ((completed / total) * 100).toFixed(0) + "%" return ((completed / total) * 100).toFixed(0) + "%"
} }
const clearBadge = () => { const clearBadge = () => {
chrome.action.setBadgeText({ text: "" }) browser.action.setBadgeText({ text: "" })
chrome.action.setTitle({ title: "" }) browser.action.setTitle({ title: "" })
} }
const streamDownload = async (url: string, model: string) => { const streamDownload = async (url: string, model: string) => {
url += "/api/pull" url += "/api/pull"
@ -42,16 +42,16 @@ const streamDownload = async (url: string, model: string) => {
completed?: number completed?: number
} }
if (json.total && json.completed) { if (json.total && json.completed) {
chrome.action.setBadgeText({ browser.action.setBadgeText({
text: progressHuman(json.completed, json.total) text: progressHuman(json.completed, json.total)
}) })
chrome.action.setBadgeBackgroundColor({ color: "#0000FF" }) browser.action.setBadgeBackgroundColor({ color: "#0000FF" })
} else { } else {
chrome.action.setBadgeText({ text: "🏋️‍♂️" }) browser.action.setBadgeText({ text: "🏋️‍♂️" })
chrome.action.setBadgeBackgroundColor({ color: "#FFFFFF" }) browser.action.setBadgeBackgroundColor({ color: "#FFFFFF" })
} }
chrome.action.setTitle({ title: json.status }) browser.action.setTitle({ title: json.status })
if (json.status === "success") { if (json.status === "success") {
isSuccess = true isSuccess = true
@ -62,13 +62,13 @@ const streamDownload = async (url: string, model: string) => {
} }
if (isSuccess) { if (isSuccess) {
chrome.action.setBadgeText({ text: "✅" }) browser.action.setBadgeText({ text: "✅" })
chrome.action.setBadgeBackgroundColor({ color: "#00FF00" }) browser.action.setBadgeBackgroundColor({ color: "#00FF00" })
chrome.action.setTitle({ title: "Model pulled successfully" }) browser.action.setTitle({ title: "Model pulled successfully" })
} else { } else {
chrome.action.setBadgeText({ text: "❌" }) browser.action.setBadgeText({ text: "❌" })
chrome.action.setBadgeBackgroundColor({ color: "#FF0000" }) browser.action.setBadgeBackgroundColor({ color: "#FF0000" })
chrome.action.setTitle({ title: "Model pull failed" }) browser.action.setTitle({ title: "Model pull failed" })
} }
setTimeout(() => { setTimeout(() => {
@ -77,29 +77,18 @@ const streamDownload = async (url: string, model: string) => {
} }
export default defineBackground({ export default defineBackground({
main() { main() {
const storage = new Storage() browser.runtime.onMessage.addListener(async (message) => {
chrome.runtime.onMessage.addListener(async (message) => {
if (message.type === "sidepanel") { if (message.type === "sidepanel") {
chrome.tabs.query( browser.sidebarAction.open()
{ active: true, currentWindow: true },
async (tabs) => {
const tab = tabs[0]
chrome.sidePanel.open({
// tabId: tab.id!,
windowId: tab.windowId!
})
}
)
} else if (message.type === "pull_model") { } else if (message.type === "pull_model") {
const ollamaURL = await getOllamaURL() const ollamaURL = await getOllamaURL()
const isRunning = await isOllamaRunning() const isRunning = await isOllamaRunning()
if (!isRunning) { if (!isRunning) {
chrome.action.setBadgeText({ text: "E" }) browser.action.setBadgeText({ text: "E" })
chrome.action.setBadgeBackgroundColor({ color: "#FF0000" }) browser.action.setBadgeBackgroundColor({ color: "#FF0000" })
chrome.action.setTitle({ title: "Ollama is not running" }) browser.action.setTitle({ title: "Ollama is not running" })
setTimeout(() => { setTimeout(() => {
clearBadge() clearBadge()
}, 5000) }, 5000)
@ -109,35 +98,25 @@ export default defineBackground({
} }
}) })
chrome.action.onClicked.addListener((tab) => { if (browser?.action) {
chrome.tabs.create({ url: chrome.runtime.getURL("options.html") }) browser.action.onClicked.addListener((tab) => {
console.log("browser.action.onClicked.addListener")
browser.tabs.create({ url: browser.runtime.getURL("/options.html") })
}) })
} else {
chrome.commands.onCommand.addListener((command) => { browser.browserAction.onClicked.addListener((tab) => {
switch (command) { console.log("browser.browserAction.onClicked.addListener")
case "execute_side_panel": browser.tabs.create({ url: browser.runtime.getURL("/options.html") })
chrome.tabs.query(
{ active: true, currentWindow: true },
async (tabs) => {
const tab = tabs[0]
chrome.sidePanel.open({
windowId: tab.windowId!
}) })
} }
)
break
default:
break
}
})
chrome.contextMenus.create({ browser.contextMenus.create({
id: "open-side-panel-pa", id: "open-side-panel-pa",
title: browser.i18n.getMessage("openSidePanelToChat"), title: browser.i18n.getMessage("openSidePanelToChat"),
contexts: ["all"] contexts: ["all"]
}) })
if (import.meta.env.BROWSER === "chrome") {
chrome.contextMenus.onClicked.addListener((info, tab) => { browser.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === "open-side-panel-pa") { if (info.menuItemId === "open-side-panel-pa") {
chrome.tabs.query( chrome.tabs.query(
{ active: true, currentWindow: true }, { active: true, currentWindow: true },
@ -150,6 +129,43 @@ export default defineBackground({
) )
} }
}) })
browser.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
}
})
}
if (import.meta.env.BROWSER === "firefox") {
browser.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === "open-side-panel-pa") {
browser.sidebarAction.toggle()
}
})
browser.commands.onCommand.addListener((command) => {
switch (command) {
case "execute_side_panel":
browser.sidebarAction.toggle()
break
default:
break
}
})
}
}, },
persistent: true persistent: true
}) })

View File

@ -9,7 +9,7 @@ export default defineContentScript({
`[Page Assist Extension] Pulling ${modelName} model. For more details, check the extension icon.` `[Page Assist Extension] Pulling ${modelName} model. For more details, check the extension icon.`
) )
await chrome.runtime.sendMessage({ await browser.runtime.sendMessage({
type: "pull_model", type: "pull_model",
modelName modelName
}) })

View File

@ -4,6 +4,7 @@
<title>Page Assist - A Web UI for Local AI Models</title> <title>Page Assist - A Web UI for Local AI Models</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="manifest.type" content="browser_action" /> <meta name="manifest.type" content="browser_action" />
<meta name="manifest.open_at_install" content="false" />
<link href="~/assets/tailwind.css" rel="stylesheet" /> <link href="~/assets/tailwind.css" rel="stylesheet" />
<meta charset="utf-8" /> <meta charset="utf-8" />
</head> </head>

View File

@ -2,7 +2,6 @@ import { useEffect, useState } from "react"
import { notification } from "antd" import { notification } from "antd"
import { getVoice, isSSMLEnabled } from "@/services/tts" import { getVoice, isSSMLEnabled } from "@/services/tts"
import { markdownToSSML } from "@/utils/markdown-to-ssml" import { markdownToSSML } from "@/utils/markdown-to-ssml"
type VoiceOptions = { type VoiceOptions = {
utterance: string utterance: string
} }
@ -17,6 +16,7 @@ export const useTTS = () => {
if (isSSML) { if (isSSML) {
utterance = markdownToSSML(utterance) utterance = markdownToSSML(utterance)
} }
if (import.meta.env.BROWSER === "chrome") {
chrome.tts.speak(utterance, { chrome.tts.speak(utterance, {
voiceName: voice, voiceName: voice,
onEvent(event) { onEvent(event) {
@ -27,6 +27,17 @@ export const useTTS = () => {
} }
} }
}) })
} else {
// browser tts
window.speechSynthesis.speak(new SpeechSynthesisUtterance(utterance))
window.speechSynthesis.onvoiceschanged = () => {
const voices = window.speechSynthesis.getVoices()
const voice = voices.find((v) => v.name === voice)
const utter = new SpeechSynthesisUtterance(utterance)
utter.voice = voice
window.speechSynthesis.speak(utter)
}
}
} catch (error) { } catch (error) {
notification.error({ notification.error({
message: "Error", message: "Error",
@ -36,7 +47,11 @@ export const useTTS = () => {
} }
const cancel = () => { const cancel = () => {
if (import.meta.env.BROWSER === "chrome") {
chrome.tts.stop() chrome.tts.stop()
} else {
window.speechSynthesis.cancel()
}
setIsSpeaking(false) setIsSpeaking(false)
} }

View File

@ -1,5 +1,6 @@
export const chromeRunTime = async function (domain: string) { export const chromeRunTime = async function (domain: string) {
if (typeof chrome !== "undefined" && chrome.runtime && chrome.runtime.id) { if (browser.runtime && browser.runtime.id) {
if (import.meta.env.BROWSER === "chrome") {
const url = new URL(domain) const url = new URL(domain)
const domains = [url.hostname] const domains = [url.hostname]
const rules = [ const rules = [
@ -22,10 +23,29 @@ export const chromeRunTime = async function (domain: string) {
} }
] ]
await chrome.declarativeNetRequest.updateDynamicRules({ await browser.declarativeNetRequest.updateDynamicRules({
removeRuleIds: rules.map((r) => r.id), removeRuleIds: rules.map((r) => r.id),
// @ts-ignore // @ts-ignore
addRules: rules addRules: rules
}) })
} }
if (import.meta.env.BROWSER === "firefox") {
const url = new URL(domain)
const domains = [`*://${url.hostname}/*`]
browser.webRequest.onBeforeSendHeaders.addListener(
(details) => {
for (let i = 0; i < details.requestHeaders.length; i++) {
if (details.requestHeaders[i].name === "Origin") {
details.requestHeaders[i].value =
`${url.protocol}//${url.hostname}`
}
}
return { requestHeaders: details.requestHeaders }
},
{ urls: domains },
["blocking", "requestHeaders"]
)
}
}
} }

View File

@ -1,4 +1,3 @@
export const isGoogleDocs = (url: string) => { export const isGoogleDocs = (url: string) => {
const GOOGLE_DOCS_REGEX = /docs\.google\.com\/document/g const GOOGLE_DOCS_REGEX = /docs\.google\.com\/document/g
return GOOGLE_DOCS_REGEX.test(url) return GOOGLE_DOCS_REGEX.test(url)

20
src/services/app.ts Normal file
View File

@ -0,0 +1,20 @@
import { Storage } from "@plasmohq/storage"
const storage = new Storage()
export const isUrlRewriteEnabled = async () => {
const enabled = await storage.get("urlRewriteEnabled")
return enabled === "true"
}
export const setUrlRewriteEnabled = async (enabled: boolean) => {
await storage.set("urlRewriteEnabled", enabled ? "true" : "false")
}
export const getRewriteUrl = async () => {
const rewriteUrl = await storage.get("rewriteUrl")
return rewriteUrl
}
export const setRewriteUrl = async (url: string) => {
await storage.set("rewriteUrl", url)
}

View File

@ -21,8 +21,16 @@ export const setTTSProvider = async (ttsProvider: string) => {
} }
export const getBrowserTTSVoices = async () => { export const getBrowserTTSVoices = async () => {
if (import.meta.env.BROWSER === "chrome") {
const tts = await chrome.tts.getVoices() const tts = await chrome.tts.getVoices()
return tts return tts
} else {
const tts = await speechSynthesis.getVoices()
return tts.map((voice) => ({
voiceName: voice.name,
lang: voice.lang
}))
}
} }
export const getVoice = async () => { export const getVoice = async () => {

View File

@ -2,35 +2,70 @@ import { defineConfig } from "wxt"
import react from "@vitejs/plugin-react" import react from "@vitejs/plugin-react"
import topLevelAwait from "vite-plugin-top-level-await" import topLevelAwait from "vite-plugin-top-level-await"
const chromeMV3Permissions = [
"storage",
"sidePanel",
"activeTab",
"scripting",
"declarativeNetRequest",
"action",
"unlimitedStorage",
"contextMenus",
"tts"
]
const firefoxMV2Permissions = [
"storage",
"activeTab",
"scripting",
"unlimitedStorage",
"contextMenus",
"webRequest",
"webRequestBlocking",
"http://*/*",
"https://*/*",
"file://*/*"
]
// See https://wxt.dev/api/config.html // See https://wxt.dev/api/config.html
export default defineConfig({ export default defineConfig({
vite: () => ({ vite: () => ({
plugins: [react(), plugins: [
react(),
topLevelAwait({ topLevelAwait({
promiseExportName: '__tla', promiseExportName: "__tla",
promiseImportName: i => `__tla_${i}`, promiseImportName: (i) => `__tla_${i}`
}), })
], ],
build: { build: {
rollupOptions: { rollupOptions: {
external: [ external: ["langchain", "@langchain/community"]
"langchain",
"@langchain/community",
]
} }
} }
}), }),
entrypointsDir: "entries", entrypointsDir: "entries",
srcDir: "src", srcDir: "src",
outDir: "build", outDir: "build",
manifest: { manifest: {
version: "1.1.6", version: "1.1.7",
name: '__MSG_extName__', name: "__MSG_extName__",
description: '__MSG_extDescription__', description: "__MSG_extDescription__",
default_locale: 'en', default_locale: "en",
action: {}, action: {},
author: "n4ze3m", author: "n4ze3m",
host_permissions: ["http://*/*", "https://*/*", "file://*/*"], browser_specific_settings:
process.env.TARGET === "firefox"
? {
gecko: {
id: "page-assist@n4ze3m"
}
}
: undefined,
host_permissions:
process.env.TARGET !== "firefox"
? ["http://*/*", "https://*/*", "file://*/*"]
: undefined,
commands: { commands: {
_execute_action: { _execute_action: {
suggested_key: { suggested_key: {
@ -44,16 +79,9 @@ export default defineConfig({
} }
} }
}, },
permissions: [ permissions:
"storage", process.env.TARGET === "firefox"
"sidePanel", ? firefoxMV2Permissions
"activeTab", : chromeMV3Permissions
"scripting",
"declarativeNetRequest",
"action",
"unlimitedStorage",
"contextMenus",
"tts"
]
} }
}) })