Repository: Mintplex-Labs/anythingllm-extension
Branch: main
Commit: 385d36c08072
Files: 18
Total size: 27.9 KB
Directory structure:
gitextract_zf654o63/
├── .gitignore
├── LICENSE
├── README.md
├── index.html
├── package.json
├── postcss.config.js
├── public/
│ ├── background.js
│ ├── contentScript.js
│ └── manifest.json
├── src/
│ ├── App.jsx
│ ├── components/
│ │ └── Config.jsx
│ ├── hooks/
│ │ └── useApiConnection.js
│ ├── index.css
│ ├── main.jsx
│ ├── models/
│ │ └── browserExtension.js
│ └── utils/
│ └── constants.js
├── tailwind.config.js
└── vite.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
================================================
FILE: LICENSE
================================================
The MIT License
Copyright (c) Mintplex Labs Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# AnythingLLM Chrome Extension
Seamlessly integrate AnythingLLM into Google Chrome.
Features •
Installation •
Development •
Usage •
Contributing •
License
## Features
- 🔗 Connect to your AnythingLLM instance with a simple connection string or automatic browser extension registration
- 📑 Save selected text to AnythingLLM directly from any webpage
- 📄 Upload entire web pages to AnythingLLM for processing
- 🗂️ Embed content into specific workspaces
- 🔄 Automatic logo synchronization with your AnythingLLM instance
## Installation
_or_
1. Clone this repository or download the latest release.
2. Open Chrome and navigate to `chrome://extensions`.
3. Enable "Developer mode" in the top right corner.
4. Click "Load unpacked" and select the `dist` folder from this project.
## Development
To set up the project for development:
1. Install dependencies:
```
yarn install
```
2. Run the development server:
```
yarn dev
```
3. To build the extension:
```
yarn build
```
The built extension will be in the `dist` folder.
## Usage
1. Click on the AnythingLLM extension icon in your Chrome toolbar.
2. Enter your AnythingLLM browser extension API key to connect to your instance (or create the API key inside AnythingLLM and have it automatically register to the extension).
3. Right-click on selected text or anywhere on a webpage to see AnythingLLM options.
4. Choose to save selected text or the entire page to AnythingLLM.
## Contributing
Contributions are welcome! Feel free to submit a PR.
## Acknowledgements
- This extension is designed to work with [AnythingLLM](https://github.com/Mintplex-Labs/anything-llm).
---
Copyright © 2024 [Mintplex Labs](https://github.com/Mintplex-Labs).
This project is [MIT](../LICENSE) licensed.
================================================
FILE: index.html
================================================
AnythingLLM Document Saver
================================================
FILE: package.json
================================================
{
"name": "anything-llm-extension",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "nodemon --watch src --watch public -e js,jsx,css,html --exec \"yarn dev:build\"",
"dev:build": "vite build && cp public/background.js dist/",
"build": "vite build && cp public/background.js dist/",
"lint": "yarn prettier --ignore-path ../.prettierignore --write ./src",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.19",
"nodemon": "^3.1.4",
"postcss": "^8.4.40",
"prettier": "^3.0.3",
"tailwindcss": "^3.4.7",
"vite": "^5.3.4"
}
}
================================================
FILE: postcss.config.js
================================================
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
================================================
FILE: public/background.js
================================================
const ContextMenuModel = {
async create(workspaces) {
await chrome.contextMenus.removeAll();
if (workspaces && workspaces.length > 0) {
chrome.contextMenus.create({
id: "saveToAnythingLLM",
title: "Save selected to AnythingLLM",
contexts: ["selection"],
});
chrome.contextMenus.create({
id: "embedToWorkspace",
title: "Embed selected content to workspace",
contexts: ["selection"],
});
chrome.contextMenus.create({
id: "saveEntirePageToAnythingLLM",
title: "Save entire page to AnythingLLM",
contexts: ["page"],
});
chrome.contextMenus.create({
id: "embedEntirePageToWorkspace",
title: "Embed entire page to workspace",
contexts: ["page"],
});
workspaces.forEach((workspace) => {
chrome.contextMenus.create({
id: `workspace-selected-${workspace.id}`,
parentId: "embedToWorkspace",
title: workspace.name,
contexts: ["selection"],
});
chrome.contextMenus.create({
id: `workspace-page-${workspace.id}`,
parentId: "embedEntirePageToWorkspace",
title: workspace.name,
contexts: ["page"],
});
});
} else {
chrome.contextMenus.create({
id: "saveToAnythingLLM",
title: "Save selected to AnythingLLM",
contexts: ["selection"],
});
chrome.contextMenus.create({
id: "saveEntirePageToAnythingLLM",
title: "Save entire page to AnythingLLM",
contexts: ["page"],
});
}
},
async remove() {
await chrome.contextMenus.removeAll();
},
};
const ExtensionModel = {
async checkApiKeyValidity() {
const { apiBase, apiKey } = await chrome.storage.sync.get([
"apiBase",
"apiKey",
]);
if (!apiBase || !apiKey) {
await ContextMenuModel.remove();
return false;
}
const data = await fetch(`${apiBase}/browser-extension/check`, {
headers: { Authorization: `Bearer ${apiKey}` },
})
.then((res) => {
if (!res.ok) throw new Error('Response not ok.')
return res.json();
})
.catch(() => null);
if (data === null) {
await chrome.storage.sync.remove(["apiBase", "apiKey"]);
await ContextMenuModel.remove();
return false;
}
await ContextMenuModel.create(data.workspaces);
return true;
},
async updateWorkspaces() {
const { apiBase, apiKey } = await chrome.storage.sync.get([
"apiBase",
"apiKey",
]);
if (!apiBase || !apiKey) return await ContextMenuModel.remove();
const data = await fetch(`${apiBase}/browser-extension/check`, {
headers: { Authorization: `Bearer ${apiKey}` },
})
.then((res) => {
if (!res.ok) throw new Error('Response not ok.')
return res.json();
})
.catch(() => null);
if (data === null) return await ContextMenuModel.remove();
await ContextMenuModel.create(data.workspaces);
return;
},
async saveToAnythingLLM(selectedText, pageTitle, pageUrl) {
const { apiBase, apiKey } = await chrome.storage.sync.get([
"apiBase",
"apiKey",
]);
if (!apiBase || !apiKey) return;
this.showNotification(
'loading',
"Uploading entire page into available documents. Please wait."
);
const response = await fetch(
`${apiBase}/browser-extension/upload-content`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
textContent: selectedText,
metadata: { title: pageTitle, url: pageUrl },
}),
}
);
this.handleResponse(response, "save content");
},
async embedToWorkspace(workspaceId, selectedText, pageTitle, pageUrl) {
const { apiBase, apiKey } = await chrome.storage.sync.get([
"apiBase",
"apiKey",
]);
if (!apiBase || !apiKey) return;
this.showNotification(
'loading',
"Uploading selected text into workspace. Please wait."
);
const response = await fetch(`${apiBase}/browser-extension/embed-content`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
workspaceId,
textContent: selectedText,
metadata: { title: pageTitle, url: pageUrl },
}),
});
this.handleResponse(response, "embed content");
},
async saveEntirePageToAnythingLLM(pageContent, pageTitle, pageUrl) {
const { apiBase, apiKey } = await chrome.storage.sync.get([
"apiBase",
"apiKey",
]);
if (!apiBase || !apiKey) return;
this.showNotification(
'loading',
"Uploading entire page text into available documents. Please wait."
);
const response = await fetch(
`${apiBase}/browser-extension/upload-content`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
textContent: pageContent,
metadata: { title: pageTitle, url: pageUrl },
}),
}
);
this.handleResponse(response, "save entire page");
},
async embedEntirePageToWorkspace(
workspaceId,
pageContent,
pageTitle,
pageUrl
) {
const { apiBase, apiKey } = await chrome.storage.sync.get([
"apiBase",
"apiKey",
]);
if (!apiBase || !apiKey) return;
this.showNotification(
'loading',
"Embedding entire page into workspace. Please wait."
);
const response = await fetch(`${apiBase}/browser-extension/embed-content`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
workspaceId,
textContent: pageContent,
metadata: { title: pageTitle, url: pageUrl },
}),
});
this.handleResponse(response, "embed entire page");
},
async handleResponse(response, action) {
if (response.status === 401 || response.status === 403) {
await chrome.storage.sync.remove(["apiBase", "apiKey"]);
await ContextMenuModel.remove();
this.showNotification(
'error',
"Authentication failed. Please reconnect the extension."
);
} else if (!response.ok) {
await this.checkApiKeyValidity();
this.showNotification("error", `Failed to ${action}. Please try again.`);
} else {
this.showNotification(
"success",
"Successfully saved content to AnythingLLM."
);
}
},
/**
* Shows badge notification on extension icon
* @param {"success"|"error"|"loading"} type
* @param {string} message
*/
showNotification(type, message) {
const NOTIFICATION_MAP = {
success: {
title: "Success",
icon: "✅",
},
error: {
title: "Error",
icon: "❌",
},
loading: {
title: "Loading",
icon: "⏳",
}
}
if (!NOTIFICATION_MAP.hasOwnProperty(type)) return;
const { icon, title } = NOTIFICATION_MAP[type];
chrome.action.setBadgeText({ text: icon })
chrome.action.setTitle({ title: `${title}: ${message}` });
setTimeout(() => {
chrome.action.setBadgeText({ text: "" });
chrome.action.setTitle({ title: "AnythingLLM Extension" });
}, 5000);
},
};
// Event Listeners
chrome.runtime.onInstalled.addListener(async () => {
await ExtensionModel.checkApiKeyValidity();
});
chrome.runtime.onMessage.addListener((message, _sender, _sendResponse) => {
if (message.action === "connectionUpdated") return ExtensionModel.checkApiKeyValidity();
if (message.action === "newApiKey") {
const [apiBase, apiKey] = message.connectionString.split("|");
chrome.storage.sync.set({ apiBase, apiKey }, () => {
ExtensionModel.checkApiKeyValidity();
chrome.action.openPopup();
});
return;
}
});
function getPageContent(tabId) {
return new Promise((resolve, reject) => {
chrome.tabs.sendMessage(tabId, { action: "getPageContent" }, (response) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else if (response && response.content) {
resolve(response.content);
} else {
reject(new Error("Failed to get page content"));
}
});
});
}
chrome.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === "saveToAnythingLLM") {
ExtensionModel.saveToAnythingLLM(info.selectionText, tab.title, tab.url);
return;
}
if (info.menuItemId.startsWith("workspace-selected-")) {
const workspaceId = info.menuItemId.split("-")[2];
ExtensionModel.embedToWorkspace(
workspaceId,
info.selectionText,
tab.title,
tab.url
);
return;
}
if (info.menuItemId === "saveEntirePageToAnythingLLM") {
getPageContent(tab.id)
.then((content) => {
ExtensionModel.saveEntirePageToAnythingLLM(content, tab.title, tab.url);
})
.catch((error) => {
console.error("Error getting page content:", error);
ExtensionModel.showNotification(
"error",
"Failed to get page content. Please try again."
);
});
return;
}
if (info.menuItemId.startsWith("workspace-page-")) {
const workspaceId = info.menuItemId.split("-")[2];
getPageContent(tab.id)
.then((content) => {
ExtensionModel.embedEntirePageToWorkspace(
workspaceId,
content,
tab.title,
tab.url
);
})
.catch((error) => {
console.error("Error getting page content:", error);
ExtensionModel.showNotification(
"error",
"Failed to get page content. Please try again."
);
});
return;
}
});
// Remove context menu items when connection is lost
chrome.storage.onChanged.addListener((changes, namespace) => {
if (namespace === "sync" && (changes.apiBase || changes.apiKey)) {
if (!changes.apiBase?.newValue || !changes.apiKey?.newValue) {
ContextMenuModel.remove();
}
}
});
// Update workspaces periodically
chrome.alarms.create("updateWorkspaces", { periodInMinutes: 1 });
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === "updateWorkspaces") {
ExtensionModel.updateWorkspaces();
}
});
================================================
FILE: public/contentScript.js
================================================
window.addEventListener("message", (event) => {
if (event.data.type === "NEW_BROWSER_EXTENSION_CONNECTION") {
chrome.runtime.sendMessage({
action: "newApiKey",
connectionString: event.data.apiKey,
});
}
});
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === "getPageContent") {
sendResponse({ content: document.body.innerText });
}
});
================================================
FILE: public/manifest.json
================================================
{
"manifest_version": 3,
"name": "AnythingLLM Browser Companion",
"version": "1.0.0",
"description": "Bring AnythingLLM directly into your browser to curate and collect content on the web directly into your AnythingLLM workspaces.",
"icons": {
"16": "icon16.png",
"32": "icon32.png",
"48": "icon48.png",
"128": "icon128.png"
},
"permissions": [
"contextMenus",
"activeTab",
"storage",
"notifications",
"alarms"
],
"host_permissions": [
""
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "index.html",
"default_icon": {
"16": "icon16.png",
"32": "icon32.png",
"48": "icon48.png",
"128": "icon128.png"
}
},
"content_scripts": [
{
"matches": [
""
],
"js": [
"contentScript.js"
]
}
]
}
================================================
FILE: src/App.jsx
================================================
import React from "react";
import Config from "./components/Config";
import useApiConnection from "./hooks/useApiConnection";
const App = () => {
const { status, checkApiKeyStatus, logoUrl } = useApiConnection();
return (
Right click on any page and send selected text or entire pages to
AnythingLLM.
);
};
export default App;
================================================
FILE: src/components/Config.jsx
================================================
import React, { useState, useEffect } from "react";
import BrowserExtension from "../models/browserExtension";
export default function Config({ status, onStatusChange }) {
const [connectionString, setConnectionString] = useState("");
const [saveStatus, setSaveStatus] = useState("");
useEffect(() => {
if (saveStatus) {
const timer = setTimeout(() => {
setSaveStatus("");
}, 5000);
return () => clearTimeout(timer);
}
}, [saveStatus]);
// Disconnects & de-registers the current extension from the set API key.
async function disconnectFromExtension() {
await chrome.storage.sync.remove(["apiBase", "apiKey"]);
onStatusChange();
setSaveStatus("Successfully disconnected from AnythingLLM");
chrome.runtime.sendMessage({ action: "connectionUpdated" });
}
const handleConnect = async () => {
try {
const [apiBase, apiKey] = connectionString.split("|");
if (!apiBase || !apiKey) {
setSaveStatus("Invalid connection string format.");
return;
}
const { online } = await BrowserExtension.checkOnline(apiBase);
if (!online) {
setSaveStatus(
"AnythingLLM is currently offline. Please try again later."
);
return;
}
const { response } = await BrowserExtension.checkApiKey(apiBase, apiKey);
if (!response.ok)
return setSaveStatus("Failed to connect: Invalid API key");
// Saves the apiBase and apiKey to storage sync.
await chrome.storage.sync.set({ apiBase, apiKey });
onStatusChange();
setSaveStatus("Successfully connected to AnythingLLM");
chrome.runtime.sendMessage({ action: "connectionUpdated" });
} catch (error) {
setSaveStatus(`An error occurred during connection: ${error.message}`);
}
};
const handleDisconnect = async () => {
try {
const { apiBase, apiKey } = await chrome.storage.sync.get([
"apiBase",
"apiKey",
]);
if (!apiBase || !apiKey) throw new Error("No connection found");
const { success, error } = await BrowserExtension.disconnect(
apiBase,
apiKey
);
if (!success)
throw new Error(error || "Failed to disconnect from the server");
await disconnectFromExtension();
} catch (error) {
setSaveStatus(`An error occurred during disconnection: ${error.message}`);
}
};
return (
{status === "notConnected" && (
)}
{status === "connected" && (
)}
{status === "offline" && (
Disconnect
AnythingLLM is currently offline. Please try again later.
)}
{status === "error" && (
An error occurred. Please try again later.
)}
{saveStatus && (
{saveStatus}
)}
);
}
================================================
FILE: src/hooks/useApiConnection.js
================================================
import { useState, useEffect } from "react";
import AnythingLLMLogo from "@/media/anything-llm.png";
import BrowserExtension from "@/models/browserExtension";
/**
* Fetches connection information for API key provided
* @returns {{
* status: ("loading"|"notConnected"|"offline"|"connected")
* }}
*/
export default function useApiConnection() {
const [status, setStatus] = useState("loading");
const [logoUrl, setLogoUrl] = useState(AnythingLLMLogo);
useEffect(() => {
checkApiKeyStatus();
fetchLogo();
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "newApiKey") {
checkApiKeyStatus();
fetchLogo();
}
});
}, []);
const checkApiKeyStatus = async () => {
const { apiBase, apiKey } = await chrome.storage.sync.get([
"apiBase",
"apiKey",
]);
if (!apiBase || !apiKey) {
setStatus("notConnected");
return;
}
try {
const { online } = await BrowserExtension.checkOnline(apiBase);
if (!online) {
setStatus("offline");
return;
}
const { response } = await BrowserExtension.checkApiKey(apiBase, apiKey);
if (response.ok) {
setStatus("connected");
chrome.runtime.sendMessage({ action: "connectionUpdated" });
} else {
await chrome.storage.sync.remove(["apiBase", "apiKey"]);
setStatus("notConnected");
chrome.runtime.sendMessage({ action: "connectionUpdated" });
}
} catch (error) {
setStatus("error");
}
};
const fetchLogo = async () => {
const { apiBase } = await chrome.storage.sync.get(["apiBase"]);
if (!apiBase) return;
const { success, logoURL } = await BrowserExtension.fetchLogo(apiBase);
setLogoUrl(success ? logoURL : AnythingLLMLogo);
};
return { status, logoUrl, checkApiKeyStatus };
}
================================================
FILE: src/index.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
================================================
FILE: src/main.jsx
================================================
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")).render(
);
================================================
FILE: src/models/browserExtension.js
================================================
const BrowserExtension = {
checkApiKey: async function (apiBase, apiKey) {
return await fetch(`${apiBase}/browser-extension/check`, {
headers: { Authorization: `Bearer ${apiKey}` },
})
.then((res) => {
if (!res.ok) throw new Error("Bad response to /check");
return res.json();
})
.then((data) => ({ response: { ok: true }, data, error: null }))
.catch((e) => {
console.error(e);
return { response: { ok: false }, data: null, error: e.message };
});
},
checkOnline: async function (apiBase) {
return await fetch(`${apiBase}/ping`)
.then((res) => {
if (!res.ok) throw new Error("Bad response to /ping");
return res.json();
})
.then((data) => ({ online: true, data, error: null }))
.catch((e) => {
return { online: false, data: null, error: e.message };
});
},
fetchLogo: async function (apiBase) {
try {
const response = await fetch(`${apiBase}/system/logo`, {
method: "GET",
cache: "no-cache",
});
if (response.ok && response.status !== 204) {
const blob = await response.blob();
const logoURL = URL.createObjectURL(blob);
return { success: true, error: null, logoURL };
} else {
return { success: false, error: "Logo not available", logoURL: null };
}
} catch (error) {
console.error("Error fetching logo:", error);
return { success: false, error: error.message, logoURL: null };
}
},
disconnect: async function (apiBase, apiKey) {
try {
await fetch(`${apiBase}/browser-extension/disconnect`, {
method: "DELETE",
headers: { Authorization: `Bearer ${apiKey}` },
})
.then((res) => res.json())
.then((data) => {
if (!!data.error)
throw new Error(errorData.error || "Failed to disconnect");
return;
});
return { success: true, error: null };
} catch (error) {
console.error("Disconnect error:", error);
return { success: false, error: error.message };
}
},
};
export default BrowserExtension;
================================================
FILE: src/utils/constants.js
================================================
// Constants for the chrome extension
================================================
FILE: tailwind.config.js
================================================
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {}
},
plugins: []
}
================================================
FILE: vite.config.js
================================================
import { defineConfig } from "vite"
import { fileURLToPath, URL } from "url"
import react from "@vitejs/plugin-react"
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
input: {
main: "index.html"
}
},
outDir: "dist"
},
resolve: {
alias: [
{
find: "@",
replacement: fileURLToPath(new URL("./src", import.meta.url))
},
]
}
})