From f9f621c9207b12eb08569bf58e0f5942341e3f20 Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Sat, 11 May 2024 19:32:36 +0530 Subject: [PATCH 01/16] chore: Update .gitignore and add .idea folder, update npm dependencies, and improve code logic for chat history and knowledge export/import --- .gitignore | 5 +- bun.lockb | Bin 407578 -> 408050 bytes package.json | 13 +- page-share.md | 2 +- src/components/Option/Models/index.tsx | 2 +- src/components/Option/Settings/about.tsx | 2 +- src/db/index.ts | 35 +++-- src/db/knowledge.ts | 113 ++++++++------- src/db/vector.ts | 176 ++++++++++++++--------- src/entries/background.ts | 138 ++++++++++-------- src/entries/ollama-pull.content.ts | 2 +- src/entries/sidepanel/index.html | 1 + src/hooks/useTTS.tsx | 35 +++-- src/libs/runtime.ts | 72 ++++++---- src/parser/google-docs.ts | 7 +- src/services/app.ts | 20 +++ src/services/tts.ts | 12 +- wxt.config.ts | 76 ++++++---- 18 files changed, 449 insertions(+), 262 deletions(-) create mode 100644 src/services/app.ts diff --git a/.gitignore b/.gitignore index c76b5c4..a396c30 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,7 @@ keys.json # typescript .tsbuildinfo -.wxt \ No newline at end of file +# WXT +.wxt +# WebStorm +.idea \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index a4deaa6df68de385556b7c30ab2c4cef9681ab38..11bd10c885c60add1bcbc85a7861b8eb65824abf 100644 GIT binary patch delta 60253 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 delta 59667 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 z zP-q^cQ$vgL&}osHzR&w_HP!yNhK5rZR9Ia>IlTVsA1hJu7Zuqs%Imi zkXqgNvQqY)qJJ8db@w;kZPnBNWLn9M7gNduW%<9VzzArd?gI$1maSbJ^RRTy0p1{_ zdI=?|Xh~-nwO>cF&&Mi?+my&^cS2VC3`ORHm2tF|Yl>g6Y)p2Klo3*cBH+p+*}zvr z9Z-~*X26XJXBT`0f;2q2+N#nrC;>?H3Q1~$dO8`bS^e7YP`WMGHc;~@ZevwRHKuIkr@g+{{ zcu}j7G-=n}y9V{MUfBXr-WuKU>ozp+1%`&NRZ!@90>?47?E5pwe*XG&1@UkQF%F0U zlmmeKbO1Px;PX}9)WN-iI~V|~P{SDm@3T2+EBgEqWT@FgjxTvZ0bq9P<(*8QT($Eo z1K=U~0RYcGgmE<;rOi6|)8h2A=O+28H|wzDi19d-(`hs!-55jf8UJ}Gy1I*0+y>Vw9!?}OQq~K)H5PlHrxXhoGb{Zitlg%#{+ltkpR+ayeSoh?*A>z@MA=6`8O$POO2WHh&Au}NoC;S-_(qK%>Q)P=&Vb|a3Q(-m8 zyFjriAI(3*gFmN28d9LNhREY{5^w2n1f?eyOd5aJeF>gf7(zx|Xm`WkrNXD5emF&b{mS)Thm|o7JK>g-WY>Upq^! zo8swyN;AGv;Q;>KHT=EhU#Y@h`4(e5Th?@@0gaSul=4Bzo0?EMIl<(nQ;M;R$qke8 go*oj{+Jhc?nl!5LoBBMPx# diff --git a/package.json b/package.json index 98f5370..ec33b1b 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,12 @@ "description": "Use your locally running AI models to assist you in your web browsing.", "author": "n4ze3m", "scripts": { - "dev": "wxt", - "dev:firefox": "wxt -b firefox", - "build": "wxt build", - "build:firefox": "wxt build -b firefox", - "zip": "wxt zip", - "zip:firefox": "wxt zip -b firefox", + "dev": "cross-env TARGET=chrome wxt", + "dev:firefox": "cross-env TARGET=firefox wxt -b firefox", + "build": "cross-env TARGET=chrome wxt build", + "build:firefox": "cross-env TARGET=chrome cross-env TARGET=firefox wxt build -b firefox", + "zip": "cross-env TARGET=chrome wxt zip", + "zip:firefox": "cross-env TARGET=firefox wxt zip -b firefox", "compile": "tsc --noEmit", "postinstall": "wxt prepare" }, @@ -66,6 +66,7 @@ "@types/react-syntax-highlighter": "^15.5.11", "@types/turndown": "^5.0.4", "autoprefixer": "^10.4.17", + "cross-env": "^7.0.3", "postcss": "^8.4.33", "prettier": "3.2.4", "tailwindcss": "^3.4.1", diff --git a/page-share.md b/page-share.md index f0646f2..a0b43d4 100644 --- a/page-share.md +++ b/page-share.md @@ -23,7 +23,7 @@ Click the button below to deploy the code to Railway. ```bash git clone https://github.com/n4ze3m/page-share-app.git -cd page-assist-app +cd page-share-app ``` 2. Run the server diff --git a/src/components/Option/Models/index.tsx b/src/components/Option/Models/index.tsx index 7728484..e342edc 100644 --- a/src/components/Option/Models/index.tsx +++ b/src/components/Option/Models/index.tsx @@ -60,7 +60,7 @@ export const ModelsBody = () => { form.reset() - chrome.runtime.sendMessage({ + browser.runtime.sendMessage({ type: "pull_model", modelName }) diff --git a/src/components/Option/Settings/about.tsx b/src/components/Option/Settings/about.tsx index 024bf82..6250ab1 100644 --- a/src/components/Option/Settings/about.tsx +++ b/src/components/Option/Settings/about.tsx @@ -11,7 +11,7 @@ export const AboutApp = () => { const { data, status } = useQuery({ queryKey: ["fetchOllamURL"], queryFn: async () => { - const chromeVersion = chrome.runtime.getManifest().version + const chromeVersion = browser.runtime.getManifest().version try { const url = await getOllamaURL() const req = await fetch(`${cleanUrl(url)}/api/version`) diff --git a/src/db/index.ts b/src/db/index.ts index 6ca341f..9f6e5b9 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -2,6 +2,7 @@ import { type ChatHistory as ChatHistoryType, type Message as MessageType } from "~/store/option" +import { Storage, browser } from "wxt/browser" type HistoryInfo = { id: string @@ -57,15 +58,18 @@ type ChatHistory = HistoryInfo[] type Prompts = Prompt[] export class PageAssitDatabase { - db: chrome.storage.StorageArea + db: Storage.LocalStorageArea constructor() { - this.db = chrome.storage.local + this.db = browser.storage.local } async getChatHistory(id: string): Promise { 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] || []) }) }) @@ -73,7 +77,10 @@ export class PageAssitDatabase { async getChatHistories(): Promise { 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 || []) }) }) @@ -126,7 +133,10 @@ export class PageAssitDatabase { async getAllPrompts(): Promise { 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 || []) }) }) @@ -169,7 +179,10 @@ export class PageAssitDatabase { async getWebshare(id: string) { 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] || []) }) }) @@ -177,7 +190,10 @@ export class PageAssitDatabase { async getAllWebshares(): Promise { 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 || []) }) }) @@ -197,7 +213,10 @@ export class PageAssitDatabase { async getUserID() { 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 || "") }) }) diff --git a/src/db/knowledge.ts b/src/db/knowledge.ts index 9579536..a46c68f 100644 --- a/src/db/knowledge.ts +++ b/src/db/knowledge.ts @@ -1,3 +1,4 @@ +import { Storage, browser } from "wxt/browser" import { deleteVector, deleteVectorByFileId } from "./vector" export type Source = { @@ -24,89 +25,105 @@ export const generateID = () => { }) } export class PageAssistKnowledge { - db: chrome.storage.StorageArea + db: Storage.LocalStorageArea constructor() { - this.db = chrome.storage.local + this.db = browser.storage.local } getAll = async (): Promise => { return new Promise((resolve, reject) => { - this.db.get(null, (result) => { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError) - } else { - const data = Object.keys(result).map((key) => result[key]) - resolve(data) - } + // this.db.get(null, (result) => { + // if (chrome.runtime.lastError) { + // reject(chrome.runtime.lastError) + // } 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]) + resolve(data) }) }) } getById = async (id: string): Promise => { return new Promise((resolve, reject) => { - this.db.get(id, (result) => { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError) - } else { - resolve(result[id]) - } + this.db.get(id).then((result) => { + resolve(result[id]) }) }) + } - create = async (knowledge: Knowledge): Promise => { return new Promise((resolve, reject) => { - this.db.set({ [knowledge.id]: knowledge }, () => { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError) - } else { - resolve() - } + // this.db.set({ [knowledge.id]: knowledge }, () => { + // if (chrome.runtime.lastError) { + // reject(chrome.runtime.lastError) + // } else { + // resolve() + // } + // }) + this.db.set({ [knowledge.id]: knowledge }).then(() => { + resolve() }) }) } update = async (knowledge: Knowledge): Promise => { return new Promise((resolve, reject) => { - this.db.set({ [knowledge.id]: knowledge }, () => { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError) - } else { - resolve() - } + // this.db.set({ [knowledge.id]: knowledge }, () => { + // if (chrome.runtime.lastError) { + // reject(chrome.runtime.lastError) + // } else { + // resolve() + // } + // }) + this.db.set({ [knowledge.id]: knowledge }).then(() => { + resolve() }) }) } delete = async (id: string): Promise => { return new Promise((resolve, reject) => { - this.db.remove(id, () => { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError) - } else { - resolve() - } + // this.db.remove(id, () => { + // if (chrome.runtime.lastError) { + // reject(chrome.runtime.lastError) + // } else { + // resolve() + // } + // }) + this.db.remove(id).then(() => { + resolve() }) }) } deleteSource = async (id: string, source_id: string): Promise => { return new Promise((resolve, reject) => { - this.db.get(id, (result) => { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError) - } 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, (result) => { + // if (chrome.runtime.lastError) { + // reject(chrome.runtime.lastError) + // } 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 + data.source = data.source.filter((s) => s.source_id !== source_id) + this.db.set({ [id]: data }).then(() => { + resolve() + }) }) }) } diff --git a/src/db/vector.ts b/src/db/vector.ts index 09624fc..761f1a1 100644 --- a/src/db/vector.ts +++ b/src/db/vector.ts @@ -1,3 +1,5 @@ +import { Storage, browser } from "wxt/browser" + interface PageAssistVector { file_id: string content: string @@ -11,10 +13,10 @@ export type VectorData = { } export class PageAssistVectorDb { - db: chrome.storage.StorageArea + db: Storage.LocalStorageArea constructor() { - this.db = chrome.storage.local + this.db = browser.storage.local } insertVector = async ( @@ -22,36 +24,55 @@ export class PageAssistVectorDb { vector: PageAssistVector[] ): Promise => { return new Promise((resolve, reject) => { - this.db.get(id, (result) => { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError) + // this.db.get(id, (result) => { + // if (chrome.runtime.lastError) { + // reject(chrome.runtime.lastError) + // } 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 + if (!data) { + this.db.set({ [id]: { id, vectors: vector } }).then(() => { + resolve() + }) } 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() + this.db + .set({ + [id]: { + ...data, + vectors: data.vectors.concat(vector) } }) - } else { - this.db.set( - { - [id]: { - ...data, - vectors: data.vectors.concat(vector) - } - }, - () => { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError) - } else { - resolve() - } - } - ) - } + .then(() => { + resolve() + }) } }) }) @@ -59,56 +80,72 @@ export class PageAssistVectorDb { deleteVector = async (id: string): Promise => { return new Promise((resolve, reject) => { - this.db.remove(id, () => { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError) - } else { - resolve() - } + // this.db.remove(id, () => { + // if (chrome.runtime.lastError) { + // reject(chrome.runtime.lastError) + // } else { + // resolve() + // } + // }) + this.db.remove(id).then(() => { + resolve() }) }) } deleteVectorByFileId = async (id: string, file_id: string): Promise => { return new Promise((resolve, reject) => { - this.db.get(id, (result) => { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError) - } 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, (result) => { + // if (chrome.runtime.lastError) { + // reject(chrome.runtime.lastError) + // } 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 + data.vectors = data.vectors.filter((v) => v.file_id !== file_id) + this.db.set({ [id]: data }).then(() => { + resolve() + }) }) }) } getVector = async (id: string): Promise => { return new Promise((resolve, reject) => { - this.db.get(id, (result) => { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError) - } else { - resolve(result[id] as VectorData) - } + // this.db.get(id, (result) => { + // if (chrome.runtime.lastError) { + // reject(chrome.runtime.lastError) + // } else { + // resolve(result[id] as VectorData) + // } + // }) + this.db.get(id).then((result) => { + resolve(result[id] as VectorData) }) }) } getAll = async (): Promise => { return new Promise((resolve, reject) => { - this.db.get(null, (result) => { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError) - } else { - resolve(Object.values(result)) - } + // this.db.get(null, (result) => { + // if (chrome.runtime.lastError) { + // reject(chrome.runtime.lastError) + // } else { + // resolve(Object.values(result)) + // } + // }) + this.db.get(null).then((result) => { + resolve(Object.values(result)) }) }) } @@ -119,12 +156,15 @@ export class PageAssistVectorDb { data.forEach((d) => { obj[d.id] = d }) - this.db.set(obj, () => { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError) - } else { - resolve() - } + // this.db.set(obj, () => { + // if (chrome.runtime.lastError) { + // reject(chrome.runtime.lastError) + // } else { + // resolve() + // } + // }) + this.db.set(obj).then(() => { + resolve() }) }) } @@ -164,5 +204,5 @@ export const exportVectors = async () => { export const importVectors = async (data: VectorData[]) => { const db = new PageAssistVectorDb() - return db.saveImportedData(data) + return db.saveImportedData(data) } diff --git a/src/entries/background.ts b/src/entries/background.ts index f671467..61e7328 100644 --- a/src/entries/background.ts +++ b/src/entries/background.ts @@ -1,13 +1,13 @@ import { getOllamaURL, isOllamaRunning } from "../services/ollama" -import { Storage } from "@plasmohq/storage" +import { browser } from "wxt/browser" const progressHuman = (completed: number, total: number) => { return ((completed / total) * 100).toFixed(0) + "%" } const clearBadge = () => { - chrome.action.setBadgeText({ text: "" }) - chrome.action.setTitle({ title: "" }) + browser.action.setBadgeText({ text: "" }) + browser.action.setTitle({ title: "" }) } const streamDownload = async (url: string, model: string) => { url += "/api/pull" @@ -42,16 +42,16 @@ const streamDownload = async (url: string, model: string) => { completed?: number } if (json.total && json.completed) { - chrome.action.setBadgeText({ + browser.action.setBadgeText({ text: progressHuman(json.completed, json.total) }) - chrome.action.setBadgeBackgroundColor({ color: "#0000FF" }) + browser.action.setBadgeBackgroundColor({ color: "#0000FF" }) } else { - chrome.action.setBadgeText({ text: "🏋️‍♂️" }) - chrome.action.setBadgeBackgroundColor({ color: "#FFFFFF" }) + browser.action.setBadgeText({ text: "🏋️‍♂️" }) + browser.action.setBadgeBackgroundColor({ color: "#FFFFFF" }) } - chrome.action.setTitle({ title: json.status }) + browser.action.setTitle({ title: json.status }) if (json.status === "success") { isSuccess = true @@ -62,13 +62,13 @@ const streamDownload = async (url: string, model: string) => { } if (isSuccess) { - chrome.action.setBadgeText({ text: "✅" }) - chrome.action.setBadgeBackgroundColor({ color: "#00FF00" }) - chrome.action.setTitle({ title: "Model pulled successfully" }) + browser.action.setBadgeText({ text: "✅" }) + browser.action.setBadgeBackgroundColor({ color: "#00FF00" }) + browser.action.setTitle({ title: "Model pulled successfully" }) } else { - chrome.action.setBadgeText({ text: "❌" }) - chrome.action.setBadgeBackgroundColor({ color: "#FF0000" }) - chrome.action.setTitle({ title: "Model pull failed" }) + browser.action.setBadgeText({ text: "❌" }) + browser.action.setBadgeBackgroundColor({ color: "#FF0000" }) + browser.action.setTitle({ title: "Model pull failed" }) } setTimeout(() => { @@ -77,29 +77,18 @@ const streamDownload = async (url: string, model: string) => { } export default defineBackground({ main() { - const storage = new Storage() - - chrome.runtime.onMessage.addListener(async (message) => { + browser.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!, - windowId: tab.windowId! - }) - } - ) + browser.sidebarAction.open() } 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" }) + browser.action.setBadgeText({ text: "E" }) + browser.action.setBadgeBackgroundColor({ color: "#FF0000" }) + browser.action.setTitle({ title: "Ollama is not running" }) setTimeout(() => { clearBadge() }, 5000) @@ -109,47 +98,74 @@ export default defineBackground({ } }) - chrome.action.onClicked.addListener((tab) => { - chrome.tabs.create({ url: chrome.runtime.getURL("options.html") }) - }) + if (browser?.action) { + browser.action.onClicked.addListener((tab) => { + console.log("browser.action.onClicked.addListener") + browser.tabs.create({ url: browser.runtime.getURL("/options.html") }) + }) + } else { + browser.browserAction.onClicked.addListener((tab) => { + console.log("browser.browserAction.onClicked.addListener") + browser.tabs.create({ url: browser.runtime.getURL("/options.html") }) + }) + } - chrome.commands.onCommand.addListener((command) => { - switch (command) { - case "execute_side_panel": + browser.contextMenus.create({ + id: "open-side-panel-pa", + title: browser.i18n.getMessage("openSidePanelToChat"), + contexts: ["all"] + }) + if (import.meta.env.BROWSER === "chrome") { + browser.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] chrome.sidePanel.open({ - windowId: tab.windowId! + tabId: tab.id! }) } ) - break - default: - break - } - }) - - chrome.contextMenus.create({ - id: "open-side-panel-pa", - title: browser.i18n.getMessage("openSidePanelToChat"), - contexts: ["all"] - }) + } + }) + + 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 + } + }) + } - 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] - chrome.sidePanel.open({ - tabId: tab.id! - }) - } - ) - } - }) + 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 }) diff --git a/src/entries/ollama-pull.content.ts b/src/entries/ollama-pull.content.ts index 2740f44..6acbccd 100644 --- a/src/entries/ollama-pull.content.ts +++ b/src/entries/ollama-pull.content.ts @@ -9,7 +9,7 @@ export default defineContentScript({ `[Page Assist Extension] Pulling ${modelName} model. For more details, check the extension icon.` ) - await chrome.runtime.sendMessage({ + await browser.runtime.sendMessage({ type: "pull_model", modelName }) diff --git a/src/entries/sidepanel/index.html b/src/entries/sidepanel/index.html index 2abb927..f18de84 100644 --- a/src/entries/sidepanel/index.html +++ b/src/entries/sidepanel/index.html @@ -4,6 +4,7 @@ Page Assist - A Web UI for Local AI Models + diff --git a/src/hooks/useTTS.tsx b/src/hooks/useTTS.tsx index 075f005..5274956 100644 --- a/src/hooks/useTTS.tsx +++ b/src/hooks/useTTS.tsx @@ -2,7 +2,6 @@ import { useEffect, useState } from "react" import { notification } from "antd" import { getVoice, isSSMLEnabled } from "@/services/tts" import { markdownToSSML } from "@/utils/markdown-to-ssml" - type VoiceOptions = { utterance: string } @@ -17,16 +16,28 @@ export const useTTS = () => { if (isSSML) { utterance = markdownToSSML(utterance) } - chrome.tts.speak(utterance, { - voiceName: voice, - onEvent(event) { - if (event.type === "start") { - setIsSpeaking(true) - } else if (event.type === "end") { - setIsSpeaking(false) + if (import.meta.env.BROWSER === "chrome") { + chrome.tts.speak(utterance, { + voiceName: voice, + onEvent(event) { + if (event.type === "start") { + setIsSpeaking(true) + } else if (event.type === "end") { + setIsSpeaking(false) + } } + }) + } 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) { notification.error({ message: "Error", @@ -36,7 +47,11 @@ export const useTTS = () => { } const cancel = () => { - chrome.tts.stop() + if (import.meta.env.BROWSER === "chrome") { + chrome.tts.stop() + } else { + window.speechSynthesis.cancel() + } setIsSpeaking(false) } diff --git a/src/libs/runtime.ts b/src/libs/runtime.ts index 8f55cc0..ce447f6 100644 --- a/src/libs/runtime.ts +++ b/src/libs/runtime.ts @@ -1,31 +1,51 @@ export const chromeRunTime = async function (domain: string) { - if (typeof chrome !== "undefined" && chrome.runtime && chrome.runtime.id) { - const url = new URL(domain) - const domains = [url.hostname] - const rules = [ - { - id: 1, - priority: 1, - condition: { - requestDomains: domains - }, - action: { - type: "modifyHeaders", - requestHeaders: [ - { - header: "Origin", - operation: "set", - value: `${url.protocol}//${url.hostname}` - } - ] + if (browser.runtime && browser.runtime.id) { + if (import.meta.env.BROWSER === "chrome") { + const url = new URL(domain) + const domains = [url.hostname] + const rules = [ + { + id: 1, + priority: 1, + condition: { + requestDomains: domains + }, + action: { + type: "modifyHeaders", + requestHeaders: [ + { + header: "Origin", + operation: "set", + value: `${url.protocol}//${url.hostname}` + } + ] + } } - } - ] + ] + + await browser.declarativeNetRequest.updateDynamicRules({ + removeRuleIds: rules.map((r) => r.id), + // @ts-ignore + addRules: rules + }) + } - await chrome.declarativeNetRequest.updateDynamicRules({ - removeRuleIds: rules.map((r) => r.id), - // @ts-ignore - 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"] + ) + } } } diff --git a/src/parser/google-docs.ts b/src/parser/google-docs.ts index 23e7316..e63a913 100644 --- a/src/parser/google-docs.ts +++ b/src/parser/google-docs.ts @@ -1,7 +1,6 @@ - export const isGoogleDocs = (url: string) => { - const GOOGLE_DOCS_REGEX = /docs\.google\.com\/document/g - return GOOGLE_DOCS_REGEX.test(url) + const GOOGLE_DOCS_REGEX = /docs\.google\.com\/document/g + return GOOGLE_DOCS_REGEX.test(url) } const getGoogleDocs = () => { @@ -114,6 +113,6 @@ export const parseGoogleDocs = async () => { }> const { content } = await result - + return content } diff --git a/src/services/app.ts b/src/services/app.ts new file mode 100644 index 0000000..e9d0cbb --- /dev/null +++ b/src/services/app.ts @@ -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) +} diff --git a/src/services/tts.ts b/src/services/tts.ts index 2d38d3e..847efb4 100644 --- a/src/services/tts.ts +++ b/src/services/tts.ts @@ -21,8 +21,16 @@ export const setTTSProvider = async (ttsProvider: string) => { } export const getBrowserTTSVoices = async () => { - const tts = await chrome.tts.getVoices() - return tts + if (import.meta.env.BROWSER === "chrome") { + const tts = await chrome.tts.getVoices() + return tts + } else { + const tts = await speechSynthesis.getVoices() + return tts.map((voice) => ({ + voiceName: voice.name, + lang: voice.lang + })) + } } export const getVoice = async () => { diff --git a/wxt.config.ts b/wxt.config.ts index d80478d..d14a859 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -2,35 +2,70 @@ import { defineConfig } from "wxt" import react from "@vitejs/plugin-react" 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 export default defineConfig({ vite: () => ({ - plugins: [react(), + plugins: [ + react(), topLevelAwait({ - promiseExportName: '__tla', - promiseImportName: i => `__tla_${i}`, - }), + promiseExportName: "__tla", + promiseImportName: (i) => `__tla_${i}` + }) ], build: { rollupOptions: { - external: [ - "langchain", - "@langchain/community", - ] + external: ["langchain", "@langchain/community"] } } }), entrypointsDir: "entries", srcDir: "src", outDir: "build", + manifest: { - version: "1.1.6", - name: '__MSG_extName__', - description: '__MSG_extDescription__', - default_locale: 'en', + version: "1.1.7", + name: "__MSG_extName__", + description: "__MSG_extDescription__", + default_locale: "en", action: {}, 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: { _execute_action: { suggested_key: { @@ -44,16 +79,9 @@ export default defineConfig({ } } }, - permissions: [ - "storage", - "sidePanel", - "activeTab", - "scripting", - "declarativeNetRequest", - "action", - "unlimitedStorage", - "contextMenus", - "tts" - ] + permissions: + process.env.TARGET === "firefox" + ? firefoxMV2Permissions + : chromeMV3Permissions } }) From 79e2013fbd80f7a611d6ee4050165c125fe75285 Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Sun, 12 May 2024 11:38:01 +0530 Subject: [PATCH 02/16] few bug fixes --- src/db/index.ts | 37 ++------ src/db/knowledge.ts | 115 +++++++++++------------- src/db/vector.ts | 178 +++++++++++++++----------------------- src/entries/background.ts | 39 ++++----- src/libs/get-html.ts | 37 +++++--- src/parser/google-docs.ts | 37 +++++--- src/utils/action.ts | 25 ++++++ 7 files changed, 225 insertions(+), 243 deletions(-) create mode 100644 src/utils/action.ts diff --git a/src/db/index.ts b/src/db/index.ts index 9f6e5b9..f709905 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -2,7 +2,6 @@ import { type ChatHistory as ChatHistoryType, type Message as MessageType } from "~/store/option" -import { Storage, browser } from "wxt/browser" type HistoryInfo = { id: string @@ -58,18 +57,15 @@ type ChatHistory = HistoryInfo[] type Prompts = Prompt[] export class PageAssitDatabase { - db: Storage.LocalStorageArea + db: chrome.storage.StorageArea constructor() { - this.db = browser.storage.local + this.db = chrome.storage.local } async getChatHistory(id: string): Promise { return new Promise((resolve, reject) => { - // this.db.get(id, (result) => { - // resolve(result[id] || []) - // }) - this.db.get(id).then((result) => { + this.db.get(id, (result) => { resolve(result[id] || []) }) }) @@ -77,10 +73,7 @@ export class PageAssitDatabase { async getChatHistories(): Promise { return new Promise((resolve, reject) => { - // this.db.get("chatHistories", (result) => { - // resolve(result.chatHistories || []) - // }) - this.db.get("chatHistories").then((result) => { + this.db.get("chatHistories", (result) => { resolve(result.chatHistories || []) }) }) @@ -133,10 +126,7 @@ export class PageAssitDatabase { async getAllPrompts(): Promise { return new Promise((resolve, reject) => { - // this.db.get("prompts", (result) => { - // resolve(result.prompts || []) - // }) - this.db.get("prompts").then((result) => { + this.db.get("prompts", (result) => { resolve(result.prompts || []) }) }) @@ -179,10 +169,7 @@ export class PageAssitDatabase { async getWebshare(id: string) { return new Promise((resolve, reject) => { - // this.db.get(id, (result) => { - // resolve(result[id] || []) - // }) - this.db.get(id).then((result) => { + this.db.get(id, (result) => { resolve(result[id] || []) }) }) @@ -190,10 +177,7 @@ export class PageAssitDatabase { async getAllWebshares(): Promise { return new Promise((resolve, reject) => { - // this.db.get("webshares", (result) => { - // resolve(result.webshares || []) - // }) - this.db.get("webshares").then((result) => { + this.db.get("webshares", (result) => { resolve(result.webshares || []) }) }) @@ -213,10 +197,7 @@ export class PageAssitDatabase { async getUserID() { return new Promise((resolve, reject) => { - // this.db.get("user_id", (result) => { - // resolve(result.user_id || "") - // }) - this.db.get("user_id").then((result) => { + this.db.get("user_id", (result) => { resolve(result.user_id || "") }) }) @@ -474,4 +455,4 @@ export const importPrompts = async (prompts: Prompts) => { for (const prompt of prompts) { await db.addPrompt(prompt) } -} +} \ No newline at end of file diff --git a/src/db/knowledge.ts b/src/db/knowledge.ts index a46c68f..3f90880 100644 --- a/src/db/knowledge.ts +++ b/src/db/knowledge.ts @@ -1,4 +1,3 @@ -import { Storage, browser } from "wxt/browser" import { deleteVector, deleteVectorByFileId } from "./vector" export type Source = { @@ -25,105 +24,89 @@ export const generateID = () => { }) } export class PageAssistKnowledge { - db: Storage.LocalStorageArea + db: chrome.storage.StorageArea constructor() { - this.db = browser.storage.local + this.db = chrome.storage.local } getAll = async (): Promise => { return new Promise((resolve, reject) => { - // this.db.get(null, (result) => { - // if (chrome.runtime.lastError) { - // reject(chrome.runtime.lastError) - // } 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]) - resolve(data) + this.db.get(null, (result) => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError) + } else { + const data = Object.keys(result).map((key) => result[key]) + resolve(data) + } }) }) } getById = async (id: string): Promise => { return new Promise((resolve, reject) => { - this.db.get(id).then((result) => { - resolve(result[id]) + this.db.get(id, (result) => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError) + } else { + resolve(result[id]) + } }) }) - } + create = async (knowledge: Knowledge): Promise => { return new Promise((resolve, reject) => { - // this.db.set({ [knowledge.id]: knowledge }, () => { - // if (chrome.runtime.lastError) { - // reject(chrome.runtime.lastError) - // } else { - // resolve() - // } - // }) - this.db.set({ [knowledge.id]: knowledge }).then(() => { - resolve() + this.db.set({ [knowledge.id]: knowledge }, () => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError) + } else { + resolve() + } }) }) } update = async (knowledge: Knowledge): Promise => { return new Promise((resolve, reject) => { - // this.db.set({ [knowledge.id]: knowledge }, () => { - // if (chrome.runtime.lastError) { - // reject(chrome.runtime.lastError) - // } else { - // resolve() - // } - // }) - this.db.set({ [knowledge.id]: knowledge }).then(() => { - resolve() + this.db.set({ [knowledge.id]: knowledge }, () => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError) + } else { + resolve() + } }) }) } delete = async (id: string): Promise => { return new Promise((resolve, reject) => { - // this.db.remove(id, () => { - // if (chrome.runtime.lastError) { - // reject(chrome.runtime.lastError) - // } else { - // resolve() - // } - // }) - this.db.remove(id).then(() => { - resolve() + this.db.remove(id, () => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError) + } else { + resolve() + } }) }) } deleteSource = async (id: string, source_id: string): Promise => { return new Promise((resolve, reject) => { - // this.db.get(id, (result) => { - // if (chrome.runtime.lastError) { - // reject(chrome.runtime.lastError) - // } 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 - data.source = data.source.filter((s) => s.source_id !== source_id) - this.db.set({ [id]: data }).then(() => { - resolve() - }) + this.db.get(id, (result) => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError) + } 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() + } + }) + } }) }) } @@ -219,4 +202,4 @@ export const importKnowledge = async (data: Knowledge[]) => { for (const d of data) { await db.create(d) } -} +} \ No newline at end of file diff --git a/src/db/vector.ts b/src/db/vector.ts index 761f1a1..f5dd141 100644 --- a/src/db/vector.ts +++ b/src/db/vector.ts @@ -1,5 +1,3 @@ -import { Storage, browser } from "wxt/browser" - interface PageAssistVector { file_id: string content: string @@ -13,10 +11,10 @@ export type VectorData = { } export class PageAssistVectorDb { - db: Storage.LocalStorageArea + db: chrome.storage.StorageArea constructor() { - this.db = browser.storage.local + this.db = chrome.storage.local } insertVector = async ( @@ -24,55 +22,36 @@ export class PageAssistVectorDb { vector: PageAssistVector[] ): Promise => { return new Promise((resolve, reject) => { - // this.db.get(id, (result) => { - // if (chrome.runtime.lastError) { - // reject(chrome.runtime.lastError) - // } 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 - if (!data) { - this.db.set({ [id]: { id, vectors: vector } }).then(() => { - resolve() - }) + this.db.get(id, (result) => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError) } else { - this.db - .set({ - [id]: { - ...data, - vectors: data.vectors.concat(vector) + 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() } }) - .then(() => { - resolve() - }) + } else { + this.db.set( + { + [id]: { + ...data, + vectors: data.vectors.concat(vector) + } + }, + () => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError) + } else { + resolve() + } + } + ) + } } }) }) @@ -80,72 +59,56 @@ export class PageAssistVectorDb { deleteVector = async (id: string): Promise => { return new Promise((resolve, reject) => { - // this.db.remove(id, () => { - // if (chrome.runtime.lastError) { - // reject(chrome.runtime.lastError) - // } else { - // resolve() - // } - // }) - this.db.remove(id).then(() => { - resolve() + this.db.remove(id, () => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError) + } else { + resolve() + } }) }) } deleteVectorByFileId = async (id: string, file_id: string): Promise => { return new Promise((resolve, reject) => { - // this.db.get(id, (result) => { - // if (chrome.runtime.lastError) { - // reject(chrome.runtime.lastError) - // } 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 - data.vectors = data.vectors.filter((v) => v.file_id !== file_id) - this.db.set({ [id]: data }).then(() => { - resolve() - }) + this.db.get(id, (result) => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError) + } 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() + } + }) + } }) }) } getVector = async (id: string): Promise => { return new Promise((resolve, reject) => { - // this.db.get(id, (result) => { - // if (chrome.runtime.lastError) { - // reject(chrome.runtime.lastError) - // } else { - // resolve(result[id] as VectorData) - // } - // }) - this.db.get(id).then((result) => { - resolve(result[id] as VectorData) + this.db.get(id, (result) => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError) + } else { + resolve(result[id] as VectorData) + } }) }) } getAll = async (): Promise => { return new Promise((resolve, reject) => { - // this.db.get(null, (result) => { - // if (chrome.runtime.lastError) { - // reject(chrome.runtime.lastError) - // } else { - // resolve(Object.values(result)) - // } - // }) - this.db.get(null).then((result) => { - resolve(Object.values(result)) + this.db.get(null, (result) => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError) + } else { + resolve(Object.values(result)) + } }) }) } @@ -156,15 +119,12 @@ export class PageAssistVectorDb { data.forEach((d) => { obj[d.id] = d }) - // this.db.set(obj, () => { - // if (chrome.runtime.lastError) { - // reject(chrome.runtime.lastError) - // } else { - // resolve() - // } - // }) - this.db.set(obj).then(() => { - resolve() + this.db.set(obj, () => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError) + } else { + resolve() + } }) }) } @@ -204,5 +164,5 @@ export const exportVectors = async () => { export const importVectors = async (data: VectorData[]) => { const db = new PageAssistVectorDb() - return db.saveImportedData(data) -} + return db.saveImportedData(data) +} \ No newline at end of file diff --git a/src/entries/background.ts b/src/entries/background.ts index 61e7328..91c71a7 100644 --- a/src/entries/background.ts +++ b/src/entries/background.ts @@ -1,13 +1,13 @@ 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 = () => { - browser.action.setBadgeText({ text: "" }) - browser.action.setTitle({ title: "" }) + setBadgeText({ text: "" }) + setTitle({ title: "" }) } const streamDownload = async (url: string, model: string) => { url += "/api/pull" @@ -42,16 +42,16 @@ const streamDownload = async (url: string, model: string) => { completed?: number } if (json.total && json.completed) { - browser.action.setBadgeText({ + setBadgeText({ text: progressHuman(json.completed, json.total) }) - browser.action.setBadgeBackgroundColor({ color: "#0000FF" }) + setBadgeBackgroundColor({ color: "#0000FF" }) } else { - browser.action.setBadgeText({ text: "🏋️‍♂️" }) - browser.action.setBadgeBackgroundColor({ color: "#FFFFFF" }) + setBadgeText({ text: "🏋️‍♂️" }) + setBadgeBackgroundColor({ color: "#FFFFFF" }) } - browser.action.setTitle({ title: json.status }) + setTitle({ title: json.status }) if (json.status === "success") { isSuccess = true @@ -62,13 +62,13 @@ const streamDownload = async (url: string, model: string) => { } if (isSuccess) { - browser.action.setBadgeText({ text: "✅" }) - browser.action.setBadgeBackgroundColor({ color: "#00FF00" }) - browser.action.setTitle({ title: "Model pulled successfully" }) + setBadgeText({ text: "✅" }) + setBadgeBackgroundColor({ color: "#00FF00" }) + setTitle({ title: "Model pulled successfully" }) } else { - browser.action.setBadgeText({ text: "❌" }) - browser.action.setBadgeBackgroundColor({ color: "#FF0000" }) - browser.action.setTitle({ title: "Model pull failed" }) + setBadgeText({ text: "❌" }) + setBadgeBackgroundColor({ color: "#FF0000" }) + setTitle({ title: "Model pull failed" }) } setTimeout(() => { @@ -86,9 +86,9 @@ export default defineBackground({ const isRunning = await isOllamaRunning() if (!isRunning) { - browser.action.setBadgeText({ text: "E" }) - browser.action.setBadgeBackgroundColor({ color: "#FF0000" }) - browser.action.setTitle({ title: "Ollama is not running" }) + setBadgeText({ text: "E" }) + setBadgeBackgroundColor({ color: "#FF0000" }) + setTitle({ title: "Ollama is not running" }) setTimeout(() => { clearBadge() }, 5000) @@ -98,9 +98,8 @@ export default defineBackground({ } }) - if (browser?.action) { - browser.action.onClicked.addListener((tab) => { - console.log("browser.action.onClicked.addListener") + if (import.meta.env.BROWSER === "chrome") { + chrome.action.onClicked.addListener((tab) => { browser.tabs.create({ url: browser.runtime.getURL("/options.html") }) }) } else { diff --git a/src/libs/get-html.ts b/src/libs/get-html.ts index 40499d4..ddb2ad7 100644 --- a/src/libs/get-html.ts +++ b/src/libs/get-html.ts @@ -4,7 +4,7 @@ import { isTweet, isTwitterTimeline, parseTweet, - parseTwitterTimeline, + parseTwitterTimeline } from "@/parser/twitter" import { isGoogleDocs, parseGoogleDocs } from "@/parser/google-docs" import { cleanUnwantedUnicode } from "@/utils/clean" @@ -24,18 +24,35 @@ const _getHtml = () => { export const getDataFromCurrentTab = async () => { const result = new Promise((resolve) => { - chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { - const tab = tabs[0] + if (import.meta.env.BROWSER === "chrome") { + chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { + const tab = tabs[0] - const data = await chrome.scripting.executeScript({ - target: { tabId: tab.id }, - func: _getHtml + const data = await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: _getHtml + }) + + if (data.length > 0) { + resolve(data[0].result) + } }) + } else { + browser.tabs + .query({ active: true, currentWindow: true }) + .then(async (tabs) => { + const tab = tabs[0] - if (data.length > 0) { - resolve(data[0].result) - } - }) + const data = await browser.scripting.executeScript({ + target: { tabId: tab.id }, + func: _getHtml + }) + + if (data.length > 0) { + resolve(data[0].result) + } + }) + } }) as Promise<{ url: string content: string diff --git a/src/parser/google-docs.ts b/src/parser/google-docs.ts index e63a913..0781396 100644 --- a/src/parser/google-docs.ts +++ b/src/parser/google-docs.ts @@ -95,19 +95,36 @@ const getGoogleDocs = () => { export const parseGoogleDocs = async () => { const result = new Promise((resolve) => { - chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { - const tab = tabs[0] + if (import.meta.env.BROWSER === "chrome") { + chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { + const tab = tabs[0] - const data = await chrome.scripting.executeScript({ - target: { tabId: tab.id }, - world: "MAIN", - func: getGoogleDocs + const data = await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + world: "MAIN", + func: getGoogleDocs + }) + + if (data.length > 0) { + resolve(data[0].result) + } }) + } else { + browser.tabs + .query({ active: true, currentWindow: true }) + .then(async (tabs) => { + const tab = tabs[0] - if (data.length > 0) { - resolve(data[0].result) - } - }) + const data = await browser.scripting.executeScript({ + target: { tabId: tab.id }, + func: getGoogleDocs + }) + + if (data.length > 0) { + resolve(data[0].result) + } + }) + } }) as Promise<{ content?: string }> diff --git a/src/utils/action.ts b/src/utils/action.ts new file mode 100644 index 0000000..5881aa4 --- /dev/null +++ b/src/utils/action.ts @@ -0,0 +1,25 @@ +import { browser } from "wxt/browser" + +export const setTitle = ({ title }: { title: string }) => { + if (import.meta.env.BROWSER === "chrome") { + chrome.action.setTitle({ title }) + } else { + browser.browserAction.setTitle({ title }) + } +} + +export const setBadgeBackgroundColor = ({ color }: { color: string }) => { + if (import.meta.env.BROWSER === "chrome") { + chrome.action.setBadgeBackgroundColor({ color }) + } else { + browser.browserAction.setBadgeBackgroundColor({ color }) + } +} + +export const setBadgeText = ({ text }: { text: string }) => { + if (import.meta.env.BROWSER === "chrome") { + chrome.action.setBadgeText({ text }) + } else { + browser.browserAction.setBadgeText({ text }) + } +} From 62ffe8346e6210ebd9b295ed2c6f494f5b5a40ef Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Sun, 12 May 2024 21:14:27 +0530 Subject: [PATCH 03/16] chore: Update logic of runtime url rewrite --- bun.lockb | Bin 408050 -> 408050 bytes docs/connection-issue.md | 1 + .../Common/AdvanceOllamaSettings.tsx | 49 +++++++++++++++++ src/components/Option/Settings/ollama.tsx | 50 ++++++++++++++---- src/libs/runtime.ts | 19 +++++-- src/loader/html.ts | 4 +- src/services/app.ts | 25 +++++++-- src/services/ollama.ts | 8 +-- src/web/search-engines/duckduckgo.ts | 4 +- src/web/search-engines/google.ts | 4 +- src/web/search-engines/sogou.ts | 4 +- 11 files changed, 139 insertions(+), 29 deletions(-) create mode 100644 docs/connection-issue.md create mode 100644 src/components/Common/AdvanceOllamaSettings.tsx diff --git a/bun.lockb b/bun.lockb index 11bd10c885c60add1bcbc85a7861b8eb65824abf..c6cbef9f5c016fc5a384a6a4834d305e8a188216 100644 GIT binary patch delta 39 tcmezLS>n@YiG~)&7N!>FEiB7**%{-^^$hfk+t=x`05R+Kb-HXWJODY~4XOYD delta 39 qcmezLS>n@YiG~)&7N!>FEiB7**_jx?pnaV#3lOtzU#H9F!UF&zJq%X> diff --git a/docs/connection-issue.md b/docs/connection-issue.md new file mode 100644 index 0000000..d2190d8 --- /dev/null +++ b/docs/connection-issue.md @@ -0,0 +1 @@ +# Ollama Connection Issues diff --git a/src/components/Common/AdvanceOllamaSettings.tsx b/src/components/Common/AdvanceOllamaSettings.tsx new file mode 100644 index 0000000..0291f16 --- /dev/null +++ b/src/components/Common/AdvanceOllamaSettings.tsx @@ -0,0 +1,49 @@ +import { useStorage } from "@plasmohq/storage/hook" +import { Input, Switch } from "antd" +import { useTranslation } from "react-i18next" + +export const AdvanceOllamaSettings = () => { + const [urlRewriteEnabled, setUrlRewriteEnabled] = useStorage( + "urlRewriteEnabled", + true + ) + + const [rewriteUrl, setRewriteUrl] = useStorage( + "rewriteUrl", + "http://127.0.0.1:11434" + ) + const { t } = useTranslation("settings") + + return ( +
+
+ + {t("generalSettings.settings.advanced.urlRewriteEnabled.label")} + +
+ setUrlRewriteEnabled(checked)} + /> +
+
+
+ + {t("generalSettings.settings.advanced.urlRewriteEnabled.label")} + +
+ setRewriteUrl(e.target.value)} + /> +
+
+
+ ) +} diff --git a/src/components/Option/Settings/ollama.tsx b/src/components/Option/Settings/ollama.tsx index e71045b..534b8a0 100644 --- a/src/components/Option/Settings/ollama.tsx +++ b/src/components/Option/Settings/ollama.tsx @@ -1,5 +1,5 @@ import { useMutation, useQuery } from "@tanstack/react-query" -import { Form, InputNumber, Select, Skeleton } from "antd" +import { Collapse, Form, InputNumber, Select, Skeleton } from "antd" import { useState } from "react" import { SaveButton } from "~/components/Common/SaveButton" import { @@ -13,9 +13,12 @@ import { } from "~/services/ollama" import { SettingPrompt } from "./prompt" import { useTranslation } from "react-i18next" +import { useStorage } from "@plasmohq/storage/hook" +import { AdvanceOllamaSettings } from "@/components/Common/AdvanceOllamaSettings" export const SettingsOllama = () => { const [ollamaURL, setOllamaURL] = useState("") + const { t } = useTranslation("settings") const { data: ollamaInfo, status } = useQuery({ @@ -61,7 +64,7 @@ export const SettingsOllama = () => {
-
+
+ + {t("ollamaSettings.settings.advanced.label")} + + ), + children: + } + ]} + /> +
{ @@ -130,7 +148,9 @@ export const SettingsOllama = () => { 0 } showSearch - placeholder={t("ollamaSettings.settings.ragSettings.model.placeholder")} + placeholder={t( + "ollamaSettings.settings.ragSettings.model.placeholder" + )} style={{ width: "100%" }} className="mt-4" options={ollamaInfo.models?.map((model) => ({ @@ -144,26 +164,38 @@ export const SettingsOllama = () => { name="chunkSize" label={t("ollamaSettings.settings.ragSettings.chunkSize.label")} rules={[ - { required: true, message: t("ollamaSettings.settings.ragSettings.chunkSize.required") - } + { + required: true, + message: t( + "ollamaSettings.settings.ragSettings.chunkSize.required" + ) + } ]}> diff --git a/src/libs/runtime.ts b/src/libs/runtime.ts index ce447f6..cf4aa96 100644 --- a/src/libs/runtime.ts +++ b/src/libs/runtime.ts @@ -1,8 +1,15 @@ -export const chromeRunTime = async function (domain: string) { +import { getAdvancedOllamaSettings } from "@/services/app" + +export const urlRewriteRuntime = async function (domain: string) { if (browser.runtime && browser.runtime.id) { + const { isEnableRewriteUrl, rewriteUrl } = await getAdvancedOllamaSettings() if (import.meta.env.BROWSER === "chrome") { const url = new URL(domain) const domains = [url.hostname] + let origin = `${url.protocol}//${url.hostname}` + if (!isEnableRewriteUrl && rewriteUrl) { + origin = rewriteUrl + } const rules = [ { id: 1, @@ -16,13 +23,12 @@ export const chromeRunTime = async function (domain: string) { { header: "Origin", operation: "set", - value: `${url.protocol}//${url.hostname}` + value: origin } ] } } ] - await browser.declarativeNetRequest.updateDynamicRules({ removeRuleIds: rules.map((r) => r.id), // @ts-ignore @@ -35,10 +41,13 @@ export const chromeRunTime = async function (domain: string) { const domains = [`*://${url.hostname}/*`] browser.webRequest.onBeforeSendHeaders.addListener( (details) => { + let origin = `${url.protocol}//${url.hostname}` + if (!isEnableRewriteUrl && rewriteUrl) { + origin = rewriteUrl + } for (let i = 0; i < details.requestHeaders.length; i++) { if (details.requestHeaders[i].name === "Origin") { - details.requestHeaders[i].value = - `${url.protocol}//${url.hostname}` + details.requestHeaders[i].value = origin } } return { requestHeaders: details.requestHeaders } diff --git a/src/loader/html.ts b/src/loader/html.ts index 94eaed0..37a07f1 100644 --- a/src/loader/html.ts +++ b/src/loader/html.ts @@ -1,7 +1,7 @@ import { BaseDocumentLoader } from "langchain/document_loaders/base" import { Document } from "@langchain/core/documents" import { compile } from "html-to-text" -import { chromeRunTime } from "~/libs/runtime" +import { urlRewriteRuntime } from "~/libs/runtime" import { YtTranscript } from "yt-transcript" import { isWikipedia, parseWikipedia } from "@/parser/wiki" @@ -102,7 +102,7 @@ export class PageAssistHtmlLoader } ] } - await chromeRunTime(this.url) + await urlRewriteRuntime(this.url) const fetchHTML = await fetch(this.url) let html = await fetchHTML.text() diff --git a/src/services/app.ts b/src/services/app.ts index e9d0cbb..3b0f671 100644 --- a/src/services/app.ts +++ b/src/services/app.ts @@ -1,20 +1,39 @@ import { Storage } from "@plasmohq/storage" const storage = new Storage() +const DEFAULT_URL_REWRITE_URL = "http://127.0.0.1:11434" + export const isUrlRewriteEnabled = async () => { - const enabled = await storage.get("urlRewriteEnabled") - return enabled === "true" + const enabled = await storage.get("urlRewriteEnabled") + if (typeof enabled === "undefined") { + return true + } + return enabled } - export const setUrlRewriteEnabled = async (enabled: boolean) => { await storage.set("urlRewriteEnabled", enabled ? "true" : "false") } export const getRewriteUrl = async () => { const rewriteUrl = await storage.get("rewriteUrl") + if (!rewriteUrl || rewriteUrl.trim() === "") { + return DEFAULT_URL_REWRITE_URL + } return rewriteUrl } export const setRewriteUrl = async (url: string) => { await storage.set("rewriteUrl", url) } + +export const getAdvancedOllamaSettings = async () => { + const [isEnableRewriteUrl, rewriteUrl] = await Promise.all([ + isUrlRewriteEnabled(), + getRewriteUrl() + ]) + + return { + isEnableRewriteUrl, + rewriteUrl + } +} diff --git a/src/services/ollama.ts b/src/services/ollama.ts index ccca697..e1ce53b 100644 --- a/src/services/ollama.ts +++ b/src/services/ollama.ts @@ -1,6 +1,6 @@ import { Storage } from "@plasmohq/storage" import { cleanUrl } from "../libs/clean-url" -import { chromeRunTime } from "../libs/runtime" +import { urlRewriteRuntime } from "../libs/runtime" const storage = new Storage() @@ -22,10 +22,10 @@ Search results: export const getOllamaURL = async () => { const ollamaURL = await storage.get("ollamaURL") if (!ollamaURL || ollamaURL.length === 0) { - await chromeRunTime(DEFAULT_OLLAMA_URL) + await urlRewriteRuntime(DEFAULT_OLLAMA_URL) return DEFAULT_OLLAMA_URL } - await chromeRunTime(cleanUrl(ollamaURL)) + await urlRewriteRuntime(cleanUrl(ollamaURL)) return ollamaURL } @@ -163,7 +163,7 @@ export const setOllamaURL = async (ollamaURL: string) => { "http://127.0.0.1:" ) } - await chromeRunTime(cleanUrl(formattedUrl)) + await urlRewriteRuntime(cleanUrl(formattedUrl)) await storage.set("ollamaURL", cleanUrl(formattedUrl)) } diff --git a/src/web/search-engines/duckduckgo.ts b/src/web/search-engines/duckduckgo.ts index 089e571..f9258f5 100644 --- a/src/web/search-engines/duckduckgo.ts +++ b/src/web/search-engines/duckduckgo.ts @@ -1,5 +1,5 @@ import { cleanUrl } from "@/libs/clean-url" -import { chromeRunTime } from "@/libs/runtime" +import { urlRewriteRuntime } from "@/libs/runtime" import { PageAssistHtmlLoader } from "@/loader/html" import { defaultEmbeddingChunkOverlap, @@ -18,7 +18,7 @@ import { RecursiveCharacterTextSplitter } from "langchain/text_splitter" import { MemoryVectorStore } from "langchain/vectorstores/memory" export const localDuckDuckGoSearch = async (query: string) => { - await chromeRunTime(cleanUrl("https://html.duckduckgo.com/html/?q=" + query)) + await urlRewriteRuntime(cleanUrl("https://html.duckduckgo.com/html/?q=" + query)) const abortController = new AbortController() setTimeout(() => abortController.abort(), 10000) diff --git a/src/web/search-engines/google.ts b/src/web/search-engines/google.ts index 98fa2a8..7828546 100644 --- a/src/web/search-engines/google.ts +++ b/src/web/search-engines/google.ts @@ -7,7 +7,7 @@ import type { Document } from "@langchain/core/documents" import { RecursiveCharacterTextSplitter } from "langchain/text_splitter" import { MemoryVectorStore } from "langchain/vectorstores/memory" import { cleanUrl } from "~/libs/clean-url" -import { chromeRunTime } from "~/libs/runtime" +import { urlRewriteRuntime } from "~/libs/runtime" import { PageAssistHtmlLoader } from "~/loader/html" import { defaultEmbeddingChunkOverlap, @@ -18,7 +18,7 @@ import { export const localGoogleSearch = async (query: string) => { - await chromeRunTime( + await urlRewriteRuntime( cleanUrl("https://www.google.com/search?hl=en&q=" + query) ) const abortController = new AbortController() diff --git a/src/web/search-engines/sogou.ts b/src/web/search-engines/sogou.ts index ffa7e6f..14fcf86 100644 --- a/src/web/search-engines/sogou.ts +++ b/src/web/search-engines/sogou.ts @@ -1,5 +1,5 @@ import { cleanUrl } from "@/libs/clean-url" -import { chromeRunTime } from "@/libs/runtime" +import { urlRewriteRuntime } from "@/libs/runtime" import { PageAssistHtmlLoader } from "@/loader/html" import { defaultEmbeddingChunkOverlap, @@ -25,7 +25,7 @@ const getCorrectTargeUrl = async (url: string) => { return matches?.[1] || "" } export const localSogouSearch = async (query: string) => { - await chromeRunTime(cleanUrl("https://www.sogou.com/web?query=" + query)) + await urlRewriteRuntime(cleanUrl("https://www.sogou.com/web?query=" + query)) const abortController = new AbortController() From e94edf5c89cb9046f82a0656dd15bc059ff03d6c Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Sun, 12 May 2024 23:54:48 +0530 Subject: [PATCH 04/16] chore: Update URL rewrite logic for runtime --- src/assets/locale/en/playground.json | 3 ++- src/assets/locale/en/settings.json | 11 +++++++++ .../Common/AdvanceOllamaSettings.tsx | 10 ++++---- .../Option/Playground/PlaygroundEmpty.tsx | 20 +++++++++++++++- src/components/Option/Settings/ollama.tsx | 23 +++++++++++++++---- src/components/Sidepanel/Chat/empty.tsx | 19 ++++++++++++++- src/libs/runtime.ts | 9 +++++--- src/loader/html.ts | 7 +----- src/services/app.ts | 3 --- src/web/search-engines/duckduckgo.ts | 2 +- src/web/search-engines/google.ts | 3 ++- src/web/search-engines/sogou.ts | 5 +++- 12 files changed, 88 insertions(+), 27 deletions(-) diff --git a/src/assets/locale/en/playground.json b/src/assets/locale/en/playground.json index 1d0794d..5df01bb 100644 --- a/src/assets/locale/en/playground.json +++ b/src/assets/locale/en/playground.json @@ -2,7 +2,8 @@ "ollamaState": { "searching": "Searching for Your Ollama 🦙", "running": "Ollama is running 🦙", - "notRunning": "Unable to connect to Ollama 🦙" + "notRunning": "Unable to connect to Ollama 🦙", + "connectionError": "It seems like you are having a connection error. Please refer to this documentation for troubleshooting." }, "formError": { "noModel": "Please select a model", diff --git a/src/assets/locale/en/settings.json b/src/assets/locale/en/settings.json index 589f557..acfcfa5 100644 --- a/src/assets/locale/en/settings.json +++ b/src/assets/locale/en/settings.json @@ -245,6 +245,17 @@ "webSearchFollowUpPromptHelp": "Do not remove `{chat_history}` and `{question}` from the prompt.", "webSearchFollowUpPromptError": "Please input your Web Search Follow Up Prompt!", "webSearchFollowUpPromptPlaceholder": "Your Web Search Follow Up Prompt" + }, + "advanced": { + "label": "Advance Ollama URL Configuration", + "urlRewriteEnabled": { + "label": "Enable or Disable Custom Origin URL" + }, + "rewriteUrl": { + "label": "Custom Origin URL", + "placeholder": "Enter Custom Origin URL" + }, + "help": "If you have connection issues with Ollama on Page Assist, you can configure a custom origin URL. To learn more about the configuration, click here." } } }, diff --git a/src/components/Common/AdvanceOllamaSettings.tsx b/src/components/Common/AdvanceOllamaSettings.tsx index 0291f16..0027fb2 100644 --- a/src/components/Common/AdvanceOllamaSettings.tsx +++ b/src/components/Common/AdvanceOllamaSettings.tsx @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next" export const AdvanceOllamaSettings = () => { const [urlRewriteEnabled, setUrlRewriteEnabled] = useStorage( "urlRewriteEnabled", - true + false ) const [rewriteUrl, setRewriteUrl] = useStorage( @@ -18,7 +18,7 @@ export const AdvanceOllamaSettings = () => {
- {t("generalSettings.settings.advanced.urlRewriteEnabled.label")} + {t("ollamaSettings.settings.advanced.urlRewriteEnabled.label")}
{
- {t("generalSettings.settings.advanced.urlRewriteEnabled.label")} + {t("ollamaSettings.settings.advanced.rewriteUrl.label")}
setRewriteUrl(e.target.value)} /> diff --git a/src/components/Option/Playground/PlaygroundEmpty.tsx b/src/components/Option/Playground/PlaygroundEmpty.tsx index 6636605..7fb94ab 100644 --- a/src/components/Option/Playground/PlaygroundEmpty.tsx +++ b/src/components/Option/Playground/PlaygroundEmpty.tsx @@ -1,7 +1,8 @@ +import { cleanUrl } from "@/libs/clean-url" import { useQuery } from "@tanstack/react-query" import { RotateCcw } from "lucide-react" import { useEffect, useState } from "react" -import { useTranslation } from "react-i18next" +import { Trans, useTranslation } from "react-i18next" import { getOllamaURL, isOllamaRunning, @@ -79,6 +80,23 @@ export const PlaygroundEmpty = () => { {t("common:retry")} + + {ollamaURL && + cleanUrl(ollamaURL) !== "http://127.0.0.1:11434" && ( +

+ + ) + }} + /> +

+ )}
) ) : null} diff --git a/src/components/Option/Settings/ollama.tsx b/src/components/Option/Settings/ollama.tsx index 534b8a0..8e24a42 100644 --- a/src/components/Option/Settings/ollama.tsx +++ b/src/components/Option/Settings/ollama.tsx @@ -12,7 +12,7 @@ import { setOllamaURL as saveOllamaURL } from "~/services/ollama" import { SettingPrompt } from "./prompt" -import { useTranslation } from "react-i18next" +import { Trans, useTranslation } from "react-i18next" import { useStorage } from "@plasmohq/storage/hook" import { AdvanceOllamaSettings } from "@/components/Common/AdvanceOllamaSettings" @@ -87,9 +87,24 @@ export const SettingsOllama = () => { { key: "1", label: ( -

- {t("ollamaSettings.settings.advanced.label")} -

+
+

+ {t("ollamaSettings.settings.advanced.label")} +

+

+ + ) + }} + /> +

+
), children: } diff --git a/src/components/Sidepanel/Chat/empty.tsx b/src/components/Sidepanel/Chat/empty.tsx index 499e320..000d4e3 100644 --- a/src/components/Sidepanel/Chat/empty.tsx +++ b/src/components/Sidepanel/Chat/empty.tsx @@ -1,8 +1,9 @@ +import { cleanUrl } from "@/libs/clean-url" import { useQuery, useQueryClient } from "@tanstack/react-query" import { Select } from "antd" import { RotateCcw } from "lucide-react" import { useEffect, useState } from "react" -import { useTranslation } from "react-i18next" +import { Trans, useTranslation } from "react-i18next" import { useMessage } from "~/hooks/useMessage" import { getAllModels, @@ -91,6 +92,22 @@ export const EmptySidePanel = () => { {t("common:retry")} + {ollamaURL && + cleanUrl(ollamaURL) !== "http://127.0.0.1:11434" && ( +

+ + ) + }} + /> +

+ )}
) ) : null} diff --git a/src/libs/runtime.ts b/src/libs/runtime.ts index cf4aa96..dc54c4a 100644 --- a/src/libs/runtime.ts +++ b/src/libs/runtime.ts @@ -1,13 +1,16 @@ import { getAdvancedOllamaSettings } from "@/services/app" -export const urlRewriteRuntime = async function (domain: string) { +export const urlRewriteRuntime = async function ( + domain: string, + type = "ollama" +) { if (browser.runtime && browser.runtime.id) { const { isEnableRewriteUrl, rewriteUrl } = await getAdvancedOllamaSettings() if (import.meta.env.BROWSER === "chrome") { const url = new URL(domain) const domains = [url.hostname] let origin = `${url.protocol}//${url.hostname}` - if (!isEnableRewriteUrl && rewriteUrl) { + if (isEnableRewriteUrl && rewriteUrl && type === "ollama") { origin = rewriteUrl } const rules = [ @@ -42,7 +45,7 @@ export const urlRewriteRuntime = async function (domain: string) { browser.webRequest.onBeforeSendHeaders.addListener( (details) => { let origin = `${url.protocol}//${url.hostname}` - if (!isEnableRewriteUrl && rewriteUrl) { + if (isEnableRewriteUrl && rewriteUrl && type === "ollama") { origin = rewriteUrl } for (let i = 0; i < details.requestHeaders.length; i++) { diff --git a/src/loader/html.ts b/src/loader/html.ts index 37a07f1..786c60e 100644 --- a/src/loader/html.ts +++ b/src/loader/html.ts @@ -102,7 +102,7 @@ export class PageAssistHtmlLoader } ] } - await urlRewriteRuntime(this.url) + await urlRewriteRuntime(this.url, "web") const fetchHTML = await fetch(this.url) let html = await fetchHTML.text() @@ -111,11 +111,6 @@ export class PageAssistHtmlLoader html = parseWikipedia(await fetchHTML.text()) } - // else if (isTwitter(this.url)) { - // console.log("Twitter URL detected") - // html = parseTweet(await fetchHTML.text(), this.url) - // } - const htmlCompiler = compile({ wordwrap: false, selectors: [ diff --git a/src/services/app.ts b/src/services/app.ts index 3b0f671..8f0cc66 100644 --- a/src/services/app.ts +++ b/src/services/app.ts @@ -5,9 +5,6 @@ const DEFAULT_URL_REWRITE_URL = "http://127.0.0.1:11434" export const isUrlRewriteEnabled = async () => { const enabled = await storage.get("urlRewriteEnabled") - if (typeof enabled === "undefined") { - return true - } return enabled } export const setUrlRewriteEnabled = async (enabled: boolean) => { diff --git a/src/web/search-engines/duckduckgo.ts b/src/web/search-engines/duckduckgo.ts index f9258f5..374ce60 100644 --- a/src/web/search-engines/duckduckgo.ts +++ b/src/web/search-engines/duckduckgo.ts @@ -18,7 +18,7 @@ import { RecursiveCharacterTextSplitter } from "langchain/text_splitter" import { MemoryVectorStore } from "langchain/vectorstores/memory" export const localDuckDuckGoSearch = async (query: string) => { - await urlRewriteRuntime(cleanUrl("https://html.duckduckgo.com/html/?q=" + query)) + await urlRewriteRuntime(cleanUrl("https://html.duckduckgo.com/html/?q=" + query), "duckduckgo") const abortController = new AbortController() setTimeout(() => abortController.abort(), 10000) diff --git a/src/web/search-engines/google.ts b/src/web/search-engines/google.ts index 7828546..8f0a16a 100644 --- a/src/web/search-engines/google.ts +++ b/src/web/search-engines/google.ts @@ -19,7 +19,8 @@ import { export const localGoogleSearch = async (query: string) => { await urlRewriteRuntime( - cleanUrl("https://www.google.com/search?hl=en&q=" + query) + cleanUrl("https://www.google.com/search?hl=en&q=" + query), + "google" ) const abortController = new AbortController() setTimeout(() => abortController.abort(), 10000) diff --git a/src/web/search-engines/sogou.ts b/src/web/search-engines/sogou.ts index 14fcf86..274d03f 100644 --- a/src/web/search-engines/sogou.ts +++ b/src/web/search-engines/sogou.ts @@ -25,7 +25,10 @@ const getCorrectTargeUrl = async (url: string) => { return matches?.[1] || "" } export const localSogouSearch = async (query: string) => { - await urlRewriteRuntime(cleanUrl("https://www.sogou.com/web?query=" + query)) + await urlRewriteRuntime( + cleanUrl("https://www.sogou.com/web?query=" + query), + "sogou" + ) const abortController = new AbortController() From d9024148725ac35753800b005a5f2f91f90e233c Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Sun, 12 May 2024 23:59:51 +0530 Subject: [PATCH 05/16] feat: Add connection error message for Ollama in multiple locales --- src/assets/locale/ja-JP/playground.json | 3 ++- src/assets/locale/ml/playground.json | 3 ++- src/assets/locale/ru/playground.json | 3 ++- src/assets/locale/zh/playground.json | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/assets/locale/ja-JP/playground.json b/src/assets/locale/ja-JP/playground.json index a559d18..b5ebedf 100644 --- a/src/assets/locale/ja-JP/playground.json +++ b/src/assets/locale/ja-JP/playground.json @@ -2,7 +2,8 @@ "ollamaState": { "searching": "Ollamaを検索中 🦙", "running": "Ollamaが実行中 🦙", - "notRunning": "Ollamaに接続できません 🦙" + "notRunning": "Ollamaに接続できません 🦙", + "connectionError": "接続エラーが発生しているようです。トラブルシューティングについてはドキュメントをご覧ください。" }, "formError": { "noModel": "モデルを選択してください", diff --git a/src/assets/locale/ml/playground.json b/src/assets/locale/ml/playground.json index 0c6b7ee..708782c 100644 --- a/src/assets/locale/ml/playground.json +++ b/src/assets/locale/ml/playground.json @@ -2,7 +2,8 @@ "ollamaState": { "searching": "നിങ്ങളുടെ ഒല്ലാമയ്ക്കായി തിരയുന്നു 🦙", "running": "ഒല്ലാമ പ്രവര്‍ത്തിക്കുന്നു 🦙", - "notRunning": "ഒല്ലാമയുമായി ബന്ധിപ്പിക്കാന്‍ കഴിയുന്നില്ല 🦙" + "notRunning": "ഒല്ലാമയുമായി ബന്ധിപ്പിക്കാന്‍ കഴിയുന്നില്ല 🦙", + "connectionError": "നിങ്ങൾക്ക് കണക്ഷൻ പ്രശ്നം ഉണ്ടെന്നു കാണുന്നു. ഈ ഡോക്യുമെന്റേഷൻ പരിശോധിക്കാൻ കൂടുതൽ സഹായത്തിനായി." }, "formError": { "noModel": "ദയവായി ഒരു മോഡല്‍ തിരഞ്ഞെടുക്കുക", diff --git a/src/assets/locale/ru/playground.json b/src/assets/locale/ru/playground.json index 498f2c8..433c67d 100644 --- a/src/assets/locale/ru/playground.json +++ b/src/assets/locale/ru/playground.json @@ -2,7 +2,8 @@ "ollamaState": { "searching": "Поиск вашего Ollama 🦙", "running": "Ollama работает 🦙", - "notRunning": "Не удалось подключиться к Ollama 🦙" + "notRunning": "Не удалось подключиться к Ollama 🦙", + "connectionError": "Похоже, у вас возникла ошибка соединения. Пожалуйста, обратитесь к этой документации для устранения неисправностей." }, "formError": { "noModel": "Пожалуйста, выберите модель", diff --git a/src/assets/locale/zh/playground.json b/src/assets/locale/zh/playground.json index 84a591c..bb74e7a 100644 --- a/src/assets/locale/zh/playground.json +++ b/src/assets/locale/zh/playground.json @@ -2,7 +2,8 @@ "ollamaState": { "searching": "正在搜索您的Ollama 🦙", "running": "Ollama正在运行 🦙", - "notRunning": "无法连接到Ollama 🦙" + "notRunning": "无法连接到Ollama 🦙", + "connectionError": "看起来你正在遇到连接错误。请参阅这文档进行故障排除。" }, "formError": { "noModel": "请选择一个模型", From 5984959aa5479ce9d382412af6bd2c3050c640be Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Mon, 13 May 2024 00:13:44 +0530 Subject: [PATCH 06/16] feat: Add advanced configuration options for Ollama URL --- src/assets/locale/ja-JP/settings.json | 11 +++++++++++ src/assets/locale/ml/settings.json | 17 ++++++++++++++--- src/assets/locale/ru/settings.json | 11 +++++++++++ src/assets/locale/zh/settings.json | 11 +++++++++++ 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/assets/locale/ja-JP/settings.json b/src/assets/locale/ja-JP/settings.json index 1fae4f5..8fd2860 100644 --- a/src/assets/locale/ja-JP/settings.json +++ b/src/assets/locale/ja-JP/settings.json @@ -248,6 +248,17 @@ "webSearchFollowUpPromptHelp": "プロンプトから`{chat_history}`と`{question}`を削除しないでください。", "webSearchFollowUpPromptError": "Web検索フォローアッププロンプトを入力してください!", "webSearchFollowUpPromptPlaceholder": "Web検索フォローアッププロンプト" + }, + "advanced": { + "label": "Ollama URL の高度な設定", + "urlRewriteEnabled": { + "label": "カスタムOriginのURLを有効化または無効化する" + }, + "rewriteUrl": { + "label": "カスタムOriginのURL", + "placeholder": "カスタムOriginのURLを入力" + }, + "help": "PageAssistでOllamaに接続の問題がある場合は、カスタムOriginのURLを設定できます。設定の詳細については、ここをクリックしてください。" } } }, diff --git a/src/assets/locale/ml/settings.json b/src/assets/locale/ml/settings.json index 97152cd..b3384c7 100644 --- a/src/assets/locale/ml/settings.json +++ b/src/assets/locale/ml/settings.json @@ -47,13 +47,13 @@ "label": "ചാറ്റ് ചരിത്രം, അറിവ് അടിസ്ഥാനം, പ്രോംപ്റ്റുകൾ എക്സ്പോർട്ട് ചെയ്യുക", "button": "ഡാറ്റ എക്സ്പോർട്ട് ചെയ്യുക", "success": "എക്സ്പോർട്ട് വിജയകരമായി" - }, - "import": { + }, + "import": { "label": "ചാറ്റ് ചരിത്രം, അറിവ് അടിസ്ഥാനം, പ്രോംപ്റ്റുകൾ ഇമ്പോർട്ട് ചെയ്യുക", "button": "ഡാറ്റ ഇമ്പോർട്ട് ചെയ്യുക", "success": "ഇമ്പോർട്ട് വിജയകരമായി", "error": "ഇമ്പോർട്ട് പിശക്" - } + } }, "tts": { "heading": "ടെക്സ്റ്റ്-ടു-സ്പീച്ച് ക്രമീകരണങ്ങൾ", @@ -248,6 +248,17 @@ "webSearchFollowUpPromptHelp": "പ്രോംപ്റ്റില്‍ നിന്ന് `{chat_history}` യും `{question}` യും നീക്കം ചെയ്യരുത്.", "webSearchFollowUpPromptError": "ദയവായി നിങ്ങളുടെ വെബ് തിരയല്‍ തുടര്‍പ്രോംപ്റ്റ് നല്കുക!", "webSearchFollowUpPromptPlaceholder": "നിങ്ങളുടെ വെബ് തിരയല്‍ തുടര്‍പ്രോംപ്റ്റ്" + }, + "advanced": { + "label": "Advance Ollama URL Configuration", + "urlRewriteEnabled": { + "label": "Enable or Disable Custom Origin URL" + }, + "rewriteUrl": { + "label": "Custom Origin URL", + "placeholder": "Enter Custom Origin URL" + }, + "help": "ഏജ് അസിസ്റ്റന്റിൽ Ollama-യുമായി ബന്ധപ്പെടുമ്പോൾ ബന്ധതടസ്സം ഉണ്ടെങ്കിൽ, നിങ്ങൾക്ക് ഒരു വ്യക്തിഗത അസ്ഥിരത്വം URL കോൺഫിഗർ ചെയ്യാം. കോൺഫിഗറേഷനെക്കുറിച്ച് കൂടുതലറിയാൻ, ഇവിടെ ക്ലിക്കുചെയ്യുക." } } }, diff --git a/src/assets/locale/ru/settings.json b/src/assets/locale/ru/settings.json index c48c357..51fa44e 100644 --- a/src/assets/locale/ru/settings.json +++ b/src/assets/locale/ru/settings.json @@ -245,6 +245,17 @@ "webSearchFollowUpPromptHelp": "Не удаляйте `{chat_history}` и `{question}` из подсказки.", "webSearchFollowUpPromptError": "Введите подсказку для последующего веб-поиска!", "webSearchFollowUpPromptPlaceholder": "Ваша подсказка для последующего веб-поиска" + }, + "advanced": { + "label": "Расширенная конфигурация URL Ollama", + "urlRewriteEnabled": { + "label": "Включить или отключить пользовательский исходный URL" + }, + "rewriteUrl": { + "label": "Пользовательский исходный URL", + "placeholder": "Введите пользовательский исходный URL" + }, + "help": "Если у вас возникают проблемы с подключением к Ollama на странице помощника, вы можете настроить пользовательский исходный URL. Чтобы узнать больше о конфигурации, нажмите здесь." } } }, diff --git a/src/assets/locale/zh/settings.json b/src/assets/locale/zh/settings.json index 70b8d3c..883ae22 100644 --- a/src/assets/locale/zh/settings.json +++ b/src/assets/locale/zh/settings.json @@ -249,6 +249,17 @@ "webSearchFollowUpPromptHelp": "请勿从提示词中删除 `{chat_history}` 和 `{question}`。", "webSearchFollowUpPromptError": "请输入您的网页搜索追问提示词!", "webSearchFollowUpPromptPlaceholder": "您的网页搜索追问提示词" + }, + "advanced": { + "label": "Ollama URL 高级配置", + "urlRewriteEnabled": { + "label": "启用或禁用自定义来源 URL" + }, + "rewriteUrl": { + "label": "自定义来源 URL", + "placeholder": "输入自定义来源 URL" + }, + "help": "如果您在 Page Assist 上与 Ollama 有连接问题,您可以配置自定义来源 URL。要了解更多关于配置的信息,点击此处。" } } }, From db7b49e073dba3ca68550eda737c6552f49dce7e Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Mon, 13 May 2024 00:41:47 +0530 Subject: [PATCH 07/16] chore: Update .gitignore to include .idea folder --- .gitignore | 2 +- docs/connection-issue.md | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a396c30..cf3d5c9 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,4 @@ keys.json # WXT .wxt # WebStorm -.idea \ No newline at end of file +.idea diff --git a/docs/connection-issue.md b/docs/connection-issue.md index d2190d8..a5681f4 100644 --- a/docs/connection-issue.md +++ b/docs/connection-issue.md @@ -1 +1,33 @@ # Ollama Connection Issues + +Connection issues can be caused by a number of reasons. Here are some common issues and how to resolve them on Page Assist. You will see the following error message if there is a connection issue: + +### 1. Direct Connection Error +![Direct connection error](https://image.pageassist.xyz/Screenshot%202024-05-13%20001742.png) + +### 2. `403` Error When Sending a Message +![403 error when sending a message](https://image.pageassist.xyz/Screenshot%202024-05-13%20001940.png) + +This issue usually occurs when Ollama is not running on [http://127.0.0.1:11434/](http://127.0.0.1:11434/), and the connection is from the private network or a different network. + +### Solutions + +Since Ollama has connection issues when directly accessed from the browser extension, Page Assist rewrites the request headers to make it work. However, automatic rewriting of headers only works on `http://127.0.0.1:*` and `http://localhost:*` URLs. To resolve the connection issue, you can try the following solutions: + +1. Go to Page Assist and click on the `Settings` icon. + +2. Click on the `Ollama Settings` tab. + +3. There you will see the `Advance Ollama URL Configuration` option. You need to expand it. + +![Advance Ollama URL Configuration](https://image.pageassist.xyz/Screenshot%202024-05-13%20003123.png) + +4. Enable the `Enable or Disable Custom Origin URL` option. + +![Enable or Disable Custom Origin URL](https://image.pageassist.xyz/Screenshot%202024-05-13%20003225.png) + +5. (Optional) If Ollama is running on a different port or host, then change the URL in the `Custom Origin URL` field; otherwise, leave it as it is. + +This will resolve the connection issue, and you will be able to use Ollama without any issues on Page Assist ❤ + +If you still face any issues, feel free to contact us [here](https://github.com/n4ze3m/page-assist/issues/new), and we will be happy to help you out. \ No newline at end of file From 26fc5b4e76b7256ca7a551a1e2cb0de0eb0750ab Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Mon, 13 May 2024 00:48:36 +0530 Subject: [PATCH 08/16] feat: Enable Firefox support and update browser compatibility table --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index db7ef0b..a226cb4 100644 --- a/README.md +++ b/README.md @@ -89,10 +89,10 @@ This will start a development server and watch for changes in the source files. | -------- | ------- | ----------------- | ------ | | Chrome | ✅ | ✅ | ✅ | | Brave | ✅ | ✅ | ✅ | +| Firefox | ✅ | ✅ | ✅ | | Edge | ✅ | ❌ | ✅ | | Opera GX | ❌ | ❌ | ✅ | | Arc | ❌ | ❌ | ✅ | -| Firefox | ❌ | ❌ | ❌ | ## Local AI Provider @@ -100,7 +100,7 @@ This will start a development server and watch for changes in the source files. ## Roadmap -- [ ] Firefox Support +- [X] Firefox Support - [ ] More Local AI Providers - [ ] More Features - [ ] More Customization Options From 79f31460f1709394885543ecb20d7fed91434323 Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Mon, 13 May 2024 11:09:19 +0530 Subject: [PATCH 09/16] fix: Update Ollama connection issue link in SettingsOllama component --- src/components/Option/Settings/ollama.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Option/Settings/ollama.tsx b/src/components/Option/Settings/ollama.tsx index 8e24a42..969a845 100644 --- a/src/components/Option/Settings/ollama.tsx +++ b/src/components/Option/Settings/ollama.tsx @@ -97,7 +97,7 @@ export const SettingsOllama = () => { components={{ anchor: ( ) From e6cf76d270e37117ae706515d66bb83bd39cef74 Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Mon, 13 May 2024 12:44:44 +0530 Subject: [PATCH 10/16] chore: Refactor route components for better organization and consistency --- src/routes/chrome.tsx | 35 ++++++++++++++++++ src/routes/firefox.tsx | 39 ++++++++++++++++++++ src/routes/index.tsx | 45 ++++++++++-------------- src/routes/option-index.tsx | 4 ++- src/routes/option-settings-about.tsx | 4 ++- src/routes/option-settings-knowledge.tsx | 4 ++- src/routes/option-settings-model.tsx | 4 ++- src/routes/option-settings-prompt.tsx | 4 ++- src/routes/option-settings-share.tsx | 4 ++- src/routes/option-settings.tsx | 4 ++- src/routes/options-settings-ollama.tsx | 4 ++- src/routes/sidepanel-chat.tsx | 4 ++- src/routes/sidepanel-settings.tsx | 4 ++- 13 files changed, 123 insertions(+), 36 deletions(-) create mode 100644 src/routes/chrome.tsx create mode 100644 src/routes/firefox.tsx diff --git a/src/routes/chrome.tsx b/src/routes/chrome.tsx new file mode 100644 index 0000000..a7ccb7c --- /dev/null +++ b/src/routes/chrome.tsx @@ -0,0 +1,35 @@ +import { Route, Routes } from "react-router-dom" +import OptionIndex from "./option-index" +import OptionSettings from "./option-settings" +import OptionModal from "./option-settings-model" +import OptionPrompt from "./option-settings-prompt" +import OptionOllamaSettings from "./options-settings-ollama" +import OptionShare from "./option-settings-share" +import OptionKnowledgeBase from "./option-settings-knowledge" +import OptionAbout from "./option-settings-about" +import SidepanelChat from "./sidepanel-chat" +import SidepanelSettings from "./sidepanel-settings" + +export const OptionRoutingChrome = () => { + return ( + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + ) +} + +export const SidepanelRoutingChrome = () => { + return ( + + } /> + } /> + + ) +} diff --git a/src/routes/firefox.tsx b/src/routes/firefox.tsx new file mode 100644 index 0000000..1281a72 --- /dev/null +++ b/src/routes/firefox.tsx @@ -0,0 +1,39 @@ +// this is a temp fix for firefox +// because chunks getting 4mb+ and it's not working on firefox addon store +import { lazy } from "react" +import { Route , Routes} from "react-router-dom" + +const SidepanelChat = lazy(() => import("./sidepanel-chat")) +const SidepanelSettings = lazy(() => import("./sidepanel-settings")) +const OptionIndex = lazy(() => import("./option-index")) +const OptionModal = lazy(() => import("./option-settings-model")) +const OptionPrompt = lazy(() => import("./option-settings-prompt")) +const OptionOllamaSettings = lazy(() => import("./options-settings-ollama")) +const OptionSettings = lazy(() => import("./option-settings")) +const OptionShare = lazy(() => import("./option-settings-share")) +const OptionKnowledgeBase = lazy(() => import("./option-settings-knowledge")) +const OptionAbout = lazy(() => import("./option-settings-about")) + +export const OptionRoutingFirefox = () => { + return ( + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + ) +} + +export const SidepanelRoutingFirefox = () => { + return ( + + } /> + } /> + + ) +} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 87581f8..4d5459f 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,16 +1,9 @@ -import { Route, Routes } from "react-router-dom" -import { SidepanelChat } from "./sidepanel-chat" -import { useDarkMode } from "~/hooks/useDarkmode" -import { SidepanelSettings } from "./sidepanel-settings" -import { OptionIndex } from "./option-index" -import { OptionModal } from "./option-settings-model" -import { OptionPrompt } from "./option-settings-prompt" -import { OptionOllamaSettings } from "./options-settings-ollama" -import { OptionSettings } from "./option-settings" -import { OptionShare } from "./option-settings-share" -import { OptionKnowledgeBase } from "./option-settings-knowledge" -import { OptionAbout } from "./option-settings-about" +import { Suspense } from "react" import { useTranslation } from "react-i18next" +import { useDarkMode } from "~/hooks/useDarkmode" +import { Skeleton } from "antd" +import { OptionRoutingChrome, SidepanelRoutingChrome } from "./chrome" +import { OptionRoutingFirefox, SidepanelRoutingFirefox } from "./firefox" export const OptionRouting = () => { const { mode } = useDarkMode() @@ -21,16 +14,13 @@ export const OptionRouting = () => { className={`${mode === "dark" ? "dark" : "light"} ${ i18n.language === "ru" ? "onest" : "inter" }`}> - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - + }> + {import.meta.env.BROWSER === "chrome" ? ( + + ) : ( + + )} +
) } @@ -44,10 +34,13 @@ export const SidepanelRouting = () => { className={`${mode === "dark" ? "dark" : "light"} ${ i18n.language === "ru" ? "onest" : "inter" }`}> - - } /> - } /> - + }> + {import.meta.env.BROWSER === "chrome" ? ( + + ) : ( + + )} +
) } diff --git a/src/routes/option-index.tsx b/src/routes/option-index.tsx index 6178d0a..776bcce 100644 --- a/src/routes/option-index.tsx +++ b/src/routes/option-index.tsx @@ -1,10 +1,12 @@ import OptionLayout from "~/components/Layouts/Layout" import { Playground } from "~/components/Option/Playground/Playground" -export const OptionIndex = () => { + const OptionIndex = () => { return ( ) } + +export default OptionIndex \ No newline at end of file diff --git a/src/routes/option-settings-about.tsx b/src/routes/option-settings-about.tsx index 982f1ea..9d00233 100644 --- a/src/routes/option-settings-about.tsx +++ b/src/routes/option-settings-about.tsx @@ -2,7 +2,7 @@ import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout" import OptionLayout from "~/components/Layouts/Layout" import { AboutApp } from "@/components/Option/Settings/about" -export const OptionAbout = () => { +const OptionAbout = () => { return ( @@ -11,3 +11,5 @@ export const OptionAbout = () => { ) } + +export default OptionAbout diff --git a/src/routes/option-settings-knowledge.tsx b/src/routes/option-settings-knowledge.tsx index aedddd3..504035c 100644 --- a/src/routes/option-settings-knowledge.tsx +++ b/src/routes/option-settings-knowledge.tsx @@ -2,7 +2,7 @@ import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout" import OptionLayout from "~/components/Layouts/Layout" import { KnowledgeSettings } from "@/components/Option/Knowledge" -export const OptionKnowledgeBase = () => { + const OptionKnowledgeBase = () => { return ( @@ -11,3 +11,5 @@ export const OptionKnowledgeBase = () => { ) } + +export default OptionKnowledgeBase \ No newline at end of file diff --git a/src/routes/option-settings-model.tsx b/src/routes/option-settings-model.tsx index 99264d9..456e1dd 100644 --- a/src/routes/option-settings-model.tsx +++ b/src/routes/option-settings-model.tsx @@ -2,7 +2,7 @@ import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout" import OptionLayout from "~/components/Layouts/Layout" import { ModelsBody } from "~/components/Option/Models" -export const OptionModal = () => { +const OptionModal = () => { return ( @@ -11,3 +11,5 @@ export const OptionModal = () => { ) } + +export default OptionModal diff --git a/src/routes/option-settings-prompt.tsx b/src/routes/option-settings-prompt.tsx index f7a35cf..f796403 100644 --- a/src/routes/option-settings-prompt.tsx +++ b/src/routes/option-settings-prompt.tsx @@ -2,7 +2,7 @@ import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout" import OptionLayout from "~/components/Layouts/Layout" import { PromptBody } from "~/components/Option/Prompt" -export const OptionPrompt = () => { + const OptionPrompt = () => { return ( @@ -11,3 +11,5 @@ export const OptionPrompt = () => { ) } + +export default OptionPrompt \ No newline at end of file diff --git a/src/routes/option-settings-share.tsx b/src/routes/option-settings-share.tsx index dcd0da0..3ca7a30 100644 --- a/src/routes/option-settings-share.tsx +++ b/src/routes/option-settings-share.tsx @@ -2,7 +2,7 @@ import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout" import OptionLayout from "~/components/Layouts/Layout" import { OptionShareBody } from "~/components/Option/Share" -export const OptionShare = () => { + const OptionShare = () => { return ( @@ -11,3 +11,5 @@ export const OptionShare = () => { ) } + +export default OptionShare \ No newline at end of file diff --git a/src/routes/option-settings.tsx b/src/routes/option-settings.tsx index 6849f7f..3bb60c0 100644 --- a/src/routes/option-settings.tsx +++ b/src/routes/option-settings.tsx @@ -2,7 +2,7 @@ import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout" import OptionLayout from "~/components/Layouts/Layout" import { SettingOther } from "~/components/Option/Settings/other" -export const OptionSettings = () => { + const OptionSettings = () => { return ( @@ -11,3 +11,5 @@ export const OptionSettings = () => { ) } + +export default OptionSettings \ No newline at end of file diff --git a/src/routes/options-settings-ollama.tsx b/src/routes/options-settings-ollama.tsx index cdbbad1..dad6498 100644 --- a/src/routes/options-settings-ollama.tsx +++ b/src/routes/options-settings-ollama.tsx @@ -2,7 +2,7 @@ import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout" import OptionLayout from "~/components/Layouts/Layout" import { SettingsOllama } from "~/components/Option/Settings/ollama" -export const OptionOllamaSettings = () => { + const OptionOllamaSettings = () => { return ( @@ -11,3 +11,5 @@ export const OptionOllamaSettings = () => { ) } + +export default OptionOllamaSettings \ No newline at end of file diff --git a/src/routes/sidepanel-chat.tsx b/src/routes/sidepanel-chat.tsx index 0e87879..5921a2b 100644 --- a/src/routes/sidepanel-chat.tsx +++ b/src/routes/sidepanel-chat.tsx @@ -4,7 +4,7 @@ import { SidepanelForm } from "~/components/Sidepanel/Chat/form" import { SidepanelHeader } from "~/components/Sidepanel/Chat/header" import { useMessage } from "~/hooks/useMessage" -export const SidepanelChat = () => { + const SidepanelChat = () => { const drop = React.useRef(null) const [dropedFile, setDropedFile] = React.useState() const [dropState, setDropState] = React.useState< @@ -90,3 +90,5 @@ export const SidepanelChat = () => {
) } + +export default SidepanelChat diff --git a/src/routes/sidepanel-settings.tsx b/src/routes/sidepanel-settings.tsx index ad51649..eae0658 100644 --- a/src/routes/sidepanel-settings.tsx +++ b/src/routes/sidepanel-settings.tsx @@ -1,7 +1,7 @@ import { SettingsBody } from "~/components/Sidepanel/Settings/body" import { SidepanelSettingsHeader } from "~/components/Sidepanel/Settings/header" -export const SidepanelSettings = () => { +const SidepanelSettings = () => { return (
@@ -11,3 +11,5 @@ export const SidepanelSettings = () => {
) } + +export default SidepanelSettings From 084dc57b0fc2e02aa926ce205bf2140ffccd1e09 Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Mon, 13 May 2024 12:58:52 +0530 Subject: [PATCH 11/16] chore: Update page loading fallback components --- src/components/Common/PageAssistLoader.tsx | 10 ++++++++++ src/routes/index.tsx | 5 +++-- tailwind.config.js | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 src/components/Common/PageAssistLoader.tsx diff --git a/src/components/Common/PageAssistLoader.tsx b/src/components/Common/PageAssistLoader.tsx new file mode 100644 index 0000000..f98e014 --- /dev/null +++ b/src/components/Common/PageAssistLoader.tsx @@ -0,0 +1,10 @@ + +export const PageAssistLoader = () => { + return ( +
+

+ Loading... +

+
+ ) +} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 4d5459f..47c4cdd 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -4,6 +4,7 @@ import { useDarkMode } from "~/hooks/useDarkmode" import { Skeleton } from "antd" import { OptionRoutingChrome, SidepanelRoutingChrome } from "./chrome" import { OptionRoutingFirefox, SidepanelRoutingFirefox } from "./firefox" +import { PageAssistLoader } from "@/components/Common/PageAssistLoader" export const OptionRouting = () => { const { mode } = useDarkMode() @@ -14,7 +15,7 @@ export const OptionRouting = () => { className={`${mode === "dark" ? "dark" : "light"} ${ i18n.language === "ru" ? "onest" : "inter" }`}> - }> + }> {import.meta.env.BROWSER === "chrome" ? ( ) : ( @@ -34,7 +35,7 @@ export const SidepanelRouting = () => { className={`${mode === "dark" ? "dark" : "light"} ${ i18n.language === "ru" ? "onest" : "inter" }`}> - }> + }> {import.meta.env.BROWSER === "chrome" ? ( ) : ( diff --git a/tailwind.config.js b/tailwind.config.js index a30235c..f4b105c 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -3,5 +3,5 @@ module.exports = { mode: "jit", darkMode: "class", content: ["./src/**/*.tsx"], - plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")] + plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography"),] } From b908e670dbeb84a82295e73a62e7e8dce6035dc9 Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Mon, 13 May 2024 13:13:01 +0530 Subject: [PATCH 12/16] chore: Remove unused Skeleton import in index.tsx --- src/routes/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 47c4cdd..86af217 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,7 +1,6 @@ import { Suspense } from "react" import { useTranslation } from "react-i18next" import { useDarkMode } from "~/hooks/useDarkmode" -import { Skeleton } from "antd" import { OptionRoutingChrome, SidepanelRoutingChrome } from "./chrome" import { OptionRoutingFirefox, SidepanelRoutingFirefox } from "./firefox" import { PageAssistLoader } from "@/components/Common/PageAssistLoader" From 44dd95c4942f491966d7fd4a9b35d1f67ab30598 Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Mon, 13 May 2024 13:15:19 +0530 Subject: [PATCH 13/16] chore: Update README with manual installation instructions for Ollama --- README.md | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a226cb4..e926ffd 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,13 @@ Note: You can install the extension on any Chromium-based browser. It is not lim ### Manual Installation +#### Pre-requisites + +- Node.js (v18 or higher) +- npm +- Ollama (Local AI Provider) + + 1. Clone the repository ```bash @@ -43,13 +50,19 @@ cd page-assist npm install ``` -3. Build the extension +3. Build the extension (by default it will build for Chrome) ```bash npm run build ``` -4. Load the extension +or you can build for Firefox + +```bash +npm run build:firefox +``` + +4. Load the extension (chrome) - Open the Extension Management page by navigating to `chrome://extensions`. @@ -57,6 +70,13 @@ npm run build - Click the `Load unpacked` button and select the `build` directory. +5. Load the extension (firefox) + +- Open the Add-ons page by navigating to `about:addons`. +- Click the `Extensions` tab. +- Click the `Manage Your Extensions` button. +- Click the `Load Temporary Add-on` button and select the `manifest.json` file from the `build` directory. + ## Usage ### Sidebar From b75e5a11eee772c4b4a12ce9a3b5a86170d16e21 Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Mon, 13 May 2024 13:15:57 +0530 Subject: [PATCH 14/16] chore: Update README with manual installation instructions for Ollama --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e926ffd..c1fdc5a 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,9 @@ Note: You can install the extension on any Chromium-based browser. It is not lim #### Pre-requisites -- Node.js (v18 or higher) +- Node.js (v18 or higher) - [Installation Guide](https://nodejs.org) - npm -- Ollama (Local AI Provider) +- Ollama (Local AI Provider) - [Installation Guide](https://ollama.com) 1. Clone the repository From 4f98ed7e1441595c693545761b131d2813b761ef Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Mon, 13 May 2024 13:32:45 +0530 Subject: [PATCH 15/16] chore: Update manifest name for Firefox support --- wxt.config.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/wxt.config.ts b/wxt.config.ts index d14a859..e2950e1 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -49,7 +49,10 @@ export default defineConfig({ manifest: { version: "1.1.7", - name: "__MSG_extName__", + name: + process.env.TARGET === "firefox" + ? "Page Assist - A Web UI for Local AI Models" + : "__MSG_extName__", description: "__MSG_extDescription__", default_locale: "en", action: {}, From 8f8fac826e02342a5253cad2bcadda7cab134cec Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Mon, 13 May 2024 13:45:17 +0530 Subject: [PATCH 16/16] chore: Update gecko ID for Firefox support --- wxt.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wxt.config.ts b/wxt.config.ts index e2950e1..0f5aeba 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -61,7 +61,7 @@ export default defineConfig({ process.env.TARGET === "firefox" ? { gecko: { - id: "page-assist@n4ze3m" + id: "page-assist@nazeem" } } : undefined,