[
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License\n\nCopyright (c) Mintplex Labs Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE."
  },
  {
    "path": "README.md",
    "content": "# AnythingLLM Chrome Extension\n\n<p align=\"center\">\n  <img src=\"src/media/anything-llm.png\" alt=\"AnythingLLM Chrome Extension logo\" width=\"200\">\n</p>\n\n<p align=\"center\">\n  Seamlessly integrate AnythingLLM into Google Chrome.\n</p>\n\n<p align=\"center\">\n  <a href=\"#features\">Features</a> •\n  <a href=\"#installation\">Installation</a> •\n  <a href=\"#development\">Development</a> •\n  <a href=\"#usage\">Usage</a> •\n  <a href=\"#contributing\">Contributing</a> •\n  <a href=\"#license\">License</a>\n</p>\n\n## Features\n\n- 🔗 Connect to your AnythingLLM instance with a simple connection string or automatic browser extension registration\n- 📑 Save selected text to AnythingLLM directly from any webpage\n- 📄 Upload entire web pages to AnythingLLM for processing\n- 🗂️ Embed content into specific workspaces\n- 🔄 Automatic logo synchronization with your AnythingLLM instance\n\n## Installation\n\n<a href=\"https://chromewebstore.google.com/detail/anythingllm-browser-compa/pncmdlebcopjodenlllcomedphdmeogm\">\n  <img src=\"https://storage.googleapis.com/web-dev-uploads/image/WlD8wC6g8khYWPJUsQceQkhXSlv1/iNEddTyWiMfLSwFD6qGq.png\" alt=\"Chrome Extension\" width=\"200\">\n</a>\n\n_or_\n\n1. Clone this repository or download the latest release.\n2. Open Chrome and navigate to `chrome://extensions`.\n3. Enable \"Developer mode\" in the top right corner.\n4. Click \"Load unpacked\" and select the `dist` folder from this project.\n\n## Development\n\nTo set up the project for development:\n\n1. Install dependencies:\n\n   ```\n   yarn install\n   ```\n\n2. Run the development server:\n\n   ```\n   yarn dev\n   ```\n\n3. To build the extension:\n   ```\n   yarn build\n   ```\n\nThe built extension will be in the `dist` folder.\n\n## Usage\n\n1. Click on the AnythingLLM extension icon in your Chrome toolbar.\n2. 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).\n3. Right-click on selected text or anywhere on a webpage to see AnythingLLM options.\n4. Choose to save selected text or the entire page to AnythingLLM.\n\n## Contributing\n\nContributions are welcome! Feel free to submit a PR.\n\n## Acknowledgements\n\n- This extension is designed to work with [AnythingLLM](https://github.com/Mintplex-Labs/anything-llm).\n\n---\n\nCopyright © 2024 [Mintplex Labs](https://github.com/Mintplex-Labs). <br />\nThis project is [MIT](../LICENSE) licensed.\n"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>AnythingLLM Document Saver</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.jsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"anything-llm-extension\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"nodemon --watch src --watch public -e js,jsx,css,html --exec \\\"yarn dev:build\\\"\",\n    \"dev:build\": \"vite build && cp public/background.js dist/\",\n    \"build\": \"vite build && cp public/background.js dist/\",\n    \"lint\": \"yarn prettier --ignore-path ../.prettierignore --write ./src\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^18.3.1\",\n    \"react-dom\": \"^18.3.1\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^18.3.3\",\n    \"@types/react-dom\": \"^18.3.0\",\n    \"@vitejs/plugin-react\": \"^4.3.1\",\n    \"autoprefixer\": \"^10.4.19\",\n    \"nodemon\": \"^3.1.4\",\n    \"postcss\": \"^8.4.40\",\n    \"prettier\": \"^3.0.3\",\n    \"tailwindcss\": \"^3.4.7\",\n    \"vite\": \"^5.3.4\"\n  }\n}"
  },
  {
    "path": "postcss.config.js",
    "content": "export default {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "public/background.js",
    "content": "const ContextMenuModel = {\n  async create(workspaces) {\n    await chrome.contextMenus.removeAll();\n\n    if (workspaces && workspaces.length > 0) {\n      chrome.contextMenus.create({\n        id: \"saveToAnythingLLM\",\n        title: \"Save selected to AnythingLLM\",\n        contexts: [\"selection\"],\n      });\n\n      chrome.contextMenus.create({\n        id: \"embedToWorkspace\",\n        title: \"Embed selected content to workspace\",\n        contexts: [\"selection\"],\n      });\n\n      chrome.contextMenus.create({\n        id: \"saveEntirePageToAnythingLLM\",\n        title: \"Save entire page to AnythingLLM\",\n        contexts: [\"page\"],\n      });\n\n      chrome.contextMenus.create({\n        id: \"embedEntirePageToWorkspace\",\n        title: \"Embed entire page to workspace\",\n        contexts: [\"page\"],\n      });\n\n      workspaces.forEach((workspace) => {\n        chrome.contextMenus.create({\n          id: `workspace-selected-${workspace.id}`,\n          parentId: \"embedToWorkspace\",\n          title: workspace.name,\n          contexts: [\"selection\"],\n        });\n        chrome.contextMenus.create({\n          id: `workspace-page-${workspace.id}`,\n          parentId: \"embedEntirePageToWorkspace\",\n          title: workspace.name,\n          contexts: [\"page\"],\n        });\n      });\n    } else {\n      chrome.contextMenus.create({\n        id: \"saveToAnythingLLM\",\n        title: \"Save selected to AnythingLLM\",\n        contexts: [\"selection\"],\n      });\n      chrome.contextMenus.create({\n        id: \"saveEntirePageToAnythingLLM\",\n        title: \"Save entire page to AnythingLLM\",\n        contexts: [\"page\"],\n      });\n    }\n  },\n\n  async remove() {\n    await chrome.contextMenus.removeAll();\n  },\n};\n\nconst ExtensionModel = {\n  async checkApiKeyValidity() {\n    const { apiBase, apiKey } = await chrome.storage.sync.get([\n      \"apiBase\",\n      \"apiKey\",\n    ]);\n\n    if (!apiBase || !apiKey) {\n      await ContextMenuModel.remove();\n      return false;\n    }\n\n    const data = await fetch(`${apiBase}/browser-extension/check`, {\n      headers: { Authorization: `Bearer ${apiKey}` },\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error('Response not ok.')\n        return res.json();\n      })\n      .catch(() => null);\n\n    if (data === null) {\n      await chrome.storage.sync.remove([\"apiBase\", \"apiKey\"]);\n      await ContextMenuModel.remove();\n      return false;\n    }\n\n    await ContextMenuModel.create(data.workspaces);\n    return true;\n  },\n\n  async updateWorkspaces() {\n    const { apiBase, apiKey } = await chrome.storage.sync.get([\n      \"apiBase\",\n      \"apiKey\",\n    ]);\n\n    if (!apiBase || !apiKey) return await ContextMenuModel.remove();\n\n    const data = await fetch(`${apiBase}/browser-extension/check`, {\n      headers: { Authorization: `Bearer ${apiKey}` },\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error('Response not ok.')\n        return res.json();\n      })\n      .catch(() => null);\n\n    if (data === null) return await ContextMenuModel.remove();\n    await ContextMenuModel.create(data.workspaces);\n    return;\n  },\n\n  async saveToAnythingLLM(selectedText, pageTitle, pageUrl) {\n    const { apiBase, apiKey } = await chrome.storage.sync.get([\n      \"apiBase\",\n      \"apiKey\",\n    ]);\n    if (!apiBase || !apiKey) return;\n\n    this.showNotification(\n      'loading',\n      \"Uploading entire page into available documents. Please wait.\"\n    );\n    const response = await fetch(\n      `${apiBase}/browser-extension/upload-content`,\n      {\n        method: \"POST\",\n        headers: {\n          \"Content-Type\": \"application/json\",\n          Authorization: `Bearer ${apiKey}`,\n        },\n        body: JSON.stringify({\n          textContent: selectedText,\n          metadata: { title: pageTitle, url: pageUrl },\n        }),\n      }\n    );\n\n    this.handleResponse(response, \"save content\");\n  },\n\n  async embedToWorkspace(workspaceId, selectedText, pageTitle, pageUrl) {\n    const { apiBase, apiKey } = await chrome.storage.sync.get([\n      \"apiBase\",\n      \"apiKey\",\n    ]);\n    if (!apiBase || !apiKey) return;\n\n    this.showNotification(\n      'loading',\n      \"Uploading selected text into workspace. Please wait.\"\n    );\n    const response = await fetch(`${apiBase}/browser-extension/embed-content`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Authorization: `Bearer ${apiKey}`,\n      },\n      body: JSON.stringify({\n        workspaceId,\n        textContent: selectedText,\n        metadata: { title: pageTitle, url: pageUrl },\n      }),\n    });\n\n    this.handleResponse(response, \"embed content\");\n  },\n\n  async saveEntirePageToAnythingLLM(pageContent, pageTitle, pageUrl) {\n    const { apiBase, apiKey } = await chrome.storage.sync.get([\n      \"apiBase\",\n      \"apiKey\",\n    ]);\n    if (!apiBase || !apiKey) return;\n\n    this.showNotification(\n      'loading',\n      \"Uploading entire page text into available documents. Please wait.\"\n    );\n    const response = await fetch(\n      `${apiBase}/browser-extension/upload-content`,\n      {\n        method: \"POST\",\n        headers: {\n          \"Content-Type\": \"application/json\",\n          Authorization: `Bearer ${apiKey}`,\n        },\n        body: JSON.stringify({\n          textContent: pageContent,\n          metadata: { title: pageTitle, url: pageUrl },\n        }),\n      }\n    );\n\n    this.handleResponse(response, \"save entire page\");\n  },\n\n  async embedEntirePageToWorkspace(\n    workspaceId,\n    pageContent,\n    pageTitle,\n    pageUrl\n  ) {\n    const { apiBase, apiKey } = await chrome.storage.sync.get([\n      \"apiBase\",\n      \"apiKey\",\n    ]);\n    if (!apiBase || !apiKey) return;\n\n    this.showNotification(\n      'loading',\n      \"Embedding entire page into workspace. Please wait.\"\n    );\n    const response = await fetch(`${apiBase}/browser-extension/embed-content`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Authorization: `Bearer ${apiKey}`,\n      },\n      body: JSON.stringify({\n        workspaceId,\n        textContent: pageContent,\n        metadata: { title: pageTitle, url: pageUrl },\n      }),\n    });\n\n    this.handleResponse(response, \"embed entire page\");\n  },\n\n  async handleResponse(response, action) {\n    if (response.status === 401 || response.status === 403) {\n      await chrome.storage.sync.remove([\"apiBase\", \"apiKey\"]);\n      await ContextMenuModel.remove();\n      this.showNotification(\n        'error',\n        \"Authentication failed. Please reconnect the extension.\"\n      );\n    } else if (!response.ok) {\n      await this.checkApiKeyValidity();\n      this.showNotification(\"error\", `Failed to ${action}. Please try again.`);\n    } else {\n      this.showNotification(\n        \"success\",\n        \"Successfully saved content to AnythingLLM.\"\n      );\n    }\n  },\n\n  /**\n   * Shows badge notification on extension icon\n   * @param {\"success\"|\"error\"|\"loading\"} type \n   * @param {string} message \n   */\n  showNotification(type, message) {\n    const NOTIFICATION_MAP = {\n      success: {\n        title: \"Success\",\n        icon: \"✅\",\n      },\n      error: {\n        title: \"Error\",\n        icon: \"❌\",\n      },\n      loading: {\n        title: \"Loading\",\n        icon: \"⏳\",\n      }\n    }\n    if (!NOTIFICATION_MAP.hasOwnProperty(type)) return;\n    const { icon, title } = NOTIFICATION_MAP[type];\n    chrome.action.setBadgeText({ text: icon })\n    chrome.action.setTitle({ title: `${title}: ${message}` });\n\n    setTimeout(() => {\n      chrome.action.setBadgeText({ text: \"\" });\n      chrome.action.setTitle({ title: \"AnythingLLM Extension\" });\n    }, 5000);\n  },\n};\n\n// Event Listeners\nchrome.runtime.onInstalled.addListener(async () => {\n  await ExtensionModel.checkApiKeyValidity();\n});\n\nchrome.runtime.onMessage.addListener((message, _sender, _sendResponse) => {\n  if (message.action === \"connectionUpdated\") return ExtensionModel.checkApiKeyValidity();\n\n  if (message.action === \"newApiKey\") {\n    const [apiBase, apiKey] = message.connectionString.split(\"|\");\n    chrome.storage.sync.set({ apiBase, apiKey }, () => {\n      ExtensionModel.checkApiKeyValidity();\n      chrome.action.openPopup();\n    });\n    return;\n  }\n});\n\nfunction getPageContent(tabId) {\n  return new Promise((resolve, reject) => {\n    chrome.tabs.sendMessage(tabId, { action: \"getPageContent\" }, (response) => {\n      if (chrome.runtime.lastError) {\n        reject(chrome.runtime.lastError);\n      } else if (response && response.content) {\n        resolve(response.content);\n      } else {\n        reject(new Error(\"Failed to get page content\"));\n      }\n    });\n  });\n}\n\nchrome.contextMenus.onClicked.addListener((info, tab) => {\n  if (info.menuItemId === \"saveToAnythingLLM\") {\n    ExtensionModel.saveToAnythingLLM(info.selectionText, tab.title, tab.url);\n    return;\n  }\n\n  if (info.menuItemId.startsWith(\"workspace-selected-\")) {\n    const workspaceId = info.menuItemId.split(\"-\")[2];\n    ExtensionModel.embedToWorkspace(\n      workspaceId,\n      info.selectionText,\n      tab.title,\n      tab.url\n    );\n    return;\n  }\n\n  if (info.menuItemId === \"saveEntirePageToAnythingLLM\") {\n    getPageContent(tab.id)\n      .then((content) => {\n        ExtensionModel.saveEntirePageToAnythingLLM(content, tab.title, tab.url);\n      })\n      .catch((error) => {\n        console.error(\"Error getting page content:\", error);\n        ExtensionModel.showNotification(\n          \"error\",\n          \"Failed to get page content. Please try again.\"\n        );\n      });\n    return;\n  }\n\n  if (info.menuItemId.startsWith(\"workspace-page-\")) {\n    const workspaceId = info.menuItemId.split(\"-\")[2];\n    getPageContent(tab.id)\n      .then((content) => {\n        ExtensionModel.embedEntirePageToWorkspace(\n          workspaceId,\n          content,\n          tab.title,\n          tab.url\n        );\n      })\n      .catch((error) => {\n        console.error(\"Error getting page content:\", error);\n        ExtensionModel.showNotification(\n          \"error\",\n          \"Failed to get page content. Please try again.\"\n        );\n      });\n    return;\n  }\n});\n\n// Remove context menu items when connection is lost\nchrome.storage.onChanged.addListener((changes, namespace) => {\n  if (namespace === \"sync\" && (changes.apiBase || changes.apiKey)) {\n    if (!changes.apiBase?.newValue || !changes.apiKey?.newValue) {\n      ContextMenuModel.remove();\n    }\n  }\n});\n\n// Update workspaces periodically\nchrome.alarms.create(\"updateWorkspaces\", { periodInMinutes: 1 });\nchrome.alarms.onAlarm.addListener((alarm) => {\n  if (alarm.name === \"updateWorkspaces\") {\n    ExtensionModel.updateWorkspaces();\n  }\n});\n"
  },
  {
    "path": "public/contentScript.js",
    "content": "window.addEventListener(\"message\", (event) => {\n  if (event.data.type === \"NEW_BROWSER_EXTENSION_CONNECTION\") {\n    chrome.runtime.sendMessage({\n      action: \"newApiKey\",\n      connectionString: event.data.apiKey,\n    });\n  }\n});\n\nchrome.runtime.onMessage.addListener((request, sender, sendResponse) => {\n  if (request.action === \"getPageContent\") {\n    sendResponse({ content: document.body.innerText });\n  }\n});\n"
  },
  {
    "path": "public/manifest.json",
    "content": "{\n  \"manifest_version\": 3,\n  \"name\": \"AnythingLLM Browser Companion\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Bring AnythingLLM directly into your browser to curate and collect content on the web directly into your AnythingLLM workspaces.\",\n  \"icons\": {\n    \"16\": \"icon16.png\",\n    \"32\": \"icon32.png\",\n    \"48\": \"icon48.png\",\n    \"128\": \"icon128.png\"\n  },\n  \"permissions\": [\n    \"contextMenus\",\n    \"activeTab\",\n    \"storage\",\n    \"notifications\",\n    \"alarms\"\n  ],\n  \"host_permissions\": [\n    \"<all_urls>\"\n  ],\n  \"background\": {\n    \"service_worker\": \"background.js\"\n  },\n  \"action\": {\n    \"default_popup\": \"index.html\",\n    \"default_icon\": {\n      \"16\": \"icon16.png\",\n      \"32\": \"icon32.png\",\n      \"48\": \"icon48.png\",\n      \"128\": \"icon128.png\"\n    }\n  },\n  \"content_scripts\": [\n    {\n      \"matches\": [\n        \"<all_urls>\"\n      ],\n      \"js\": [\n        \"contentScript.js\"\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "src/App.jsx",
    "content": "import React from \"react\";\nimport Config from \"./components/Config\";\nimport useApiConnection from \"./hooks/useApiConnection\";\n\nconst App = () => {\n  const { status, checkApiKeyStatus, logoUrl } = useApiConnection();\n  return (\n    <div className=\"p-6 bg-[#25272C] min-h-screen flex flex-col items-center\">\n      <img src={logoUrl} alt=\"AnythingLLM Logo\" className=\"w-40 mb-6\" />\n      <div className=\"bg-[#2C2E33] p-6 rounded-lg shadow-lg w-full max-w-md\">\n        <p className=\"text-white text-sm font-medium mb-6\">\n          Right click on any page and send selected text or entire pages to\n          AnythingLLM.\n        </p>\n        <Config status={status} onStatusChange={checkApiKeyStatus} />\n      </div>\n    </div>\n  );\n};\n\nexport default App;\n"
  },
  {
    "path": "src/components/Config.jsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport BrowserExtension from \"../models/browserExtension\";\n\nexport default function Config({ status, onStatusChange }) {\n  const [connectionString, setConnectionString] = useState(\"\");\n  const [saveStatus, setSaveStatus] = useState(\"\");\n\n  useEffect(() => {\n    if (saveStatus) {\n      const timer = setTimeout(() => {\n        setSaveStatus(\"\");\n      }, 5000);\n      return () => clearTimeout(timer);\n    }\n  }, [saveStatus]);\n\n  // Disconnects & de-registers the current extension from the set API key.\n  async function disconnectFromExtension() {\n    await chrome.storage.sync.remove([\"apiBase\", \"apiKey\"]);\n    onStatusChange();\n    setSaveStatus(\"Successfully disconnected from AnythingLLM\");\n    chrome.runtime.sendMessage({ action: \"connectionUpdated\" });\n  }\n\n  const handleConnect = async () => {\n    try {\n      const [apiBase, apiKey] = connectionString.split(\"|\");\n      if (!apiBase || !apiKey) {\n        setSaveStatus(\"Invalid connection string format.\");\n        return;\n      }\n\n      const { online } = await BrowserExtension.checkOnline(apiBase);\n      if (!online) {\n        setSaveStatus(\n          \"AnythingLLM is currently offline. Please try again later.\"\n        );\n        return;\n      }\n\n      const { response } = await BrowserExtension.checkApiKey(apiBase, apiKey);\n      if (!response.ok)\n        return setSaveStatus(\"Failed to connect: Invalid API key\");\n\n      // Saves the apiBase and apiKey to storage sync.\n      await chrome.storage.sync.set({ apiBase, apiKey });\n      onStatusChange();\n      setSaveStatus(\"Successfully connected to AnythingLLM\");\n      chrome.runtime.sendMessage({ action: \"connectionUpdated\" });\n    } catch (error) {\n      setSaveStatus(`An error occurred during connection: ${error.message}`);\n    }\n  };\n\n  const handleDisconnect = async () => {\n    try {\n      const { apiBase, apiKey } = await chrome.storage.sync.get([\n        \"apiBase\",\n        \"apiKey\",\n      ]);\n      if (!apiBase || !apiKey) throw new Error(\"No connection found\");\n      const { success, error } = await BrowserExtension.disconnect(\n        apiBase,\n        apiKey\n      );\n      if (!success)\n        throw new Error(error || \"Failed to disconnect from the server\");\n      await disconnectFromExtension();\n    } catch (error) {\n      setSaveStatus(`An error occurred during disconnection: ${error.message}`);\n    }\n  };\n\n  return (\n    <div className=\"w-full flex flex-col gap-y-4\">\n      {status === \"notConnected\" && (\n        <div className=\"w-full flex flex-col gap-y-4\">\n          <div className=\"flex flex-col w-full\">\n            <label className=\"text-white text-sm font-semibold block mb-3\">\n              AnythingLLM Connection String\n            </label>\n            <input\n              type=\"text\"\n              value={connectionString}\n              onChange={(e) => setConnectionString(e.target.value)}\n              placeholder=\"Paste connection string here\"\n              className=\"bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:outline-[#46C8FF] active:outline-[#46C8FF] outline-none block w-full p-2.5\"\n            />\n          </div>\n          <button\n            onClick={handleConnect}\n            className=\"bg-[#46C8FF] hover:bg-[#3BA3D0] text-white font-bold py-2 px-4 rounded-lg transition duration-300 border border-[#46C8FF] hover:border-[#3BA3D0] focus:outline-none focus:ring-2 focus:ring-[#46C8FF] focus:ring-opacity-50\"\n          >\n            Connect\n          </button>\n        </div>\n      )}\n\n      {status === \"connected\" && (\n        <div className=\"w-full flex flex-col gap-y-4\">\n          <div className=\"flex items-center justify-center gap-x-2 bg-zinc-900 p-2.5 rounded-lg\">\n            <div className=\"w-2 h-2 rounded-full bg-green-400 animate-pulse\" />\n            <p className=\"text-green-400 text-sm font-medium\">\n              Connected to AnythingLLM\n            </p>\n          </div>\n          <button\n            onClick={handleDisconnect}\n            className=\"bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-lg transition duration-300 border border-red-500 hover:border-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50\"\n          >\n            Disconnect\n          </button>\n        </div>\n      )}\n\n      {status === \"offline\" && (\n        <div className=\"w-full flex flex-col gap-y-4\">\n          <button\n            onClick={disconnectFromExtension}\n            className=\"bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-lg transition duration-300 border border-red-500 hover:border-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50\"\n          >\n            Disconnect\n          </button>\n          <div className=\"bg-red-500/10 border border-red-500/50 text-red-400 p-2.5 rounded-lg\">\n            AnythingLLM is currently offline. Please try again later.\n          </div>\n        </div>\n      )}\n\n      {status === \"error\" && (\n        <div className=\"bg-red-500/10 border border-red-500/50 text-red-400 p-2.5 rounded-lg\">\n          An error occurred. Please try again later.\n        </div>\n      )}\n\n      {saveStatus && (\n        <div\n          className={`p-2.5 rounded-lg ${\n            saveStatus.includes(\"Successfully\")\n              ? \"bg-green-500/10 border border-green-500/50 text-green-400\"\n              : \"bg-red-500/10 border border-red-500/50 text-red-400\"\n          }`}\n        >\n          {saveStatus}\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/hooks/useApiConnection.js",
    "content": "import { useState, useEffect } from \"react\";\nimport AnythingLLMLogo from \"@/media/anything-llm.png\";\nimport BrowserExtension from \"@/models/browserExtension\";\n\n/**\n * Fetches connection information for API key provided\n * @returns {{\n * status: (\"loading\"|\"notConnected\"|\"offline\"|\"connected\")\n * }}\n */\nexport default function useApiConnection() {\n  const [status, setStatus] = useState(\"loading\");\n  const [logoUrl, setLogoUrl] = useState(AnythingLLMLogo);\n\n  useEffect(() => {\n    checkApiKeyStatus();\n    fetchLogo();\n\n    chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {\n      if (message.action === \"newApiKey\") {\n        checkApiKeyStatus();\n        fetchLogo();\n      }\n    });\n  }, []);\n\n  const checkApiKeyStatus = async () => {\n    const { apiBase, apiKey } = await chrome.storage.sync.get([\n      \"apiBase\",\n      \"apiKey\",\n    ]);\n    if (!apiBase || !apiKey) {\n      setStatus(\"notConnected\");\n      return;\n    }\n\n    try {\n      const { online } = await BrowserExtension.checkOnline(apiBase);\n      if (!online) {\n        setStatus(\"offline\");\n        return;\n      }\n\n      const { response } = await BrowserExtension.checkApiKey(apiBase, apiKey);\n      if (response.ok) {\n        setStatus(\"connected\");\n        chrome.runtime.sendMessage({ action: \"connectionUpdated\" });\n      } else {\n        await chrome.storage.sync.remove([\"apiBase\", \"apiKey\"]);\n        setStatus(\"notConnected\");\n        chrome.runtime.sendMessage({ action: \"connectionUpdated\" });\n      }\n    } catch (error) {\n      setStatus(\"error\");\n    }\n  };\n\n  const fetchLogo = async () => {\n    const { apiBase } = await chrome.storage.sync.get([\"apiBase\"]);\n    if (!apiBase) return;\n    const { success, logoURL } = await BrowserExtension.fetchLogo(apiBase);\n    setLogoUrl(success ? logoURL : AnythingLLMLogo);\n  };\n\n  return { status, logoUrl, checkApiKeyStatus };\n}\n"
  },
  {
    "path": "src/index.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n:root {\n  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;\n  line-height: 1.5;\n  font-weight: 400;\n\n  color-scheme: light dark;\n  color: rgba(255, 255, 255, 0.87);\n  background-color: #242424;\n\n  font-synthesis: none;\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\nbody {\n  margin: 0;\n  display: flex;\n  place-items: center;\n  min-width: 320px;\n  min-height: 100vh;\n}\n\nh1 {\n  font-size: 3.2em;\n  line-height: 1.1;\n}\n"
  },
  {
    "path": "src/main.jsx",
    "content": "import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport App from \"./App.jsx\";\nimport \"./index.css\";\n\nReactDOM.createRoot(document.getElementById(\"root\")).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>\n);\n"
  },
  {
    "path": "src/models/browserExtension.js",
    "content": "const BrowserExtension = {\n  checkApiKey: async function (apiBase, apiKey) {\n    return await fetch(`${apiBase}/browser-extension/check`, {\n      headers: { Authorization: `Bearer ${apiKey}` },\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Bad response to /check\");\n        return res.json();\n      })\n      .then((data) => ({ response: { ok: true }, data, error: null }))\n      .catch((e) => {\n        console.error(e);\n        return { response: { ok: false }, data: null, error: e.message };\n      });\n  },\n\n  checkOnline: async function (apiBase) {\n    return await fetch(`${apiBase}/ping`)\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Bad response to /ping\");\n        return res.json();\n      })\n      .then((data) => ({ online: true, data, error: null }))\n      .catch((e) => {\n        return { online: false, data: null, error: e.message };\n      });\n  },\n\n  fetchLogo: async function (apiBase) {\n    try {\n      const response = await fetch(`${apiBase}/system/logo`, {\n        method: \"GET\",\n        cache: \"no-cache\",\n      });\n\n      if (response.ok && response.status !== 204) {\n        const blob = await response.blob();\n        const logoURL = URL.createObjectURL(blob);\n        return { success: true, error: null, logoURL };\n      } else {\n        return { success: false, error: \"Logo not available\", logoURL: null };\n      }\n    } catch (error) {\n      console.error(\"Error fetching logo:\", error);\n      return { success: false, error: error.message, logoURL: null };\n    }\n  },\n\n  disconnect: async function (apiBase, apiKey) {\n    try {\n      await fetch(`${apiBase}/browser-extension/disconnect`, {\n        method: \"DELETE\",\n        headers: { Authorization: `Bearer ${apiKey}` },\n      })\n        .then((res) => res.json())\n        .then((data) => {\n          if (!!data.error)\n            throw new Error(errorData.error || \"Failed to disconnect\");\n          return;\n        });\n      return { success: true, error: null };\n    } catch (error) {\n      console.error(\"Disconnect error:\", error);\n      return { success: false, error: error.message };\n    }\n  },\n};\n\nexport default BrowserExtension;\n"
  },
  {
    "path": "src/utils/constants.js",
    "content": "// Constants for the chrome extension\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nexport default {\n  content: [\"./index.html\", \"./src/**/*.{js,ts,jsx,tsx}\"],\n  theme: {\n    extend: {}\n  },\n  plugins: []\n}\n"
  },
  {
    "path": "vite.config.js",
    "content": "import { defineConfig } from \"vite\"\nimport { fileURLToPath, URL } from \"url\"\nimport react from \"@vitejs/plugin-react\"\n\nexport default defineConfig({\n  plugins: [react()],\n  build: {\n    rollupOptions: {\n      input: {\n        main: \"index.html\"\n      }\n    },\n    outDir: \"dist\"\n  },\n  resolve: {\n    alias: [\n      {\n        find: \"@\",\n        replacement: fileURLToPath(new URL(\"./src\", import.meta.url))\n      },\n    ]\n  }\n})\n"
  }
]