diff --git a/README.md b/README.md index 8ecc4e3..fff6555 100644 --- a/README.md +++ b/README.md @@ -1,131 +1,59 @@ -
- +

+Zotero-ChatPDF +

+Zotero-ChatPDF is an advanced tool that integrates seamlessly with Zotero, enabling effortless interaction with PDF documents through state-of-the-art (SOTA) large language models (LLMs). It offers users the ability to ask questions, extract insights, and converse with PDFs directly, providing a powerful research assistant for scholars, researchers, and anyone who deals with large amounts of text in PDF format. +# Key Features +Effortless PDF Interaction: Chat with your PDFs directly in Zotero, asking questions and receiving detailed answers in natural language. + +SOTA Language Models: Powered by cutting-edge LLMs, such as gpt-4o, claude-3.5-sonnet and gemini-1.5-pro, offering highly accurate and contextually relevant responses. +For Mac users, there are some excellent free and open-source models built in, such as llama3.1, gemma2, phi-3.5, etc. Now after free registration, they can be automatically downloaded, installed and used with just one click on the plugin page, and the model data is all locally stored, ensuring absolute privacy and security of the data -# Awesome GPT +Annotations and Highlights: Extract annotations and highlights from your PDFs and use them for deeper analysis and conversation. +Full-text Search: Automatically scan and index the full text of PDFs to enable more precise question-answering. -👋 +Seamless Zotero Integration: Syncs directly with your Zotero library, making it easy to manage and query your documents without leaving the Zotero interface. + -Welcome to share your command tag [here](https://github.com/MuiseDestiny/zotero-gpt/discussions/3) using [Meet API](src/modules/Meet/api.ts). +# How to Use +Installation: Download [here](), Open Zotero. In the top menu bar, click on `Tools > Add-ons`. + + Click on the gear icon at the top right of the window. Click on `Install Add-on From File` and open the downloaded plugin file zotero-chatpdf.xpi. -[![Using Zotero Plugin Template](https://img.shields.io/badge/Using-Zotero%20Plugin%20Template-blue?style=flat-round&logo=github)](https://github.com/windingwind/zotero-plugin-template) -[![Latest release](https://img.shields.io/github/v/release/MuiseDestiny/zotero-gpt)](https://github.com/MuiseDestiny/zotero-gpt/releases) -![Release Date](https://img.shields.io/github/release-date/MuiseDestiny/zotero-gpt?color=9cf) -[![License](https://img.shields.io/github/license/MuiseDestiny/zotero-gpt)](https://github.com/MuiseDestiny/zotero-gpt/blob/master/LICENSE) -![Downloads latest release](https://img.shields.io/github/downloads/MuiseDestiny/zotero-gpt/latest/total?color=yellow) +Startup: In Zotero, press the keys to start the plugin, MacOS(command + enter), Windows(ctrl + enter). +Select LLM models: For Windows users, after registering the OpenAI, Claude, and Gemini models can all be accessed and switched by one click. - + For Mac users, after registering llama3.1, gemma2, phi-3.5 and mistral can all be used by just one click in plugin without extra need to install many additional tools or softwares. + Now the registration is open and free! + +Chat with PDFs: Open any PDF and start asking questions. Zotero-ChatPDF will process the document and provide insightful responses. -
+Manage Insights: Save, export, or share the extracted insights, answers, and annotations from your conversations. +Quit: Press esc key to exit. - ---- - -## 🚀 Main Features -Features about GPT: -- [x] 🔗 **Integrate with Zotero**: You can use the plugin to search and ask items in the library based on the selected text or the PDF file. -- [x] 🧠 Use GPT to generate reply text: support `gpt-3.5-turbo` and `gpt-4` -- [x] 🏷️ [Command tags](https://github.com/MuiseDestiny/zotero-gpt#command-tags): **Click once** to accelerate your research. - - [x] 💬 Ask questions about current **PDF file** (full-text or selected text). - - [x] 💬 Ask questions about **selected paper** (Abstract). - - [x] 📝 **Summarize the selected paper** into several highly condensed sentences. - - [x] 🔍 **Search items** in the library based on the selected text. - - [x] ... ... -- [x] ⚙️ **Advanced settings for GPT**: You can set the [api key](https://platform.openai.com/account/api-keys), [model name](https://platform.openai.com/docs/api-reference/chat/create#chat/create-model), [api url](https://platform.openai.com/docs/api-reference/chat/create), [temperature](https://platform.openai.com/docs/api-reference/chat/create#chat/create-temperature). -- [x] 📚 **Integrate with Better Notes**: You can directly open this plugin when using [Better Notes](https://github.com/windingwind/zotero-better-notes). - -Features about UI: -- [x] 🎨 **Real-time markdown rendering** for reply text: Latex and mathjax are supported. -- [x] 🔍 **Zoom in and out** of the reply text or the size of the plugin window. -- [x] 🖱️ **Move the plugin window to any position** on the screen. -- [x] 📋 **Copy the reply text** to the clipboard. -- [x] ⚠️ Detailed **error message** will be displayed when the request fails. -- [x] 🔧 Compatible with **Zotero 6** and **Zotero 7**. -- [x] 🎉 Discover more exciting features that are not listed here. - - -## How to use -- [x] Get `.xpi` file - - [ ] [download latest](https://github.com/MuiseDestiny/zotero-gpt/releases/latest/download/zotero-gpt.xpi) release `.xpi` file - - [ ] or build this project [1] to generate a `.xpi` file -- [x] Install `.xpi` file in Zotero [2] -- [x] Open Zotero GPT [3] -- [x] Set your `OpenAI` secret key [4] - -### [1] Build the project -Here is an example on how to build this project. For more information on how to build, please visit this project: [https://github.com/windingwind/zotero-plugin-template](https://github.com/windingwind/zotero-plugin-template) +# Build the plugin +If you like to build the plugin by yourself, do as the below commands: ```bash -git clone https://github.com/MuiseDestiny/zotero-gpt.git -cd zotero-gpt +git clone https://github.com/ljeagle/zotero-chatpdf.git +cd zotero-chatpdf npm install npm run build ``` -The generated `.xpi` file in the build directory is the extension that you can install in Zotero. - -### [2] Install the extension in Zotero -Open Zotero. In the top menu bar, click on `Tools > Add-ons`. -Click on the gear icon at the top right of the window. Click on `Install Add-on From File` and open the generated `.xpi` file in the build directory from the previous step. - -### [3] Open/Exit Zotero GPT - -|Action|Shortcut| -|--|--| -|Open|| -|Exit|`ESC`| -|Multi-line editing| `Shift` + `Enter`| - -### [4] Set up the API key - -![image](https://github.com/MuiseDestiny/zotero-gpt/assets/51939531/225c468a-acfc-43be-b5ac-cf6aaaa33e96) - -## Hi, Command Tag. -> 👻 Follow the steps below, and you will gain a new understanding of command tags. - -|Step| Description | Supplementary Information | -|----|-------------|---------------------------| -|1 | Open Zotero GPT | Refer to [3] Open/Exit Zotero GPT | -|2 | Type `#Tag Name` and press `Enter` | ![image](https://github.com/MuiseDestiny/zotero-gpt/assets/51939531/52f776fc-5592-4c17-8c36-7769c537ef79) | -|3 | Input your prompt or code | ![image](https://github.com/MuiseDestiny/zotero-gpt/assets/51939531/6f6d9985-69e5-4d29-ba78-df31e30e9cd1) | -|4 | **R**un your tag | Press `Ctrl + R` | -|5 | **S**ave your tag | Press `Ctrl + S` | -|6 | Long press a command tag to access the editing interface | ![image](https://github.com/MuiseDestiny/zotero-gpt/assets/51939531/28235117-79ab-43c6-b175-079e609683f4) | -|7 | Modify the tag's color, position, or trigger; remember to save with `Ctrl + S` | ![image](https://github.com/MuiseDestiny/zotero-gpt/assets/51939531/5261878a-30ce-4ea5-b3be-9c6b9ef29f70) | -|8 | Press `ESC` to exit the editing interface | Remember to save your changes with `Ctrl + S` before exiting | -|9 | Long press the right mouse button to delete a tag | Note: Build-in tags do not support deletion | - -### How to run a command tag -> Trigger is an attribute of a command tag, as are color and position. Long press any label to view/modify its trigger word. It supports both plain text and JS regular expressions. - -![How to run a command tag](https://github.com/MuiseDestiny/zotero-gpt/assets/51939531/fdfc369a-1e96-478c-a7c2-4a93d2d7a580) - -![image](https://github.com/MuiseDestiny/zotero-gpt/assets/51939531/d7f857a4-9ed9-42af-8662-6336ce70a881) - - -### How to write a code block - -You can find some build-in APIs [here](https://github.com/MuiseDestiny/zotero-gpt/blob/bootstrap/src/modules/Meet/api.ts). - -A simple example: -``` -Summarize the following paragraph for me: - -${Meet.Zotero.getPDFSelection()} -``` - -Here, the `Summarize the following paragraph for me:` represents plain text, while `${your code}` denotes a code snippet. Undoubtedly, if you are familiar with Zotero APIs, you can develop your own code. The code snippet will be executed, and the text returned by the code snippet will replace the code snippet. Finally, the replaced text will be input to GPT. So, theoretically, you can **accomplish all interactions** between Zotero and GPT using command tags. - -### How to navigate historical chats - -> Press the up (↑) and down (↓) keys on the keyboard to navigate. - -![image](https://github.com/MuiseDestiny/zotero-gpt/assets/51939531/ca2dcfbf-efb4-4ba3-8339-5277a879e3ea) - -## Support the project - -[Here](https://github.com/MuiseDestiny/zotero-reference#%E8%B5%9E%E5%8A%A9) +The plugin file(zotero-chatpdf.xpi) will be built and generated into the build directory + + +# Use Cases +Research Assistance: Summarize research papers, identify key concepts, and quickly get answers to your questions. + +Academic Writing: Generate insights for literature reviews or dive deep into specific sections of papers. + +Collaborative Projects: Share annotated PDFs and responses with colleagues and teams for smoother collaboration. + +# Contributions +Contributions to Zotero-ChatPDF are welcome! Please follow the standard GitHub process for submitting pull requests or reporting issues. diff --git a/addon/chrome/content/icons/favicon.png b/addon/chrome/content/icons/favicon.png deleted file mode 100644 index 43f30c4..0000000 Binary files a/addon/chrome/content/icons/favicon.png and /dev/null differ diff --git a/addon/chrome/content/md.css b/addon/chrome/content/md.css index 526cb2d..e3f4085 100644 --- a/addon/chrome/content/md.css +++ b/addon/chrome/content/md.css @@ -1078,4 +1078,4 @@ .markdown-body ::-webkit-calendar-picker-indicator { filter: invert(50%) -} \ No newline at end of file +} diff --git a/addon/install.rdf b/addon/install.rdf index 3b73c4f..31cc5db 100644 --- a/addon/install.rdf +++ b/addon/install.rdf @@ -12,7 +12,7 @@ em:creator="__author__" em:description="__description__" em:homepageURL="__homepage__" - em:iconURL="chrome://__addonRef__/content/icons/favicon.png" + em:iconURL="chrome://__addonRef__/content/icons/favicon.ico" em:optionsURL="chrome://__addonRef__/content/preferences.xul" em:updateURL="__updaterdf__" em:multiprocessCompatible="true" diff --git a/addon/manifest.json b/addon/manifest.json index 029bf11..c2b9a30 100644 --- a/addon/manifest.json +++ b/addon/manifest.json @@ -5,9 +5,13 @@ "description": "__description__", "author": "__author__", "icons": { - "48": "chrome/content/icons/favicon@0.5x.png", - "96": "chrome/content/icons/favicon.png" + "48": "chrome/content/icons/favicon@0.5x.ico", + "96": "chrome/content/icons/favicon.ico" }, + "permissions": [ + "downloads", + "downloads.open" + ], "applications": { "zotero": { "id": "__addonID__", diff --git a/addon/prefs.js b/addon/prefs.js index c083468..a6180e4 100644 --- a/addon/prefs.js +++ b/addon/prefs.js @@ -1,5 +1,20 @@ pref("extensions.zotero.__addonRef__.enable", true); pref("extensions.zotero.__addonRef__.tags", "[]"); +pref("extensions.zotero.__addonRef__.languages", "[\"Arabic\", \"Bengali\", \"Bulgarian\", \"Chinese\", \"Croatian\", \"Czech\", \"Danish\", \"Dutch\", \"English\", \"Estonian\", \"Finnish\", \"French\", \"German\", \"Greek\", \"Hebrew\", \"Hindi\", \"Hungarian\", \"Italian\", \"Indonesian\", \"Japanese\", \"Korean\", \"Latvian\", \"Norwegian\", \"Polish\", \"Portuguese\", \"Romanian\", \"Russian\", \"Serbian\", \"Slovak\", \"Slovenian\", \"Spanish\", \"Swahili\", \"Swedish\", \"Thai\", \"Turkish\", \"Ukrainian\", \"Vietnamese\"]"); +pref("extensions.zotero.__addonRef__.startLocalServer", false); +pref("extensions.zotero.__addonRef__.email", ""); +pref("extensions.zotero.__addonRef__.token", ""); +pref("extensions.zotero.__addonRef__.isLicenseActivated", false); +pref("extensions.zotero.__addonRef__.grade", ""); +pref("extensions.zotero.__addonRef__.supportedLLMs", ""); +pref("extensions.zotero.__addonRef__.usingLanguage", ""); +pref("extensions.zotero.__addonRef__.usingPublisher", "OpenAI"); +pref("extensions.zotero.__addonRef__.usingModel", "gpt-3.5-turbo"); +pref("extensions.zotero.__addonRef__.usingAPIKEY", ""); +pref("extensions.zotero.__addonRef__.usingAPIURL", "https://api.openai.com/v1/chat/completions"); +pref("extensions.zotero.__addonRef__.openaiApiKey", ""); +pref("extensions.zotero.__addonRef__.geminiApiKey", ""); +pref("extensions.zotero.__addonRef__.claudeApiKey", ""); pref("extensions.zotero.__addonRef__.secretKey", ""); pref("extensions.zotero.__addonRef__.model", "gpt-3.5-turbo"); pref("extensions.zotero.__addonRef__.api", "https://api.openai.com"); @@ -10,11 +25,3 @@ pref("extensions.zotero.__addonRef__.tagsMore", "expand"); pref("extensions.zotero.__addonRef__.chatNumber", 3); pref("extensions.zotero.__addonRef__.relatedNumber", 5); pref("extensions.zotero.__addonRef__.embeddingBatchNum", 10); - - - - - - - - diff --git a/imgs/apikey.png b/imgs/apikey.png deleted file mode 100644 index 35fd817..0000000 Binary files a/imgs/apikey.png and /dev/null differ diff --git a/imgs/background.png b/imgs/background.png deleted file mode 100644 index 0498b54..0000000 Binary files a/imgs/background.png and /dev/null differ diff --git a/imgs/background.svg b/imgs/background.svg deleted file mode 100644 index 96b4da5..0000000 --- a/imgs/background.svg +++ /dev/null @@ -1 +0,0 @@ -ZMuiseDestiny/zotero-gptGPTMeet Zotero \ No newline at end of file diff --git a/imgs/demo.png b/imgs/demo.png deleted file mode 100644 index 4bb6f3e..0000000 Binary files a/imgs/demo.png and /dev/null differ diff --git a/imgs/demo2.png b/imgs/demo2.png deleted file mode 100644 index d919f75..0000000 Binary files a/imgs/demo2.png and /dev/null differ diff --git a/imgs/prompt.png b/imgs/prompt.png deleted file mode 100644 index 82679e8..0000000 Binary files a/imgs/prompt.png and /dev/null differ diff --git a/package.json b/package.json index 2327233..36b03a2 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { - "name": "zotero-gpt", - "version": "0.2.8", - "description": "GPT Meet Zotero", + "name": "zotero-chatpdf", + "version": "0.0.1", + "description": "Integrates Zotero with ChatPDF", "config": { - "addonName": "Zotero GPT", - "addonID": "zoterogpt@polygon.org", - "addonRef": "zoterogpt", - "addonInstance": "ZoteroGPT", + "addonName": "Zotero ChatPDF", + "addonID": "zoterochatpdf@chatpdflocal.com", + "addonRef": "zoterochatpdf", + "addonInstance": "ZoteroChatPDF", "releasepage": "", "updaterdf": "" }, @@ -28,24 +28,26 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/MuiseDestiny/zotero-gpt.git" + "url": "git+https://github.com/ljeagle/zotero-chatpdf.git" }, - "author": "Polygon", + "author": "Vincent", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/MuiseDestiny/zotero-gpt/issues" + "url": "https://github.com/ljeagle/zotero-chatpdf/issues" }, "homepage": "", "dependencies": { "@dqbd/tiktoken": "^1.0.6", "@iktakahiro/markdown-it-katex": "^4.0.1", "@pinecone-database/pinecone": "^0.0.14", + "ansi-styles": "^6.2.1", "blueimp-md5": "^2.19.0", "chromadb": "^1.3.1", "compute-cosine-similarity": "^1.0.0", "crypto": "^1.0.1", "crypto-js": "^4.1.1", "dotenv": "^16.0.3", + "fs": "^0.0.1-security", "gpt-3-encoder": "^1.1.4", "highlight": "^0.2.4", "highlight.js": "^11.7.0", @@ -54,7 +56,7 @@ "lighten-darken-color": "^1.0.0", "markdown-it": "^13.0.1", "markdown-it-mathjax3": "^4.3.2", - "node-fetch": "^3.3.1", + "node-fetch": "^3.3.2", "pdf-parse": "^1.1.1", "pdfreader": "^3.0.0", "proxy-agent": "^5.0.0", @@ -62,7 +64,7 @@ "showdown": "^2.1.0", "terser": "^5.17.1", "zotero-adv-installer": "file:..", - "zotero-plugin-toolkit": "^2.0.1" + "zotero-plugin-toolkit": "2.1.3" }, "devDependencies": { "@types/crypto-js": "^4.1.1", diff --git a/scripts/start.js b/scripts/start.js index 2485412..7640c26 100644 --- a/scripts/start.js +++ b/scripts/start.js @@ -21,5 +21,6 @@ const startZotero = `${zoteroPath} --debugger --purgecaches ${ profile ? `-p ${profile}` : "" }`; +console.log("hello hello hello....") execSync(startZotero); exit(0); diff --git a/src/hooks.ts b/src/hooks.ts index db7f46f..850a024 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -2,10 +2,8 @@ import { config } from "../package.json"; import { getString, initLocale } from "./modules/locale"; import Views from "./modules/views"; import Utils from "./modules/utils"; -import { initValidation } from "../../validation/core"; async function onStartup() { - initValidation(config.addonRef); await Promise.all([ Zotero.initializationPromise, Zotero.unlockPromise, @@ -14,17 +12,150 @@ async function onStartup() { initLocale(); ztoolkit.ProgressWindow.setIconURI( "default", - `chrome://${config.addonRef}/content/icons/favicon.png` + `chrome://${config.addonRef}/content/icons/favicon.ico` ); Zotero[config.addonInstance].views = new Views(); - Zotero[config.addonInstance].utils = new Utils(); + + if (Zotero.isMac) { + // @ts-ignore + const OS = window.OS; + var filename = "ChatPDFLocal" + if (!(await OS.File.exists(filename))) { + const temp = Zotero.getTempDirectory(); + filename = OS.Path.join(temp.path.replace(temp.leafName, ""), `${filename}.dmg`); + } + + var tmp = `${config.addonRef}.startLocalServer` + + Zotero.Prefs.set(`${config.addonRef}.startLocalServer`, false) + if (!await checkFileExist(filename)) { + let url = "https://www.chatpdflocal.com/packages/ChatPDFLocal-test.dmg" + await downloadFile(url, filename) + } + + var startLocalServer = Zotero.Prefs.get(`${config.addonRef}.startLocalServer`) + + var email = Zotero.Prefs.get(`${config.addonRef}.email`) + var token = Zotero.Prefs.get(`${config.addonRef}.token`) + + if (!startLocalServer) { + await startLocalLLMEngine(filename) + Zotero.Prefs.set(`${config.addonRef}.startLocalServer`, true) + + const execFunc = async() => { + var email = Zotero.Prefs.get(`${config.addonRef}.email`) + var token = Zotero.Prefs.get(`${config.addonRef}.token`) + await Zotero[config.addonInstance].views.updatePublisherModels(email, token) + Zotero[config.addonInstance].views.createOrUpdateModelsContainer() + } + window.setTimeout(execFunc, 3000) + + } + } +} + +export async function downloadFile(url, filename) { + await Zotero.File.download(url, filename) + var signFile = filename + ".done" + var execCmd = [signFile]; + var exec = "/usr/bin/touch" + try { + await Zotero.Utilities.Internal.exec(exec, execCmd); + } catch { + } +} + +export async function checkFileExist(filename) { + const OS = window.OS + return await OS.File.exists(filename) +} + +export async function startLocalLLMEngine(filename) { + var execCmd = ['attach', filename]; + var exec = "/usr/bin/hdiutil" + try { + await Zotero.Utilities.Internal.exec(exec, execCmd); + } catch { + Zotero.log("hdiutil command error!") + } + + if (await checkFileExist("/Volumes/ChatPDFLocal/ChatPDFLocal.app")) { + execCmd = ['/Volumes/ChatPDFLocal/ChatPDFLocal.app', '--args', 'appLaunchType', 'backend'] + exec = "/usr/bin/open" + try { + await Zotero.Utilities.Internal.exec(exec, execCmd); + } catch { + } + } +} + +export async function shutdownLocalLLMEngine() { + var execCmd = ['-9', 'ChatPDFLocal'] + var exec = "/usr/bin/killall" + try { + await Zotero.Utilities.Internal.exec(exec, execCmd); + } catch { + } + + execCmd = ['-9', 'chatpdflocal-llama-server'] + exec = "/usr/bin/killall" + try { + await Zotero.Utilities.Internal.exec(exec, execCmd); + } catch { + } + + execCmd = ['-9', 'chatpdflocal-llama-server-x86'] + try { + await Zotero.Utilities.Internal.exec(exec, execCmd); + } catch { + } + + execCmd = ['-9', 'huggingface_download'] + try { + await Zotero.Utilities.Internal.exec(exec, execCmd); + } catch { + } + + execCmd = ['detach', '/Volumes/ChatPDFLocal']; + exec = "/usr/bin/hdiutil" + try { + await Zotero.Utilities.Internal.exec(exec, execCmd); + } catch { + } } function onShutdown(): void { + if (Zotero.isMac) { + Zotero.Prefs.set(`${config.addonRef}.startLocalServer`, false) + + shutdownLocalLLMEngine() + + // @ts-ignore + const OS = window.OS; + const temp = Zotero.getTempDirectory(); + var filename = "ChatPDFLocal" + filename = OS.Path.join(temp.path.replace(temp.leafName, ""), `${filename}.dmg`); + + + var execCmd = [filename]; + var exec = "/bin/rm" + try { + Zotero.Utilities.Internal.exec(exec, execCmd); + } catch { + } + + var signFile = filename + ".done" + execCmd = [signFile]; + try { + Zotero.Utilities.Internal.exec(exec, execCmd); + } catch { + } + } + ztoolkit.unregisterAll(); - // Remove addon object + addon.data.alive = false; delete Zotero[config.addonInstance]; } diff --git a/src/modules/Meet/BetterNotes.ts b/src/modules/Meet/BetterNotes.ts index 148cb75..f794a45 100644 --- a/src/modules/Meet/BetterNotes.ts +++ b/src/modules/Meet/BetterNotes.ts @@ -2,8 +2,8 @@ import Views from "../views"; import Meet from "./api"; /** - * 优先返回选中文本,再返回所在span前所有文字MD - * @param span 光标所在行,HTMLSpanElement + * Prioritize returning the selected text, and then return all the previous text MD in the span + * @param The line where the cursor is located, HTMLSpanElement * @returns */ export async function getEditorText(span: HTMLSpanElement) { @@ -38,7 +38,6 @@ export function replaceEditorText(htmlString: string) { const BNEditorApi = Zotero.BetterNotes.api.editor const editor = BNEditorApi.getEditorInstance(Zotero.BetterNotes.data.workspace.mainId); const range = BNEditorApi.getRangeAtCursor(editor) - // 删除原来 window.setTimeout(async () => { await Meet.Global.lock Meet.Global.lock = Zotero.Promise.defer() as _ZoteroTypes.PromiseObject @@ -49,7 +48,7 @@ export function replaceEditorText(htmlString: string) { } /** - * 在编辑器光标处插入文本 + * Insert text at the editor cursor * @param htmlString */ export function insertEditorText(htmlString: string, editor?: any) { @@ -69,10 +68,10 @@ export function insertEditorText(htmlString: string, editor?: any) { } /** - * 让GPT UI跟随此行 + * Let UI follow this row */ export function follow() { - const views = Zotero.ZoteroGPT.views as Views + const views = Zotero.ZoteroChatPDF.views as Views const BNEditorApi = Zotero.BetterNotes.api.editor const editor = BNEditorApi.getEditorInstance(Zotero.BetterNotes.data.workspace.mainId); let getLine: any = (index: number) => { @@ -81,7 +80,6 @@ export function follow() { let place = (reBuild: boolean = false) => { const lineIndex = BNEditorApi.getLineAtCursor(editor) + 1 let line = getLine(lineIndex) - // 光标有文字就下一行 if (line.innerText.replace("\n", "").trim().length != 0) { line = getLine(lineIndex+1) } @@ -93,16 +91,12 @@ export function follow() { leftPanel.getBoundingClientRect().width views.show(x + 30, y + 38, reBuild) } - // 第一次重建UI place(true) let id = window.setInterval(async () => { - // await Meet.Global.lock; - // Meet.Global.lock = Zotero.Promise.defer() as _ZoteroTypes.PromiseObject place() - // Meet.Global.lock.resolve() }, 10) views._ids.push({ type: "follow", id: id }) -} \ No newline at end of file +} diff --git a/src/modules/Meet/OpenAI.ts b/src/modules/Meet/OpenAI.ts deleted file mode 100644 index d739de6..0000000 --- a/src/modules/Meet/OpenAI.ts +++ /dev/null @@ -1,340 +0,0 @@ -import { config } from "../../../package.json"; -import { MD5 } from "crypto-js" -import { Document } from "langchain/document"; -import LocalStorage from "../localStorage"; -import Views from "../views"; -import Meet from "./api"; -const similarity = require('compute-cosine-similarity'); -declare type RequestArg = { headers: any, api: string, body: Function, remove?: string | RegExp, process?: Function } -let chatID: string -const requestArgs: RequestArg[] = [ - { - api: "https://aigpt.one/api/chat-stream", - headers: { - "path": "v1/chat/completions" - }, - body: (requestText: string, messages: any) => { - return { - "model": "gpt-3.5-turbo", - messages: messages, - stream: true, - "max_tokens": 2000, - "presence_penalty": 0 - } - } - }, - { - api: "https://chatbot.theb.ai/api/chat-process", - headers: { - }, - body: (requestText: string, messages: any) => { - return { "prompt": requestText, "options": { "parentMessageId": chatID }} - }, - process: (text: string) => { - const res = JSON.parse(text.split("\n").slice(-1)[0]) - chatID = res.id - return res.text - } - } -] - -/** - * 给定文本和文档,返回文档列表,返回最相似的几个 - * @param queryText - * @param docs - * @param obj - * @returns - */ -export async function similaritySearch(queryText: string, docs: Document[], obj: { key: string }) { - const storage = Meet.Global.storage = Meet.Global.storage || new LocalStorage(config.addonRef) - await storage.lock.promise; - const embeddings = new OpenAIEmbeddings() as any - // 查找本地,为节省空间,只储存向量 - // 因为随着插件更新,解析出的PDF可能会有优化,因此再此进行提取MD5值作为验证 - // 但可以预测,本地JSON文件可能会越来越大 - const id = MD5(docs.map((i: any) => i.pageContent).join("\n\n")).toString() - await storage.lock - const _vv = storage.get(obj, id) - ztoolkit.log(_vv) - let vv: any - if (_vv) { - Meet.Global.popupWin.createLine({ text: "Reading embeddings...", type: "default" }) - vv = _vv - } else { - Meet.Global.popupWin.createLine({ text: "Generating embeddings...", type: "default" }) - vv = await embeddings.embedDocuments(docs.map((i: any) => i.pageContent)) - window.setTimeout(async () => { - await storage.set(obj, id, vv) - }) - } - - const v0 = await embeddings.embedQuery(queryText) - // 从20个里面找出文本最长的几个,防止出现较短但相似度高的段落影响回答准确度 - const relatedNumber = Zotero.Prefs.get(`${config.addonRef}.relatedNumber`) as number - Meet.Global.popupWin.createLine({ text: `Searching ${relatedNumber} related content...`, type: "default" }) - const k = relatedNumber * 5 - const pp = vv.map((v: any) => similarity(v0, v)); - docs = [...pp].sort((a, b) => b - a).slice(0, k).map((p: number) => { - return docs[pp.indexOf(p)] - }) - // return docs.slice(0, relatedNumber) - return docs.sort((a, b) => b.pageContent.length - a.pageContent.length).slice(0, relatedNumber) -} - - -class OpenAIEmbeddings { - constructor() { - } - private async request(input: string[]) { - const views = Zotero.ZoteroGPT.views as Views - let api = Zotero.Prefs.get(`${config.addonRef}.api`) as string - api = api.replace(/\/(?:v1)?\/?$/, "") - const secretKey = Zotero.Prefs.get(`${config.addonRef}.secretKey`) - const split_len = Zotero.Prefs.get(`${config.addonRef}.embeddingBatchNum`) - let res - const url = `${api}/v1/embeddings` - if (!secretKey) { - new ztoolkit.ProgressWindow(url, { closeOtherProgressWindows: true }) - .createLine({ text: "Your secretKey is not configured.", type: "default" }) - .show() - return - } - var final_embeddings=[] - for (let i = 0; i < input.length; i += split_len) { - -      const chunk = input.slice(i, i + split_len) - ztoolkit.log("input", chunk) - try { - res = await Zotero.HTTP.request( - "POST", - url, - { - responseType: "json", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${secretKey}`, - }, - body: JSON.stringify({ - model: "text-embedding-ada-002", - input: chunk - }), - } - ) - } catch (error: any) { - try { - error = error.xmlhttp.response?.error - views.setText(`# ${error.code}\n> ${url}\n\n**${error.type}**\n${error.message}`, true) - new ztoolkit.ProgressWindow(error.code, { closeOtherProgressWindows: true }) - .createLine({ text: error.message, type: "default" }) - .show() - } catch { - new ztoolkit.ProgressWindow("Error", { closeOtherProgressWindows: true }) - .createLine({ text: error.message, type: "default" }) - .show() - } - } - if (res?.response?.data) { - final_embeddings = final_embeddings.concat(res.response.data.map((i: any) => i.embedding)) - } -    } - return final_embeddings - } - - public async embedDocuments(texts: string[]) { - return await this.request(texts) - } - - public async embedQuery(text: string) { - return (await this.request([text]))?.[0] - } -} - - -export async function getGPTResponse(requestText: string) { - const secretKey = Zotero.Prefs.get(`${config.addonRef}.secretKey`) - // 这里可以补充很多免费API,然后用户设置用哪个 - if (!secretKey) { return await getGPTResponseBy(requestArgs[1], requestText) } - else { return await getGPTResponseByOpenAI(requestText) } -} - -/** - * 所有getGPTResponseTextByXXX参照此函数实现 - * gpt-3.5-turbo / gpt-4 - * @param requestText - * @returns - */ -export async function getGPTResponseByOpenAI(requestText: string) { - const views = Zotero.ZoteroGPT.views as Views - const secretKey = Zotero.Prefs.get(`${config.addonRef}.secretKey`) - const temperature = Zotero.Prefs.get(`${config.addonRef}.temperature`) - let api = Zotero.Prefs.get(`${config.addonRef}.api`) as string - api = api.replace(/\/(?:v1)?\/?$/, "") - const model = Zotero.Prefs.get(`${config.addonRef}.model`) - views.messages.push({ - role: "user", - content: requestText - }) - // outputSpan.innerText = responseText; - const deltaTime = Zotero.Prefs.get(`${config.addonRef}.deltaTime`) as number - // 储存上一次的结果 - let _textArr: string[] = [] - // 随着请求返回实时变化 - let textArr: string[] = [] - // 激活输出 - views.stopAlloutput() - views.setText("") - let responseText: string | undefined - const id: number = window.setInterval(async () => { - if (!responseText && _textArr.length == textArr.length) { return} - _textArr = textArr.slice(0, _textArr.length + 1) - let text = _textArr.join("") - text.length > 0 && views.setText(text) - if (responseText && responseText == text) { - views.setText(text, true) - window.clearInterval(id) - } - }, deltaTime) - views._ids.push({ - type: "output", - id: id - }) - const chatNumber = Zotero.Prefs.get(`${config.addonRef}.chatNumber`) as number - const url = `${api}/v1/chat/completions` - try { - await Zotero.HTTP.request( - "POST", - url, - { - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${secretKey}`, - }, - body: JSON.stringify({ - model: model, - messages: views.messages.slice(-chatNumber), - stream: true, - temperature: Number(temperature) - }), - responseType: "text", - requestObserver: (xmlhttp: XMLHttpRequest) => { - xmlhttp.onprogress = (e: any) => { - try { - textArr = e.target.response.match(/data: (.+)/g).filter((s: string) => s.indexOf("content") >= 0).map((s: string) => { - try { - return JSON.parse(s.replace("data: ", "")).choices[0].delta.content.replace(/\n+/g, "\n") - } catch { - return false - } - }).filter(Boolean) - } catch { - // 出错一般是token超出限制 - ztoolkit.log(e.target.response) - } - if (e.target.timeout) { - e.target.timeout = 0; - } - }; - }, - } - ); - } catch (error: any) { - try { - error = JSON.parse(error?.xmlhttp?.response).error - textArr = [`# ${error.code}\n> ${url}\n\n**${error.type}**\n${error.message}`] - new ztoolkit.ProgressWindow(error.code, { closeOtherProgressWindows: true }) - .createLine({ text: error.message, type: "default" }) - .show() - } catch { - new ztoolkit.ProgressWindow("Error", { closeOtherProgressWindows: true }) - .createLine({ text: error.message, type: "default" }) - .show() - } - } - responseText = textArr.join("") - ztoolkit.log("responseText", responseText) - // if (views._ids.map(i=>i.id).indexOf(id) >= 0 ) { - // views.setText(responseText, true) - // } - // window.clearInterval(id) - views.messages.push({ - role: "assistant", - content: responseText - }) - return responseText -} - -/** - * 返回值要是纯文本 - * @param requestArg - * @param requestText - * @param views - * @returns - */ -export async function getGPTResponseBy( - requestArg: RequestArg, - requestText: string, -) { - const views = Zotero.ZoteroGPT.views as Views - const deltaTime = Zotero.Prefs.get(`${config.addonRef}.deltaTime`) as number - let responseText: string | undefined - let _responseText = "" - views.messages.push({ - role: "user", - content: requestText - }) - // 储存上一次的结果 - // 激活输出 - views.stopAlloutput() - views.setText("") - const id = window.setInterval(() => { - _responseText.trim().length > 0 && views.setText(_responseText) - if (responseText && responseText == _responseText) { - views.setText(_responseText, true) - window.clearInterval(id) - } - }, deltaTime) - views._ids.push({ type: "output", id: id }) - const chatNumber = Zotero.Prefs.get(`${config.addonRef}.chatNumber`) as number - const body = JSON.stringify(requestArg.body(requestText, views.messages.slice(-chatNumber))) - await Zotero.HTTP.request( - "POST", - requestArg.api, - { - headers: { - "Content-Type": "application/json", - ...requestArg.headers - }, - body, - responseType: "text", - requestObserver: (xmlhttp: XMLHttpRequest) => { - xmlhttp.onprogress = (e: any) => { - _responseText = e.target.response.replace(requestArg.remove, "") - if (requestArg.process) { - _responseText = requestArg.process(_responseText) - } - if (e.target.timeout) { - e.target.timeout = 0; - } - }; - }, - } - ); - // if (views._ids.map(i => i.id).indexOf(id) >= 0) { - // views.setText(responseText, true) - // } - // window.clearInterval(id) - // if (views.isInNote) { - // window.setTimeout(async () => { - // Meet.BetterNotes.replaceEditorText( - // // await Zotero.BetterNotes.api.convert.md2html(responseText) - // views.container.querySelector(".markdown-body")!.innerHTML - // ) - // }) - // } - responseText = _responseText - views.messages.push({ - role: "assistant", - content: responseText - }) - return responseText -} diff --git a/src/modules/Meet/Zotero.ts b/src/modules/Meet/Zotero.ts index 7e58210..2a31e0f 100644 --- a/src/modules/Meet/Zotero.ts +++ b/src/modules/Meet/Zotero.ts @@ -1,12 +1,13 @@ import { config } from "../../../package.json"; import { MD5 } from "crypto-js" import { Document } from "langchain/document"; -import { similaritySearch } from "./OpenAI"; +import { similaritySearch } from "./integratellms"; +import { search, isDocumentExist, addDoc } from "./chatpdflocal"; import Meet from "./api"; import ZoteroToolkit from "zotero-plugin-toolkit"; /** - * 读取剪贴板 + * Read clipboard * @returns string */ export function getClipboardText(): string { @@ -15,7 +16,7 @@ export function getClipboardText(): string { // @ts-ignore const transferable = window.Cc['@mozilla.org/widget/transferable;1'].createInstance(Ci.nsITransferable); if (!transferable) { - window.alert('剪贴板服务错误:无法创建可传输的实例'); + window.alert('Clipboard service error: Unable to create transportable instance'); } transferable.addDataFlavor('text/unicode'); clipboardService.getData(transferable, clipboardService.kGlobalClipboard); @@ -24,17 +25,15 @@ export function getClipboardText(): string { try { transferable.getTransferData('text/unicode', clipboardData, clipboardLength); } catch (err: any) { - window.console.error('剪贴板服务获取失败:', err.message); + window.console.error('Clipboard service acquisition failed:', err.message); } - // @ts-ignore clipboardData = clipboardData.value.QueryInterface(Ci.nsISupportsString); - // @ts-ignore return clipboardData.data } /** - * 将选中条目处理成全文 - * 注意:这里目前是不储存得到向量的,因为条目一直在更新 + * Process selected items into full text + * Note: The vector is not currently stored here because the entries are constantly being updated * @param key * @returns */ @@ -54,7 +53,6 @@ async function selectedItems2documents(key: string) { } /** - * https://github.com/MuiseDestiny/zotero-reference/blob/743bef7ac59d644675d8ab33a0b6c138d47fdb2f/src/modules/pdf.ts#L75 * @param items * @returns */ @@ -81,7 +79,7 @@ function mergeSameLine(items: PDFItem[]) { for (j = 1; j < items.length; j++) { let line = toLine(items[j]) let lastLine = lines.slice(-1)[0] - // 考虑上标下标 + // Consider superscript and subscript if ( line.y == lastLine.y || (line.y >= lastLine.y && line.y < lastLine.y + lastLine.height) || @@ -90,15 +88,10 @@ function mergeSameLine(items: PDFItem[]) { lastLine.text += (" " + line.text) lastLine.width += line.width lastLine.url = lastLine.url || line.url - // 记录所有高度 + // Record all altitudes lastLine._height.push(line.height) } else { - // 处理已完成的行,用众数赋值高度 let hh = lastLine._height - // lastLine.height = hh.sort((a, b) => a - b)[parseInt(String(hh.length / 2))] - // 用最大值 - // lastLine.height = hh.sort((a, b) => b-a)[0] - // 众数 const num: any = {} for (let i = 0; i < hh.length; i++) { num[String(hh[i])] ??= 0 @@ -109,7 +102,6 @@ function mergeSameLine(items: PDFItem[]) { return num[h2] - num[h1] })[0] ) - // 新的一行 lines.push(line) } } @@ -124,7 +116,7 @@ declare type Box = { } /** - * 判断A和B两个矩形是否几何相交 + * Determine whether two rectangles A and B intersect geometrically * @param A * @param B * @returns @@ -143,7 +135,7 @@ function isIntersect(A: Box, B: Box): boolean { } /** - * 判断两行是否是跨页同位置行 + * Determine whether two rows are cross-page rows at the same position置行 * @param lineA * @param lineB * @param maxWidth @@ -167,9 +159,9 @@ function isIntersectLines(lineA: any, lineB: any, maxWidth: number, maxHeight: n } /** - * 读取PDF全文,因为读取速度一般较快,所以不储存 - * 当然排除学位论文,书籍等 - * 此函数遇到reference关键词会停止读取,因为参考文献太影响最后计算相似度了 + * Read the full text of the PDF. Because the reading speed is generally faster, it is not stored. + * Of course, dissertations, books, etc. are excluded + * This function will stop reading when it encounters the reference keyword, because the reference too affects the final calculation of similarity. */ async function pdf2documents(itemkey: string) { const reader = await ztoolkit.Reader.getReader() as _ZoteroTypes.ReaderInstance @@ -183,7 +175,7 @@ async function pdf2documents(itemkey: string) { // .show() const popupWin = Meet.Global.popupWin.createLine({ text: `[1/${totalPageNum}] Reading PDF`, progress: 1, type: "success" }) .show() - // 读取所有页面lines + // Read all lines of the page const pageLines: any = {} let docs: Document[] = [] for (let pageNum = 0; pageNum < totalPageNum; pageNum++) { @@ -197,7 +189,6 @@ async function pdf2documents(itemkey: string) { } pageLines[pageNum] = lines popupWin.changeLine({ idx: popupWin.lines.length - 1, text: `[${pageNum + 1}/${totalPageNum}] Reading PDF`, progress: (pageNum + 1) / totalPageNum * 100}) - // 防止误杀 if (index != -1 && pageNum / totalPageNum >= .9) { break } @@ -210,27 +201,27 @@ async function pdf2documents(itemkey: string) { const maxWidth = pdfPage._pageInfo.view[2]; const maxHeight = pdfPage._pageInfo.view[3]; let lines = [...pageLines[pageNum]] - // 去除页眉页脚信息 + // Remove header and footer information let removeLines = new Set() let removeNumber = (text: string) => { - // 英文页码 + // page number if (/^[A-Z]{1,3}$/.test(text)) { text = "" } - // 正常页码1,2,3 + // Normal page numbers 1, 2, 3 text = text.replace(/\x20+/g, "").replace(/\d+/g, "") return text } - // 是否为重复 + // whether duplicated let isRepeat = (line: PDFLine, _line: PDFLine) => { let text = removeNumber(line.text) let _text = removeNumber(_line.text) return text == _text && isIntersectLines(line, _line, maxWidth, maxHeight) } - // 存在于数据起始结尾的无效行 + // Invalid rows exist at the beginning and end of the data for (let i of Object.keys(pageLines)) { if (Number(i) == pageNum) { continue } - // 两个不同页,开始对比 + // Compare two different pages let _lines = pageLines[i] let directions = { forward: { @@ -250,7 +241,7 @@ async function pdf2documents(itemkey: string) { let line = lines.slice(index)[0] let _line = _lines.slice(index)[0] if (isRepeat(line, _line)) { - // 认为是相同的 + // considered to be the same line[direction] = true removeLines.add(line) } else { @@ -258,8 +249,7 @@ async function pdf2documents(itemkey: string) { } }) } - // 内部的 - // 设定一个百分百正文区域防止误杀 + // Set a 100% text area to prevent accidental wrong const content = { x: 0.2 * maxWidth, width: .6 * maxWidth, y: .2 * maxHeight, height: .6 * maxHeight } for (let j = 0; j < lines.length; j++) { let line = lines[j] @@ -275,8 +265,8 @@ async function pdf2documents(itemkey: string) { } } lines = lines.filter((e: any) => !(e.forward || e.backward || (e.repeat && e.repeat > 3))); - // 段落聚类 - // 原则:字体从大到小,合并;从小变大,断开 + // paragraph clustering + // principle: Font size from large to small, merge; From small to big let abs = (x: number) => x > 0 ? x : -x const paragraphs = [[lines[0]]] for (let i = 1; i < lines.length; i++) { @@ -284,34 +274,34 @@ async function pdf2documents(itemkey: string) { let currentLine = lines[i] let nextLine = lines[i + 1] const isNewParagraph = - // 达到一定行数阈值 + // Reach a certain row count threshold paragraphs.slice(-1)[0].length >= 5 && ( - // 当前行存在一个非常大的字体的文字 + // There is text in a very large font on the current line currentLine._height.some((h2: number) => lastLine._height.every((h1: number) => h2 > h1)) || - // 是摘要自动为一段 + // The abstract is automatically one paragraph /abstract/i.test(currentLine.text) || - // 与上一行间距过大 + // The distance from the previous line is too large abs(lastLine.y - currentLine.y) > currentLine.height * 2 || - // 首行缩进分段 + // First line indented paragraph (currentLine.x > lastLine.x && nextLine && nextLine.x < currentLine.x) ) - // 开新段落 + // Open new paragraph if (isNewParagraph) { paragraphs.push([currentLine]) } - // 否则纳入当前段落 + // Otherwise, include it in the current paragraph else { paragraphs.slice(-1)[0].push(currentLine) } } ztoolkit.log(paragraphs) - // 段落合并 + // Paragraph merge for (let i = 0; i < paragraphs.length; i++) { let box: { page: number, left: number; top: number; right: number; bottom: number } /** - * 所有line是属于一个段落的 - * 合并同时计算它的边界 + * All lines belong to a paragraph + * Merge while calculating its bounds */ let _pageText = "" let line, nextLine @@ -319,7 +309,7 @@ async function pdf2documents(itemkey: string) { line = paragraphs[i][j] if (!line) { continue } nextLine = paragraphs[i]?.[j + 1] - // 更新边界 + // Update boundaries box ??= { page: pageNum, left: line.x, right: line.x + line.width, top: line.y + line.height, bottom: line.y } if (line.x < box.left) { box.left = line.x @@ -363,39 +353,82 @@ async function pdf2documents(itemkey: string) { } /** - * 如果当前在主面板,根据选中条目生成文本,查找相关 - 用于搜索条目 - * 如果在PDF阅读界面,阅读PDF原文,查找返回相应段落 - 用于总结问题 + * If you are currently in the main panel, generate text based on the selected item and find related - used to search for items + * If you are in the PDF reading UI, read the original PDF text, search and return the corresponding paragraph - used to summarize the problem * @param queryText * @returns */ export async function getRelatedText(queryText: string) { - // @ts-ignore - const cache = (window._GPTGlobal ??= {cache: []}).cache - let docs: Document[], key: string - switch (Zotero_Tabs.selectedIndex) { - case 0: - // 只有再次选中相同条目,且条目没有更新变化,才会复用,不然会一直重复建立索引 - // TODO - 优化 - key = MD5(ZoteroPane.getSelectedItems().map(i => i.key).join("")).toString() - docs = cache[key] || await selectedItems2documents(key) - break; - default: - let pdfItem = Zotero.Items.get( - Zotero.Reader.getByTabID(Zotero_Tabs.selectedID)!.itemID as number - ) - key = pdfItem.key - docs = cache[key] || await pdf2documents(key) - break + + const usingPublisher = Zotero.Prefs.get(`${config.addonRef}.usingPublisher`) + if (usingPublisher != "Local LLM") { + // @ts-ignore + const cache = (window._GPTGlobal ??= {cache: []}).cache + let docs: Document[], key: string + switch (Zotero_Tabs.selectedIndex) { + case 0: + // Only when the same entry is selected again and the entry has not been updated will it be reused, otherwise the index will be created repeatedly + key = MD5(ZoteroPane.getSelectedItems().map(i => i.key).join("")).toString() + docs = cache[key] || await selectedItems2documents(key) + break; + default: + let pdfItem = Zotero.Items.get( + Zotero.Reader.getByTabID(Zotero_Tabs.selectedID)!.itemID as number + ) + key = pdfItem.key + docs = cache[key] || await pdf2documents(key) + break + } + cache[key] = docs + docs = await similaritySearch(queryText, docs, { key }) as Document[] + Zotero[config.addonInstance].views.insertAuxiliary(docs) + return docs.map((doc: Document, index: number) => `[${index + 1}]${doc.pageContent}`).join("\n\n") + } else { + var docs: Document[], key: string + let topn = 4 + var packFields: string + switch (Zotero_Tabs.selectedIndex) { + case 0: + // Only when the same entry is selected again and the entry has not been updated will it be reused, otherwise the index will be created repeatedly + key = MD5(ZoteroPane.getSelectedItems().map(i => i.key).join("")).toString() + if (!isDocumentExist(key)) { + docs = await selectedItems2documents(key) + await addDoc(key, docs, "id") + } + + packFields = "text-string:type-string:id-int" + + break; + default: + let pdfItem = Zotero.Items.get( + Zotero.Reader.getByTabID(Zotero_Tabs.selectedID)!.itemID as number + ) + key = pdfItem.key + let isKeyProcessed = await isDocumentExist(key) + if (!isKeyProcessed) { + docs = await pdf2documents(key) + await addDoc(key, docs, "box") + } + packFields = "text-string:type-string:box_page-int:box_left-float:box_right-float:box_bottom-float:box_top-float" + + break + } + const usingModel = Zotero.Prefs.get(`${config.addonRef}.usingModel`) + var results = await search("Local LLM", usingModel, key, queryText, topn, "", packFields) + + + Zotero[config.addonInstance].views.insertAuxiliary(results) + return results.map((doc: Document, index: number) => `[${index + 1}]${doc.pageContent}`).join("\n\n") } - cache[key] = docs - docs = await similaritySearch(queryText, docs, { key }) as Document[] - ztoolkit.log("docs", docs) - Zotero[config.addonInstance].views.insertAuxiliary(docs) - return docs.map((doc: Document, index: number) => `[${index + 1}]${doc.pageContent}`).join("\n\n") + +} + +export function getTranslatingLanguage() { + return Zotero.Prefs.get(`${config.addonRef}.usingLanguage`) as string } /** - * 获取选中条目某个字段 + * Get a field of the selected item * @param fieldName * @returns */ @@ -404,7 +437,7 @@ export function getItemField(fieldName: any) { } /** - * 获取PDF页面文字 + * Get PDF page text * @returns */ export function getPDFSelection() { diff --git a/src/modules/Meet/api.ts b/src/modules/Meet/api.ts index ba374c3..51124fb 100644 --- a/src/modules/Meet/api.ts +++ b/src/modules/Meet/api.ts @@ -3,7 +3,8 @@ import { getItemField, getPDFSelection, getRelatedText, - getPDFAnnotations + getPDFAnnotations, + getTranslatingLanguage } from "./Zotero" import { @@ -16,7 +17,7 @@ import { import { getGPTResponse -} from "./OpenAI" +} from "./integratellms" import Views from "../views"; const Meet: { @@ -27,42 +28,44 @@ const Meet: { } } = { /** - * 开放给用户 - * 示例:Meet.Zotero.xxx() + * Open to users + * Example: Meet.Zotero.xxx() */ Zotero: { /** - * 返回系统剪贴板复制的内容 + * Returns the contents copied from the system clipboard */ getClipboardText, /** - * 返回选中条目的某个字段值,多个选中返回第一个选中的某个字段值 - * @fieldName 接收字段的名称 - * 比如摘要,Meet.Zotero.getItemField("abstractNote") + * Returns a field value of the selected entry. Multiple selections return a field value of the first selected item + * @fieldName The received field name + * Such as abstract, Meet.Zotero.getItemField("abstractNote") */ getItemField, + + getTranslatingLanguage, /** - * 返回阅读PDF时选中的文字 + * Returns the text selected when reading PDF */ getPDFSelection, /** - * 返回相关段落,如你选中多条条目,则返回与问题最相关的5个条目 - * 如果你在PDF中则会读取整个PDF,返回与问题最相关的5个段落 - * @queryText 接收一个查询字符串 - * Meet.Zotero.getItemField("本文提到的XXX是什么意思?") + * Return relevant paragraphs. If you select multiple items, return the 5 items most relevant to the question + * If you are in PDF it will read the entire PDF and return the 5 paragraphs most relevant to the question + * @queryText Receive a query string + * Meet.Zotero.getItemField("What does the XXX mentioned in this article mean?") */ getRelatedText, /** - * 获取PDF注释内容 - * @select 接收一个boolean,是否返回选中的标注 - * getPDFAnnotations(true) 会返回选中的标注 - * getPDFAnnotations() 默认返回所有标注 + * Get PDF annotation content + * @select Receives a boolean, whether to return the selected label + * getPDFAnnotations(true) Return selected annotation + * getPDFAnnotations() Returns all annotations by default */ getPDFAnnotations, }, /** - * 部分开放 - * 下列函数只针对主笔记 + * Partially open + * The following functions are only for main notes */ BetterNotes: { getEditorText, @@ -71,7 +74,7 @@ const Meet: { follow, reFocus }, - OpenAI: { + integratellms: { getGPTResponse }, Global: { @@ -83,4 +86,4 @@ const Meet: { } } -export default Meet \ No newline at end of file +export default Meet diff --git a/src/modules/base.ts b/src/modules/base.ts index e2a0617..39e8a0c 100644 --- a/src/modules/base.ts +++ b/src/modules/base.ts @@ -41,7 +41,7 @@ You can type the question in my header, then press \`Enter\` to ask me. You can press \`Ctrl + Enter\` to execute last executed command tag again. You can press \`Shift + Enter\` to enter long text editing mode and press \`Ctrl + R\` to execute long text. ` -// 这是 OpenAI ChatGPT 的字体 +// This is OpenAI ChatGPT font style const fontFamily = `Söhne,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,Helvetica Neue,Arial,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji` function parseTag(text: string) { @@ -65,13 +65,13 @@ function parseTag(text: string) { if (tagString) { tagString = tagString[0] tag.tag = tagString.match(/^#([^\[\n]+)/)[1] - // 解析颜色 + // parse color let color = tagString.match(/\[c(?:olor)?="?(#.+?)"?\]/) tag.color = color?.[1] || tag.color - // 解析位置 + // parse position let position = tagString.match(/\[pos(?:ition)?="?(\d+?)"?\]/) tag.position = Number(position?.[1] || tag.position) - // 解析关键词 + // parse trigger keyword let trigger = tagString.match(/\[tr(?:igger)?="?(.+)"?\]/) tag.trigger = trigger?.[1] || tag.trigger tag.text = `#${tag.tag}[position=${tag.position}][color=${tag.color}][trigger=${tag.trigger}]` + "\n" + text.replace(/^#.+\n/, "") @@ -80,11 +80,11 @@ function parseTag(text: string) { } /** - * 这里默认标签无法删除,但可以更改里面的内容,比如颜色位置,内部prompt + * The default label here cannot be deleted, but the content inside can be changed, such as color position and internal prompt */ let defaultTags: any = [ ` -#🪐AskPDF[color=#0EA293][position=10][trigger=/^(本文|这篇文章|论文)/] +#ChatPDF[color=#0EA293][position=10][trigger=/^(本文|这篇文章|论文)/] You are a helpful assistant. Context information is below. $\{ Meet.Global.views.messages = []; @@ -97,8 +97,8 @@ Answer the question: $\{Meet.Global.input\} Reply in ${Zotero.locale} `, ` -#🌟Translate[c=#D14D72][pos=11][trigger=/^翻译/] -Translate these content to 简体中文: +#Translate[c=#D14D72][pos=11][trigger=/^翻译/] +Translate these content to $\{Meet.Zotero.getTranslatingLanguage\}: $\{ Meet.Global.input.replace("翻译", "") || Meet.Zotero.getPDFSelection() || @@ -107,7 +107,7 @@ Meet.Global.views.messages[0].content `, ` -#✨Improve writing[color=#8e44ad][pos=12][trigger=/^润色/] +#Improve writing[color=#8e44ad][pos=12][trigger=/^润色/] Below is a paragraph from an academic paper. Polish the writing to meet the academic style, improve the spelling, grammar, clarity, concision and overall readability. When necessary, rewrite the whole sentence. Furthermore, list all modification and explain the reasons to do so in markdown table. Paragraph: "$\{ Meet.Global.input.replace("润色", "") || Meet.Global.views.messages[0].content @@ -163,4 +163,4 @@ My question is: $\{Meet.Global.input\} defaultTags = defaultTags.map(parseTag) -export { help, fontFamily, defaultTags, parseTag } \ No newline at end of file +export { help, fontFamily, defaultTags, parseTag } diff --git a/src/modules/utils.ts b/src/modules/utils.ts index 1ff766b..8f42739 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -5,9 +5,7 @@ export default class Utils { } public getRGB(color: string) { var sColor = color.toLowerCase(); - // 十六进制颜色值的正则表达式 var reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/; - // 如果是16进制颜色 if (sColor && reg.test(sColor)) { if (sColor.length === 4) { var sColorNew = "#"; @@ -16,7 +14,6 @@ export default class Utils { } sColor = sColorNew; } - //处理六位的颜色值 var sColorChange = []; for (var i = 1; i < 7; i += 2) { sColorChange.push(parseInt("0x" + sColor.slice(i, i + 2))); @@ -27,7 +24,6 @@ export default class Utils { } /** - * 兼容旧版 * @deprecated * @param queryText * @returns diff --git a/src/modules/views.ts b/src/modules/views.ts index 5d54d9d..049224b 100644 --- a/src/modules/views.ts +++ b/src/modules/views.ts @@ -3,38 +3,53 @@ import Meet from "./Meet/api" import Utils from "./utils"; import { Document } from "langchain/document"; import { help, fontFamily, defaultTags, parseTag } from "./base" +import { getLocalModelDownloadProgress, setApiKey, getSupportedLLMs, ModelConfig, selectModel } from "./Meet/chatpdflocal"; +import { checkFileExist, startLocalLLMEngine, shutdownLocalLLMEngine } from "../hooks"; + + const markdown = require("markdown-it")({ - breaks: true, // 将行结束符\n转换为
标签 - xhtmlOut: true, // 使用 /> 关闭标签,而不是 > + breaks: true, // Convert line terminators \n to
tags + xhtmlOut: true, // Use /> to close the tag, not > typographer: true, html: true, }); const mathjax3 = require('markdown-it-mathjax3'); markdown.use(mathjax3); +export function sleep(time) { + return new Promise((resolve) => window.setTimeout(resolve, time)); +} + export default class Views { private id = "zotero-GPT-container"; /** - * OpenAI接口历史消息记录,需要暴露给GPT响应函数 + * OpenAI interface historical message records need to be exposed to the GPT response function */ public messages: { role: "user" | "assistant"; content: string }[] = []; /** - * 用于储存历史执行的输入,配合方向上下键来快速回退 + * Used to store historical execution input, and use the up and down arrow keys to quickly recall */ private _history: { input: string; output: string }[] = [] /** - * 用于储存上一个执行的标签,配合 Ctrl + Enter 快速再次执行 + * Used to store the last executed tag and use Ctrl + Enter to quickly execute it again */ private _tag: Tag | undefined; /** - * 记录当前GPT输出流setInterval的id,防止终止后仍有输出,需要暴露给GPT响应函数 + * Record the id of the current GPT output stream setInterval to prevent there is still output after termination, which needs to be exposed to the GPT response function */ public _ids: {type: "follow"| "output", id: number}[] = [] + + public publisher2models: Map = new Map() + public publishers: string[] = [] + + public supportedLanguages: string[] = [] + /** - * 是否在笔记环境下 + * Whether in note-taking environment */ public isInNote: boolean = true public container!: HTMLDivElement; + private toolbarContainer!: HTMLDivElement; private inputContainer!: HTMLDivElement; private outputContainer!: HTMLDivElement; private dotsContainer!: HTMLDivElement; @@ -44,6 +59,7 @@ export default class Views { this.utils = new Utils() this.registerKey() this.addStyle() + // @ts-ignore window.Meet = Meet Meet.Global.views = this @@ -95,7 +111,10 @@ export default class Views { } .gpt-menu-box .menu-item:hover, .gpt-menu-box .menu-item.selected{ background-color: rgba(89, 192, 188, .23) !important; - } + } + .popover.show { + display: block; + } #${this.id} .tag { position: relative; overflow: hidden; @@ -124,7 +143,6 @@ export default class Views { } ` }, - // #output-container div.streaming span:after, }, document.documentElement); ztoolkit.UI.appendElement({ @@ -139,7 +157,7 @@ export default class Views { } /** - * 设置GPT回答区域文字 + * Set answer area text * @param text * @param isDone */ @@ -155,13 +173,13 @@ export default class Views { } ready() /** - * 根据差异渲染,只为保全光标闪烁 + * Render based on differences, just to preserve cursor blinking */ let md2html = () => { let result = markdown.render(text) // .replace(/]*>.*?<\/mjx-assistive-mml>/g, "") /** - * 监测差异,替换节点或文字 + * Monitor differences and replace nodes or text * @param oldNode * @param newNode * @returns @@ -182,7 +200,7 @@ export default class Views { return } } - // 老的比新的多要去除 + // There are more old ones than new ones and need to be removed [...oldNode.childNodes].slice(newNode.childNodes.length).forEach((e: any)=>e.remove()) for (let i = 0; i < newNode.childNodes.length; i++) { if (i < oldNode.childNodes.length) { @@ -202,7 +220,7 @@ export default class Views { } } } - // 纯文本本身不需要MD渲染,防止样式不一致出现变形 + // Plain text itself does not require MD rendering to prevent deformation due to inconsistent styles let _outputDiv = outputDiv.cloneNode(true) as HTMLDivElement try { _outputDiv.innerHTML = result @@ -220,7 +238,7 @@ export default class Views { // @ts-ignore scrollToNewLine && this.outputContainer.scrollBy(0, this.outputContainer.scrollTopMax) if (isDone) { - // 任何实时预览的错误到最后,应该因为下面这句消失 + // Any live preview errors at the end should disappear because of the following sentence outputDiv.innerHTML = markdown.render(text) if (isRecord) { this._history.push({ input: Meet.Global.input, output: text }) @@ -228,26 +246,19 @@ export default class Views { outputDiv.classList.remove("streaming") if (this.isInNote) { this.hide() - // 下面是完成回答后写入 Better Notes 主笔记的两种方案 + // The following is written after completing the answer Better Notes. Two options for master notes Meet.BetterNotes.insertEditorText(outputDiv.innerHTML) - // window.setTimeout(async () => { - // Meet.BetterNotes.insertEditorText(await Zotero.BetterNotes.api.convert.md2html(text)) - // }) } } } - /** - * GPT写的 - * @param node - */ private addDragEvent(node: HTMLDivElement) { let posX: number, posY: number let currentX: number, currentY: number let isDragging: boolean = false function handleMouseDown(event: MouseEvent) { - // 如果是input或textarea元素,跳过拖拽逻辑 + // If it is an input or textarea element, skip the drag logic if ( event.target instanceof window.HTMLInputElement || event.target instanceof window.HTMLTextAreaElement || @@ -280,12 +291,7 @@ export default class Views { } - /** - * GPT写的 - * @param inputNode - */ private bindUpDownKeys(inputNode: HTMLInputElement) { - // let currentIdx = this._history.length; inputNode.addEventListener("keydown", (e) => { this._history = this._history.filter(i=>i.input) let currentIdx = this._history.map(i=>i.input).indexOf(this.inputContainer!.querySelector("input")!.value) @@ -317,13 +323,13 @@ export default class Views { } /** - * 绑定ctrl+滚轮放大缩小 + * Bind ctrl+scroll wheel to zoom in and out * @param div */ private bindCtrlScrollZoom(div: HTMLDivElement) { - // 为指定的div绑定wheel事件 + // Bind the wheel event to the specified div div.addEventListener('DOMMouseScroll', (event: any) => { - // 检查是否按下了ctrl键 + // Check if the ctrl key is pressed if (event.ctrlKey || event.metaKey) { let _scale = div.style.transform.match(/scale\((.+)\)/) let scale = _scale ? parseFloat(_scale[1]) : 1 @@ -334,11 +340,11 @@ export default class Views { div.style.transformOrigin = "center center" } if (event.detail > 0) { - // 缩小 + // zoom out scale = scale - step div.style.transform = `scale(${scale < minScale ? minScale : scale})`; } else { - // 放大 + // zoom in scale = scale + step div.style.transform = `scale(${scale > maxScale ? maxScale : scale})`; } @@ -347,7 +353,7 @@ export default class Views { } /** - * 绑定ctrl+滚轮放大缩小控件内的所有元素 + * Bind the ctrl wheel to zoom in and out of all elements within the control * @param div */ private bindCtrlScrollZoomOutput(div: HTMLDivElement) { @@ -363,7 +369,7 @@ export default class Views { type StyleAttributes = { [K in StyleAttributeKeys]: string; }; - // 获取子元素的初始样式 + // Get the initial style of the child element const getChildStyles = (child: Element): StyleAttributes => { const style = window.getComputedStyle(child); const result: Partial = {}; @@ -374,7 +380,7 @@ export default class Views { return result as StyleAttributes; }; - // 更新并应用子元素的样式 + // Update and apply styles to child elements const applyNewStyles = (child: HTMLElement, style: StyleAttributes, scale: number) => { const newStyle = (value: string) => parseFloat(value) * scale + 'px'; @@ -382,13 +388,12 @@ export default class Views { child.style && (child.style[key as StyleAttributeKeys] = newStyle(style[key as StyleAttributeKeys])) } }; - // 为指定的div绑定wheel事件 + // Bind the wheel event to the specified div div.addEventListener('DOMMouseScroll', (event: any) => { const children = div.children[0].children; if (event.ctrlKey || event.metaKey) { const step = 0.05; event.preventDefault(); - // 阻止事件冒泡 event.stopPropagation(); const scale = event.detail > 0 ? 1 - step : 1 + step; Array.from(children).forEach((child) => { @@ -400,8 +405,462 @@ export default class Views { }); } + public createOrUpdateModelsContainer() { + var curPublisher = Zotero.Prefs.get(`${config.addonRef}.usingPublisher`) as string + const toolbarContainer = this.toolbarContainer + if (toolbarContainer == null) { + Zotero.Prefs.set(`${config.addonRef}.startLocalServer`, false) + return + } + const publishConfigContainer = toolbarContainer.querySelector(".publisher")! + var publishSelectContainer = toolbarContainer.querySelector(".publisherSelect")! + if (publishSelectContainer) { + publishSelectContainer.remove() + } + + const publishId = "publishid" + publishSelectContainer = ztoolkit.UI.appendElement({ + tag: "select", + id: publishId, + classList: ["publisherSelect"], + properties: { + value: "", + } + }, publishConfigContainer) as HTMLSelectElement//HTMLDivElement + + var publisherSelectIdx = 0 + for (var i = 0; i < this.publishers.length; i++) { + if (this.publishers[i] == curPublisher) { + publisherSelectIdx = i + } + var optionId = "option" + i + const optionContainer = ztoolkit.UI.appendElement({ + tag: "option", + id: optionId, + properties: { + innerHTML: this.publishers[i], + value: this.publishers[i] + } + }, publishSelectContainer) as HTMLDivElement + } + publishSelectContainer.selectedIndex = publisherSelectIdx + + publishSelectContainer.addEventListener("change", async event => { + event.stopPropagation(); + + curPublisher = publishSelectContainer.value + Zotero.Prefs.set(`${config.addonRef}.usingPublisher`, curPublisher) + var curPublisherElement = this.publisher2models.get(curPublisher) + if (curPublisherElement == null) return + var curAPIKey: string = curPublisherElement.apiKey + var curAPIUrl: string = curPublisherElement.apiUrl + Zotero.Prefs.set(`${config.addonRef}.usingAPIKEY`, curAPIKey) + Zotero.Prefs.set(`${config.addonRef}.usingAPIURL`, curAPIUrl) + + for (var i = 0; i < this.publishers.length; i++) { + if (this.publishers[i] == curPublisher) { + publishSelectContainer.selectedIndex = i + break + } + } + + curShowModels = curPublisherElement.models + + var modelNode = document.getElementById("modelSelect") as HTMLSelectElement + if (modelNode != null) { + modelNode.innerHTML = "" + } + + for (var i = 0; i < curShowModels.length; i++) { + var optionId = "optionModel" + i + var modelName = curShowModels[i] + if (modelName.includes(":")) { + let index = modelName.indexOf(":") + modelName = modelName.substr(0, index) + } + const optionContainer = ztoolkit.UI.appendElement({ + tag: "option", + id: optionId, + properties: { + innerHTML: modelName, + value: modelName + } + }, modelNode) as HTMLDivElement + } + modelNode.selectedIndex = curPublisherElement.defaultModelIdx + var curModel = curShowModels[modelNode.selectedIndex] + + Zotero.Prefs.set(`${config.addonRef}.usingModel`, curModel) + + var apiDivNode = document.getElementById("apidiv") + if (curPublisher == "Local LLM") { + if (apiDivNode != null) { + apiDivNode.remove() + } + + const progressContainer = toolbarContainer.querySelector(".progress")! as HTMLProgressElement + if (progressContainer != null) { + progressContainer.remove() + } + + var isModelReady = curPublisherElement.areModelsReady.get(curModel) + if (isModelReady) { + var retValue = await selectModel(curPublisher, curModel) + if (!retValue) { + Zotero.log("invoke selectModel error!") + } + } else { + var ret = await getLocalModelDownloadProgress(curModel) + + var trycount = 0 + while (ret < 0 || ret > 210) { + if (trycount >= 5) break + await sleep(1000) + ret = await getLocalModelDownloadProgress(curModel) + trycount = trycount + 1 + } + + if (ret == 200) { + curPublisherElement.areModelsReady.set(curModel, true) + } else if (/*ret == -1 ||*/ (ret >= 0 && ret <= 100)) { + //if (ret == -1) ret = 0 + + const modelConfigContainer = toolbarContainer.querySelector(".model")! as HTMLDivElement + + if (modelConfigContainer != null) { + const progressContainer = ztoolkit.UI.appendElement({ + tag: "progress", + id: "progress", + classList: ["progress"], + properties: { + max: "100", + value: ret + } + }, modelConfigContainer) as HTMLProgressElement + + + var timer: undefined | number; + const interval = async () =>{ + var ret = await getLocalModelDownloadProgress(curModel) + + var usingModel = Zotero.Prefs.get(`${config.addonRef}.usingModel`) + if (usingModel != curModel) { + window.clearTimeout(timer) + return + } + + if (ret >= 0 && ret < 100) { + progressContainer.value = ret + timer = window.setTimeout(interval, 2000) + } else { + if (ret == 100 || ret == 200) { + var curPublisherElement = this.publisher2models.get(curPublisher) + if (curPublisherElement != null) { + curPublisherElement.areModelsReady.set(curModel, true) + } + } + progressContainer.remove() + window.clearTimeout(timer) + } + } + + window.setTimeout(interval, 2000) + } + } + } + } else { + if (apiDivNode != null) { + apiDivNode.remove() + } + + const progressContainer = toolbarContainer.querySelector(".progress")! as HTMLProgressElement + if (progressContainer != null) { + progressContainer.remove() + } + + const modelConfigContainer = toolbarContainer.querySelector(".model")! as HTMLDivElement + var apiDivId = "apidiv" + const apiDivContainer = ztoolkit.UI.appendElement({ + tag: "div", + id: apiDivId, + classList: [apiDivId], + styles: { + margin: "6px" + } + }, modelConfigContainer) as HTMLDivElement + + var apiId = "api" + var apitext = curPublisher + " API KEY" + var apiContainer: HTMLDivElement + if (curPublisherElement.apiKey.length > 0) { + apitext = curPublisherElement.apiKey + + apiContainer = ztoolkit.UI.appendElement({ + tag: "input", + id: apiId, + styles: { + width: "150px" + }, + properties: { + type: "text", + value: apitext + } + }, apiDivContainer) as HTMLDivElement + } else { + apiContainer = ztoolkit.UI.appendElement({ + tag: "input", + id: apiId, + styles: { + width: "150px" + }, + properties: { + type: "text", + placeholder: apitext + } + }, apiDivContainer) as HTMLDivElement + } + + apiContainer.addEventListener("change", async event => { + if ((apiContainer).value == null) return + const curPublisherElement = this.publisher2models.get(curPublisher) + if (curPublisherElement != null) { + curPublisherElement.apiKey = (apiContainer).value + Zotero.Prefs.set(`${config.addonRef}.usingAPIKEY`, (apiContainer).value) + if (curPublisher == "OpenAI") { + Zotero.Prefs.set(`${config.addonRef}.openaiApiKey`, (apiContainer).value) + } else if (curPublisher == "Claude-3") { + Zotero.Prefs.set(`${config.addonRef}.claudeApiKey`, (apiContainer).value) + } else if (curPublisher == "Gemini") { + Zotero.Prefs.set(`${config.addonRef}.geminiApiKey`, (apiContainer).value) + } + if (Zotero.isMac) { + const response = await setApiKey(curPublisher, (apiContainer).value) + } + } + }) + } + }); + + + const modelConfigContainer = toolbarContainer.querySelector(".model")! as HTMLDivElement + + var modelSelectDivContainer = toolbarContainer.querySelector(".modelSelectDivCSS") + + if (modelSelectDivContainer != null) { + modelSelectDivContainer.remove() + } + + var modelSelectDivId = "modelSelectDiv" + modelSelectDivContainer = ztoolkit.UI.appendElement({ + tag: "div", + id: modelSelectDivId, + classList: ["modelSelectDivCSS"], + styles: { + margin: "6px" + } + }, modelConfigContainer) as HTMLDivElement + + + var modelSelectContainer = toolbarContainer.querySelector(".modelSelect")! + if (modelSelectContainer) { + modelSelectContainer.remove() + } + + var modelSelectId = "modelSelect" + modelSelectContainer = ztoolkit.UI.appendElement({ + tag: "select", + id: modelSelectId, + classList: ["modelSelect"], + }, modelSelectDivContainer) as HTMLSelectElement // DivElement + + var curShowPublisher = this.publisher2models.get(curPublisher) + if (curShowPublisher == null) { return} + var curShowModels = curShowPublisher.models + + for (var i = 0; i < curShowModels.length; i++) { + var optionId = "optionModel" + i + + var modelName = curShowModels[i] + if (modelName.includes(":")) { + let index = modelName.indexOf(":") + modelName = modelName.substr(0, index) + } + const optionContainer = ztoolkit.UI.appendElement({ + tag: "option", + id: optionId, + properties: { + innerHTML: modelName, + value: modelName + } + }, modelSelectContainer) as HTMLDivElement + } + + modelSelectContainer.selectedIndex = curShowPublisher.defaultModelIdx + + modelSelectContainer.addEventListener("change", async event => { + var curModel = modelSelectContainer.value + Zotero.Prefs.set(`${config.addonRef}.usingModel`, curModel) + + + for (var i = 0; i < curShowModels.length; i++) { + if (curModel == curShowModels[i] || curShowModels[i].includes(curModel)) { + modelSelectContainer.selectedIndex = i + var curPublisherElement = this.publisher2models.get(curPublisher) + if (curPublisherElement != null) { + curPublisherElement.defaultModelIdx = i + } + break + } + } + + if (curPublisher == "Local LLM") { + + const progressContainer = modelConfigContainer.querySelector(".progress")! as HTMLProgressElement + if (progressContainer != null) { + progressContainer.remove() + } + var curPublisherElement = this.publisher2models.get(curPublisher) + var isModelReady = curPublisherElement.areModelsReady.get(curModel) + if (isModelReady) { + var retValue = await selectModel(curPublisher, curModel) + if (!retValue) { + Zotero.log("invoke selectModel error!") + } + } else if (curPublisherElement != null + && !isModelReady) { + var ret = await getLocalModelDownloadProgress(curModel) + + var trycount = 0 + while (ret < 0 || ret > 210) { + if (trycount >= 5) break + await sleep(1000) + ret = await getLocalModelDownloadProgress(curModel) + trycount = trycount + 1 + } + + if (ret == 200) { + curPublisherElement.areModelsReady.set(curModel, true) + } else if (/*ret == -1 ||*/ (ret >= 0 && ret <= 100)) { + //if (ret == -1) ret = 0 + + const progressContainer = ztoolkit.UI.appendElement({ + tag: "progress", + id: "progress", + classList: ["progress"], + properties: { + max: "100", + value: ret + } + }, modelConfigContainer) as HTMLProgressElement + var timer: undefined | number; + const interval = async () =>{ + var ret = await getLocalModelDownloadProgress(curModel) + + var usingModel = Zotero.Prefs.get(`${config.addonRef}.usingModel`) + if (usingModel != curModel) { + window.clearTimeout(timer) + return + } + if (ret >= 0 && ret < 100) { + progressContainer.value = ret + timer = window.setTimeout(interval, 2000) + } else if (ret == 100 || ret == 200) { + var curPublisherElement = this.publisher2models.get(curPublisher) + if (curPublisherElement != null) { + curPublisherElement.areModelsReady.set(curModel, true) + } + progressContainer.remove() + window.clearTimeout(timer) + } + } + + window.setTimeout(interval, 2000) + } + } + } + }); + + var curPublisherConfig = this.publisher2models.get(curPublisher) + if (curPublisherConfig != null) { + if (curPublisherConfig != null && curPublisherConfig.hasApiKey) { + var apiDivId = "apidiv" + + var apiDivContainer = toolbarContainer.querySelector(".apidiv")! + if (apiDivContainer) { + apiDivContainer.remove() + } + + apiDivContainer = ztoolkit.UI.appendElement({ + tag: "div", + id: apiDivId, + classList: [apiDivId], + styles: { + margin: "6px" + } + }, modelConfigContainer) as HTMLDivElement + + var apiId = "api" + var apitext = curPublisher + " API KEY" + if (curPublisherConfig.apiKey.length > 0) { + apitext = curPublisherConfig.apiKey + } + + var apiContainer: HTMLDivElement + if (curPublisherConfig.apiKey.length > 0) { + apitext = curPublisherConfig.apiKey + + apiContainer = ztoolkit.UI.appendElement({ + tag: "input", + id: apiId, + styles: { + width: "150px" + }, + properties: { + type: "text", + value: apitext + } + }, apiDivContainer) as HTMLDivElement + + } else { + apiContainer = ztoolkit.UI.appendElement({ + tag: "input", + id: apiId, + styles: { + width: "150px" + }, + properties: { + type: "text", + placeholder: apitext + } + }, apiDivContainer) as HTMLDivElement + } + + apiContainer.addEventListener("change", async event => { + var curPublisherElement = this.publisher2models.get(curPublisher) + if (curPublisherElement != null) { + curPublisherElement.apiKey = (apiContainer).value + Zotero.Prefs.set(`${config.addonRef}.usingAPIKEY`, curPublisherElement.apiKey) + + if (curPublisher == "OpenAI") { + Zotero.Prefs.set(`${config.addonRef}.openaiApiKey`, (apiContainer).value) + } else if (curPublisher == "Claude-3") { + Zotero.Prefs.set(`${config.addonRef}.claudeApiKey`, (apiContainer).value) + } else if (curPublisher == "Gemini") { + Zotero.Prefs.set(`${config.addonRef}.geminiApiKey`, (apiContainer).value) + } + + if (Zotero.isMac) { + const response = await setApiKey(curPublisher, curPublisherElement.apiKey) + } + } + }) + } + } + } + + + private buildContainer() { - // 顶层容器 const container = ztoolkit.UI.createElement(document, "div", { id: this.id, styles: { @@ -419,11 +878,719 @@ export default class Views { 0px 6.3px 24.7px rgba(0, 0, 0, 0.112), 0px 30px 90px rgba(0, 0, 0, 0.2)`, fontFamily: fontFamily, + zIndex:1 } }) this.addDragEvent(container) this.bindCtrlScrollZoom(container) - // 输入 + + var curPublisher = Zotero.Prefs.get(`${config.addonRef}.usingPublisher`) as string + var curModel = Zotero.Prefs.get(`${config.addonRef}.usingModel`) as string + + // toolbar + const toolbarContainer = this.toolbarContainer = ztoolkit.UI.appendElement({ + tag: "div", + id: "toolbar-container", + styles: { + borderBottom: "1px solid #f6f6f6", + width: "100%", + display: "flex", + alignItems: "center", + }, + + children: [ + { + tag: "div", + id: "publishers", + classList: ["publisher"], + styles: { + margin: "6px", + float: "left" + } + }, + { + tag: "div", + id: "models", + classList: ["model"], + styles: { + margin: "6px", + float: "left" + } + }, + + { + tag: "div", + id: "registers", + classList: ["register"], + styles: { + marginLeft: "30%", + float: "right", + color: "blue", + fontSize: "20px" + }, + + children: [ + { + tag: "img", + id: "registerImg", + classList: ["registerImg"], + styles: { + width: "20px", + height: "20px", + backgroundColor: "#fff", + }, + properties: { + src: `chrome://${config.addonRef}/content/icons/subscribe.png` + } + } + ] + + } + + ] + }, container) as HTMLDivElement + + //create + this.createOrUpdateModelsContainer() + + const registerContainer = toolbarContainer.querySelector(".register")! as HTMLDivElement + + registerContainer.addEventListener("mouseup", async event => { + window.alert = function(msg, container) { + + const backgroundContainer = ztoolkit.UI.createElement(document, "div", { + id: "languagesBg", + styles: { + display: "block", + flexDirection: "column", + justifyContent: "flex-start", + alignItems: "center", + position: "fixed", + width: Zotero.Prefs.get(`${config.addonRef}.width`) as string, + fontSize: "18px", + borderRadius: "10px", + backgroundColor: "#000", + boxShadow: `0px 1.8px 7.3px rgba(0, 0, 0, 0.071), + 0px 6.3px 24.7px rgba(0, 0, 0, 0.112), + 0px 30px 90px rgba(0, 0, 0, 0.2)`, + fontFamily: fontFamily, + opacity: 0.6, + zIndex:2, + }, + }) + + const subscriberShowContainer = ztoolkit.UI.createElement(document, "div", { + id: "subscriber", + styles: { + display: "none", + //flexDirection: "column", + //justifyContent: "center", + //alignItems: "center", + position: "fixed", + width: Zotero.Prefs.get(`${config.addonRef}.width`) as string, + fontSize: "18px", + borderRadius: "10px", + backgroundColor: "#fff", + boxShadow: `0px 1.8px 7.3px rgba(0, 0, 0, 0.071), + 0px 6.3px 24.7px rgba(0, 0, 0, 0.112), + 0px 30px 90px rgba(0, 0, 0, 0.2)`, + fontFamily: fontFamily, + zIndex:3, + }, + }) + + const subscriberCloseContainer = ztoolkit.UI.appendElement({ + tag: "div", + id: "subscriberClose", + styles: { + display: "flex", + flexDirection: "column", + justifyContent: "flex-start", + //justifyContent: "center", + alignItems: "start", + position: "fixed", + //width: Zotero.Prefs.get(`${config.addonRef}.width`) as string, + fontSize: "15px", + borderRadius: "10px", + backgroundColor: "#fff", + boxShadow: `0px 1.8px 7.3px rgba(0, 0, 0, 0.071), + 0px 6.3px 24.7px rgba(0, 0, 0, 0.112), + 0px 30px 90px rgba(0, 0, 0, 0.2)`, + fontFamily: fontFamily, + color: "#1e90ff", + cursor: "pointer", + zIndex:3, + margin: "10px" + }, + properties: { + value: "", + innerHTML: "X" + } + }, subscriberShowContainer) as HTMLDivElement + + subscriberCloseContainer.addEventListener("click", async event => { + event.stopPropagation(); + backgroundContainer.style.display = "none" + subscriberShowContainer.style.display = "none" + }) + + + const subscriberNoteContainer = ztoolkit.UI.appendElement({ + tag: "div", + id: "subscriberNote", + styles: { + display: "flex", + //flexDirection: "column", + justifyContent: "center", + position: "fixed", + width: Zotero.Prefs.get(`${config.addonRef}.width`) as string, + fontSize: "25px", + //borderRadius: "10px", + //backgroundColor: "#fff", + //boxShadow: `0px 1.8px 7.3px rgba(0, 0, 0, 0.071), + //0px 6.3px 24.7px rgba(0, 0, 0, 0.112), + //0px 30px 90px rgba(0, 0, 0, 0.2)`, + fontFamily: fontFamily, + //color: "#1e90ff", + //cursor: "pointer", + zIndex:3, + //margin: "10px" + }, + + properties: { + value: "", + innerHTML: "Thank you for using zotero-chatpdf!" + } + }, subscriberCloseContainer) as HTMLDivElement + + const grade = Zotero.Prefs.get(`${config.addonRef}.grade`) as string + const imgLink = `chrome://${config.addonRef}/content/icons/` + grade + ".png" + const subscriberGradeContainer = ztoolkit.UI.appendElement({ + tag: "img", + id: "subscriberGrade", + styles: { + display: "flex", + justifyContent: "center", + position: "fixed", + width: "64px", + height: "64px", + backgroundColor: "#fff", + margin: "50px" + }, + + properties: { + src: imgLink + } + }, subscriberNoteContainer) as HTMLDivElement + + + + const registerWrapContainer = ztoolkit.UI.createElement(document, "div", { + id: "registerWrap", + styles: { + display: "flex", + flexDirection: "column", + //justifyContent: "flex-start", + justifyContent: "center", + alignItems: "center", + position: "fixed", + width: Zotero.Prefs.get(`${config.addonRef}.width`) as string, + fontSize: "18px", + borderRadius: "10px", + backgroundColor: "#fff", + boxShadow: `0px 1.8px 7.3px rgba(0, 0, 0, 0.071), + 0px 6.3px 24.7px rgba(0, 0, 0, 0.112), + 0px 30px 90px rgba(0, 0, 0, 0.2)`, + fontFamily: fontFamily, + //cursor: "pointer", + //spacing: "20px", + zIndex:3, + + }, + }) + + + const subscribeContainer = ztoolkit.UI.appendElement({ + tag: "input", + id: "subscribeInput", + styles: { + display: "flex", + flexDirection: "column", + //justifyContent: "flex-start", + justifyContent: "center", + alignItems: "center", + position: "fixed", + width: Zotero.Prefs.get(`${config.addonRef}.width`) as string, + fontSize: "15px", + borderRadius: "10px", + backgroundColor: "#fff", + boxShadow: `0px 1.8px 7.3px rgba(0, 0, 0, 0.071), + 0px 6.3px 24.7px rgba(0, 0, 0, 0.112), + 0px 30px 90px rgba(0, 0, 0, 0.2)`, + fontFamily: fontFamily, + zIndex:3, + + }, + properties: { + type: "text", + placeholder: "Email" + } + }, registerWrapContainer) as HTMLDivElement + + const subscribeWarnNoteContainer = ztoolkit.UI.appendElement({ + tag: "div", + id: "subscribeWarnNote", + styles: { + display: "none", + flexDirection: "column", + //justifyContent: "flex-start", + justifyContent: "center", + alignItems: "center", + position: "fixed", + width: Zotero.Prefs.get(`${config.addonRef}.width`) as string, + fontSize: "12px", + color: "red", + //borderRadius: "10px", + //backgroundColor: "#fff", + //boxShadow: `0px 1.8px 7.3px rgba(0, 0, 0, 0.071), + //0px 6.3px 24.7px rgba(0, 0, 0, 0.112), + //0px 30px 90px rgba(0, 0, 0, 0.2)`, + fontFamily: fontFamily, + //cursor: "pointer", + zIndex:3, + + }, + properties: { + innerHTML: "" + } + }, registerWrapContainer) as HTMLDivElement + + + const verifyWarnNoteContainer = ztoolkit.UI.appendElement({ + tag: "div", + id: "verifyWarnNote", + styles: { + display: "none", + flexDirection: "column", + //justifyContent: "flex-start", + justifyContent: "center", + alignItems: "center", + position: "fixed", + width: Zotero.Prefs.get(`${config.addonRef}.width`) as string, + fontSize: "12px", + color: "red", + //borderRadius: "10px", + //backgroundColor: "#fff", + //boxShadow: `0px 1.8px 7.3px rgba(0, 0, 0, 0.071), + //0px 6.3px 24.7px rgba(0, 0, 0, 0.112), + //0px 30px 90px rgba(0, 0, 0, 0.2)`, + fontFamily: fontFamily, + //cursor: "pointer", + zIndex:3, + + }, + properties: { + innerHTML: "" + } + }, registerWrapContainer) as HTMLDivElement + + + const registerNoteContainer = ztoolkit.UI.appendElement({ + tag: "div", + id: "registerNote", + styles: { + display: "flex", + flexDirection: "column", + //justifyContent: "flex-start", + //justifyContent: "center", + //alignItems: "center", + position: "fixed", + width: Zotero.Prefs.get(`${config.addonRef}.width`) as string, + fontSize: "15px", + //borderRadius: "10px", + //backgroundColor: "#fff", + //boxShadow: `0px 1.8px 7.3px rgba(0, 0, 0, 0.071), + //0px 6.3px 24.7px rgba(0, 0, 0, 0.112), + //0px 30px 90px rgba(0, 0, 0, 0.2)`, + fontFamily: fontFamily, + //color: "#1e90ff", + //cursor: "pointer", + zIndex:3, + //margin: "20px" + }, + + properties: { + value: "", + innerHTML: "Now subscribe for free to get the enhanced features:
1. For Mac users, chat with local SOTA LLMs(llama) without pay.
2. Access GPT-4o, Gemini and Claude in one client.
3. Secure for your data, All stored locally, not upload to the Cloud." + } + }, registerWrapContainer) as HTMLDivElement + + const closeContainer = ztoolkit.UI.appendElement({ + tag: "div", + id: "close", + styles: { + display: "flex", + flexDirection: "column", + position: "fixed", + width: Zotero.Prefs.get(`${config.addonRef}.width`) as string, + fontSize: "15px", + borderRadius: "10px", + backgroundColor: "#fff", + boxShadow: `0px 1.8px 7.3px rgba(0, 0, 0, 0.071), + 0px 6.3px 24.7px rgba(0, 0, 0, 0.112), + 0px 30px 90px rgba(0, 0, 0, 0.2)`, + fontFamily: fontFamily, + color: "#1e90ff", + cursor: "pointer", + zIndex:3, + margin: "20px" + }, + properties: { + value: "", + innerHTML: "X" + } + }, registerWrapContainer) as HTMLDivElement + + closeContainer.addEventListener("click", async event => { + event.stopPropagation(); + backgroundContainer.style.display = "none" + registerWrapContainer.style.display = "none" + }) + + const subscribeSubmitContainer = ztoolkit.UI.appendElement({ + tag: "div", + id: "subscribeSubmit", + styles: { + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", + position: "fixed", + + backgroundColor: "#fff", + fontSize: "15px", + boxShadow: `0px 1.8px 7.3px rgba(0, 0, 0, 0.071), + 0px 6.3px 24.7px rgba(0, 0, 0, 0.112), + 0px 30px 90px rgba(0, 0, 0, 0.2)`, + borderRadius: "8px", + border: "1px solid #fff", + cursor: "pointer", + whiteSpace: "nowrap", + zIndex: 3 + }, + properties: { + innerHTML: "Subscribe" + }, + listeners: [ + { + type: "mousedown", + listener: (event: any) => { + subscribeSubmitContainer.style.backgroundColor = "#C0C0C0"; + } + }, + { + type: "mouseup", + listener: async (event: any) => { + event.stopPropagation(); + var emailRegExp=/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/; + var ok = emailRegExp.test(subscribeContainer.value) + + var message = "" + let res + if (ok) { + subscribeContainer.style.border = "" + const url = `https://www.chatpdflocal.com/api/zoterosubscribe` + try { + res = await Zotero.HTTP.request( + "POST", + url, + { + responseType: "json", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email: subscribeContainer.value + }), + }) + } catch (error: any) { + message = "Network error! Please check your network and try it again!" + + subscribeWarnNoteContainer.style.display = "flex" + subscribeWarnNoteContainer.innerHTML = message + subscribeContainer.style.border = "1px solid red" + } + + if (res?.response) { + var code = res.response.status + if (code == 200) { + message = "Success! Please check license in email and activate!" + subscribeWarnNoteContainer.style.display = "flex" + subscribeWarnNoteContainer.innerHTML = message + subscribeWarnNoteContainer.style.color = "green" + subscribeWarnNoteContainer.style.justifyContent = "flex-start" + } else { + message = res.response.message + subscribeWarnNoteContainer.style.display = "flex" + subscribeWarnNoteContainer.innerHTML = message + subscribeContainer.style.border = "1px solid red" + } + } + } else { + message = "Email not valid!" + subscribeContainer.style.border = "1px solid red" + subscribeWarnNoteContainer.style.display = "flex" + subscribeWarnNoteContainer.innerHTML = message + } + + subscribeSubmitContainer.style.backgroundColor = "#fff"; + } + } + ] + }, registerWrapContainer) as HTMLSelectElement + + + const licenseContainer = ztoolkit.UI.appendElement({ + tag: "input", + id: "lcenseInput", + styles: { + display: "flex", + flexDirection: "column", + //justifyContent: "flex-start", + justifyContent: "center", + alignItems: "center", + position: "fixed", + width: Zotero.Prefs.get(`${config.addonRef}.width`) as string, + fontSize: "15px", + borderRadius: "10px", + backgroundColor: "#fff", + boxShadow: `0px 1.8px 7.3px rgba(0, 0, 0, 0.071), + 0px 6.3px 24.7px rgba(0, 0, 0, 0.112), + 0px 30px 90px rgba(0, 0, 0, 0.2)`, + fontFamily: fontFamily, + //color: "#1e90ff", + //cursor: "pointer", + //spacing: "20px", + zIndex:3, + + }, + properties: { + type: "text", + placeholder: "License" + } + }, registerWrapContainer) as HTMLDivElement + + const verifyLicenseContainer = ztoolkit.UI.appendElement({ + tag: "div", + id: "verifyLicense", + styles: { + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", + position: "fixed", + width: Zotero.Prefs.get(`${config.addonRef}.width`) as string, + fontSize: "15px", + borderRadius: "8px", + backgroundColor: "#fff", + boxShadow: `0px 1.8px 7.3px rgba(0, 0, 0, 0.071), + 0px 6.3px 24.7px rgba(0, 0, 0, 0.112), + 0px 30px 90px rgba(0, 0, 0, 0.2)`, + fontFamily: fontFamily, + cursor: "pointer", + zIndex:3, + }, + properties: { + value: "", + innerHTML: "Activate" + }, + listeners: [ + { + type: "mousedown", + listener: (event: any) => { + verifyLicenseContainer.style.backgroundColor = "#C0C0C0"; + } + }, + { + type: "mouseup", + listener: async (event: any) => { + event.stopPropagation(); + + let res + const url = `https://www.chatpdflocal.com/api/zoteroactivate` + try { + res = await Zotero.HTTP.request( + "POST", + url, + { + responseType: "json", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email: subscribeContainer.value, + license: licenseContainer.value, + }), + }) + } catch (error: any) { + licenseContainer.style.border = "1px solid red" + verifyWarnNoteContainer.style.display = "flex" + verifyWarnNoteContainer.innerHTML = "Network error! Please check your network and try it again!" + } + + if (res?.response) { + if (res.response.status && res.response.status == 200) { + const email = subscribeContainer.value + const token = licenseContainer.value + Zotero.Prefs.set(`${config.addonRef}.email`, email) + Zotero.Prefs.set(`${config.addonRef}.token`, token) + Zotero.Prefs.set(`${config.addonRef}.isLicenseActivated`, true) + Zotero.Prefs.set(`${config.addonRef}.grade`, res.response.grader) + + + await Zotero[config.addonInstance].views.updatePublisherModels(email, token) + Zotero[config.addonInstance].views.createOrUpdateModelsContainer() + + + backgroundContainer.style.display = "none" + + registerWrapContainer.style.display = "none" + + return + } else { + licenseContainer.style.border = "1px solid red" + verifyWarnNoteContainer.style.display = "flex" + verifyWarnNoteContainer.innerHTML = res.response.message + return + } + } + + verifyLicenseContainer.style.backgroundColor = "#fff"; + } + }] + }, registerWrapContainer) as HTMLDivElement + + + var curShowContainer = registerWrapContainer + var isActivated = Zotero.Prefs.get(`${config.addonRef}.isLicenseActivated`) + if (isActivated) { + registerWrapContainer.style.display = "none" + subscriberShowContainer.style.display = "flex" + curShowContainer = subscriberShowContainer + } + + document.documentElement.append(backgroundContainer) + document.documentElement.append(subscriberShowContainer) + document.documentElement.append(registerWrapContainer) + + backgroundContainer.style.display = "flex" + + backgroundContainer.style.height = "50%" + backgroundContainer.style.width = container.style.width + + backgroundContainer.style.left = container.style.left + backgroundContainer.style.top = container.style.top + + + var x = -1 + var y = -1 + if (x + y < 0) { + const rect = document.documentElement.getBoundingClientRect() + //x = rect.width / 2 - registerWrapContainer.offsetWidth / 2; + x = rect.width / 2 - curShowContainer.offsetWidth / 2; + //y = rect.height / 2 - registerWrapContainer.offsetHeight / 2; + y = rect.height / 2 - curShowContainer.offsetHeight / 2; + } + + // ensure container doesn't go off the right side of the screen + //if (x + registerWrapContainer.offsetWidth > window.innerWidth) { + if (x + curShowContainer.offsetWidth > window.innerWidth) { + //x = window.innerWidth - registerWrapContainer.offsetWidth + x = window.innerWidth - curShowContainer.offsetWidth + } + + // ensure container doesn't go off the bottom of the screen + //if (y + registerWrapContainer.offsetHeight > window.innerHeight) { + if (y + curShowContainer.offsetHeight > window.innerHeight) { + //y = window.innerHeight - registerWrapContainer.offsetHeight + y = window.innerHeight - curShowContainer.offsetHeight + } + + // ensure container doesn't go off the left side of the screen + if (x < 0) { + x = 0 + } + + // ensure container doesn't go off the top of the screen + if (y < 0) { + y = 0 + } + // this.container.style.display = "flex" + + + registerWrapContainer.style.left = `${x}px` + registerWrapContainer.style.top = `${y}px` + registerWrapContainer.style.height = "300px" + + subscriberShowContainer.style.left = `${x}px` + subscriberShowContainer.style.top = `${y}px` + subscriberShowContainer.style.height = "150px" + + + closeContainer.style.left = `${x}px` + closeContainer.style.top = `${y}px` + closeContainer.style.width = "6px" + closeContainer.style.height = "6px" + + subscriberCloseContainer.style.width = "6px" + subscriberCloseContainer.style.height = "6px" + + + registerNoteContainer.style.left = `${x + container.clientWidth * 0.1}px` + registerNoteContainer.style.top = `${y + 20}px` + registerNoteContainer.style.width = `${container.clientWidth * 0.85}px` + registerNoteContainer.style.height = "100px" + + subscribeContainer.style.left = `${x + container.clientWidth * 0.2}px` + subscribeContainer.style.top = `${y + 135}px` + subscribeContainer.style.width = `${container.clientWidth * 0.6}px` + subscribeContainer.style.height = "32px" + + subscribeWarnNoteContainer.style.left = `${x + container.clientWidth * 0.2}px` + subscribeWarnNoteContainer.style.top = `${y + 172}px` + subscribeWarnNoteContainer.style.width = `${container.clientWidth * 0.6}px` + subscribeWarnNoteContainer.style.height = "28px" + + + subscribeSubmitContainer.style.left = `${x + container.clientWidth * 0.8 + 15}px` + subscribeSubmitContainer.style.top = `${y + 134}px` + subscribeSubmitContainer.style.width = "68px" + subscribeSubmitContainer.style.height = "39px" + + verifyLicenseContainer.style.left = `${x + container.clientWidth * 0.8 + 15}px` + verifyLicenseContainer.style.top = `${y + 210}px` + verifyLicenseContainer.style.width = "68px" + verifyLicenseContainer.style.height = "39px" + + + + + + licenseContainer.style.left = `${x + container.clientWidth * 0.2}px` + licenseContainer.style.top = `${y + 210}px` + licenseContainer.style.width = `${container.clientWidth * 0.6}px` + licenseContainer.style.height = "32px" + + verifyWarnNoteContainer.style.left = `${x + container.clientWidth * 0.2}px` + verifyWarnNoteContainer.style.top = `${y + 240}px` + verifyWarnNoteContainer.style.width = `${container.clientWidth * 0.6}px` + verifyWarnNoteContainer.style.height = "28px" + + } + window.alert('Subscribe', this.container!); + }) + + // input const inputContainer = this.inputContainer = ztoolkit.UI.appendElement({ tag: "div", id: "input-container", @@ -466,7 +1633,6 @@ export default class Views { } } ] - }, container) as HTMLDivElement const inputNode = inputContainer.querySelector("input")! this.bindUpDownKeys(inputNode) @@ -479,13 +1645,13 @@ export default class Views { // @ts-ignore let text = Meet.Global.input = this.value if ((event.ctrlKey || event.metaKey) && ["s", "r"].indexOf(event.key) >= 0 && textareaNode.style.display != "none") { - // 必定保存,但未必运行 + // must save,but not necessary to execute const tag = parseTag(text) if (tag) { // @ts-ignore this.value = tag.text let tags = that.getTags() - // 如果tags存在,可能是更新,先从tags里将其移除 + // If tags exist, maybe to update, removed from tags tags = tags.filter((_tag: Tag) => { return _tag.tag != tag.tag }) @@ -498,16 +1664,15 @@ export default class Views { .show() return } - // 运行代码,并保存标签 + // Execute codes, and then save the tags if (event.key == "r") { return that.execTag(tag) } } - // 普通文本 + // normal text else { - // 运行文本呢 if (event.key == "r") { - // 长文本当作未保存的命令标签执行,长文本里可以写js + // Long text is executed as an unsaved command label, You can write js in long text return that.execTag({tag: "Untitled", position: -1, color: "", trigger: "", text}) } } @@ -517,14 +1682,11 @@ export default class Views { outputContainer.querySelector(".auxiliary")?.remove() - // 同时按Ctrl,会点击第一个标签 if (event.ctrlKey || event.metaKey) { - // 查找第一个点击 ztoolkit.log("Ctrl + Enter") let tag = that._tag || that.getTags()[0] return that.execTag(tag) } - // 按住Shift,进入长文本编辑模式,此时应该通过Ctrl+R来运行 if (event.shiftKey) { if (inputNode.style.display != "none") { inputNode.style.display = "none" @@ -534,7 +1696,6 @@ export default class Views { } return } - // 优先级最高,防止中文输入法回车转化成英文 if (text.length != lastInputText.length) { lastInputText = text return @@ -544,7 +1705,6 @@ export default class Views { inputNode.style.display = "none" textareaNode.style.display = "" textareaNode.focus() - // 判断本地是否存在这个标签 const tags = that.getTags(); const tag = tags.find((tag: any) => tag.text.startsWith(text.split("\n")[0])) if (tag) { @@ -555,8 +1715,6 @@ export default class Views { } } else if (text.startsWith("/")) { that._history.push(text) - // 尝试结束其它stream的生命 - // that._id = undefined that.stopAlloutput() text = text.slice(1) let [key, value] = text.split(" ") @@ -569,9 +1727,6 @@ export default class Views { that.setText(help, true, false) } else if (key == "report") { const secretKey = Zotero.Prefs.get(`${config.addonRef}.secretKey`) as string - // window.setTimeout(() => { - // Zotero.launchURL("https://platform.openai.com/account/usage") - // }, 1000) return that.setText(`\`api\` ${Zotero.Prefs.get(`${config.addonRef}.api`)}\n\`secretKey\` ${secretKey.slice(0, 3) + "..." + secretKey.slice(-4)}\n\`model\` ${Zotero.Prefs.get(`${config.addonRef}.model`)}\n\`temperature\` ${Zotero.Prefs.get(`${config.addonRef}.temperature`)}`, true, false) } else if (["secretKey", "model", "api", "temperature", "deltaTime", "width", "tagsMore", "chatNumber", "relatedNumber"].indexOf(key) >= 0) { if (value?.length > 0) { @@ -626,7 +1781,7 @@ export default class Views { } } else if (event.key == "Escape") { outputContainer.style.display = "none" - // 退出长文编辑模式 + // Exit long article editing mode if (textareaNode.style.display != "none") { textareaNode.style.display = "none" inputNode.value = "" @@ -642,6 +1797,17 @@ export default class Views { that.hide() that.container!.remove() that.isInNote && Meet.BetterNotes.reFocus() + if (Zotero.isMac) { + const window = Zotero.getMainWindow(); + const OS = window.OS; + var filename = "ChatPDFLocal" + if (!(OS.File.exists(filename))) { + const temp = Zotero.getTempDirectory(); + filename = OS.Path.join(temp.path.replace(temp.leafName, ""), `${filename}.dmg`); + } + shutdownLocalLLMEngine() + Zotero.Prefs.set(`${config.addonRef}.startLocalServer`, false) + } } else if (event.key == "/" && text == "/" && that.container.querySelector("input")?.style.display != "none") { const rect = that.container.querySelector("input")!.getBoundingClientRect() const commands = ["clear", "help", "report", "secretKey", "model", "api", "temperature", "chatNumber", "relatedNumber" , "deltaTime", "tagsMore", "width"] @@ -662,7 +1828,6 @@ export default class Views { } inputNode.addEventListener("keyup", inputListener) textareaNode.addEventListener("keyup", inputListener) - // 输出 const outputContainer = this.outputContainer = ztoolkit.UI.appendElement({ tag: "div", id: "output-container", @@ -687,7 +1852,7 @@ export default class Views { // margin: ".5em 0" }, properties: { - // 用于复制 + // Used to copy pureText: "" } } @@ -743,7 +1908,7 @@ export default class Views { ] }, container) as HTMLDivElement this.bindCtrlScrollZoomOutput(outputContainer) - // 命令标签 + // command tag const tagsMore = Zotero.Prefs.get(`${config.addonRef}.tagsMore`) as string const tagsContainer = this.tagsContainer = ztoolkit.UI.appendElement({ tag: "div", @@ -820,7 +1985,7 @@ export default class Views { }, container) as HTMLDivElement document.documentElement.append(container) this.renderTags() - // 聚焦 + // focus window.setTimeout(() => { container.focus() inputContainer.focus() @@ -830,7 +1995,7 @@ export default class Views { } /** - * 渲染标签,要根据position排序 + * Render tags, sorted according to position */ private renderTags() { this.tagsContainer!?.querySelectorAll("div").forEach(e=>e.remove()) @@ -841,11 +2006,12 @@ export default class Views { } /** - * 添加一个标签 + * add a tag */ private addTag(tag: Tag, index: number) { let [red, green, blue] = this.utils.getRGB(tag.color) let timer: undefined | number; + let container = this.tagsContainer! ztoolkit.UI.appendElement({ tag: "div", id: `tag-${index}`, @@ -874,7 +2040,7 @@ export default class Views { timer = window.setTimeout(() => { timer = undefined if (event.buttons == 1) { - // 进入编辑模式 + // Enter edit mode const textareaNode = this.inputContainer?.querySelector("textarea")! const inputNode = this.inputContainer?.querySelector("input")! inputNode.style.display = "none"; @@ -897,14 +2063,596 @@ export default class Views { window.clearTimeout(timer) timer = undefined this.outputContainer.querySelector(".auxiliary")?.remove() - await this.execTag(tag) + var curLanguage = Zotero.Prefs.get(`${config.addonRef}.usingLanguage`) as string + if (tag.tag.includes("Translate") && curLanguage.length == 0) { + window.alert = function(msg, parentContainer) { + const backgroundContainer = ztoolkit.UI.createElement(document, "div", { + id: "languagesBg", + + styles: { + display: "block", + flexDirection: "column", + justifyContent: "flex-start", + alignItems: "center", + position: "fixed", + //left: "0px", + //top: "0px", + width: Zotero.Prefs.get(`${config.addonRef}.width`) as string, + fontSize: "18px", + borderRadius: "10px", + backgroundColor: "#000", + boxShadow: `0px 1.8px 7.3px rgba(0, 0, 0, 0.071), + 0px 6.3px 24.7px rgba(0, 0, 0, 0.112), + 0px 30px 90px rgba(0, 0, 0, 0.2)`, + fontFamily: fontFamily, + opacity: 0.6, + zIndex:2, + }, + }) + + const allLanguagesContainer = ztoolkit.UI.createElement(document, "div", { + id: "allLanguages", + + styles: { + display: "block", + flexDirection: "column", + //justifyContent: "flex-start", + justifyContent: "center", + alignItems: "center", + position: "fixed", + width: Zotero.Prefs.get(`${config.addonRef}.width`) as string, + fontSize: "18px", + borderRadius: "10px", + backgroundColor: "#fff", + boxShadow: `0px 1.8px 7.3px rgba(0, 0, 0, 0.071), + 0px 6.3px 24.7px rgba(0, 0, 0, 0.112), + 0px 30px 90px rgba(0, 0, 0, 0.2)`, + fontFamily: fontFamily, + //cursor: "pointer", + //spacing: "20px", + zIndex:3, + + }, + }) + + + const languageContainer = ztoolkit.UI.appendElement({ + tag: "div", + id: "languages", + + styles: { + display: "flex", + //flexDirection: "column", + //justifyContent: "flex-start", + justifyContent: "center", + alignItems: "center", + position: "fixed", + width: Zotero.Prefs.get(`${config.addonRef}.width`) as string, + fontSize: "18px", + //borderRadius: "10px", + //backgroundColor: "#fff", + //boxShadow: `0px 1.8px 7.3px rgba(0, 0, 0, 0.071), + //0px 6.3px 24.7px rgba(0, 0, 0, 0.112), + //0px 30px 90px rgba(0, 0, 0, 0.2)`, + fontFamily: fontFamily, + color: "red", + cursor: "pointer", + //spacing: "20px", + zIndex:3, + + }, + properties: { + innerHTML: msg + } + }, allLanguagesContainer) as HTMLDivElement + + + + const closeContainer = ztoolkit.UI.appendElement({ + tag: "div", + id: "close", + styles: { + display: "flex", + flexDirection: "column", + //justifyContent: "flex-start", + justifyContent: "center", + alignItems: "center", + position: "fixed", + width: Zotero.Prefs.get(`${config.addonRef}.width`) as string, + fontSize: "15px", + borderRadius: "10px", + backgroundColor: "#fff", + boxShadow: `0px 1.8px 7.3px rgba(0, 0, 0, 0.071), + 0px 6.3px 24.7px rgba(0, 0, 0, 0.112), + 0px 30px 90px rgba(0, 0, 0, 0.2)`, + fontFamily: fontFamily, + color: "#1e90ff", + cursor: "pointer", + zIndex:3, + margin: "20px" + }, + properties: { + value: "", + innerHTML: "X" + } + }, allLanguagesContainer) as HTMLDivElement + + closeContainer.addEventListener("click", async event => { + event.stopPropagation(); + + backgroundContainer.style.display = "none" + allLanguagesContainer.style.display = "none" + + }) + + + + + const languageSelectContainer = ztoolkit.UI.appendElement({ + tag: "select", + id: "languagesSelect", + styles: { + margin: "20px" + }, + properties: { + value: "" + } + }, languageContainer) as HTMLSelectElement//HTMLDivElement + + + + let languagesJson + try { + languagesJson = Zotero.Prefs.get(`${config.addonRef}.languages`) as string + } catch {} + + var curLanguage = Zotero.Prefs.get(`${config.addonRef}.usingLanguage`) as string + var languageSelectIdx = 0 + this.supportedLanguages = JSON.parse(languagesJson) + if (this.supportedLanguages.length == 0) { + const defaultLanguages = ["Arbic","Chinese", "English", "French", "German", "Hindi", "Italian", "Japanese", "Portuguese", "Russian", "Spanish"] + for (let defaultLanguage of defaultLanguages) { + this.supportedLanguages.push(defaultLanguage) + } + } + + var idx = 0 + for (let language of this.supportedLanguages) { + if (curLanguage == language) { + languageSelectIdx = idx + 1 + break + } + idx = idx + 1 + } + + var optionId = "languageOption0" + const optionContainer = ztoolkit.UI.appendElement({ + tag: "option", + id: optionId, + properties: { + innerHTML: "", + value: "" + } + }, languageSelectContainer) as HTMLDivElement + + for (var i = 0; i < this.supportedLanguages.length; i++) { + if (this.supportedLanguages[i] == curLanguage) { + languageSelectIdx = i + 1 + } + var optionId = "languageOption" + (i + 1) + const optionContainer = ztoolkit.UI.appendElement({ + tag: "option", + id: optionId, + properties: { + innerHTML: this.supportedLanguages[i], + value: this.supportedLanguages[i] + } + }, languageSelectContainer) as HTMLDivElement + } + languageSelectContainer.selectedIndex = languageSelectIdx + + languageSelectContainer.addEventListener("change", async event => { + event.stopPropagation(); + curLanguage = languageSelectContainer.value + Zotero.Prefs.set(`${config.addonRef}.usingLanguage`, curLanguage) + + for (var i = 0; i < this.supportedLanguages.length; i++) { + if (this.supportedLanguages[i] == curLanguage) { + languageSelectContainer.selectedIndex = i + 1 + break + } + } + + backgroundContainer.style.display = "none" + allLanguagesContainer.style.display = "none" + + }) + + + + + + document.documentElement.append(backgroundContainer) + document.documentElement.append(allLanguagesContainer) + + backgroundContainer.style.display = "flex" + + //const rect = document.documentElement.getBoundingClientRect() + + backgroundContainer.style.height = "30%" + backgroundContainer.style.width = parentContainer.style.width + languageContainer.style.display = "flex" + + backgroundContainer.style.left = parentContainer.style.left + backgroundContainer.style.top = parentContainer.style.top + + + var x = -1 + var y = -1 + if (x + y < 0) { + const rect = document.documentElement.getBoundingClientRect() + x = rect.width / 2 - languageContainer.offsetWidth / 2; + y = rect.height / 2 - languageContainer.offsetHeight / 2; + } + + // ensure container doesn't go off the right side of the screen + if (x + languageContainer.offsetWidth > window.innerWidth) { + x = window.innerWidth - languageContainer.offsetWidth + } + + // ensure container doesn't go off the bottom of the screen + if (y + languageContainer.offsetHeight > window.innerHeight) { + y = window.innerHeight - languageContainer.offsetHeight + } + + // ensure container doesn't go off the left side of the screen + if (x < 0) { + x = 0 + } + + // ensure container doesn't go off the top of the screen + if (y < 0) { + y = 0 + } + // this.container.style.display = "flex" + languageContainer.style.left = `${x}px` + languageContainer.style.top = `${y}px` + + //returnConfirmContainer.style.left = `${x + allLanguagesContainer.clientWidth/2}px` + //returnConfirmContainer.style.left = `${window.innerWidth - 10}px` + //returnConfirmContainer.style.top = `${y + allLanguagesContainer.clientHeight/2}px` + //returnConfirmContainer.style.width = "80px" + //returnConfirmContainer.style.height = "40px" + + + allLanguagesContainer.style.left = `${x}px` + allLanguagesContainer.style.top = `${y}px` + allLanguagesContainer.style.height = "80px" + + + const percent = Number(Zotero.Prefs.get(`${config.addonRef}.width`)) + closeContainer.style.left = `${x}px` + closeContainer.style.top = `${y + 6}px`//allLanguagesContainer.style.top + closeContainer.style.width = "3px" + closeContainer.style.height = "5px" + + } + window.alert('Please specify language first:', this.container); + + } else { + await this.execTag(tag) + } } } } ] }, this.tagsContainer!) as HTMLDivElement + + if (tag.tag.includes("Translate")) { + var curLanguage = Zotero.Prefs.get(`${config.addonRef}.usingLanguage`) as string + if (curLanguage.length > 0) { + ztoolkit.UI.appendElement({ + tag: "div", + id: `translateLanguageConfig`, + styles: { + display: "inline-block", + flexShrink: "0", + fontSize: "0.5em", + height: "1.5em", + //color: `rgba(${red}, ${green}, ${blue}, 1)`, + backgroundColor: `rgba(${red}, ${green}, ${blue}, 0.15)`, + borderRadius: "1em", + border: "1px solid #fff", + margin: ".15em", + padding: "0 .6em", + cursor: "pointer", + whiteSpace: "nowrap" + }, + properties: { + innerHTML: "..." + }, + listeners: [ + { + type: "click", + listener: async () => { + var curLanguage = Zotero.Prefs.get(`${config.addonRef}.usingLanguage`) as string + window.alert = function(msg, parentContainer) { + const backgroundContainer = ztoolkit.UI.createElement(document, "div", { + id: "languagesBg", + + styles: { + display: "block", + flexDirection: "column", + justifyContent: "flex-start", + alignItems: "center", + position: "fixed", + //left: "0px", + //top: "0px", + width: Zotero.Prefs.get(`${config.addonRef}.width`) as string, + fontSize: "18px", + borderRadius: "10px", + backgroundColor: "#000", + boxShadow: `0px 1.8px 7.3px rgba(0, 0, 0, 0.071), + 0px 6.3px 24.7px rgba(0, 0, 0, 0.112), + 0px 30px 90px rgba(0, 0, 0, 0.2)`, + fontFamily: fontFamily, + opacity: 0.6, + zIndex:2, + }, + }) + + const allLanguagesContainer = ztoolkit.UI.createElement(document, "div", { + id: "allLanguages", + + styles: { + display: "block", + flexDirection: "column", + //justifyContent: "flex-start", + justifyContent: "center", + alignItems: "center", + position: "fixed", + width: Zotero.Prefs.get(`${config.addonRef}.width`) as string, + fontSize: "18px", + borderRadius: "10px", + backgroundColor: "#fff", + boxShadow: `0px 1.8px 7.3px rgba(0, 0, 0, 0.071), + 0px 6.3px 24.7px rgba(0, 0, 0, 0.112), + 0px 30px 90px rgba(0, 0, 0, 0.2)`, + fontFamily: fontFamily, + //cursor: "pointer", + //spacing: "20px", + zIndex:3, + + }, + }) + + + const languageContainer = ztoolkit.UI.appendElement({ + tag: "div", + id: "languages", + + styles: { + display: "flex", + //flexDirection: "column", + //justifyContent: "flex-start", + justifyContent: "center", + alignItems: "center", + position: "fixed", + width: Zotero.Prefs.get(`${config.addonRef}.width`) as string, + fontSize: "18px", + //borderRadius: "10px", + //backgroundColor: "#fff", + //boxShadow: `0px 1.8px 7.3px rgba(0, 0, 0, 0.071), + //0px 6.3px 24.7px rgba(0, 0, 0, 0.112), + //0px 30px 90px rgba(0, 0, 0, 0.2)`, + fontFamily: fontFamily, + color: "red", + cursor: "pointer", + //spacing: "20px", + zIndex:3, + + }, + properties: { + innerHTML: msg + } + }, allLanguagesContainer) as HTMLDivElement + + + + const closeContainer = ztoolkit.UI.appendElement({ + tag: "div", + id: "close", + styles: { + display: "flex", + flexDirection: "column", + //justifyContent: "flex-start", + justifyContent: "center", + alignItems: "center", + position: "fixed", + width: Zotero.Prefs.get(`${config.addonRef}.width`) as string, + fontSize: "15px", + borderRadius: "10px", + backgroundColor: "#fff", + boxShadow: `0px 1.8px 7.3px rgba(0, 0, 0, 0.071), + 0px 6.3px 24.7px rgba(0, 0, 0, 0.112), + 0px 30px 90px rgba(0, 0, 0, 0.2)`, + fontFamily: fontFamily, + color: "#1e90ff", + cursor: "pointer", + zIndex:3, + margin: "20px" + }, + properties: { + value: "", + innerHTML: "X" + } + }, allLanguagesContainer) as HTMLDivElement + + closeContainer.addEventListener("click", async event => { + event.stopPropagation(); + + backgroundContainer.style.display = "none" + allLanguagesContainer.style.display = "none" + + }) + + + + const languageSelectContainer = ztoolkit.UI.appendElement({ + tag: "select", + id: "languagesSelect", + styles: { + margin: "20px" + }, + properties: { + value: "" + } + }, languageContainer) as HTMLSelectElement//HTMLDivElement + + + + let languagesJson + try { + languagesJson = Zotero.Prefs.get(`${config.addonRef}.languages`) as string + } catch {} + + var curLanguage = Zotero.Prefs.get(`${config.addonRef}.usingLanguage`) as string + var languageSelectIdx = 0 + this.supportedLanguages = JSON.parse(languagesJson) + if (this.supportedLanguages.length == 0) { + const defaultLanguages = ["Arbic","Chinese", "English", "French", "German", "Hindi", "Italian", "Japanese", "Portuguese", "Russian", "Spanish"] + for (let defaultLanguage of defaultLanguages) { + this.supportedLanguages.push(defaultLanguage) + } + } + + var idx = 0 + for (let language of this.supportedLanguages) { + if (curLanguage == language) { + languageSelectIdx = idx + 1 + break + } + idx = idx + 1 + } + + var optionId = "languageOption0" + const optionContainer = ztoolkit.UI.appendElement({ + tag: "option", + id: optionId, + properties: { + innerHTML: "", + value: "" + } + }, languageSelectContainer) as HTMLDivElement + + for (var i = 0; i < this.supportedLanguages.length; i++) { + if (this.supportedLanguages[i] == curLanguage) { + languageSelectIdx = i + 1 + } + var optionId = "languageOption" + (i + 1) + const optionContainer = ztoolkit.UI.appendElement({ + tag: "option", + id: optionId, + properties: { + innerHTML: this.supportedLanguages[i], + value: this.supportedLanguages[i] + } + }, languageSelectContainer) as HTMLDivElement + } + languageSelectContainer.selectedIndex = languageSelectIdx + + languageSelectContainer.addEventListener("change", async event => { + event.stopPropagation(); + curLanguage = languageSelectContainer.value + Zotero.Prefs.set(`${config.addonRef}.usingLanguage`, curLanguage) + + for (var i = 0; i < this.supportedLanguages.length; i++) { + if (this.supportedLanguages[i] == curLanguage) { + languageSelectContainer.selectedIndex = i + 1 + break + } + } + + backgroundContainer.style.display = "none" + allLanguagesContainer.style.display = "none" + + }) + + + document.documentElement.append(backgroundContainer) + document.documentElement.append(allLanguagesContainer) + + backgroundContainer.style.display = "flex" + + backgroundContainer.style.height = "30%" + backgroundContainer.style.width = parentContainer.style.width + languageContainer.style.display = "flex" + + backgroundContainer.style.left = parentContainer.style.left + backgroundContainer.style.top = parentContainer.style.top + + + var x = -1 + var y = -1 + if (x + y < 0) { + const rect = document.documentElement.getBoundingClientRect() + x = rect.width / 2 - languageContainer.offsetWidth / 2; + y = rect.height / 2 - languageContainer.offsetHeight / 2; + } + + // ensure container doesn't go off the right side of the screen + if (x + languageContainer.offsetWidth > window.innerWidth) { + x = window.innerWidth - languageContainer.offsetWidth + } + + // ensure container doesn't go off the bottom of the screen + if (y + languageContainer.offsetHeight > window.innerHeight) { + y = window.innerHeight - languageContainer.offsetHeight + } + + // ensure container doesn't go off the left side of the screen + if (x < 0) { + x = 0 + } + + // ensure container doesn't go off the top of the screen + if (y < 0) { + y = 0 + } + // this.container.style.display = "flex" + languageContainer.style.left = `${x}px` + languageContainer.style.top = `${y}px` + + //returnConfirmContainer.style.left = `${x + allLanguagesContainer.clientWidth/2}px` + //returnConfirmContainer.style.left = `${window.innerWidth - 10}px` + //returnConfirmContainer.style.top = `${y + allLanguagesContainer.clientHeight/2}px` + //returnConfirmContainer.style.width = "80px" + //returnConfirmContainer.style.height = "40px" + + + allLanguagesContainer.style.left = `${x}px` + allLanguagesContainer.style.top = `${y}px` + allLanguagesContainer.style.height = "80px" + + const percent = Number(Zotero.Prefs.get(`${config.addonRef}.width`)) + closeContainer.style.left = `${x}px` + closeContainer.style.top = `${y + 6}px`//allLanguagesContainer.style.top + closeContainer.style.width = "3px" + closeContainer.style.height = "5px" + + } + + window.alert('Change translate language:', this.container); + } + } + ] + }, this.tagsContainer!) as HTMLDivElement + } + } + } + private rippleEffect(div: HTMLDivElement, color: string) { let [red, green, blue] = this.utils.getRGB(color) ztoolkit.UI.appendElement({ @@ -916,7 +2664,7 @@ export default class Views { }, div) } /** - * 执行标签 + * execute tag */ private async execTag(tag: Tag) { Meet.Global.input = this.inputContainer.querySelector("input")?.value as string @@ -938,14 +2686,13 @@ export default class Views { outputDiv.innerHTML = "" outputDiv.setAttribute("pureText", ""); let text = tag.text.replace(/^#.+\n/, "") - // 旧版语法不宜传播,MD语法会被转义 + // new match version for (let rawString of text.match(/```j(?:ava)?s(?:cript)?\n([\s\S]+?)\n```/g)! || []) { let codeString = rawString.match(/```j(?:ava)?s(?:cript)?\n([\s\S]+?)\n```/)![1] try { text = text.replace(rawString, await window.eval(`${codeString}`)) } catch { } } - // 新版语法容易分享传播 for (let rawString of text.match(/\$\{[\s\S]+?\}/g)! || []) { let codeString = rawString.match(/\$\{([\s\S]+?)\}/)![1] try { @@ -954,8 +2701,7 @@ export default class Views { } popunWin.createLine({ text: `Characters ${text.length}`, type: "success" }) popunWin.createLine({ text: "Answering...", type: "default" }) - // 运行替换其中js代码 - text = await Meet.OpenAI.getGPTResponse(text) as string + text = await Meet.integratellms.getGPTResponse(text) as string this.dotsContainer?.classList.remove("loading") if (text.trim().length) { try { @@ -974,12 +2720,12 @@ export default class Views { } /** - * 执行输入框文本 + * Execute input box text * @param text * @returns */ private async execText(text: string) { - // 如果文本中存在某一标签预设的关键词|正则表达式,则转为执行该标签 + // If there is a preset keyword | regular expression for a certain tag in the text, it will be converted to execute the tag const tag = this.getTags() .filter((tag: Tag) => tag.trigger?.length > 0) .find((tag: Tag) => { @@ -992,23 +2738,21 @@ export default class Views { }) if (tag) { return this.execTag(tag) } - // 没有匹配执行文本 this.outputContainer.style.display = "none" const outputDiv = this.outputContainer.querySelector("div")! outputDiv.innerHTML = "" outputDiv.setAttribute("pureText", ""); if (text.trim().length == 0) { return } this.dotsContainer?.classList.add("loading") - await Meet.OpenAI.getGPTResponse(text) + await Meet.integratellms.getGPTResponse(text) this.dotsContainer?.classList.remove("loading") } /** - * 从Zotero.Prefs获取所有已保存标签 - * 按照position顺序排序后返回 + * Get all saved tags from Zotero.Prefs + * Return after sorting according to position order */ private getTags() { - // 进行一个简单的处理,应该是中文/表情写入prefs.js导致的bug let tagsJson try { tagsJson = Zotero.Prefs.get(`${config.addonRef}.tags`) as string @@ -1030,11 +2774,6 @@ export default class Views { Zotero.Prefs.set(`${config.addonRef}.tags`, JSON.stringify(tags)) } - /** - * 下面代码是GPT写的 - * @param x - * @param y - */ public show(x: number = -1, y: number = -1, reBuild: boolean = true) { reBuild = reBuild || !this.container if (reBuild) { @@ -1075,7 +2814,7 @@ export default class Views { } /** - * 关闭界面清除所有setInterval + * Shutdown the ui and clear all the setIntervall */ public hide() { this.container.style.display = "none" @@ -1088,9 +2827,9 @@ export default class Views { } /** - * 在输出界面插入辅助按钮 - * 这是一个极具扩展性的函数 - * 帮助定位,比如定位条目,PDF段落,PDF注释 + * Enter auxiliary buttons on the output interface + * This is a very extensible function + * Help with positioning, such as locating entries, PDF comments, PDF paragraphs */ public insertAuxiliary(docs: Document[]) { this.outputContainer.querySelector(".auxiliary")?.remove() @@ -1147,9 +2886,6 @@ export default class Views { }) } - /** - * 创建选项 - */ public createMenuNode( rect: { x: number, y: number, width: number, height: number }, items: { name: string, listener: Function }[], @@ -1252,7 +2988,6 @@ export default class Views { const winRect = document.documentElement.getBoundingClientRect() const nodeRect = menuNode.getBoundingClientRect() - // 避免溢出 if (nodeRect.bottom > winRect.bottom) { menuNode.style.top = "" menuNode.style.bottom = "0px" @@ -1280,6 +3015,17 @@ export default class Views { removeNode() } else if (event.code == "Escape") { removeNode() + if (Zotero.isMac) { + const window = Zotero.getMainWindow(); + const OS = window.OS; + var filename = "ChatPDFLocal" + if (!(OS.File.exists(filename))) { + const temp = Zotero.getTempDirectory(); + filename = OS.Path.join(temp.path.replace(temp.leafName, ""), `${filename}.dmg`); + } + shutdownLocalLLMEngine() + Zotero.Prefs.set(`${config.addonRef}.startLocalServer`, false) + } } nodes.forEach(e => e.classList.remove("selected")) nodes[currentIndex].classList.add("selected") @@ -1288,12 +3034,62 @@ export default class Views { return menuNode } + public async updatePublisherModels(email: string, token: string) { + await getSupportedLLMs(this.publisher2models, this.publishers, email, token) + } + /** - * 绑定快捷键 + * Bind shortcut key */ private registerKey() { const callback = async () => { + this.publisher2models.clear() + this.publishers = [] + this.isInNote = false + const defaultModelApiKey = Zotero.Prefs.get(`${config.addonRef}.openaiApiKey`) + let modelConfig: ModelConfig = { + models: ["gpt-3.5-turbo", "gpt-4"], + hasApiKey: true, + apiKey: defaultModelApiKey, + areModelsReady: new Map(), + defaultModelIdx: 0, + apiUrl: "https://api.openai.com/v1/chat/completions" + } + + this.publisher2models.set("OpenAI", modelConfig) + this.publishers.push("OpenAI") + Zotero.Prefs.set(`${config.addonRef}.usingPublisher`, "OpenAI") + Zotero.Prefs.set(`${config.addonRef}.usingModel`, "gpt-3.5-turbo") + Zotero.Prefs.set(`${config.addonRef}.usingAPIURL`, "https://api.openai.com/v1/chat/completions") + Zotero.Prefs.set(`${config.addonRef}.usingAPIKEY`, defaultModelApiKey) + + var email = Zotero.Prefs.get(`${config.addonRef}.email`) + var token = Zotero.Prefs.get(`${config.addonRef}.token`) + if (Zotero.isMac) { + const OS = window.OS; + var filename = "ChatPDFLocal" + if (!(await OS.File.exists(filename))) { + const temp = Zotero.getTempDirectory(); + filename = OS.Path.join(temp.path.replace(temp.leafName, ""), `${filename}.dmg`); + } + + if (await checkFileExist(filename + ".done")) { + var startLocalServer = Zotero.Prefs.get(`${config.addonRef}.startLocalServer`) + if (!startLocalServer) { + await startLocalLLMEngine(filename) + + Zotero.Prefs.set(`${config.addonRef}.startLocalServer`, true) + const execFunc = async() => { + var email = Zotero.Prefs.get(`${config.addonRef}.email`) + var token = Zotero.Prefs.get(`${config.addonRef}.token`) + await Zotero[config.addonInstance].views.updatePublisherModels(email, token) + Zotero[config.addonInstance].views.createOrUpdateModelsContainer() + } + window.setTimeout(execFunc, 3000) + } + } + } if (Zotero_Tabs.selectedIndex == 0) { const div = document.querySelector("#item-tree-main-default .row.selected")! if (div) { @@ -1304,7 +3100,6 @@ export default class Views { } } else { const reader = await ztoolkit.Reader.getReader() - // const div = reader?._iframeWindow?.document.querySelector("#selection-menu")! const div = reader?._iframeWindow?.document.querySelector(".selection-popup")! if (div) { window.setTimeout(() => { @@ -1333,17 +3128,18 @@ export default class Views { } } if (Zotero.isMac) { + ztoolkit.Shortcut.register("event", { id: config.addonRef, modifiers: "meta", - key: "/", + key: "enter", callback: callback }) } else { ztoolkit.Shortcut.register("event", { id: config.addonRef, modifiers: "control", - key: "/", + key: "enter", callback: callback }) } @@ -1351,7 +3147,6 @@ export default class Views { document.addEventListener( "keydown", async (event: any) => { - // 笔记内按空格 if ( Zotero_Tabs.selectedIndex == 1 && event.explicitOriginalTarget.baseURI.indexOf("note-editor") >= 0 && diff --git a/tags/Readme.md b/tags/Readme.md index e8755ce..9825eda 100644 --- a/tags/Readme.md +++ b/tags/Readme.md @@ -10,4 +10,4 @@ You can long press me without releasing, then move me to a suitable position bef ## About Input Text You can type the question in my header, enter and ask me a question. -You can exit me by pressing Esc above my head and wake me up by pressing Shift + / in the Zotero window. \ No newline at end of file +You can exit me by pressing Esc above my head and wake me up by pressing meta(ctrl) + enter in the Zotero window. diff --git a/update.json b/update.json index 2c1406a..14bb36b 100644 --- a/update.json +++ b/update.json @@ -1,9 +1,9 @@ { "addons": { - "zoterogpt@polygon.org": { + "zoterochatpdf@chatpdflocal.com": { "updates": [ { - "version": "0.2.9", + "version": "0.0.1", "update_link": "", "applications": { "gecko": { @@ -12,7 +12,7 @@ } }, { - "version": "0.2.8", + "version": "0.0.1", "update_link": "", "applications": { "zotero": { diff --git a/update.rdf b/update.rdf index bfb2c5b..b95e6e3 100644 --- a/update.rdf +++ b/update.rdf @@ -1,11 +1,11 @@ - + - 0.2.9 + 0.0.1 zotero@chnm.gmu.edu @@ -27,4 +27,4 @@ - + \ No newline at end of file