Merge branch 'master' of github.com:Mintplex-Labs/anything-llm into render

This commit is contained in:
timothycarambat 2024-09-12 16:09:16 -07:00
commit f8a40faeaf
148 changed files with 3518 additions and 6575 deletions

View File

@ -41,11 +41,12 @@ Checklist:
:warning: **Important for all developers** :warning: :warning: **Important for all developers** :warning:
- [ ] Whe you are using the `NODE_ENV=development` the server will not store the configurations you set for security reasons. Please set the proper config on file `.env.development`. The side-effect if you don't, everytime you restart the server, you will be sent to the "Onboarding" page again. - [ ] When you are using the `NODE_ENV=development` the server will not store the configurations you set for security reasons. Please set the proper config on file `.env.development`. The side-effect if you don't, everytime you restart the server, you will be sent to the "Onboarding" page again.
:warning: **Important for Github Codespaces** :warning: **Note when using Github Codespaces**
- [ ] When running the "Server" for the first time, it will automatically configure its port to be publicly accessible by default, as this is required for the front end to reach the server backend. To know more, read the content of the `.env` file on the frontend folder about this, and if any issues occur, make sure to manually set the port "Visibility" of the "Server" is set to "Public" if needed. Again, this is only needed for developing on Github Codespaces.
- [ ] When running the "Server" for the first time, its port will be automatically forward, but privately. Read the content of the `.env` file on the frontend folder about this, and make sure the port "Visibility" is set to "Public", so the frontend can reach the backend. Again, this is only needed for developing on Github Codespaces. We appreciate to know if you have a better solution.
**For the Collector:** **For the Collector:**

View File

@ -21,7 +21,8 @@ on:
- '**/.env.example' - '**/.env.example'
- '.github/ISSUE_TEMPLATE/**/*' - '.github/ISSUE_TEMPLATE/**/*'
- '.devcontainer/**/*' - '.devcontainer/**/*'
- 'embed/**/*' # Embed should be published to frontend (yarn build:publish) if any changes are introduced - 'embed/**/*' # Embed is submodule
- 'browser-extension/**/*' # Chrome extension is submodule
- 'server/utils/agents/aibitat/example/**/*' # Do not push new image for local dev testing of new aibitat images. - 'server/utils/agents/aibitat/example/**/*' # Do not push new image for local dev testing of new aibitat images.
jobs: jobs:

View File

@ -6,7 +6,7 @@ concurrency:
on: on:
push: push:
branches: ['encrypt-jwt-value'] # put your current branch to create a build. Core team only. branches: ['agent-skill-plugins'] # put your current branch to create a build. Core team only.
paths-ignore: paths-ignore:
- '**.md' - '**.md'
- 'cloud-deployments/*' - 'cloud-deployments/*'

7
.gitmodules vendored Normal file
View File

@ -0,0 +1,7 @@
[submodule "browser-extension"]
path = browser-extension
url = git@github.com:Mintplex-Labs/anythingllm-extension.git
[submodule "embed"]
path = embed
url = git@github.com:Mintplex-Labs/anythingllm-embed.git
branch = main

View File

@ -41,8 +41,10 @@
"Qdrant", "Qdrant",
"royalblue", "royalblue",
"searxng", "searxng",
"SearchApi",
"Serper", "Serper",
"Serply", "Serply",
"streamable",
"textgenwebui", "textgenwebui",
"togetherai", "togetherai",
"Unembed", "Unembed",

View File

@ -69,7 +69,7 @@ AnythingLLM divides your documents into objects called `workspaces`. A Workspace
### Supported LLMs, Embedder Models, Speech models, and Vector Databases ### Supported LLMs, Embedder Models, Speech models, and Vector Databases
**Language Learning Models:** **Large Language Models (LLMs):**
- [Any open-source llama.cpp compatible model](/server/storage/models/README.md#text-generation-llm-selection) - [Any open-source llama.cpp compatible model](/server/storage/models/README.md#text-generation-llm-selection)
- [OpenAI](https://openai.com) - [OpenAI](https://openai.com)
@ -137,7 +137,8 @@ This monorepo consists of three main sections:
- `server`: A NodeJS express server to handle all the interactions and do all the vectorDB management and LLM interactions. - `server`: A NodeJS express server to handle all the interactions and do all the vectorDB management and LLM interactions.
- `collector`: NodeJS express server that process and parses documents from the UI. - `collector`: NodeJS express server that process and parses documents from the UI.
- `docker`: Docker instructions and build process + information for building from source. - `docker`: Docker instructions and build process + information for building from source.
- `embed`: Code specifically for generation of the [embed widget](./embed/README.md). - `embed`: Submodule for generation & creation of the [web embed widget](https://github.com/Mintplex-Labs/anythingllm-embed).
- `browser-extension`: Submodule for the [chrome browser extension](https://github.com/Mintplex-Labs/anythingllm-extension).
## 🛳 Self Hosting ## 🛳 Self Hosting
@ -146,9 +147,9 @@ Mintplex Labs & the community maintain a number of deployment methods, scripts,
|----------------------------------------|----:|-----|---------------|------------| |----------------------------------------|----:|-----|---------------|------------|
| [![Deploy on Docker][docker-btn]][docker-deploy] | [![Deploy on AWS][aws-btn]][aws-deploy] | [![Deploy on GCP][gcp-btn]][gcp-deploy] | [![Deploy on DigitalOcean][do-btn]][do-deploy] | [![Deploy on Render.com][render-btn]][render-deploy] | | [![Deploy on Docker][docker-btn]][docker-deploy] | [![Deploy on AWS][aws-btn]][aws-deploy] | [![Deploy on GCP][gcp-btn]][gcp-deploy] | [![Deploy on DigitalOcean][do-btn]][do-deploy] | [![Deploy on Render.com][render-btn]][render-deploy] |
| Railway | RepoCloud | | Railway | RepoCloud | Elestio |
| --- | --- | | --- | --- | --- |
| [![Deploy on Railway][railway-btn]][railway-deploy] | [![Deploy on RepoCloud][repocloud-btn]][repocloud-deploy] | | [![Deploy on Railway][railway-btn]][railway-deploy] | [![Deploy on RepoCloud][repocloud-btn]][repocloud-deploy] | [![Deploy on Elestio][elestio-btn]][elestio-deploy] |
[or set up a production AnythingLLM instance without Docker →](./BARE_METAL.md) [or set up a production AnythingLLM instance without Docker →](./BARE_METAL.md)
@ -246,3 +247,5 @@ This project is [MIT](./LICENSE) licensed.
[railway-deploy]: https://railway.app/template/HNSCS1?referralCode=WFgJkn [railway-deploy]: https://railway.app/template/HNSCS1?referralCode=WFgJkn
[repocloud-btn]: https://d16t0pc4846x52.cloudfront.net/deploylobe.svg [repocloud-btn]: https://d16t0pc4846x52.cloudfront.net/deploylobe.svg
[repocloud-deploy]: https://repocloud.io/details/?app_id=276 [repocloud-deploy]: https://repocloud.io/details/?app_id=276
[elestio-btn]: https://elest.io/images/logos/deploy-to-elestio-btn.png
[elestio-deploy]: https://elest.io/open-source/anythingllm

1
browser-extension Submodule

@ -0,0 +1 @@
Subproject commit 385d36c0807221e6674d1325ef06505746c49ceb

View File

@ -38,7 +38,6 @@ function extensions(app) {
reqBody(request), reqBody(request),
response, response,
); );
console.log({ success, reason, data })
response.status(200).json({ response.status(200).json({
success, success,
reason, reason,

View File

@ -119,6 +119,7 @@ class GitHubRepoLoader {
maxConcurrency: 5, maxConcurrency: 5,
unknown: "ignore", unknown: "ignore",
ignorePaths: this.ignorePaths, ignorePaths: this.ignorePaths,
verbose: true,
}); });
const docs = []; const docs = [];

View File

@ -252,6 +252,10 @@ GID='1000'
# AGENT_GSE_KEY= # AGENT_GSE_KEY=
# AGENT_GSE_CTX= # AGENT_GSE_CTX=
#------ SearchApi.io ----------- https://www.searchapi.io/
# AGENT_SEARCHAPI_API_KEY=
# AGENT_SEARCHAPI_ENGINE=google
#------ Serper.dev ----------- https://serper.dev/ #------ Serper.dev ----------- https://serper.dev/
# AGENT_SERPER_DEV_KEY= # AGENT_SERPER_DEV_KEY=

View File

@ -117,8 +117,8 @@ services:
- WHISPER_PROVIDER=local - WHISPER_PROVIDER=local
- TTS_PROVIDER=native - TTS_PROVIDER=native
- PASSWORDMINCHAR=8 - PASSWORDMINCHAR=8
- AGENT_SERPER_DEV_KEY="SERPER DEV API KEY" # Add any other keys here for services or settings
- AGENT_SERPLY_API_KEY="Serply.io API KEY" # you can find in the docker/.env.example file
volumes: volumes:
- anythingllm_storage:/app/server/storage - anythingllm_storage:/app/server/storage
restart: always restart: always

1
embed Submodule

@ -0,0 +1 @@
Subproject commit 22a0848d58e3a758d85d93d9204a72a65854ea94

25
embed/.gitignore vendored
View File

@ -1,25 +0,0 @@
# 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
!yarn.lock
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -1,112 +0,0 @@
# AnythingLLM Embedded Chat Widget
> [!WARNING]
> The use of the AnythingLLM embed is currently in beta. Please request a feature or
> report a bug via a Github Issue if you have any issues.
> [!WARNING]
> The core AnythingLLM team publishes a pre-built version of the script that is bundled
> with the main application. You can find it at the frontend URL `/embed/anythingllm-chat-widget.min.js`.
> You should only be working in this repo if you are wanting to build your own custom embed.
This folder of AnythingLLM contains the source code for how the embedded version of AnythingLLM works to provide a public facing interface of your workspace.
The AnythingLLM Embedded chat widget allows you to expose a workspace and its embedded knowledge base as a chat bubble via a `<script>` or `<iframe>` element that you can embed in a website or HTML.
### Security
- Users will _not_ be able to view or read context snippets like they can in the core AnythingLLM application
- Users are assigned a random session ID that they use to persist a chat session.
- **Recommended** You can limit both the number of chats an embedding can process **and** per-session.
_by using the AnythingLLM embedded chat widget you are responsible for securing and configuration of the embed as to not allow excessive chat model abuse of your instance_
### Developer Setup
- `cd embed` from the root of the repo
- `yarn` to install all dev and script dependencies
- `yarn dev` to boot up an example HTML page to use the chat embed widget.
While in development mode (`yarn dev`) the script will rebuild on any changes to files in the `src` directory. Ensure that the required keys for the development embed are accurate and set.
`yarn build` will compile and minify your build of the script. You can then host and link your built script wherever you like.
## Integrations & Embed Types
### `<script>` tag HTML embed
The primary way of embedding a workspace as a chat widget is via a simple `<script>`
```html
<!--
An example of a script tag embed
REQUIRED data attributes:
data-embed-id // The unique id of your embed with its default settings
data-base-api-url // The URL of your anythingLLM instance backend
-->
<script
data-embed-id="5fc05aaf-2f2c-4c84-87a3-367a4692c1ee"
data-base-api-url="http://localhost:3001/api/embed"
src="http://localhost:3000/embed/anythingllm-chat-widget.min.js"
></script>
```
### `<script>` Customization Options
**LLM Overrides**
- `data-prompt` — Override the chat window with a custom system prompt. This is not visible to the user. If undefined it will use the embeds attached workspace system prompt.
- `data-model` — Override the chat model used for responses. This must be a valid model string for your AnythingLLM LLM provider. If unset it will use the embeds attached workspace model selection or the system setting.
- `data-temperature` — Override the chat model temperature. This must be a valid value for your AnythingLLM LLM provider. If unset it will use the embeds attached workspace model temperature or the system setting.
**Style Overrides**
- `data-chat-icon` — The chat bubble icon show when chat is closed. Options are `plus`, `chatBubble`, `support`, `search2`, `search`, `magic`.
- `data-button-color` — The chat bubble background color shown when chat is closed. Value must be hex color code.
- `data-user-bg-color` — The background color of the user chat bubbles when chatting. Value must be hex color code.
- `data-assistant-bg-color` — The background color of the assistant response chat bubbles when chatting. Value must be hex color code.
- `data-brand-image-url` — URL to image that will be show at the top of the chat when chat is open.
- `data-greeting` — Default text message to be shown when chat is opened and no previous message history is found.
- `data-no-sponsor` — Setting this attribute to anything will hide the custom or default sponsor at the bottom of an open chat window.
- `data-sponsor-link` — A clickable link in the sponsor section in the footer of an open chat window.
- `data-sponsor-text` — The text displays in sponsor text in the footer of an open chat window.
- `data-position` - Adjust the positioning of the embed chat widget and open chat button. Default `bottom-right`. Options are `bottom-right`, `bottom-left`, `top-right`, `top-left`.
- `data-assistant-name` - Set the chat assistant name that appears above each chat message. Default `AnythingLLM Chat Assistant`
- `data-assistant-icon` - Set the icon of the chat assistant.
- `data-window-height` - Set the chat window height. **must include CSS suffix:** `px`,`%`,`rem`
- `data-window-width` - Set the chat window width. **must include CSS suffix:** `px`,`%`,`rem`
- `data-text-size` - Set the text size of the chats in pixels.
- `data-username` - A specific readable name or identifier for the client for your reference. Will be shown in AnythingLLM chat logs. If empty it will not be reported.
- `data-default-messages` - A string of comma-separated messages you want to display to the user when the chat widget has no history. Example: `"How are you?, What is so interesting about this project?, Tell me a joke."`
**Behavior Overrides**
- `data-open-on-load` — Once loaded, open the chat as default. It can still be closed by the user. To enable set this attribute to `on`. All other values will be ignored.
- `data-support-email` — Shows a support email that the user can used to draft an email via the "three dot" menu in the top right. Option will not appear if it is not set.
### `<iframe>` tag HTML embed
_work in progress_
### `<iframe>` Customization Options
_work in progress_

View File

@ -1,17 +0,0 @@
<!doctype html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<html lang="en">
<body>
<h1>This is an example testing page for embedded AnythingLLM.</h1>
<!--
<script data-embed-id="example-uuid" data-base-api-url='http://localhost:3001/api/embed' data-open-on-load="on"
src="/dist/anythingllm-chat-widget.js"> USE THIS SRC FOR DEVELOPMENT SO CHANGES APPEAR!
</script>
-->
</body>
</html>

View File

@ -1,10 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "esnext",
"jsx": "react",
"paths": {
"@/*": ["./src/*"],
},
},
}

View File

@ -1,51 +0,0 @@
{
"name": "anythingllm-embedded-chat",
"private": false,
"license": "MIT",
"type": "module",
"scripts": {
"dev": "nodemon -e js,jsx,css --watch src --exec \"yarn run dev:preview\"",
"dev:preview": "yarn run dev:build && yarn serve . -p 3080 --no-clipboard",
"dev:build": "vite build && yarn styles",
"styles": "npx cleancss -o dist/anythingllm-chat-widget.min.css dist/style.css",
"build": "vite build && yarn styles && npx terser --compress -o dist/anythingllm-chat-widget.min.js -- dist/anythingllm-chat-widget.js",
"build:publish": "yarn build:publish:js && yarn build:publish:css",
"build:publish:js": "yarn build && mkdir -p ../frontend/public/embed && cp -r dist/anythingllm-chat-widget.min.js ../frontend/public/embed/anythingllm-chat-widget.min.js",
"build:publish:css": "cp -r dist/anythingllm-chat-widget.min.css ../frontend/public/embed/anythingllm-chat-widget.min.css",
"lint": "yarn prettier --ignore-path ../.prettierignore --write ./src"
},
"dependencies": {
"@microsoft/fetch-event-source": "^2.0.1",
"@phosphor-icons/react": "^2.0.13",
"dompurify": "^3.0.8",
"he": "^1.2.0",
"highlight.js": "^11.9.0",
"lodash.debounce": "^4.0.8",
"markdown-it": "^13.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"uuid": "^9.0.1"
},
"devDependencies": {
"@rollup/plugin-image": "^3.0.3",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.14",
"clean-css": "^5.3.3",
"clean-css-cli": "^5.6.3",
"eslint": "^8.53.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"globals": "^13.21.0",
"nodemon": "^2.0.22",
"postcss": "^8.4.23",
"prettier": "^3.0.3",
"serve": "^14.2.1",
"tailwindcss": "3.4.1",
"terser": "^5.27.0",
"vite": "^5.0.0",
"vite-plugin-singlefile": "^0.13.5"
}
}

View File

@ -1,10 +0,0 @@
import tailwind from 'tailwindcss'
import autoprefixer from 'autoprefixer'
import tailwindConfig from './tailwind.config.js'
export default {
plugins: [
tailwind(tailwindConfig),
autoprefixer,
],
}

View File

@ -1,35 +0,0 @@
// What is this script?
// We want to support code syntax highlighting in the embed modal, but we cannot afford to have the static build
// be large in size. To prevent HighlightJs from loading all 193+ language stylings and bloating the script, we instead take a large subset that
// covers most languages and then dynamically build and register each language since HLJS cannot just register with an array of langs.
// Since the embed is a single script - we need to statically import each library and register that associate language.
// we can then export this as a custom implementation of HLJS and call it a day and keep the bundle small.
import fs from 'fs'
const SUPPORTED_HIGHLIGHTS = ['apache', 'bash', 'c', 'cpp', 'csharp', 'css', 'diff', 'go', 'graphql', 'ini', 'java', 'javascript', 'json', 'kotlin', 'less', 'lua', 'makefile', 'markdown', 'nginx', 'objectivec', 'perl', 'pgsql', 'php', 'php-template', 'plaintext', 'python', 'python-repl', 'r', 'ruby', 'rust', 'scss', 'shell', 'sql', 'swift', 'typescript', 'vbnet', 'wasm', 'xml', 'yaml'];
function quickClean(input) {
return input.replace(/[^a-zA-Z0-9]/g, '');
}
let content = `/*
This is a dynamically generated file to help de-bloat the app since this script is a static bundle.
You should not modify this file directly. You can regenerate it with "node scripts/updateHljs.mjd" from the embed folder.
Last generated ${(new Date).toDateString()}
----------------------
*/\n\n`
content += 'import hljs from "highlight.js/lib/core";\n';
SUPPORTED_HIGHLIGHTS.forEach((lang) => {
content += `import ${quickClean(lang)}HljsSupport from 'highlight.js/lib/languages/${lang}'\n`;
});
SUPPORTED_HIGHLIGHTS.forEach((lang) => {
content += ` hljs.registerLanguage('${lang}', ${quickClean(lang)}HljsSupport)\n`;
})
content += `// The above should now register on the languages we wish to support statically.\n`;
content += `export const staticHljs = hljs;\n`
fs.writeFileSync('src/utils/chat/hljs.js', content, { encoding: 'utf8' })
console.log(`Static build of HLJS completed - src/utils/chat/hljs.js`)

View File

@ -1,71 +0,0 @@
import useGetScriptAttributes from "@/hooks/useScriptAttributes";
import useSessionId from "@/hooks/useSessionId";
import useOpenChat from "@/hooks/useOpen";
import Head from "@/components/Head";
import OpenButton from "@/components/OpenButton";
import ChatWindow from "./components/ChatWindow";
import { useEffect } from "react";
export default function App() {
const { isChatOpen, toggleOpenChat } = useOpenChat();
const embedSettings = useGetScriptAttributes();
const sessionId = useSessionId();
useEffect(() => {
if (embedSettings.openOnLoad === "on") {
toggleOpenChat(true);
}
}, [embedSettings.loaded]);
if (!embedSettings.loaded) return null;
const positionClasses = {
"bottom-left": "allm-bottom-0 allm-left-0 allm-ml-4",
"bottom-right": "allm-bottom-0 allm-right-0 allm-mr-4",
"top-left": "allm-top-0 allm-left-0 allm-ml-4 allm-mt-4",
"top-right": "allm-top-0 allm-right-0 allm-mr-4 allm-mt-4",
};
const position = embedSettings.position || "bottom-right";
const windowWidth = embedSettings.windowWidth ?? "400px";
const windowHeight = embedSettings.windowHeight ?? "700px";
return (
<>
<Head />
<div
id="anything-llm-embed-chat-container"
className={`allm-fixed allm-inset-0 allm-z-50 ${isChatOpen ? "allm-block" : "allm-hidden"}`}
>
<div
style={{
maxWidth: windowWidth,
maxHeight: windowHeight,
}}
className={`allm-h-full allm-w-full allm-bg-white allm-fixed allm-bottom-0 allm-right-0 allm-mb-4 allm-md:mr-4 allm-rounded-2xl allm-border allm-border-gray-300 allm-shadow-[0_4px_14px_rgba(0,0,0,0.25)] ${positionClasses[position]}`}
id="anything-llm-chat"
>
{isChatOpen && (
<ChatWindow
closeChat={() => toggleOpenChat(false)}
settings={embedSettings}
sessionId={sessionId}
/>
)}
</div>
</div>
{!isChatOpen && (
<div
id="anything-llm-embed-chat-button-container"
className={`allm-fixed allm-bottom-0 ${positionClasses[position]} allm-mb-4 allm-z-50`}
>
<OpenButton
settings={embedSettings}
isOpen={isChatOpen}
toggleOpen={() => toggleOpenChat(true)}
/>
</div>
)}
</>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -1,43 +0,0 @@
import useCopyText from "@/hooks/useCopyText";
import { Check, ClipboardText } from "@phosphor-icons/react";
import { memo } from "react";
import { Tooltip } from "react-tooltip";
const Actions = ({ message }) => {
return (
<div className="allm-flex allm-justify-start allm-items-center allm-gap-x-4">
<CopyMessage message={message} />
{/* Other actions to go here later. */}
</div>
);
};
function CopyMessage({ message }) {
const { copied, copyText } = useCopyText();
return (
<>
<div className="allm-mt-3 allm-relative">
<button
data-tooltip-id="copy-assistant-text"
data-tooltip-content="Copy"
className="allm-border-none allm-text-zinc-300"
onClick={() => copyText(message)}
>
{copied ? (
<Check size={18} className="allm-mb-1" />
) : (
<ClipboardText size={18} className="allm-mb-1" />
)}
</button>
</div>
<Tooltip
id="copy-assistant-text"
place="bottom"
delayShow={300}
className="allm-tooltip !allm-text-xs"
/>
</>
);
}
export default memo(Actions);

View File

@ -1,97 +0,0 @@
import React, { memo, forwardRef } from "react";
import { Warning } from "@phosphor-icons/react";
import renderMarkdown from "@/utils/chat/markdown";
import { embedderSettings } from "@/main";
import { v4 } from "uuid";
import createDOMPurify from "dompurify";
import AnythingLLMIcon from "@/assets/anything-llm-icon.svg";
import { formatDate } from "@/utils/date";
const DOMPurify = createDOMPurify(window);
const HistoricalMessage = forwardRef(
(
{ uuid = v4(), message, role, sources = [], error = false, sentAt },
ref
) => {
const textSize = !!embedderSettings.settings.textSize
? `allm-text-[${embedderSettings.settings.textSize}px]`
: "allm-text-sm";
if (error) console.error(`ANYTHING_LLM_CHAT_WIDGET_ERROR: ${error}`);
return (
<div className="py-[5px]">
{role === "assistant" && (
<div
className={`allm-text-[10px] allm-text-gray-400 allm-ml-[54px] allm-mr-6 allm-mb-2 allm-text-left allm-font-sans`}
>
{embedderSettings.settings.assistantName ||
"Anything LLM Chat Assistant"}
</div>
)}
<div
key={uuid}
ref={ref}
className={`allm-flex allm-items-start allm-w-full allm-h-fit ${
role === "user" ? "allm-justify-end" : "allm-justify-start"
}`}
>
{role === "assistant" && (
<img
src={embedderSettings.settings.assistantIcon || AnythingLLMIcon}
alt="Anything LLM Icon"
className="allm-w-9 allm-h-9 allm-flex-shrink-0 allm-ml-2 allm-mt-2"
id="anything-llm-icon"
/>
)}
<div
style={{
wordBreak: "break-word",
backgroundColor:
role === "user"
? embedderSettings.USER_STYLES.msgBg
: embedderSettings.ASSISTANT_STYLES.msgBg,
}}
className={`allm-py-[11px] allm-px-4 allm-flex allm-flex-col allm-font-sans ${
error
? "allm-bg-red-200 allm-rounded-lg allm-mr-[37px] allm-ml-[9px]"
: role === "user"
? `${embedderSettings.USER_STYLES.base} allm-anything-llm-user-message`
: `${embedderSettings.ASSISTANT_STYLES.base} allm-anything-llm-assistant-message`
} allm-shadow-[0_4px_14px_rgba(0,0,0,0.25)]`}
>
<div className="allm-flex">
{error ? (
<div className="allm-p-2 allm-rounded-lg allm-bg-red-50 allm-text-red-500">
<span className={`allm-inline-block `}>
<Warning className="allm-h-4 allm-w-4 allm-mb-1 allm-inline-block" />{" "}
Could not respond to message.
</span>
<p className="allm-text-xs allm-font-mono allm-mt-2 allm-border-l-2 allm-border-red-500 allm-pl-2 allm-bg-red-300 allm-p-2 allm-rounded-sm">
Server error
</p>
</div>
) : (
<span
className={`allm-whitespace-pre-line allm-flex allm-flex-col allm-gap-y-1 ${textSize} allm-leading-[20px]`}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(renderMarkdown(message)),
}}
/>
)}
</div>
</div>
</div>
{sentAt && (
<div
className={`allm-font-sans allm-text-[10px] allm-text-gray-400 allm-ml-[54px] allm-mr-6 allm-mt-2 ${role === "user" ? "allm-text-right" : "allm-text-left"}`}
>
{formatDate(sentAt)}
</div>
)}
</div>
);
}
);
export default memo(HistoricalMessage);

View File

@ -1,111 +0,0 @@
import { forwardRef, memo } from "react";
import { Warning } from "@phosphor-icons/react";
import renderMarkdown from "@/utils/chat/markdown";
import { embedderSettings } from "@/main";
import AnythingLLMIcon from "@/assets/anything-llm-icon.svg";
import { formatDate } from "@/utils/date";
const PromptReply = forwardRef(
({ uuid, reply, pending, error, sources = [] }, ref) => {
if (!reply && sources.length === 0 && !pending && !error) return null;
if (error) console.error(`ANYTHING_LLM_CHAT_WIDGET_ERROR: ${error}`);
if (pending) {
return (
<div
className={`allm-flex allm-items-start allm-w-full allm-h-fit allm-justify-start`}
>
<img
src={embedderSettings.settings.assistantIcon || AnythingLLMIcon}
alt="Anything LLM Icon"
className="allm-w-9 allm-h-9 allm-flex-shrink-0 allm-ml-2"
/>
<div
style={{
wordBreak: "break-word",
backgroundColor: embedderSettings.ASSISTANT_STYLES.msgBg,
}}
className={`allm-py-[11px] allm-px-4 allm-flex allm-flex-col ${embedderSettings.ASSISTANT_STYLES.base} allm-shadow-[0_4px_14px_rgba(0,0,0,0.25)]`}
>
<div className="allm-flex allm-gap-x-5">
<div className="allm-mx-4 allm-my-1 allm-dot-falling"></div>
</div>
</div>
</div>
);
}
if (error) {
return (
<div
className={`allm-flex allm-items-end allm-w-full allm-h-fit allm-justify-start`}
>
<img
src={embedderSettings.settings.assistantIcon || AnythingLLMIcon}
alt="Anything LLM Icon"
className="allm-w-9 allm-h-9 allm-flex-shrink-0 allm-ml-2"
/>
<div
style={{ wordBreak: "break-word" }}
className={`allm-py-[11px] allm-px-4 allm-rounded-lg allm-flex allm-flex-col allm-bg-red-200 allm-shadow-[0_4px_14px_rgba(0,0,0,0.25)] allm-mr-[37px] allm-ml-[9px]`}
>
<div className="allm-flex allm-gap-x-5">
<span
className={`allm-inline-block allm-p-2 allm-rounded-lg allm-bg-red-50 allm-text-red-500`}
>
<Warning className="allm-h-4 allm-w-4 allm-mb-1 allm-inline-block" />{" "}
Could not respond to message.
<span className="allm-text-xs">Server error</span>
</span>
</div>
</div>
</div>
);
}
return (
<div className="allm-py-[5px]">
<div
className={`allm-text-[10px] allm-text-gray-400 allm-ml-[54px] allm-mr-6 allm-mb-2 allm-text-left allm-font-sans`}
>
{embedderSettings.settings.assistantName ||
"Anything LLM Chat Assistant"}
</div>
<div
key={uuid}
ref={ref}
className={`allm-flex allm-items-start allm-w-full allm-h-fit allm-justify-start`}
>
<img
src={embedderSettings.settings.assistantIcon || AnythingLLMIcon}
alt="Anything LLM Icon"
className="allm-w-9 allm-h-9 allm-flex-shrink-0 allm-ml-2"
/>
<div
style={{
wordBreak: "break-word",
backgroundColor: embedderSettings.ASSISTANT_STYLES.msgBg,
}}
className={`allm-py-[11px] allm-px-4 allm-flex allm-flex-col ${
error ? "allm-bg-red-200" : embedderSettings.ASSISTANT_STYLES.base
} allm-shadow-[0_4px_14px_rgba(0,0,0,0.25)]`}
>
<div className="allm-flex allm-gap-x-5">
<span
className={`allm-font-sans allm-reply allm-whitespace-pre-line allm-font-normal allm-text-sm allm-md:text-sm allm-flex allm-flex-col allm-gap-y-1`}
dangerouslySetInnerHTML={{ __html: renderMarkdown(reply) }}
/>
</div>
</div>
</div>
<div
className={`allm-text-[10px] allm-text-gray-400 allm-ml-[54px] allm-mr-6 allm-mt-2 allm-text-left allm-font-sans`}
>
{formatDate(Date.now() / 1000)}
</div>
</div>
);
}
);
export default memo(PromptReply);

View File

@ -1,163 +0,0 @@
import HistoricalMessage from "./HistoricalMessage";
import PromptReply from "./PromptReply";
import { useEffect, useRef, useState } from "react";
import { ArrowDown, CircleNotch } from "@phosphor-icons/react";
import { embedderSettings } from "@/main";
import debounce from "lodash.debounce";
import { SEND_TEXT_EVENT } from "..";
export default function ChatHistory({ settings = {}, history = [] }) {
const replyRef = useRef(null);
const [isAtBottom, setIsAtBottom] = useState(true);
const chatHistoryRef = useRef(null);
useEffect(() => {
scrollToBottom();
}, [history]);
const handleScroll = () => {
if (!chatHistoryRef.current) return;
const diff =
chatHistoryRef.current.scrollHeight -
chatHistoryRef.current.scrollTop -
chatHistoryRef.current.clientHeight;
// Fuzzy margin for what qualifies as "bottom". Stronger than straight comparison since that may change over time.
const isBottom = diff <= 40;
setIsAtBottom(isBottom);
};
const debouncedScroll = debounce(handleScroll, 100);
useEffect(() => {
function watchScrollEvent() {
if (!chatHistoryRef.current) return null;
const chatHistoryElement = chatHistoryRef.current;
if (!chatHistoryElement) return null;
chatHistoryElement.addEventListener("scroll", debouncedScroll);
}
watchScrollEvent();
}, []);
const scrollToBottom = () => {
if (chatHistoryRef.current) {
chatHistoryRef.current.scrollTo({
top: chatHistoryRef.current.scrollHeight,
behavior: "smooth",
});
}
};
if (history.length === 0) {
return (
<div className="allm-pb-[100px] allm-pt-[5px] allm-rounded-lg allm-px-2 allm-h-full allm-mt-2 allm-gap-y-2 allm-overflow-y-scroll allm-flex allm-flex-col allm-justify-start allm-no-scroll">
<div className="allm-flex allm-h-full allm-flex-col allm-items-center allm-justify-center">
<p className="allm-text-slate-400 allm-text-sm allm-font-sans allm-py-4 allm-text-center">
{settings?.greeting ?? "Send a chat to get started."}
</p>
<SuggestedMessages settings={settings} />
</div>
</div>
);
}
return (
<div
className="allm-pb-[30px] allm-pt-[5px] allm-rounded-lg allm-px-2 allm-h-full allm-gap-y-2 allm-overflow-y-scroll allm-flex allm-flex-col allm-justify-start allm-no-scroll allm-md:max-h-[500px]"
id="chat-history"
ref={chatHistoryRef}
>
{history.map((props, index) => {
const isLastMessage = index === history.length - 1;
const isLastBotReply =
index === history.length - 1 && props.role === "assistant";
if (isLastBotReply && props.animate) {
return (
<PromptReply
key={props.uuid}
ref={isLastMessage ? replyRef : null}
uuid={props.uuid}
reply={props.content}
pending={props.pending}
sources={props.sources}
error={props.error}
closed={props.closed}
/>
);
}
return (
<HistoricalMessage
key={index}
ref={isLastMessage ? replyRef : null}
message={props.content}
sentAt={props.sentAt || Date.now() / 1000}
role={props.role}
sources={props.sources}
chatId={props.chatId}
feedbackScore={props.feedbackScore}
error={props.error}
/>
);
})}
{!isAtBottom && (
<div className="allm-fixed allm-bottom-[10rem] allm-right-[50px] allm-z-50 allm-cursor-pointer allm-animate-pulse">
<div className="allm-flex allm-flex-col allm-items-center">
<div className="allm-p-1 allm-rounded-full allm-border allm-border-white/10 allm-bg-black/20 hover:allm-bg-black/50">
<ArrowDown
weight="bold"
className="allm-text-white/50 allm-w-5 allm-h-5"
onClick={scrollToBottom}
id="scroll-to-bottom-button"
aria-label="Scroll to bottom"
/>
</div>
</div>
</div>
)}
</div>
);
}
export function ChatHistoryLoading() {
return (
<div className="allm-h-full allm-w-full allm-relative">
<div className="allm-h-full allm-max-h-[82vh] allm-pb-[100px] allm-pt-[5px] allm-bg-gray-100 allm-rounded-lg allm-px-2 allm-h-full allm-mt-2 allm-gap-y-2 allm-overflow-y-scroll allm-flex allm-flex-col allm-justify-start allm-no-scroll">
<div className="allm-flex allm-h-full allm-flex-col allm-items-center allm-justify-center">
<CircleNotch
size={14}
className="allm-text-slate-400 allm-animate-spin"
/>
</div>
</div>
</div>
);
}
function SuggestedMessages({ settings }) {
if (!settings?.defaultMessages?.length) return null;
return (
<div className="allm-flex allm-flex-col allm-gap-y-2 allm-w-[75%]">
{settings.defaultMessages.map((content, i) => (
<button
key={i}
style={{
opacity: 0,
wordBreak: "break-word",
backgroundColor: embedderSettings.USER_STYLES.msgBg,
fontSize: settings.textSize,
}}
type="button"
onClick={() => {
window.dispatchEvent(
new CustomEvent(SEND_TEXT_EVENT, { detail: { command: content } })
);
}}
className={`msg-suggestion allm-border-none hover:allm-shadow-[0_4px_14px_rgba(0,0,0,0.5)] allm-cursor-pointer allm-px-2 allm-py-2 allm-rounded-lg allm-text-white allm-w-full allm-shadow-[0_4px_14px_rgba(0,0,0,0.25)]`}
>
{content}
</button>
))}
</div>
);
}

View File

@ -1,102 +0,0 @@
import { CircleNotch, PaperPlaneRight } from "@phosphor-icons/react";
import React, { useState, useRef, useEffect } from "react";
export default function PromptInput({
message,
submit,
onChange,
inputDisabled,
buttonDisabled,
}) {
const formRef = useRef(null);
const textareaRef = useRef(null);
const [_, setFocused] = useState(false);
useEffect(() => {
if (!inputDisabled && textareaRef.current) {
textareaRef.current.focus();
}
resetTextAreaHeight();
}, [inputDisabled]);
const handleSubmit = (e) => {
setFocused(false);
submit(e);
};
const resetTextAreaHeight = () => {
if (textareaRef.current) {
textareaRef.current.style.height = "auto";
}
};
const captureEnter = (event) => {
if (event.keyCode == 13) {
if (!event.shiftKey) {
submit(event);
}
}
};
const adjustTextArea = (event) => {
const element = event.target;
element.style.height = "auto";
element.style.height =
event.target.value.length !== 0 ? element.scrollHeight + "px" : "auto";
};
return (
<div className="allm-w-full allm-sticky allm-bottom-0 allm-z-10 allm-flex allm-justify-center allm-items-center allm-bg-white">
<form
onSubmit={handleSubmit}
className="allm-flex allm-flex-col allm-gap-y-1 allm-rounded-t-lg allm-w-full allm-items-center allm-justify-center"
>
<div className="allm-flex allm-items-center allm-w-full">
<div className="allm-bg-white allm-flex allm-flex-col allm-px-4 allm-overflow-hidden allm-w-full">
<div
style={{ border: "1.5px solid #22262833" }}
className="allm-flex allm-items-center allm-w-full allm-rounded-2xl"
>
<textarea
ref={textareaRef}
onKeyUp={adjustTextArea}
onKeyDown={captureEnter}
onChange={onChange}
required={true}
disabled={inputDisabled}
onFocus={() => setFocused(true)}
onBlur={(e) => {
setFocused(false);
adjustTextArea(e);
}}
value={message}
className="allm-font-sans allm-border-none allm-cursor-text allm-max-h-[100px] allm-text-[14px] allm-mx-2 allm-py-2 allm-w-full allm-text-black allm-bg-transparent placeholder:allm-text-slate-800/60 allm-resize-none active:allm-outline-none focus:allm-outline-none allm-flex-grow"
placeholder={"Send a message"}
id="message-input"
/>
<button
ref={formRef}
type="submit"
disabled={buttonDisabled}
className="allm-bg-transparent allm-border-none allm-inline-flex allm-justify-center allm-rounded-2xl allm-cursor-pointer allm-text-black group"
id="send-message-button"
aria-label="Send message"
>
{buttonDisabled ? (
<CircleNotch className="allm-w-4 allm-h-4 allm-animate-spin" />
) : (
<PaperPlaneRight
size={24}
className="allm-my-3 allm-text-[#22262899]/60 group-hover:allm-text-[#22262899]/90"
weight="fill"
/>
)}
<span className="allm-sr-only">Send message</span>
</button>
</div>
</div>
</div>
</form>
</div>
);
}

View File

@ -1,145 +0,0 @@
import React, { useState, useEffect } from "react";
import ChatHistory from "./ChatHistory";
import PromptInput from "./PromptInput";
import handleChat from "@/utils/chat";
import ChatService from "@/models/chatService";
export const SEND_TEXT_EVENT = "anythingllm-embed-send-prompt";
export default function ChatContainer({
sessionId,
settings,
knownHistory = [],
}) {
const [message, setMessage] = useState("");
const [loadingResponse, setLoadingResponse] = useState(false);
const [chatHistory, setChatHistory] = useState(knownHistory);
// Resync history if the ref to known history changes
// eg: cleared.
useEffect(() => {
if (knownHistory.length !== chatHistory.length)
setChatHistory([...knownHistory]);
}, [knownHistory]);
const handleMessageChange = (event) => {
setMessage(event.target.value);
};
const handleSubmit = async (event) => {
event.preventDefault();
if (!message || message === "") return false;
const prevChatHistory = [
...chatHistory,
{ content: message, role: "user" },
{
content: "",
role: "assistant",
pending: true,
userMessage: message,
animate: true,
},
];
setChatHistory(prevChatHistory);
setMessage("");
setLoadingResponse(true);
};
const sendCommand = (command, history = [], attachments = []) => {
if (!command || command === "") return false;
let prevChatHistory;
if (history.length > 0) {
// use pre-determined history chain.
prevChatHistory = [
...history,
{
content: "",
role: "assistant",
pending: true,
userMessage: command,
attachments,
animate: true,
},
];
} else {
prevChatHistory = [
...chatHistory,
{
content: command,
role: "user",
attachments,
},
{
content: "",
role: "assistant",
pending: true,
userMessage: command,
animate: true,
},
];
}
setChatHistory(prevChatHistory);
setLoadingResponse(true);
};
useEffect(() => {
async function fetchReply() {
const promptMessage =
chatHistory.length > 0 ? chatHistory[chatHistory.length - 1] : null;
const remHistory = chatHistory.length > 0 ? chatHistory.slice(0, -1) : [];
var _chatHistory = [...remHistory];
if (!promptMessage || !promptMessage?.userMessage) {
setLoadingResponse(false);
return false;
}
await ChatService.streamChat(
sessionId,
settings,
promptMessage.userMessage,
(chatResult) =>
handleChat(
chatResult,
setLoadingResponse,
setChatHistory,
remHistory,
_chatHistory
)
);
return;
}
loadingResponse === true && fetchReply();
}, [loadingResponse, chatHistory]);
const handleAutofillEvent = (event) => {
if (!event.detail.command) return;
sendCommand(event.detail.command, [], []);
};
useEffect(() => {
window.addEventListener(SEND_TEXT_EVENT, handleAutofillEvent);
return () => {
window.removeEventListener(SEND_TEXT_EVENT, handleAutofillEvent);
};
}, []);
return (
<div className="allm-h-full allm-w-full allm-flex allm-flex-col">
<div className="allm-flex-grow allm-overflow-y-auto">
<ChatHistory settings={settings} history={chatHistory} />
</div>
<PromptInput
message={message}
submit={handleSubmit}
onChange={handleMessageChange}
inputDisabled={loadingResponse}
buttonDisabled={loadingResponse}
/>
</div>
);
}

View File

@ -1,155 +0,0 @@
import AnythingLLMIcon from "@/assets/anything-llm-icon.svg";
import ChatService from "@/models/chatService";
import {
ArrowCounterClockwise,
Check,
Copy,
DotsThreeOutlineVertical,
Envelope,
X,
} from "@phosphor-icons/react";
import { useEffect, useRef, useState } from "react";
export default function ChatWindowHeader({
sessionId,
settings = {},
iconUrl = null,
closeChat,
setChatHistory,
}) {
const [showingOptions, setShowOptions] = useState(false);
const menuRef = useRef();
const buttonRef = useRef();
const handleChatReset = async () => {
await ChatService.resetEmbedChatSession(settings, sessionId);
setChatHistory([]);
setShowOptions(false);
};
useEffect(() => {
function handleClickOutside(event) {
if (
menuRef.current &&
!menuRef.current.contains(event.target) &&
buttonRef.current &&
!buttonRef.current.contains(event.target)
) {
setShowOptions(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [menuRef]);
return (
<div
style={{ borderBottom: "1px solid #E9E9E9" }}
className="allm-flex allm-items-center allm-relative allm-rounded-t-2xl"
id="anything-llm-header"
>
<div className="allm-flex allm-justify-center allm-items-center allm-w-full allm-h-[76px]">
<img
style={{ maxWidth: 48, maxHeight: 48 }}
src={iconUrl ?? AnythingLLMIcon}
alt={iconUrl ? "Brand" : "AnythingLLM Logo"}
/>
</div>
<div className="allm-absolute allm-right-0 allm-flex allm-gap-x-1 allm-items-center allm-px-[22px]">
{settings.loaded && (
<button
ref={buttonRef}
type="button"
onClick={() => setShowOptions(!showingOptions)}
className="allm-bg-transparent hover:allm-cursor-pointer allm-border-none hover:allm-bg-gray-100 allm-rounded-sm allm-text-slate-800/60"
aria-label="Options"
>
<DotsThreeOutlineVertical size={20} weight="fill" />
</button>
)}
<button
type="button"
onClick={closeChat}
className="allm-bg-transparent hover:allm-cursor-pointer allm-border-none hover:allm-bg-gray-100 allm-rounded-sm allm-text-slate-800/60"
aria-label="Close"
>
<X size={20} weight="bold" />
</button>
</div>
<OptionsMenu
settings={settings}
showing={showingOptions}
resetChat={handleChatReset}
sessionId={sessionId}
menuRef={menuRef}
/>
</div>
);
}
function OptionsMenu({ settings, showing, resetChat, sessionId, menuRef }) {
if (!showing) return null;
return (
<div
ref={menuRef}
className="allm-bg-white allm-absolute allm-z-10 allm-flex allm-flex-col allm-gap-y-1 allm-rounded-xl allm-shadow-lg allm-top-[64px] allm-right-[46px]"
>
<button
onClick={resetChat}
className="hover:allm-cursor-pointer allm-bg-white allm-gap-x-[12px] hover:allm-bg-gray-100 allm-rounded-lg allm-border-none allm-flex allm-items-center allm-text-base allm-text-[#7A7D7E] allm-font-bold allm-px-4"
>
<ArrowCounterClockwise size={24} />
<p className="allm-text-[14px]">Reset Chat</p>
</button>
<ContactSupport email={settings.supportEmail} />
<SessionID sessionId={sessionId} />
</div>
);
}
function SessionID({ sessionId }) {
if (!sessionId) return null;
const [sessionIdCopied, setSessionIdCopied] = useState(false);
const copySessionId = () => {
navigator.clipboard.writeText(sessionId);
setSessionIdCopied(true);
setTimeout(() => setSessionIdCopied(false), 1000);
};
if (sessionIdCopied) {
return (
<div className="hover:allm-cursor-pointer allm-bg-white allm-gap-x-[12px] hover:allm-bg-gray-100 allm-rounded-lg allm-border-none allm-flex allm-items-center allm-text-base allm-text-[#7A7D7E] allm-font-bold allm-px-4">
<Check size={24} />
<p className="allm-text-[14px] allm-font-sans">Copied!</p>
</div>
);
}
return (
<button
onClick={copySessionId}
className="hover:allm-cursor-pointer allm-bg-white allm-gap-x-[12px] hover:allm-bg-gray-100 allm-rounded-lg allm-border-none allm-flex allm-items-center allm-text-base allm-text-[#7A7D7E] allm-font-bold allm-px-4"
>
<Copy size={24} />
<p className="allm-text-[14px]">Session ID</p>
</button>
);
}
function ContactSupport({ email = null }) {
if (!email) return null;
const subject = `Inquiry from ${window.location.origin}`;
return (
<a
href={`mailto:${email}?Subject=${encodeURIComponent(subject)}`}
className="allm-no-underline hover:allm-underline hover:allm-cursor-pointer allm-bg-white allm-gap-x-[12px] hover:allm-bg-gray-100 allm-rounded-lg allm-border-none allm-flex allm-items-center allm-text-base allm-text-[#7A7D7E] allm-font-bold allm-px-4"
>
<Envelope size={24} />
<p className="allm-text-[14px] allm-font-sans">Email Support</p>
</a>
);
}

View File

@ -1,99 +0,0 @@
import ChatWindowHeader from "./Header";
import SessionId from "../SessionId";
import useChatHistory from "@/hooks/chat/useChatHistory";
import ChatContainer from "./ChatContainer";
import Sponsor from "../Sponsor";
import { ChatHistoryLoading } from "./ChatContainer/ChatHistory";
import ResetChat from "../ResetChat";
export default function ChatWindow({ closeChat, settings, sessionId }) {
const { chatHistory, setChatHistory, loading } = useChatHistory(
settings,
sessionId
);
if (loading) {
return (
<div className="allm-flex allm-flex-col allm-h-full">
<ChatWindowHeader
sessionId={sessionId}
settings={settings}
iconUrl={settings.brandImageUrl}
closeChat={closeChat}
setChatHistory={setChatHistory}
/>
<ChatHistoryLoading />
<div className="allm-pt-4 allm-pb-2 allm-h-fit allm-gap-y-1">
<SessionId />
<Sponsor settings={settings} />
</div>
</div>
);
}
setEventDelegatorForCodeSnippets();
return (
<div className="allm-flex allm-flex-col allm-h-full">
<ChatWindowHeader
sessionId={sessionId}
settings={settings}
iconUrl={settings.brandImageUrl}
closeChat={closeChat}
setChatHistory={setChatHistory}
/>
<div className="allm-flex-grow allm-overflow-y-auto">
<ChatContainer
sessionId={sessionId}
settings={settings}
knownHistory={chatHistory}
/>
</div>
<div className="allm-mt-4 allm-pb-4 allm-h-fit allm-gap-y-2 allm-z-10">
<Sponsor settings={settings} />
<ResetChat
setChatHistory={setChatHistory}
settings={settings}
sessionId={sessionId}
/>
</div>
</div>
);
}
// Enables us to safely markdown and sanitize all responses without risk of injection
// but still be able to attach a handler to copy code snippets on all elements
// that are code snippets.
function copyCodeSnippet(uuid) {
const target = document.querySelector(`[data-code="${uuid}"]`);
if (!target) return false;
const markdown =
target.parentElement?.parentElement?.querySelector(
"pre:first-of-type"
)?.innerText;
if (!markdown) return false;
window.navigator.clipboard.writeText(markdown);
target.classList.add("allm-text-green-500");
const originalText = target.innerHTML;
target.innerText = "Copied!";
target.setAttribute("disabled", true);
setTimeout(() => {
target.classList.remove("allm-text-green-500");
target.innerHTML = originalText;
target.removeAttribute("disabled");
}, 2500);
}
// Listens and hunts for all data-code-snippet clicks.
function setEventDelegatorForCodeSnippets() {
document?.addEventListener("click", function (e) {
const target = e.target.closest("[data-code-snippet]");
const uuidCode = target?.dataset?.code;
if (!uuidCode) return false;
copyCodeSnippet(uuidCode);
});
}

View File

@ -1,131 +0,0 @@
import { embedderSettings } from "@/main";
const hljsCss = `
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: GitHub Dark Dimmed
Description: Dark dimmed theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Colors taken from GitHub's CSS
*/.hljs{color:#adbac7;background:#22272e}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#f47067}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#dcbdfb}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#6cb6ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#96d0ff}.hljs-built_in,.hljs-symbol{color:#f69d50}.hljs-code,.hljs-comment,.hljs-formula{color:#768390}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#8ddb8c}.hljs-subst{color:#adbac7}.hljs-section{color:#316dca;font-weight:700}.hljs-bullet{color:#eac55f}.hljs-emphasis{color:#adbac7;font-style:italic}.hljs-strong{color:#adbac7;font-weight:700}.hljs-addition{color:#b4f1b4;background-color:#1b4721}.hljs-deletion{color:#ffd8d3;background-color:#78191b}
`;
const customCss = `
/**
* ==============================================
* Dot Falling
* ==============================================
*/
.allm-dot-falling {
position: relative;
left: -9999px;
width: 10px;
height: 10px;
border-radius: 5px;
background-color: #000000;
color: #5fa4fa;
box-shadow: 9999px 0 0 0 #000000;
animation: dot-falling 1.5s infinite linear;
animation-delay: 0.1s;
}
.allm-dot-falling::before,
.allm-dot-falling::after {
content: "";
display: inline-block;
position: absolute;
top: 0;
}
.allm-dot-falling::before {
width: 10px;
height: 10px;
border-radius: 5px;
background-color: #000000;
color: #000000;
animation: dot-falling-before 1.5s infinite linear;
animation-delay: 0s;
}
.allm-dot-falling::after {
width: 10px;
height: 10px;
border-radius: 5px;
background-color: #000000;
color: #000000;
animation: dot-falling-after 1.5s infinite linear;
animation-delay: 0.2s;
}
@keyframes dot-falling {
0% {
box-shadow: 9999px -15px 0 0 rgba(152, 128, 255, 0);
}
25%,
50%,
75% {
box-shadow: 9999px 0 0 0 #000000;
}
100% {
box-shadow: 9999px 15px 0 0 rgba(152, 128, 255, 0);
}
}
@keyframes dot-falling-before {
0% {
box-shadow: 9984px -15px 0 0 rgba(152, 128, 255, 0);
}
25%,
50%,
75% {
box-shadow: 9984px 0 0 0 #000000;
}
100% {
box-shadow: 9984px 15px 0 0 rgba(152, 128, 255, 0);
}
}
@keyframes dot-falling-after {
0% {
box-shadow: 10014px -15px 0 0 rgba(152, 128, 255, 0);
}
25%,
50%,
75% {
box-shadow: 10014px 0 0 0 #000000;
}
100% {
box-shadow: 10014px 15px 0 0 rgba(152, 128, 255, 0);
}
}
#chat-history::-webkit-scrollbar,
#chat-container::-webkit-scrollbar,
.allm-no-scroll::-webkit-scrollbar {
display: none !important;
}
/* Hide scrollbar for IE, Edge and Firefox */
#chat-history,
#chat-container,
.allm-no-scroll {
-ms-overflow-style: none !important; /* IE and Edge */
scrollbar-width: none !important; /* Firefox */
}
span.allm-whitespace-pre-line>p {
margin: 0px;
}
`;
export default function Head() {
return (
<head>
<style>{hljsCss}</style>
<style>{customCss}</style>
<link rel="stylesheet" href={embedderSettings.stylesSrc} />
</head>
);
}

View File

@ -1,35 +0,0 @@
import {
Plus,
ChatCircleDots,
Headset,
Binoculars,
MagnifyingGlass,
MagicWand,
} from "@phosphor-icons/react";
const CHAT_ICONS = {
plus: Plus,
chatBubble: ChatCircleDots,
support: Headset,
search2: Binoculars,
search: MagnifyingGlass,
magic: MagicWand,
};
export default function OpenButton({ settings, isOpen, toggleOpen }) {
if (isOpen) return null;
const ChatIcon = CHAT_ICONS.hasOwnProperty(settings?.chatIcon)
? CHAT_ICONS[settings.chatIcon]
: CHAT_ICONS.plus;
return (
<button
style={{ backgroundColor: settings.buttonColor }}
id="anything-llm-embed-chat-button"
onClick={toggleOpen}
className={`hover:allm-cursor-pointer allm-border-none allm-flex allm-items-center allm-justify-center allm-p-4 allm-rounded-full allm-text-white allm-text-2xl hover:allm-opacity-95`}
aria-label="Toggle Menu"
>
<ChatIcon className="text-white" />
</button>
);
}

View File

@ -1,20 +0,0 @@
import ChatService from "@/models/chatService";
export default function ResetChat({ setChatHistory, settings, sessionId }) {
const handleChatReset = async () => {
await ChatService.resetEmbedChatSession(settings, sessionId);
setChatHistory([]);
};
return (
<div className="allm-w-full allm-flex allm-justify-center">
<button
style={{ color: "#7A7D7E" }}
className="hover:allm-cursor-pointer allm-border-none allm-text-sm allm-bg-transparent hover:allm-opacity-80 hover:allm-underline"
onClick={() => handleChatReset()}
>
Reset Chat
</button>
</div>
);
}

View File

@ -1,12 +0,0 @@
import useSessionId from "@/hooks/useSessionId";
export default function SessionId() {
const sessionId = useSessionId();
if (!sessionId) return null;
return (
<div className="allm-text-xs allm-text-gray-300 allm-w-full allm-text-center">
{sessionId}
</div>
);
}

View File

@ -1,17 +0,0 @@
export default function Sponsor({ settings }) {
if (!!settings.noSponsor) return null;
return (
<div className="allm-flex allm-w-full allm-items-center allm-justify-center">
<a
style={{ color: "#0119D9" }}
href={settings.sponsorLink ?? "#"}
target="_blank"
rel="noreferrer"
className="allm-text-xs allm-font-sans hover:allm-opacity-80 hover:allm-underline"
>
{settings.sponsorText}
</a>
</div>
);
}

View File

@ -1,27 +0,0 @@
import ChatService from "@/models/chatService";
import { useEffect, useState } from "react";
export default function useChatHistory(settings = null, sessionId = null) {
const [loading, setLoading] = useState(true);
const [messages, setMessages] = useState([]);
useEffect(() => {
async function fetchChatHistory() {
if (!sessionId || !settings) return;
try {
const formattedMessages = await ChatService.embedSessionHistory(
settings,
sessionId
);
setMessages(formattedMessages);
setLoading(false);
} catch (error) {
console.error("Error fetching historical chats:", error);
setLoading(false);
}
}
fetchChatHistory();
}, [sessionId, settings]);
return { chatHistory: messages, setChatHistory: setMessages, loading };
}

View File

@ -1,16 +0,0 @@
import { CHAT_UI_REOPEN } from "@/utils/constants";
import { useState } from "react";
export default function useOpenChat() {
const [isOpen, setOpen] = useState(
!!window?.localStorage?.getItem(CHAT_UI_REOPEN) || false
);
function toggleOpenChat(newValue) {
if (newValue === true) window.localStorage.setItem(CHAT_UI_REOPEN, "1");
if (newValue === false) window.localStorage.removeItem(CHAT_UI_REOPEN);
setOpen(newValue);
}
return { isChatOpen: isOpen, toggleOpenChat };
}

View File

@ -1,104 +0,0 @@
import { useEffect, useState } from "react";
import { embedderSettings } from "../main";
const DEFAULT_SETTINGS = {
embedId: null, //required
baseApiUrl: null, // required
// Override properties that can be defined.
prompt: null, // override
model: null, // override
temperature: null, //override
// style parameters
chatIcon: "plus",
brandImageUrl: null, // will be forced into 100x50px container
greeting: null, // empty chat window greeting.
buttonColor: "#262626", // must be hex color code
userBgColor: "#2C2F35", // user text bubble color
assistantBgColor: "#2563eb", // assistant text bubble color
noSponsor: null, // Shows sponsor in footer of chat
sponsorText: "Powered by AnythingLLM", // default sponsor text
sponsorLink: "https://anythingllm.com", // default sponsor link
position: "bottom-right", // position of chat button/window
assistantName: "AnythingLLM Chat Assistant", // default assistant name
assistantIcon: null, // default assistant icon
windowHeight: null, // height of chat window in number:css-prefix
windowWidth: null, // width of chat window in number:css-prefix
textSize: null, // text size in px (number only)
// behaviors
openOnLoad: "off", // or "on"
supportEmail: null, // string of email for contact
username: null, // The display or readable name set on a script
defaultMessages: [], // list of strings for default messages.
};
export default function useGetScriptAttributes() {
const [settings, setSettings] = useState({
loaded: false,
...DEFAULT_SETTINGS,
});
useEffect(() => {
function fetchAttribs() {
if (!document) return false;
if (
!embedderSettings.settings.baseApiUrl ||
!embedderSettings.settings.embedId
)
throw new Error(
"[AnythingLLM Embed Module::Abort] - Invalid script tag setup detected. Missing required parameters for boot!"
);
setSettings({
...DEFAULT_SETTINGS,
...parseAndValidateEmbedSettings(embedderSettings.settings),
loaded: true,
});
}
fetchAttribs();
}, [document]);
return settings;
}
const validations = {
_fallbacks: {
defaultMessages: [],
},
defaultMessages: function (value = null) {
if (typeof value !== "string") return this._fallbacks.defaultMessages;
try {
const list = value.split(",");
if (
!Array.isArray(list) ||
list.length === 0 ||
!list.every((v) => typeof v === "string" && v.length > 0)
)
throw new Error(
"Invalid default-messages attribute value. Must be array of strings"
);
return list.map((v) => v.trim());
} catch (e) {
console.error("AnythingLLMEmbed", e);
return this._fallbacks.defaultMessages;
}
},
};
function parseAndValidateEmbedSettings(settings = {}) {
const validated = {};
for (let [key, value] of Object.entries(settings)) {
if (!validations.hasOwnProperty(key)) {
validated[key] = value;
continue;
}
const validatedValue = validations[key](value);
validated[key] = validatedValue;
}
return validated;
}

View File

@ -1,29 +0,0 @@
import { useEffect, useState } from "react";
import { embedderSettings } from "../main";
import { v4 } from "uuid";
export default function useSessionId() {
const [sessionId, setSessionId] = useState("");
useEffect(() => {
function getOrAssignSessionId() {
if (!window || !embedderSettings?.settings?.embedId) return;
const STORAGE_IDENTIFIER = `allm_${embedderSettings?.settings?.embedId}_session_id`;
const currentId = window.localStorage.getItem(STORAGE_IDENTIFIER);
if (!!currentId) {
console.log(`Resuming session id`, currentId);
setSessionId(currentId);
return;
}
const newId = v4();
console.log(`Registering new session id`, newId);
window.localStorage.setItem(STORAGE_IDENTIFIER, newId);
setSessionId(newId);
}
getOrAssignSessionId();
}, [window]);
return sessionId;
}

View File

@ -1,32 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.msg-suggestion {
animation-name: fadeIn;
animation-duration: 300ms;
animation-timing-function: linear;
animation-fill-mode: forwards;
}
@keyframes fadeIn {
0% {
opacity: 0%;
}
25% {
opacity: 25%;
}
50% {
opacity: 50%;
}
75% {
opacity: 75%;
}
100% {
opacity: 100%;
}
}

View File

@ -1,31 +0,0 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import { parseStylesSrc } from "./utils/constants.js";
const appElement = document.createElement("div");
document.body.appendChild(appElement);
const root = ReactDOM.createRoot(appElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
const scriptSettings = Object.assign(
{},
document?.currentScript?.dataset || {}
);
export const embedderSettings = {
settings: scriptSettings,
stylesSrc: parseStylesSrc(document?.currentScript?.src),
USER_STYLES: {
msgBg: scriptSettings?.userBgColor ?? "#3DBEF5",
base: `allm-text-white allm-rounded-t-[18px] allm-rounded-bl-[18px] allm-rounded-br-[4px] allm-mx-[20px]`,
},
ASSISTANT_STYLES: {
msgBg: scriptSettings?.assistantBgColor ?? "#FFFFFF",
base: `allm-text-[#222628] allm-rounded-t-[18px] allm-rounded-br-[18px] allm-rounded-bl-[4px] allm-mr-[37px] allm-ml-[9px]`,
},
};

View File

@ -1,109 +0,0 @@
import { fetchEventSource } from "@microsoft/fetch-event-source";
import { v4 } from "uuid";
const ChatService = {
embedSessionHistory: async function (embedSettings, sessionId) {
const { embedId, baseApiUrl } = embedSettings;
return await fetch(`${baseApiUrl}/${embedId}/${sessionId}`)
.then((res) => {
if (res.ok) return res.json();
throw new Error("Invalid response from server");
})
.then((res) => {
return res.history.map((msg) => ({
...msg,
id: v4(),
sender: msg.role === "user" ? "user" : "system",
textResponse: msg.content,
close: false,
}));
})
.catch((e) => {
console.error(e);
return [];
});
},
resetEmbedChatSession: async function (embedSettings, sessionId) {
const { baseApiUrl, embedId } = embedSettings;
return await fetch(`${baseApiUrl}/${embedId}/${sessionId}`, {
method: "DELETE",
})
.then((res) => res.ok)
.catch(() => false);
},
streamChat: async function (sessionId, embedSettings, message, handleChat) {
const { baseApiUrl, embedId, username } = embedSettings;
const overrides = {
prompt: embedSettings?.prompt ?? null,
model: embedSettings?.model ?? null,
temperature: embedSettings?.temperature ?? null,
};
const ctrl = new AbortController();
await fetchEventSource(`${baseApiUrl}/${embedId}/stream-chat`, {
method: "POST",
body: JSON.stringify({
message,
sessionId,
username,
...overrides,
}),
signal: ctrl.signal,
openWhenHidden: true,
async onopen(response) {
if (response.ok) {
return; // everything's good
} else if (response.status >= 400) {
await response
.json()
.then((serverResponse) => {
handleChat(serverResponse);
})
.catch(() => {
handleChat({
id: v4(),
type: "abort",
textResponse: null,
sources: [],
close: true,
error: `An error occurred while streaming response. Code ${response.status}`,
});
});
ctrl.abort();
throw new Error();
} else {
handleChat({
id: v4(),
type: "abort",
textResponse: null,
sources: [],
close: true,
error: `An error occurred while streaming response. Unknown Error.`,
});
ctrl.abort();
throw new Error("Unknown Error");
}
},
async onmessage(msg) {
try {
const chatResult = JSON.parse(msg.data);
handleChat(chatResult);
} catch {}
},
onerror(err) {
handleChat({
id: v4(),
type: "abort",
textResponse: null,
sources: [],
close: true,
error: `An error occurred while streaming response. ${err.message}`,
});
ctrl.abort();
throw new Error();
},
});
},
};
export default ChatService;

View File

@ -1,88 +0,0 @@
/*
This is a dynamically generated file to help de-bloat the app since this script is a static bundle.
You should not modify this file directly. You can regenerate it with "node scripts/updateHljs.mjd" from the embed folder.
Last generated Fri Feb 02 2024
----------------------
*/
import hljs from "highlight.js/lib/core";
import apacheHljsSupport from 'highlight.js/lib/languages/apache'
import bashHljsSupport from 'highlight.js/lib/languages/bash'
import cHljsSupport from 'highlight.js/lib/languages/c'
import cppHljsSupport from 'highlight.js/lib/languages/cpp'
import csharpHljsSupport from 'highlight.js/lib/languages/csharp'
import cssHljsSupport from 'highlight.js/lib/languages/css'
import diffHljsSupport from 'highlight.js/lib/languages/diff'
import goHljsSupport from 'highlight.js/lib/languages/go'
import graphqlHljsSupport from 'highlight.js/lib/languages/graphql'
import iniHljsSupport from 'highlight.js/lib/languages/ini'
import javaHljsSupport from 'highlight.js/lib/languages/java'
import javascriptHljsSupport from 'highlight.js/lib/languages/javascript'
import jsonHljsSupport from 'highlight.js/lib/languages/json'
import kotlinHljsSupport from 'highlight.js/lib/languages/kotlin'
import lessHljsSupport from 'highlight.js/lib/languages/less'
import luaHljsSupport from 'highlight.js/lib/languages/lua'
import makefileHljsSupport from 'highlight.js/lib/languages/makefile'
import markdownHljsSupport from 'highlight.js/lib/languages/markdown'
import nginxHljsSupport from 'highlight.js/lib/languages/nginx'
import objectivecHljsSupport from 'highlight.js/lib/languages/objectivec'
import perlHljsSupport from 'highlight.js/lib/languages/perl'
import pgsqlHljsSupport from 'highlight.js/lib/languages/pgsql'
import phpHljsSupport from 'highlight.js/lib/languages/php'
import phptemplateHljsSupport from 'highlight.js/lib/languages/php-template'
import plaintextHljsSupport from 'highlight.js/lib/languages/plaintext'
import pythonHljsSupport from 'highlight.js/lib/languages/python'
import pythonreplHljsSupport from 'highlight.js/lib/languages/python-repl'
import rHljsSupport from 'highlight.js/lib/languages/r'
import rubyHljsSupport from 'highlight.js/lib/languages/ruby'
import rustHljsSupport from 'highlight.js/lib/languages/rust'
import scssHljsSupport from 'highlight.js/lib/languages/scss'
import shellHljsSupport from 'highlight.js/lib/languages/shell'
import sqlHljsSupport from 'highlight.js/lib/languages/sql'
import swiftHljsSupport from 'highlight.js/lib/languages/swift'
import typescriptHljsSupport from 'highlight.js/lib/languages/typescript'
import vbnetHljsSupport from 'highlight.js/lib/languages/vbnet'
import wasmHljsSupport from 'highlight.js/lib/languages/wasm'
import xmlHljsSupport from 'highlight.js/lib/languages/xml'
import yamlHljsSupport from 'highlight.js/lib/languages/yaml'
hljs.registerLanguage('apache', apacheHljsSupport)
hljs.registerLanguage('bash', bashHljsSupport)
hljs.registerLanguage('c', cHljsSupport)
hljs.registerLanguage('cpp', cppHljsSupport)
hljs.registerLanguage('csharp', csharpHljsSupport)
hljs.registerLanguage('css', cssHljsSupport)
hljs.registerLanguage('diff', diffHljsSupport)
hljs.registerLanguage('go', goHljsSupport)
hljs.registerLanguage('graphql', graphqlHljsSupport)
hljs.registerLanguage('ini', iniHljsSupport)
hljs.registerLanguage('java', javaHljsSupport)
hljs.registerLanguage('javascript', javascriptHljsSupport)
hljs.registerLanguage('json', jsonHljsSupport)
hljs.registerLanguage('kotlin', kotlinHljsSupport)
hljs.registerLanguage('less', lessHljsSupport)
hljs.registerLanguage('lua', luaHljsSupport)
hljs.registerLanguage('makefile', makefileHljsSupport)
hljs.registerLanguage('markdown', markdownHljsSupport)
hljs.registerLanguage('nginx', nginxHljsSupport)
hljs.registerLanguage('objectivec', objectivecHljsSupport)
hljs.registerLanguage('perl', perlHljsSupport)
hljs.registerLanguage('pgsql', pgsqlHljsSupport)
hljs.registerLanguage('php', phpHljsSupport)
hljs.registerLanguage('php-template', phptemplateHljsSupport)
hljs.registerLanguage('plaintext', plaintextHljsSupport)
hljs.registerLanguage('python', pythonHljsSupport)
hljs.registerLanguage('python-repl', pythonreplHljsSupport)
hljs.registerLanguage('r', rHljsSupport)
hljs.registerLanguage('ruby', rubyHljsSupport)
hljs.registerLanguage('rust', rustHljsSupport)
hljs.registerLanguage('scss', scssHljsSupport)
hljs.registerLanguage('shell', shellHljsSupport)
hljs.registerLanguage('sql', sqlHljsSupport)
hljs.registerLanguage('swift', swiftHljsSupport)
hljs.registerLanguage('typescript', typescriptHljsSupport)
hljs.registerLanguage('vbnet', vbnetHljsSupport)
hljs.registerLanguage('wasm', wasmHljsSupport)
hljs.registerLanguage('xml', xmlHljsSupport)
hljs.registerLanguage('yaml', yamlHljsSupport)
// The above should now register on the languages we wish to support statically.
export const staticHljs = hljs;

View File

@ -1,96 +0,0 @@
// For handling of synchronous chats that are not utilizing streaming or chat requests.
export default function handleChat(
chatResult,
setLoadingResponse,
setChatHistory,
remHistory,
_chatHistory
) {
const { uuid, textResponse, type, sources = [], error, close } = chatResult;
if (type === "abort") {
setLoadingResponse(false);
setChatHistory([
...remHistory,
{
uuid,
content: textResponse,
role: "assistant",
sources,
closed: true,
error,
animate: false,
pending: false,
},
]);
_chatHistory.push({
uuid,
content: textResponse,
role: "assistant",
sources,
closed: true,
error,
animate: false,
pending: false,
});
} else if (type === "textResponse") {
setLoadingResponse(false);
setChatHistory([
...remHistory,
{
uuid,
content: textResponse,
role: "assistant",
sources,
closed: close,
error,
animate: !close,
pending: false,
},
]);
_chatHistory.push({
uuid,
content: textResponse,
role: "assistant",
sources,
closed: close,
error,
animate: !close,
pending: false,
});
} else if (type === "textResponseChunk") {
const chatIdx = _chatHistory.findIndex((chat) => chat.uuid === uuid);
if (chatIdx !== -1) {
const existingHistory = { ..._chatHistory[chatIdx] };
const updatedHistory = {
...existingHistory,
content: existingHistory.content + textResponse,
sources,
error,
closed: close,
animate: !close,
pending: false,
};
_chatHistory[chatIdx] = updatedHistory;
} else {
_chatHistory.push({
uuid,
sources,
error,
content: textResponse,
role: "assistant",
closed: close,
animate: !close,
pending: false,
});
}
setChatHistory([..._chatHistory]);
}
}
export function chatPrompt(workspace) {
return (
workspace?.openAiPrompt ??
"Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed."
);
}

View File

@ -1,49 +0,0 @@
import { encode as HTMLEncode } from "he";
import markdownIt from "markdown-it";
import { staticHljs as hljs } from "./hljs";
import { v4 } from "uuid";
const markdown = markdownIt({
html: false,
typographer: true,
highlight: function (code, lang) {
const uuid = v4();
if (lang && hljs.getLanguage(lang)) {
try {
return (
`<div class="whitespace-pre-line w-full rounded-lg bg-black-900 pb-4 relative font-mono font-normal text-sm text-slate-200">
<div class="w-full flex items-center absolute top-0 left-0 text-slate-200 bg-stone-800 px-4 py-2 text-xs font-sans justify-between rounded-t-md">
<div class="flex gap-2"><code class="text-xs">${lang}</code></div>
<button data-code-snippet data-code="code-${uuid}" class="flex items-center gap-x-2">
<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>
<p>Copy</p>
</button>
</div>
<pre class="whitespace-pre-wrap px-2">` +
hljs.highlight(code, { language: lang, ignoreIllegals: true }).value +
"</pre></div>"
);
} catch (__) {}
}
return (
`<div class="whitespace-pre-line w-full rounded-lg bg-black-900 pb-4 relative font-mono font-normal text-sm text-slate-200">
<div class="w-full flex items-center absolute top-0 left-0 text-slate-200 bg-stone-800 px-4 py-2 text-xs font-sans justify-between rounded-t-md">
<div class="flex gap-2"><code class="text-xs"></code></div>
<button data-code-snippet data-code="code-${uuid}" class="flex items-center gap-x-2">
<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>
<p>Copy</p>
</button>
</div>
<pre class="whitespace-pre-wrap px-2">` +
HTMLEncode(code) +
"</pre></div>"
);
},
})
// Enable <ol> and <ul> items to not assume an HTML structure so we can keep numbering from responses.
.disable("list");
export default function renderMarkdown(text = "") {
return markdown.render(text);
}

View File

@ -1,15 +0,0 @@
export const CHAT_UI_REOPEN = "___anythingllm-chat-widget-open___";
export function parseStylesSrc(scriptSrc = null) {
try {
const _url = new URL(scriptSrc);
_url.pathname = _url.pathname
.replace("anythingllm-chat-widget.js", "anythingllm-chat-widget.min.css")
.replace(
"anythingllm-chat-widget.min.js",
"anythingllm-chat-widget.min.css"
);
return _url.toString();
} catch {
return "";
}
}

View File

@ -1,9 +0,0 @@
export function formatDate(sentAt) {
const date = new Date(sentAt * 1000);
const timeString = date.toLocaleTimeString([], {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
return timeString;
}

View File

@ -1,103 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: 'false',
prefix: 'allm-',
corePlugins: {
preflight: false,
},
content: {
relative: true,
files: [
"./src/components/**/*.{js,jsx}",
"./src/hooks/**/*.js",
"./src/models/**/*.js",
"./src/pages/**/*.{js,jsx}",
"./src/utils/**/*.js",
"./src/*.jsx",
"./index.html",
]
},
theme: {
extend: {
rotate: {
"270": "270deg",
"360": "360deg"
},
colors: {
"black-900": "#141414",
accent: "#3D4147",
"sidebar-button": "#31353A",
sidebar: "#25272C",
"historical-msg-system": "rgba(255, 255, 255, 0.05);",
"historical-msg-user": "#2C2F35",
outline: "#4E5153",
"primary-button": "#46C8FF",
secondary: "#2C2F36",
"dark-input": "#18181B",
"mobile-onboarding": "#2C2F35",
"dark-highlight": "#1C1E21",
"dark-text": "#222628",
description: "#D2D5DB",
"x-button": "#9CA3AF"
},
backgroundImage: {
"preference-gradient":
"linear-gradient(180deg, #5A5C63 0%, rgba(90, 92, 99, 0.28) 100%);",
"chat-msg-user-gradient":
"linear-gradient(180deg, #3D4147 0%, #2C2F35 100%);",
"selected-preference-gradient":
"linear-gradient(180deg, #313236 0%, rgba(63.40, 64.90, 70.13, 0) 100%);",
"main-gradient": "linear-gradient(180deg, #3D4147 0%, #2C2F35 100%)",
"modal-gradient": "linear-gradient(180deg, #3D4147 0%, #2C2F35 100%)",
"sidebar-gradient": "linear-gradient(90deg, #5B616A 0%, #3F434B 100%)",
"login-gradient": "linear-gradient(180deg, #3D4147 0%, #2C2F35 100%)",
"menu-item-gradient":
"linear-gradient(90deg, #3D4147 0%, #2C2F35 100%)",
"menu-item-selected-gradient":
"linear-gradient(90deg, #5B616A 0%, #3F434B 100%)",
"workspace-item-gradient":
"linear-gradient(90deg, #3D4147 0%, #2C2F35 100%)",
"workspace-item-selected-gradient":
"linear-gradient(90deg, #5B616A 0%, #3F434B 100%)",
"switch-selected": "linear-gradient(146deg, #5B616A 0%, #3F434B 100%)"
},
fontFamily: {
sans: [
"plus-jakarta-sans",
"ui-sans-serif",
"system-ui",
"-apple-system",
"BlinkMacSystemFont",
'"Segoe UI"',
"Roboto",
'"Helvetica Neue"',
"Arial",
'"Noto Sans"',
"sans-serif",
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"',
'"Noto Color Emoji"'
]
},
animation: {
sweep: "sweep 0.5s ease-in-out"
},
keyframes: {
sweep: {
"0%": { transform: "scaleX(0)", transformOrigin: "bottom left" },
"100%": { transform: "scaleX(1)", transformOrigin: "bottom left" }
},
fadeIn: {
"0%": { opacity: 0 },
"100%": { opacity: 1 }
},
fadeOut: {
"0%": { opacity: 1 },
"100%": { opacity: 0 }
}
}
}
},
plugins: []
}

View File

@ -1,68 +0,0 @@
// vite.config.js
import { defineConfig } from "vite"
import { fileURLToPath, URL } from "url"
import postcss from "./postcss.config.js"
import react from "@vitejs/plugin-react"
import image from "@rollup/plugin-image"
export default defineConfig({
plugins: [react(), image()],
define: {
// In dev, we need to disable this, but in prod, we need to enable it
"process.env.NODE_ENV": JSON.stringify("production")
},
css: {
postcss
},
resolve: {
alias: [
{
find: "@",
replacement: fileURLToPath(new URL("./src", import.meta.url))
},
{
process: "process/browser",
stream: "stream-browserify",
zlib: "browserify-zlib",
util: "util",
find: /^~.+/,
replacement: (val) => {
return val.replace(/^~/, "")
}
}
]
},
build: {
lib: {
entry: "src/main.jsx",
name: "EmbeddedAnythingLLM",
formats: ["umd"],
fileName: (_format) => `anythingllm-chat-widget.js`
},
rollupOptions: {
external: [
// Reduces transformation time by 50% and we don't even use this variant, so we can ignore.
/@phosphor-icons\/react\/dist\/ssr/
]
},
commonjsOptions: {
transformMixedEsModules: true
},
cssCodeSplit: false,
assetsInlineLimit: 100000000,
minify: "esbuild",
outDir: "dist",
emptyOutDir: true,
inlineDynamicImports: true,
assetsDir: "",
sourcemap: "inline"
},
optimizeDeps: {
esbuildOptions: {
define: {
global: "globalThis"
},
plugins: []
}
}
})

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,6 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@metamask/jazzicon": "^2.0.0",
"@microsoft/fetch-event-source": "^2.0.1", "@microsoft/fetch-event-source": "^2.0.1",
"@mintplex-labs/piper-tts-web": "^1.0.4", "@mintplex-labs/piper-tts-web": "^1.0.4",
"@phosphor-icons/react": "^2.1.7", "@phosphor-icons/react": "^2.1.7",
@ -71,4 +70,4 @@
"tailwindcss": "^3.3.1", "tailwindcss": "^3.3.1",
"vite": "^4.3.0" "vite": "^4.3.0"
} }
} }

View File

@ -49,6 +49,9 @@ const GeneralVectorDatabase = lazy(
() => import("@/pages/GeneralSettings/VectorDatabase") () => import("@/pages/GeneralSettings/VectorDatabase")
); );
const GeneralSecurity = lazy(() => import("@/pages/GeneralSettings/Security")); const GeneralSecurity = lazy(() => import("@/pages/GeneralSettings/Security"));
const GeneralBrowserExtension = lazy(
() => import("@/pages/GeneralSettings/BrowserExtensionApiKey")
);
const WorkspaceSettings = lazy(() => import("@/pages/WorkspaceSettings")); const WorkspaceSettings = lazy(() => import("@/pages/WorkspaceSettings"));
const EmbedConfigSetup = lazy( const EmbedConfigSetup = lazy(
() => import("@/pages/GeneralSettings/EmbedConfigs") () => import("@/pages/GeneralSettings/EmbedConfigs")
@ -157,6 +160,10 @@ export default function App() {
path="/settings/api-keys" path="/settings/api-keys"
element={<AdminRoute Component={GeneralApiKeys} />} element={<AdminRoute Component={GeneralApiKeys} />}
/> />
<Route
path="/settings/browser-extension"
element={<ManagerRoute Component={GeneralBrowserExtension} />}
/>
<Route <Route
path="/settings/workspace-chats" path="/settings/workspace-chats"
element={<ManagerRoute Component={GeneralChats} />} element={<ManagerRoute Component={GeneralChats} />}

View File

@ -30,6 +30,8 @@ export default function VoyageAiOptions({ settings }) {
<optgroup label="Available embedding models"> <optgroup label="Available embedding models">
{[ {[
"voyage-large-2-instruct", "voyage-large-2-instruct",
"voyage-finance-2",
"voyage-multilingual-2",
"voyage-law-2", "voyage-law-2",
"voyage-code-2", "voyage-code-2",
"voyage-large-2", "voyage-large-2",

View File

@ -66,7 +66,9 @@ export default function AzureAiOptions({ settings }) {
<option value={16384}>16,384 (gpt-3.5-16k)</option> <option value={16384}>16,384 (gpt-3.5-16k)</option>
<option value={8192}>8,192 (gpt-4)</option> <option value={8192}>8,192 (gpt-4)</option>
<option value={32768}>32,768 (gpt-4-32k)</option> <option value={32768}>32,768 (gpt-4-32k)</option>
<option value={128000}>128,000 (gpt-4-turbo)</option> <option value={128000}>
128,000 (gpt-4-turbo,gpt-4o,gpt-4o-mini)
</option>
</select> </select>
</div> </div>

View File

@ -30,19 +30,34 @@ export default function GeminiLLMOptions({ settings }) {
required={true} required={true}
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5" className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
> >
{[ <optgroup label="Stable Models">
"gemini-pro", {[
"gemini-1.0-pro", "gemini-pro",
"gemini-1.5-pro-latest", "gemini-1.0-pro",
"gemini-1.5-flash-latest", "gemini-1.5-pro-latest",
"gemini-1.5-pro-exp-0801", "gemini-1.5-flash-latest",
].map((model) => { ].map((model) => {
return ( return (
<option key={model} value={model}> <option key={model} value={model}>
{model} {model}
</option> </option>
); );
})} })}
</optgroup>
<optgroup label="Experimental Models">
{[
"gemini-1.5-pro-exp-0801",
"gemini-1.5-pro-exp-0827",
"gemini-1.5-flash-exp-0827",
"gemini-1.5-flash-8b-exp-0827",
].map((model) => {
return (
<option key={model} value={model}>
{model}
</option>
);
})}
</optgroup>
</select> </select>
</div> </div>
<div className="flex flex-col w-60"> <div className="flex flex-col w-60">

View File

@ -127,19 +127,32 @@ const ModalTabSwitcher = ({ selectedTab, setSelectedTab }) => {
</div> </div>
); );
}; };
export function useManageWorkspaceModal() { export function useManageWorkspaceModal() {
const { user } = useUser(); const { user } = useUser();
const [showing, setShowing] = useState(false); const [showing, setShowing] = useState(false);
const showModal = () => { function showModal() {
if (user?.role !== "default") { if (user?.role !== "default") {
setShowing(true); setShowing(true);
} }
}; }
const hideModal = () => { function hideModal() {
setShowing(false); setShowing(false);
}; }
useEffect(() => {
function onEscape(event) {
if (!showing || event.key !== "Escape") return;
setShowing(false);
}
document.addEventListener("keydown", onEscape);
return () => {
document.removeEventListener("keydown", onEscape);
};
}, [showing]);
return { showing, showModal, hideModal }; return { showing, showModal, hideModal };
} }

View File

@ -21,7 +21,6 @@ import { useTranslation } from "react-i18next";
import showToast from "@/utils/toast"; import showToast from "@/utils/toast";
import System from "@/models/system"; import System from "@/models/system";
import Option from "./MenuOption"; import Option from "./MenuOption";
import { FineTuningAlert } from "@/pages/FineTuning/Banner";
export default function SettingsSidebar() { export default function SettingsSidebar() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -178,7 +177,6 @@ export default function SettingsSidebar() {
</div> </div>
</div> </div>
</div> </div>
<FineTuningAlert />
</> </>
); );
} }
@ -332,6 +330,12 @@ const SidebarOptions = ({ user = null, t }) => (
flex: true, flex: true,
roles: ["admin"], roles: ["admin"],
}, },
{
btnText: t("settings.browser-extension"),
href: paths.settings.browserExtension(),
flex: true,
roles: ["admin", "manager"],
},
]} ]}
/> />
<Option <Option

View File

@ -1,35 +1,42 @@
import React, { useRef, useEffect } from "react"; import React, { memo } from "react";
import JAZZ from "@metamask/jazzicon";
import usePfp from "../../hooks/usePfp"; import usePfp from "../../hooks/usePfp";
import UserDefaultPfp from "./user.svg";
import WorkspaceDefaultPfp from "./workspace.svg";
export default function UserIcon({ size = 36, user, role }) { const UserIcon = memo(({ role }) => {
const { pfp } = usePfp(); const { pfp } = usePfp();
const divRef = useRef(null);
const seed = user?.uid
? toPseudoRandomInteger(user.uid)
: Math.floor(100000 + Math.random() * 900000);
useEffect(() => {
if (!divRef.current || (role === "user" && pfp)) return;
const result = JAZZ(size, seed);
divRef.current.appendChild(result);
}, [pfp, role, seed, size]);
return ( return (
<div className="relative w-[35px] h-[35px] rounded-full flex-shrink-0 overflow-hidden"> <div className="relative w-[35px] h-[35px] rounded-full flex-shrink-0 overflow-hidden">
<div ref={divRef} /> {role === "user" && <RenderUserPfp pfp={pfp} />}
{role === "user" && pfp && ( {role !== "user" && (
<img <img
src={pfp} src={WorkspaceDefaultPfp}
alt="User profile picture" alt="system profile picture"
className="absolute top-0 left-0 w-full h-full object-cover rounded-full bg-white" className="flex items-center justify-center rounded-full border border-white/40"
/> />
)} )}
</div> </div>
); );
});
function RenderUserPfp({ pfp }) {
if (!pfp)
return (
<img
src={UserDefaultPfp}
alt="User profile picture"
className="rounded-full border-none"
/>
);
return (
<img
src={pfp}
alt="User profile picture"
className="absolute top-0 left-0 w-full h-full object-cover rounded-full border-none"
/>
);
} }
function toPseudoRandomInteger(uidString = "") { export default UserIcon;
return uidString.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
}

View File

@ -0,0 +1,12 @@
<svg width="33" height="33" viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="16.7969" cy="16.1001" r="16" fill="#2DF4D0"/>
<mask id="mask0_461_851" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="33" height="33">
<circle cx="16.7969" cy="16.1001" r="16" fill="#2DF4D0"/>
</mask>
<g mask="url(#mask0_461_851)">
<g opacity="0.7">
<circle cx="16.7969" cy="13.3476" r="6" fill="#0D2723"/>
<circle cx="16.7969" cy="33.1397" r="12" fill="#0D2723"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,20 @@
<svg width="33" height="33" viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_461_727)">
<path d="M26.6598 11.8883V20.5241C26.6598 21.5617 25.8155 22.406 24.7776 22.406H21.3764C20.8056 22.406 20.2725 22.1515 19.9134 21.7077L18.4667 19.9183C18.2926 19.7031 18.2909 19.4012 18.4624 19.184L18.9852 18.5222C19.0975 18.38 19.2656 18.2984 19.4469 18.2984C19.6281 18.2984 19.7944 18.3791 19.9068 18.5202L21.3014 20.2733C21.3689 20.3578 21.4695 20.4063 21.5775 20.4063H24.1887C24.4484 20.4063 24.6599 20.1949 24.6599 19.9352V12.4772C24.6599 12.2172 24.4484 12.0061 24.1887 12.0061H21.5752C21.4672 12.0061 21.3666 12.0544 21.2991 12.1388L13.6771 21.6973C13.3177 22.1477 12.7814 22.406 12.2055 22.406H8.81083C7.77323 22.406 6.92896 21.5617 6.92896 20.5241V11.8883C6.92896 10.8504 7.77323 10.0062 8.81083 10.0062H12.2302C12.8101 10.0062 13.3488 10.2676 13.7081 10.7235L15.0956 12.4945C15.2648 12.7093 15.2645 13.009 15.0948 13.2235L14.5682 13.8889C14.4562 14.0305 14.2881 14.1121 14.1072 14.1121C13.9262 14.1121 13.7596 14.0313 13.6472 13.8903L12.2563 12.1388C12.1891 12.0544 12.0885 12.0061 11.9805 12.0061H9.39973C9.14004 12.0061 8.92861 12.2172 8.92861 12.4772V19.9352C8.92861 20.1949 9.14004 20.4063 9.39973 20.4063H11.9794C12.0874 20.4063 12.1879 20.3578 12.2552 20.2736L19.8809 10.7146C20.24 10.2644 20.7763 10.0062 21.3523 10.0062H24.7776C25.8155 10.0062 26.6598 10.8504 26.6598 11.8883Z" fill="url(#paint0_linear_461_727)"/>
</g>
<defs>
<filter id="filter0_d_461_727" x="5.20838" y="8.41809" width="23.9657" height="16.6347" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="0.396858" dy="0.529359"/>
<feGaussianBlur stdDeviation="1.05872"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_461_727"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_461_727" result="shape"/>
</filter>
<linearGradient id="paint0_linear_461_727" x1="16.7942" y1="22.406" x2="16.7942" y2="10.0062" gradientUnits="userSpaceOnUse">
<stop stop-color="#AABBC2"/>
<stop offset="1" stop-color="white"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -76,7 +76,7 @@ const HistoricalMessage = ({
role === "user" ? USER_BACKGROUND_COLOR : AI_BACKGROUND_COLOR role === "user" ? USER_BACKGROUND_COLOR : AI_BACKGROUND_COLOR
}`} }`}
> >
<div className={`py-8 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}> <div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
<div className="flex gap-x-5"> <div className="flex gap-x-5">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<ProfileImage role={role} workspace={workspace} /> <ProfileImage role={role} workspace={workspace} />
@ -98,9 +98,9 @@ const HistoricalMessage = ({
saveChanges={saveEditedMessage} saveChanges={saveEditedMessage}
/> />
) : ( ) : (
<div> <div className="overflow-x-scroll break-words no-scroll">
<span <span
className={`flex flex-col gap-y-1`} className="flex flex-col gap-y-1"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(renderMarkdown(message)), __html: DOMPurify.sanitize(renderMarkdown(message)),
}} }}

View File

@ -1,8 +1,6 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { Tooltip } from "react-tooltip"; import { Tooltip } from "react-tooltip";
import { At, Flask, X } from "@phosphor-icons/react"; import { At } from "@phosphor-icons/react";
import ModalWrapper from "@/components/ModalWrapper";
import { useModal } from "@/hooks/useModal";
import { useIsAgentSessionActive } from "@/utils/chat/agent"; import { useIsAgentSessionActive } from "@/utils/chat/agent";
export default function AvailableAgentsButton({ showing, setShowAgents }) { export default function AvailableAgentsButton({ showing, setShowAgents }) {
@ -107,7 +105,6 @@ export function AvailableAgents({
</div> </div>
</div> </div>
</div> </div>
{showing && <FirstTimeAgentUser />}
</> </>
); );
} }
@ -116,71 +113,3 @@ export function useAvailableAgents() {
const [showAgents, setShowAgents] = useState(false); const [showAgents, setShowAgents] = useState(false);
return { showAgents, setShowAgents }; return { showAgents, setShowAgents };
} }
const SEEN_FT_AGENT_MODAL = "anythingllm_seen_first_time_agent_modal";
function FirstTimeAgentUser() {
const { isOpen, openModal, closeModal } = useModal();
useEffect(() => {
function firstTimeShow() {
if (!window) return;
if (!window.localStorage.getItem(SEEN_FT_AGENT_MODAL)) openModal();
}
firstTimeShow();
}, []);
const dismiss = () => {
closeModal();
window.localStorage.setItem(SEEN_FT_AGENT_MODAL, 1);
};
return (
<ModalWrapper isOpen={isOpen}>
<div className="relative w-[500px] max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-center gap-x-1 justify-between p-4 border-b rounded-t border-gray-600">
<Flask className="text-green-400" size={24} weight="fill" />
<h3 className="text-xl font-semibold text-white">
You just discovered Agents!
</h3>
<button
onClick={dismiss}
type="button"
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
data-modal-hide="staticModal"
>
<X className="text-gray-300 text-lg" />
</button>
</div>
<div className="p-6 space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
<p className="text-white/80 text-xs md:text-sm">
Agents are your LLM, but with special abilities that{" "}
<u>do something beyond chatting with your documents</u>.
<br />
<br />
Now you can use agents for real-time web search and scraping,
saving documents to your browser, summarizing documents, and
more.
</p>
<p className="text-green-300/60 text-xs md:text-sm">
This feature is currently early access and fully custom agents
with custom integrations & code execution will be in a future
update.
</p>
</div>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-600">
<div />
<button
onClick={dismiss}
type="button"
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
>
Continue
</button>
</div>
</div>
</div>
</ModalWrapper>
);
}

View File

@ -53,7 +53,7 @@ export function SlashCommands({ showing, setShowing, sendCommand }) {
<div className="w-full flex justify-center absolute bottom-[130px] md:bottom-[150px] left-0 z-10 px-4"> <div className="w-full flex justify-center absolute bottom-[130px] md:bottom-[150px] left-0 z-10 px-4">
<div <div
ref={cmdRef} ref={cmdRef}
className="w-[600px] overflow-auto p-2 bg-zinc-800 rounded-2xl shadow flex-col justify-center items-start gap-2.5 inline-flex" className="w-[600px] bg-zinc-800 rounded-2xl flex shadow flex-col justify-start items-start gap-2.5 p-2 overflow-y-auto max-h-[300px] no-scroll"
> >
<ResetCommand sendCommand={sendCommand} setShowing={setShowing} /> <ResetCommand sendCommand={sendCommand} setShowing={setShowing} />
<EndAgentSession sendCommand={sendCommand} setShowing={setShowing} /> <EndAgentSession sendCommand={sendCommand} setShowing={setShowing} />

View File

@ -17,6 +17,9 @@ const PROVIDER_DEFAULT_MODELS = {
"gemini-1.5-pro-latest", "gemini-1.5-pro-latest",
"gemini-1.5-flash-latest", "gemini-1.5-flash-latest",
"gemini-1.5-pro-exp-0801", "gemini-1.5-pro-exp-0801",
"gemini-1.5-pro-exp-0827",
"gemini-1.5-flash-exp-0827",
"gemini-1.5-flash-8b-exp-0827",
], ],
anthropic: [ anthropic: [
"claude-instant-1.2", "claude-instant-1.2",

View File

@ -767,34 +767,6 @@ does not extend the close button beyond the viewport. */
} }
} }
.top-banner {
animation: popTop 500ms forwards;
}
@keyframes popTop {
0% {
top: -3.5rem;
}
100% {
top: 0px;
}
}
.rm-top-banner {
animation: rmPopTop 500ms forwards;
}
@keyframes rmPopTop {
0% {
top: 0px;
}
100% {
top: -3.5rem;
}
}
/* Math/Katex formatting to prevent duplication of content on screen */ /* Math/Katex formatting to prevent duplication of content on screen */
.katex-html[aria-hidden="true"] { .katex-html[aria-hidden="true"] {
display: none; display: none;

View File

@ -37,6 +37,7 @@ const TRANSLATIONS = {
tools: "Werkzeuge", tools: "Werkzeuge",
"experimental-features": "Experimentelle Funktionen", "experimental-features": "Experimentelle Funktionen",
contact: "Support kontaktieren", contact: "Support kontaktieren",
"browser-extension": "Browser-Erweiterung",
}, },
login: { login: {

View File

@ -38,6 +38,7 @@ const TRANSLATIONS = {
tools: "Tools", tools: "Tools",
"experimental-features": "Experimental Features", "experimental-features": "Experimental Features",
contact: "Contact Support", contact: "Contact Support",
"browser-extension": "Browser Extension",
}, },
// Page Definitions // Page Definitions

View File

@ -37,6 +37,7 @@ const TRANSLATIONS = {
tools: "Herramientas", tools: "Herramientas",
"experimental-features": "Funciones Experimentales", "experimental-features": "Funciones Experimentales",
contact: "Contactar Soporte", contact: "Contactar Soporte",
"browser-extension": "Extensión del navegador",
}, },
login: { login: {

View File

@ -38,6 +38,7 @@ const TRANSLATIONS = {
tools: "Outils", tools: "Outils",
"experimental-features": "Fonctionnalités Expérimentales", "experimental-features": "Fonctionnalités Expérimentales",
contact: "Contacter le Support", contact: "Contacter le Support",
"browser-extension": "Extension de navigateur",
}, },
// Page Definitions // Page Definitions

View File

@ -38,6 +38,7 @@ const TRANSLATIONS = {
tools: "כלים", tools: "כלים",
"experimental-features": "תכונות ניסיוניות", "experimental-features": "תכונות ניסיוניות",
contact: "צור קשר עם התמיכה", contact: "צור קשר עם התמיכה",
"browser-extension": "תוסף דפדפן",
}, },
// Page Definitions // Page Definitions

View File

@ -38,6 +38,7 @@ const TRANSLATIONS = {
tools: "Strumenti", tools: "Strumenti",
"experimental-features": "Caratteristiche sperimentali", "experimental-features": "Caratteristiche sperimentali",
contact: "Contatta il Supporto", contact: "Contatta il Supporto",
"browser-extension": "Estensione del browser",
}, },
// Page Definitions // Page Definitions

View File

@ -38,6 +38,7 @@ const TRANSLATIONS = {
tools: "도구", tools: "도구",
"experimental-features": "실험적 기능", "experimental-features": "실험적 기능",
contact: "지원팀 연락", contact: "지원팀 연락",
"browser-extension": "브라우저 확장 프로그램",
}, },
// Page Definitions // Page Definitions

View File

@ -38,6 +38,7 @@ const TRANSLATIONS = {
tools: "Ferramentas", tools: "Ferramentas",
"experimental-features": "Recursos Experimentais", "experimental-features": "Recursos Experimentais",
contact: "Contato com Suporte", contact: "Contato com Suporte",
"browser-extension": "Extensão do navegador",
}, },
// Page Definitions // Page Definitions

View File

@ -37,6 +37,7 @@ const TRANSLATIONS = {
tools: "Инструменты", tools: "Инструменты",
"experimental-features": "Экспериментальные функции", "experimental-features": "Экспериментальные функции",
contact: "联系支持Связаться с Поддержкой", contact: "联系支持Связаться с Поддержкой",
"browser-extension": "Расширение браузера",
}, },
login: { login: {

View File

@ -23,13 +23,13 @@ const TRANSLATIONS = {
customization: "外观", customization: "外观",
"api-keys": "API 密钥", "api-keys": "API 密钥",
llm: "LLM 首选项", llm: "LLM 首选项",
transcription: "Transcription 模型", transcription: "转录模型",
embedder: "Embedder 首选项", embedder: "Embedder 首选项",
"text-splitting": "文本分割", "text-splitting": "文本分割",
"voice-speech": "语音和讲话", "voice-speech": "语音和讲话",
"vector-database": "向量数据库", "vector-database": "向量数据库",
embeds: "嵌入式对话", embeds: "嵌入式对话",
"embed-chats": "嵌入式对话历史", "embed-chats": "嵌入式对话历史记录",
security: "用户与安全", security: "用户与安全",
"event-logs": "事件日志", "event-logs": "事件日志",
privacy: "隐私与数据", privacy: "隐私与数据",
@ -39,6 +39,7 @@ const TRANSLATIONS = {
tools: "工具", tools: "工具",
"experimental-features": "实验功能", "experimental-features": "实验功能",
contact: "联系支持", contact: "联系支持",
"browser-extension": "浏览器扩展",
}, },
// Page Definitions // Page Definitions
@ -115,7 +116,7 @@ const TRANSLATIONS = {
add: "添加新消息", add: "添加新消息",
save: "保存消息", save: "保存消息",
heading: "向我解释", heading: "向我解释",
body: "AnythingLLM的好处", body: "AnythingLLM 的好处",
}, },
pfp: { pfp: {
title: "助理头像", title: "助理头像",
@ -137,7 +138,7 @@ const TRANSLATIONS = {
// Chat Settings // Chat Settings
chat: { chat: {
llm: { llm: {
title: "工作区LLM提供者", title: "工作区 LLM 提供者",
description: description:
"将用于此工作区的特定 LLM 提供商和模型。默认情况下,它使用系统 LLM 提供程序和设置。", "将用于此工作区的特定 LLM 提供商和模型。默认情况下,它使用系统 LLM 提供程序和设置。",
search: "搜索所有 LLM 提供商", search: "搜索所有 LLM 提供商",
@ -145,14 +146,14 @@ const TRANSLATIONS = {
model: { model: {
title: "工作区聊天模型", title: "工作区聊天模型",
description: description:
"将用于此工作区的特定聊天模型。如果为空,将使用系统LLM首选项。", "将用于此工作区的特定聊天模型。如果为空,将使用系统 LLM 首选项。",
wait: "-- 等待模型 --", wait: "-- 等待模型 --",
}, },
mode: { mode: {
title: "聊天模式", title: "聊天模式",
chat: { chat: {
title: "聊天", title: "聊天",
"desc-start": "将提供法学硕士的一般知识", "desc-start": "将提供 LLM 的一般知识",
and: "和", and: "和",
"desc-end": "找到的文档上下文的答案。", "desc-end": "找到的文档上下文的答案。",
}, },
@ -182,11 +183,11 @@ const TRANSLATIONS = {
"desc-end": "模式时,当未找到上下文时,您可能希望返回自定义拒绝响应。", "desc-end": "模式时,当未找到上下文时,您可能希望返回自定义拒绝响应。",
}, },
temperature: { temperature: {
title: "LLM Temperature", title: "LLM 温度",
"desc-start": "此设置控制您的 LLM 回答的“创意”程度", "desc-start": "此设置控制您的 LLM 回答的“创意”程度",
"desc-end": "desc-end":
"数字越高越有创意。对于某些模型,如果设置得太高,可能会导致响应不一致。", "数字越高越有创意。对于某些模型,如果设置得太高,可能会导致响应不一致。",
hint: "大多数法学硕士都有各种可接受的有效值范围。请咨询您的法学硕士提供商以获取该信息。", hint: "大多数 LLM 都有各种可接受的有效值范围。请咨询您的吗 LLM 提供商以获取该信息。",
}, },
}, },
@ -240,7 +241,7 @@ const TRANSLATIONS = {
description: description:
"使用这些预构建的技能提高默认代理的自然能力。此设置适用于所有工作区。", "使用这些预构建的技能提高默认代理的自然能力。此设置适用于所有工作区。",
rag: { rag: {
title: "RAG和长期记忆", title: "RAG 和长期记忆",
description: description:
'允许代理利用您的本地文档来回答查询,或要求代理"记住"长期记忆检索的内容片段。', '允许代理利用您的本地文档来回答查询,或要求代理"记住"长期记忆检索的内容片段。',
}, },
@ -277,11 +278,11 @@ const TRANSLATIONS = {
export: "导出", export: "导出",
table: { table: {
id: "Id", id: "Id",
by: "Sent By", by: "发送者",
workspace: "Workspace", workspace: "工作区",
prompt: "Prompt", prompt: "提示",
response: "Response", response: "响应",
at: "Sent At", at: "发送时间",
}, },
}, },
@ -330,14 +331,14 @@ const TRANSLATIONS = {
// LLM Preferences // LLM Preferences
llm: { llm: {
title: "LLM 偏好", title: "LLM 首选项",
description: description:
"这些是您首选的 LLM 聊天和嵌入提供商的凭据和设置。重要的是,这些密钥是最新的和正确的,否则 AnythingLLM 将无法正常运行。", "这些是您首选的 LLM 聊天和嵌入提供商的凭据和设置。重要的是,这些密钥是最新的和正确的,否则 AnythingLLM 将无法正常运行。",
provider: "LLM 提供商", provider: "LLM 提供商",
}, },
transcription: { transcription: {
title: "转录模型偏好", title: "转录模型首选项",
description: description:
"这些是您的首选转录模型提供商的凭据和设置。重要的是这些密钥是最新且正确的,否则媒体文件和音频将无法转录。", "这些是您的首选转录模型提供商的凭据和设置。重要的是这些密钥是最新且正确的,否则媒体文件和音频将无法转录。",
provider: "转录提供商", provider: "转录提供商",
@ -404,14 +405,14 @@ const TRANSLATIONS = {
// Embeddable Chat History // Embeddable Chat History
"embed-chats": { "embed-chats": {
title: "嵌入聊天", title: "嵌入聊天历史纪录",
description: "这些是您发布的任何嵌入的所有记录的聊天和消息。", description: "这些是您发布的任何嵌入的所有记录的聊天和消息。",
table: { table: {
embed: "嵌入", embed: "嵌入",
sender: "发送者", sender: "发送者",
message: "消息", message: "消息",
response: "响应", response: "响应",
at: "发送", at: "发送时间",
}, },
}, },
@ -456,8 +457,8 @@ const TRANSLATIONS = {
title: "隐私和数据处理", title: "隐私和数据处理",
description: description:
"这是您对如何处理连接的第三方提供商和AnythingLLM的数据的配置。", "这是您对如何处理连接的第三方提供商和AnythingLLM的数据的配置。",
llm: "LLM选择", llm: "LLM 选择",
embedding: "嵌入偏好", embedding: "嵌入首选项",
vector: "向量数据库", vector: "向量数据库",
anonymous: "启用匿名遥测", anonymous: "启用匿名遥测",
}, },

View File

@ -156,6 +156,8 @@ const Admin = {
}, },
// System Preferences // System Preferences
// TODO: remove this in favor of systemPreferencesByFields
// DEPRECATED: use systemPreferencesByFields instead
systemPreferences: async () => { systemPreferences: async () => {
return await fetch(`${API_BASE}/admin/system-preferences`, { return await fetch(`${API_BASE}/admin/system-preferences`, {
method: "GET", method: "GET",
@ -167,6 +169,26 @@ const Admin = {
return null; return null;
}); });
}, },
/**
* Fetches system preferences by fields
* @param {string[]} labels - Array of labels for settings
* @returns {Promise<{settings: Object, error: string}>} - System preferences object
*/
systemPreferencesByFields: async (labels = []) => {
return await fetch(
`${API_BASE}/admin/system-preferences-for?labels=${labels.join(",")}`,
{
method: "GET",
headers: baseHeaders(),
}
)
.then((res) => res.json())
.catch((e) => {
console.error(e);
return null;
});
},
updateSystemPreferences: async (updates = {}) => { updateSystemPreferences: async (updates = {}) => {
return await fetch(`${API_BASE}/admin/system-preferences`, { return await fetch(`${API_BASE}/admin/system-preferences`, {
method: "POST", method: "POST",

View File

@ -0,0 +1,42 @@
import { API_BASE } from "@/utils/constants";
import { baseHeaders } from "@/utils/request";
const BrowserExtensionApiKey = {
getAll: async () => {
return await fetch(`${API_BASE}/browser-extension/api-keys`, {
method: "GET",
headers: baseHeaders(),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return { success: false, error: e.message, apiKeys: [] };
});
},
generateKey: async () => {
return await fetch(`${API_BASE}/browser-extension/api-keys/new`, {
method: "POST",
headers: baseHeaders(),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return { success: false, error: e.message };
});
},
revoke: async (id) => {
return await fetch(`${API_BASE}/browser-extension/api-keys/${id}`, {
method: "DELETE",
headers: baseHeaders(),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return { success: false, error: e.message };
});
},
};
export default BrowserExtensionApiKey;

View File

@ -0,0 +1,43 @@
import { API_BASE } from "@/utils/constants";
import { baseHeaders } from "@/utils/request";
const AgentPlugins = {
toggleFeature: async function (hubId, active = false) {
return await fetch(
`${API_BASE}/experimental/agent-plugins/${hubId}/toggle`,
{
method: "POST",
headers: baseHeaders(),
body: JSON.stringify({ active }),
}
)
.then((res) => {
if (!res.ok) throw new Error("Could not update agent plugin status.");
return true;
})
.catch((e) => {
console.error(e);
return false;
});
},
updatePluginConfig: async function (hubId, updates = {}) {
return await fetch(
`${API_BASE}/experimental/agent-plugins/${hubId}/config`,
{
method: "POST",
headers: baseHeaders(),
body: JSON.stringify({ updates }),
}
)
.then((res) => {
if (!res.ok) throw new Error("Could not update agent plugin config.");
return true;
})
.catch((e) => {
console.error(e);
return false;
});
},
};
export default AgentPlugins;

View File

@ -2,6 +2,7 @@ import { API_BASE, AUTH_TIMESTAMP, fullApiUrl } from "@/utils/constants";
import { baseHeaders, safeJsonParse } from "@/utils/request"; import { baseHeaders, safeJsonParse } from "@/utils/request";
import DataConnector from "./dataConnector"; import DataConnector from "./dataConnector";
import LiveDocumentSync from "./experimental/liveSync"; import LiveDocumentSync from "./experimental/liveSync";
import AgentPlugins from "./experimental/agentPlugins";
const System = { const System = {
cacheKeys: { cacheKeys: {
@ -675,6 +676,7 @@ const System = {
}, },
experimentalFeatures: { experimentalFeatures: {
liveSync: LiveDocumentSync, liveSync: LiveDocumentSync,
agentPlugins: AgentPlugins,
}, },
}; };

View File

@ -0,0 +1,180 @@
import System from "@/models/system";
import showToast from "@/utils/toast";
import { Plug } from "@phosphor-icons/react";
import { useEffect, useState } from "react";
import { sentenceCase } from "text-case";
/**
* Converts setup_args to inputs for the form builder
* @param {object} setupArgs - The setup arguments object
* @returns {object} - The inputs object
*/
function inputsFromArgs(setupArgs) {
if (
!setupArgs ||
setupArgs.constructor?.call?.().toString() !== "[object Object]"
) {
return {};
}
return Object.entries(setupArgs).reduce(
(acc, [key, props]) => ({
...acc,
[key]: props.hasOwnProperty("value")
? props.value
: props?.input?.default || "",
}),
{}
);
}
/**
* Imported skill config component for imported skills only.
* @returns {JSX.Element}
*/
export default function ImportedSkillConfig({
selectedSkill, // imported skill config object
setImportedSkills, // function to set imported skills since config is file-write
}) {
const [config, setConfig] = useState(selectedSkill);
const [hasChanges, setHasChanges] = useState(false);
const [inputs, setInputs] = useState(
inputsFromArgs(selectedSkill?.setup_args)
);
const hasSetupArgs =
selectedSkill?.setup_args &&
Object.keys(selectedSkill.setup_args).length > 0;
async function toggleSkill() {
const updatedConfig = { ...selectedSkill, active: !config.active };
await System.experimentalFeatures.agentPlugins.updatePluginConfig(
config.hubId,
{ active: !config.active }
);
setImportedSkills((prev) =>
prev.map((s) => (s.hubId === config.hubId ? updatedConfig : s))
);
setConfig(updatedConfig);
}
async function handleSubmit(e) {
e.preventDefault();
const errors = [];
const updatedConfig = { ...config };
for (const [key, value] of Object.entries(inputs)) {
const settings = config.setup_args[key];
if (settings.required && !value) {
errors.push(`${key} is required to have a value.`);
continue;
}
if (typeof value !== settings.type) {
errors.push(`${key} must be of type ${settings.type}.`);
continue;
}
updatedConfig.setup_args[key].value = value;
}
if (errors.length > 0) {
errors.forEach((error) => showToast(error, "error"));
return;
}
await System.experimentalFeatures.agentPlugins.updatePluginConfig(
config.hubId,
updatedConfig
);
setConfig(updatedConfig);
setImportedSkills((prev) =>
prev.map((skill) =>
skill.hubId === config.hubId ? updatedConfig : skill
)
);
showToast("Skill config updated successfully.", "success");
}
useEffect(() => {
setHasChanges(
JSON.stringify(inputs) !==
JSON.stringify(inputsFromArgs(selectedSkill.setup_args))
);
}, [inputs]);
return (
<>
<div className="p-2">
<div className="flex flex-col gap-y-[18px] max-w-[500px]">
<div className="flex items-center gap-x-2">
<Plug size={24} color="white" weight="bold" />
<label htmlFor="name" className="text-white text-md font-bold">
{sentenceCase(config.name)}
</label>
<label className="border-none relative inline-flex cursor-pointer items-center ml-auto">
<input
type="checkbox"
className="peer sr-only"
checked={config.active}
onChange={() => toggleSkill()}
/>
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div>
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
</label>
</div>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
{config.description} by{" "}
<a
href={config.author_url}
target="_blank"
rel="noopener noreferrer"
className="text-white hover:underline"
>
{config.author}
</a>
</p>
{hasSetupArgs ? (
<div className="flex flex-col gap-y-2">
{Object.entries(config.setup_args).map(([key, props]) => (
<div key={key} className="flex flex-col gap-y-1">
<label htmlFor={key} className="text-white text-sm font-bold">
{key}
</label>
<input
type={props?.input?.type || "text"}
required={props?.input?.required}
defaultValue={
props.hasOwnProperty("value")
? props.value
: props?.input?.default || ""
}
onChange={(e) =>
setInputs({ ...inputs, [key]: e.target.value })
}
placeholder={props?.input?.placeholder || ""}
className="bg-transparent border border-white border-opacity-20 rounded-md p-2 text-white text-sm"
/>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
{props?.input?.hint}
</p>
</div>
))}
{hasChanges && (
<button
onClick={handleSubmit}
type="button"
className="bg-blue-500 text-white rounded-md p-2"
>
Save
</button>
)}
</div>
) : (
<p className="text-white text-opacity-60 text-sm font-medium py-1.5">
There are no options to modify for this skill.
</p>
)}
</div>
</div>
</>
);
}

View File

@ -0,0 +1,59 @@
import { CaretRight } from "@phosphor-icons/react";
import { isMobile } from "react-device-detect";
import { sentenceCase } from "text-case";
export default function ImportedSkillList({
skills = [],
selectedSkill = null,
handleClick = null,
}) {
if (skills.length === 0)
return (
<div className="text-white/60 text-center text-xs flex flex-col gap-y-2">
<p>No imported skills found</p>
<p>
Learn about agent skills in the{" "}
<a
href="https://docs.anythingllm.com/agent/custom/developer-guide"
target="_blank"
className="text-white/80 hover:underline"
>
AnythingLLM Agent Docs
</a>
.
</p>
</div>
);
return (
<div
className={`bg-white/5 text-white rounded-xl ${
isMobile ? "w-full" : "min-w-[360px] w-fit"
}`}
>
{skills.map((config, index) => (
<div
key={config.hubId}
className={`py-3 px-4 flex items-center justify-between ${
index === 0 ? "rounded-t-xl" : ""
} ${
index === Object.keys(skills).length - 1
? "rounded-b-xl"
: "border-b border-white/10"
} cursor-pointer transition-all duration-300 hover:bg-white/5 ${
selectedSkill === config.hubId ? "bg-white/10" : ""
}`}
onClick={() => handleClick?.({ ...config, imported: true })}
>
<div className="text-sm font-light">{sentenceCase(config.name)}</div>
<div className="flex items-center gap-x-2">
<div className="text-sm text-white/60 font-medium">
{config.active ? "On" : "Off"}
</div>
<CaretRight size={14} weight="bold" className="text-white/80" />
</div>
</div>
))}
</div>
);
}

View File

@ -50,6 +50,83 @@ export function GoogleSearchOptions({ settings }) {
); );
} }
const SearchApiEngines = [
{ name: "Google Search", value: "google" },
{ name: "Google Maps", value: "google_maps" },
{ name: "Google Shopping", value: "google_shopping" },
{ name: "Google News", value: "google_news" },
{ name: "Google Jobs", value: "google_jobs" },
{ name: "Google Scholar", value: "google_scholar" },
{ name: "Google Finance", value: "google_finance" },
{ name: "Google Patents", value: "google_patents" },
{ name: "YouTube", value: "youtube" },
{ name: "Bing", value: "bing" },
{ name: "Bing News", value: "bing_news" },
{ name: "Amazon Product Search", value: "amazon_search" },
{ name: "Baidu", value: "baidu" },
];
export function SearchApiOptions({ settings }) {
return (
<>
<p className="text-sm text-white/60 my-2">
You can get a free API key{" "}
<a
href="https://www.searchapi.io/"
target="_blank"
rel="noreferrer"
className="text-blue-300 underline"
>
from SearchApi.
</a>
</p>
<div className="flex gap-x-4">
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
API Key
</label>
<input
type="password"
name="env::AgentSearchApiKey"
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="SearchApi API Key"
defaultValue={settings?.AgentSearchApiKey ? "*".repeat(20) : ""}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
Engine
</label>
<select
name="env::AgentSearchApiEngine"
required={true}
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
defaultValue={settings?.AgentSearchApiEngine || "google"}
>
{SearchApiEngines.map(({ name, value }) => (
<option key={name} value={value}>
{name}
</option>
))}
</select>
{/* <input
type="text"
name="env::AgentSearchApiEngine"
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="SearchApi engine (Google, Bing...)"
defaultValue={settings?.AgentSearchApiEngine || "google"}
required={true}
autoComplete="off"
spellCheck={false}
/> */}
</div>
</div>
</>
);
}
export function SerperDotDevOptions({ settings }) { export function SerperDotDevOptions({ settings }) {
return ( return (
<> <>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -1,6 +1,7 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png"; import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
import GoogleSearchIcon from "./icons/google.png"; import GoogleSearchIcon from "./icons/google.png";
import SearchApiIcon from "./icons/searchapi.png";
import SerperDotDevIcon from "./icons/serper.png"; import SerperDotDevIcon from "./icons/serper.png";
import BingSearchIcon from "./icons/bing.png"; import BingSearchIcon from "./icons/bing.png";
import SerplySearchIcon from "./icons/serply.png"; import SerplySearchIcon from "./icons/serply.png";
@ -14,6 +15,7 @@ import {
import SearchProviderItem from "./SearchProviderItem"; import SearchProviderItem from "./SearchProviderItem";
import WebSearchImage from "@/media/agents/scrape-websites.png"; import WebSearchImage from "@/media/agents/scrape-websites.png";
import { import {
SearchApiOptions,
SerperDotDevOptions, SerperDotDevOptions,
GoogleSearchOptions, GoogleSearchOptions,
BingSearchOptions, BingSearchOptions,
@ -38,6 +40,14 @@ const SEARCH_PROVIDERS = [
description: description:
"Web search powered by a custom Google Search Engine. Free for 100 queries per day.", "Web search powered by a custom Google Search Engine. Free for 100 queries per day.",
}, },
{
name: "SearchApi",
value: "searchapi",
logo: SearchApiIcon,
options: (settings) => <SearchApiOptions settings={settings} />,
description:
"SearchApi delivers structured data from multiple search engines. Free for 100 queries, but then paid. ",
},
{ {
name: "Serper.dev", name: "Serper.dev",
value: "serper-dot-dev", value: "serper-dot-dev",

View File

@ -4,18 +4,21 @@ import { isMobile } from "react-device-detect";
import Admin from "@/models/admin"; import Admin from "@/models/admin";
import System from "@/models/system"; import System from "@/models/system";
import showToast from "@/utils/toast"; import showToast from "@/utils/toast";
import { CaretLeft, CaretRight, Robot } from "@phosphor-icons/react"; import { CaretLeft, CaretRight, Plug, Robot } from "@phosphor-icons/react";
import ContextualSaveBar from "@/components/ContextualSaveBar"; import ContextualSaveBar from "@/components/ContextualSaveBar";
import { castToType } from "@/utils/types"; import { castToType } from "@/utils/types";
import { FullScreenLoader } from "@/components/Preloader"; import { FullScreenLoader } from "@/components/Preloader";
import { defaultSkills, configurableSkills } from "./skills"; import { defaultSkills, configurableSkills } from "./skills";
import { DefaultBadge } from "./Badges/default"; import { DefaultBadge } from "./Badges/default";
import ImportedSkillList from "./Imported/SkillList";
import ImportedSkillConfig from "./Imported/ImportedSkillConfig";
export default function AdminAgents() { export default function AdminAgents() {
const [hasChanges, setHasChanges] = useState(false); const [hasChanges, setHasChanges] = useState(false);
const [settings, setSettings] = useState({}); const [settings, setSettings] = useState({});
const [selectedSkill, setSelectedSkill] = useState(""); const [selectedSkill, setSelectedSkill] = useState("");
const [agentSkills, setAgentSkills] = useState([]); const [agentSkills, setAgentSkills] = useState([]);
const [importedSkills, setImportedSkills] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [showSkillModal, setShowSkillModal] = useState(false); const [showSkillModal, setShowSkillModal] = useState(false);
const formEl = useRef(null); const formEl = useRef(null);
@ -37,9 +40,13 @@ export default function AdminAgents() {
useEffect(() => { useEffect(() => {
async function fetchSettings() { async function fetchSettings() {
const _settings = await System.keys(); const _settings = await System.keys();
const _preferences = await Admin.systemPreferences(); const _preferences = await Admin.systemPreferencesByFields([
"default_agent_skills",
"imported_agent_skills",
]);
setSettings({ ..._settings, preferences: _preferences.settings } ?? {}); setSettings({ ..._settings, preferences: _preferences.settings } ?? {});
setAgentSkills(_preferences.settings?.default_agent_skills ?? []); setAgentSkills(_preferences.settings?.default_agent_skills ?? []);
setImportedSkills(_preferences.settings?.imported_agent_skills ?? []);
setLoading(false); setLoading(false);
} }
fetchSettings(); fetchSettings();
@ -84,9 +91,13 @@ export default function AdminAgents() {
if (success) { if (success) {
const _settings = await System.keys(); const _settings = await System.keys();
const _preferences = await Admin.systemPreferences(); const _preferences = await Admin.systemPreferencesByFields([
"default_agent_skills",
"imported_agent_skills",
]);
setSettings({ ..._settings, preferences: _preferences.settings } ?? {}); setSettings({ ..._settings, preferences: _preferences.settings } ?? {});
setAgentSkills(_preferences.settings?.default_agent_skills ?? []); setAgentSkills(_preferences.settings?.default_agent_skills ?? []);
setImportedSkills(_preferences.settings?.imported_agent_skills ?? []);
showToast(`Agent preferences saved successfully.`, "success", { showToast(`Agent preferences saved successfully.`, "success", {
clear: true, clear: true,
}); });
@ -97,9 +108,10 @@ export default function AdminAgents() {
setHasChanges(false); setHasChanges(false);
}; };
const SelectedSkillComponent = const SelectedSkillComponent = selectedSkill.imported
configurableSkills[selectedSkill]?.component || ? ImportedSkillConfig
defaultSkills[selectedSkill]?.component; : configurableSkills[selectedSkill]?.component ||
defaultSkills[selectedSkill]?.component;
if (loading) { if (loading) {
return ( return (
@ -157,6 +169,16 @@ export default function AdminAgents() {
}} }}
activeSkills={agentSkills} activeSkills={agentSkills}
/> />
<div className="text-white flex items-center gap-x-2">
<Plug size={24} />
<p className="text-lg font-medium">Custom Skills</p>
</div>
<ImportedSkillList
skills={importedSkills}
selectedSkill={selectedSkill}
handleClick={setSelectedSkill}
/>
</div> </div>
{/* Selected agent skill modal */} {/* Selected agent skill modal */}
@ -181,17 +203,27 @@ export default function AdminAgents() {
<div className="flex-1 overflow-y-auto p-4"> <div className="flex-1 overflow-y-auto p-4">
<div className="bg-[#303237] text-white rounded-xl p-4"> <div className="bg-[#303237] text-white rounded-xl p-4">
{SelectedSkillComponent ? ( {SelectedSkillComponent ? (
<SelectedSkillComponent <>
skill={configurableSkills[selectedSkill]?.skill} {selectedSkill.imported ? (
settings={settings} <ImportedSkillConfig
toggleSkill={toggleAgentSkill} key={selectedSkill.hubId}
enabled={agentSkills.includes( selectedSkill={selectedSkill}
configurableSkills[selectedSkill]?.skill setImportedSkills={setImportedSkills}
/>
) : (
<SelectedSkillComponent
skill={configurableSkills[selectedSkill]?.skill}
settings={settings}
toggleSkill={toggleAgentSkill}
enabled={agentSkills.includes(
configurableSkills[selectedSkill]?.skill
)}
setHasChanges={setHasChanges}
{...(configurableSkills[selectedSkill] ||
defaultSkills[selectedSkill])}
/>
)} )}
setHasChanges={setHasChanges} </>
{...(configurableSkills[selectedSkill] ||
defaultSkills[selectedSkill])}
/>
) : ( ) : (
<div className="flex flex-col items-center justify-center h-full text-white/60"> <div className="flex flex-col items-center justify-center h-full text-white/60">
<Robot size={40} /> <Robot size={40} />
@ -216,7 +248,7 @@ export default function AdminAgents() {
> >
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
onChange={() => setHasChanges(true)} onChange={() => !selectedSkill.imported && setHasChanges(true)}
ref={formEl} ref={formEl}
className="flex-1 flex gap-x-6 p-4 mt-10" className="flex-1 flex gap-x-6 p-4 mt-10"
> >
@ -247,23 +279,43 @@ export default function AdminAgents() {
handleClick={setSelectedSkill} handleClick={setSelectedSkill}
activeSkills={agentSkills} activeSkills={agentSkills}
/> />
<div className="text-white flex items-center gap-x-2">
<Plug size={24} />
<p className="text-lg font-medium">Custom Skills</p>
</div>
<ImportedSkillList
skills={importedSkills}
selectedSkill={selectedSkill}
handleClick={setSelectedSkill}
/>
</div> </div>
{/* Selected agent skill setting panel */} {/* Selected agent skill setting panel */}
<div className="flex-[2] flex flex-col gap-y-[18px] mt-10"> <div className="flex-[2] flex flex-col gap-y-[18px] mt-10">
<div className="bg-[#303237] text-white rounded-xl flex-1 p-4"> <div className="bg-[#303237] text-white rounded-xl flex-1 p-4">
{SelectedSkillComponent ? ( {SelectedSkillComponent ? (
<SelectedSkillComponent <>
skill={configurableSkills[selectedSkill]?.skill} {selectedSkill.imported ? (
settings={settings} <ImportedSkillConfig
toggleSkill={toggleAgentSkill} key={selectedSkill.hubId}
enabled={agentSkills.includes( selectedSkill={selectedSkill}
configurableSkills[selectedSkill]?.skill setImportedSkills={setImportedSkills}
/>
) : (
<SelectedSkillComponent
skill={configurableSkills[selectedSkill]?.skill}
settings={settings}
toggleSkill={toggleAgentSkill}
enabled={agentSkills.includes(
configurableSkills[selectedSkill]?.skill
)}
setHasChanges={setHasChanges}
{...(configurableSkills[selectedSkill] ||
defaultSkills[selectedSkill])}
/>
)} )}
setHasChanges={setHasChanges} </>
{...(configurableSkills[selectedSkill] ||
defaultSkills[selectedSkill])}
/>
) : ( ) : (
<div className="flex flex-col items-center justify-center h-full text-white/60"> <div className="flex flex-col items-center justify-center h-full text-white/60">
<Robot size={40} /> <Robot size={40} />

View File

@ -115,7 +115,6 @@ export default function AdminSystem() {
}} }}
value={messageLimit.limit} value={messageLimit.limit}
min={1} min={1}
max={300}
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-60 p-2.5" className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-60 p-2.5"
/> />
</div> </div>

View File

@ -1,66 +0,0 @@
import { useEffect, useState } from "react";
import useUser from "@/hooks/useUser";
import FineTuning from "@/models/experimental/fineTuning";
import { createPortal } from "react-dom";
import { Sparkle } from "@phosphor-icons/react";
import { Link, useLocation } from "react-router-dom";
import paths from "@/utils/paths";
export function FineTuningAlert() {
const { user } = useUser();
const location = useLocation();
const [className, setClassName] = useState("top-banner");
const [isEligible, setIsEligible] = useState(false);
function dismissAlert() {
setClassName("rm-top-banner");
window?.localStorage?.setItem(FineTuning.cacheKeys.dismissed_cta, "1");
setTimeout(() => {
setIsEligible(false);
}, 550);
}
useEffect(() => {
if (!FineTuning.canAlert(user)) return;
if (
location.pathname === paths.orderFineTune() ||
location.pathname === paths.settings.chats()
)
return;
FineTuning.checkEligibility()
.then((eligible) => setIsEligible(eligible))
.catch(() => null);
}, [user]);
if (!isEligible) return null;
return createPortal(
<div
className={`fixed ${className} left-0 right-0 h-14 bg-orange-400 flex items-center justify-end px-4 z-[9999]`}
>
<Link
onClick={dismissAlert}
to={paths.orderFineTune()}
className="grow w-full h-full ml-4 py-1"
>
<div className="flex flex-col items-center w-full">
<div className="flex w-full justify-center items-center gap-x-2">
<Sparkle size={20} className="text-white" />
<p className="text-white font-medium text-lg">
You have enough data for a fine-tune!
</p>
</div>
<p className="text-xs text-white">click to learn more &rarr;</p>
</div>
</Link>
<div className="flex items-center gap-x-2 shrink-0">
<button
onClick={dismissAlert}
className="border-none text-white font-medium text-sm px-[10px] py-[6px] rounded-md bg-white/5 hover:bg-white/10"
>
Dismiss
</button>
</div>
</div>,
document.getElementById("root")
);
}

View File

@ -0,0 +1,120 @@
import { useRef, useState } from "react";
import BrowserExtensionApiKey from "@/models/browserExtensionApiKey";
import showToast from "@/utils/toast";
import { Trash, Copy, Check, Plug } from "@phosphor-icons/react";
import { POPUP_BROWSER_EXTENSION_EVENT } from "@/utils/constants";
import { Tooltip } from "react-tooltip";
export default function BrowserExtensionApiKeyRow({
apiKey,
removeApiKey,
connectionString,
isMultiUser,
}) {
const rowRef = useRef(null);
const [copied, setCopied] = useState(false);
const handleRevoke = async () => {
if (
!window.confirm(
`Are you sure you want to revoke this browser extension API key?\nAfter you do this it will no longer be useable.\n\nThis action is irreversible.`
)
)
return false;
const result = await BrowserExtensionApiKey.revoke(apiKey.id);
if (result.success) {
removeApiKey(apiKey.id);
showToast("Browser Extension API Key permanently revoked", "info", {
clear: true,
});
} else {
showToast("Failed to revoke API Key", "error", {
clear: true,
});
}
};
const handleCopy = () => {
navigator.clipboard.writeText(connectionString);
showToast("Connection string copied to clipboard", "success", {
clear: true,
});
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const handleConnect = () => {
// Sending a message to Chrome extension to pop up the extension window
// This will open the extension window and attempt to connect with the API key
window.postMessage(
{ type: POPUP_BROWSER_EXTENSION_EVENT, apiKey: connectionString },
"*"
);
showToast("Attempting to connect to browser extension...", "info", {
clear: true,
});
};
return (
<tr
ref={rowRef}
className="bg-transparent text-white text-opacity-80 text-sm font-medium"
>
<td scope="row" className="px-6 py-4 whitespace-nowrap flex items-center">
<span className="mr-2 font-mono">{connectionString}</span>
<div className="flex items-center space-x-2">
<button
onClick={handleCopy}
data-tooltip-id={`copy-connection-text-${apiKey.id}`}
data-tooltip-content="Copy connection string"
className="text-white hover:text-white/80 transition-colors duration-200 p-1 rounded"
>
{copied ? (
<Check className="h-5 w-5 text-green-500" />
) : (
<Copy className="h-5 w-5" />
)}
<Tooltip
id={`copy-connection-text-${apiKey.id}`}
place="bottom"
delayShow={300}
className="allm-tooltip !allm-text-xs"
/>
</button>
<button
onClick={handleConnect}
data-tooltip-id={`auto-connection-${apiKey.id}`}
data-tooltip-content="Automatically connect to extension"
className="text-white hover:text-white/80 transition-colors duration-200 p-1 rounded"
>
<Plug className="h-5 w-5" />
<Tooltip
id={`auto-connection-${apiKey.id}`}
place="bottom"
delayShow={300}
className="allm-tooltip !allm-text-xs"
/>
</button>
</div>
</td>
{isMultiUser && (
<td className="px-6 py-4">
{apiKey.user ? apiKey.user.username : "N/A"}
</td>
)}
<td className="px-6 py-4">
{new Date(apiKey.createdAt).toLocaleString()}
</td>
<td className="px-6 py-4">
<button
onClick={handleRevoke}
className="font-medium px-2 py-1 rounded-lg hover:bg-sidebar-gradient text-white hover:text-white/80 hover:bg-opacity-20"
>
<Trash className="h-5 w-5" />
</button>
</td>
</tr>
);
}

View File

@ -0,0 +1,127 @@
import React, { useEffect, useState } from "react";
import { X } from "@phosphor-icons/react";
import BrowserExtensionApiKey from "@/models/browserExtensionApiKey";
import { fullApiUrl, POPUP_BROWSER_EXTENSION_EVENT } from "@/utils/constants";
export default function NewBrowserExtensionApiKeyModal({
closeModal,
onSuccess,
isMultiUser,
}) {
const [apiKey, setApiKey] = useState(null);
const [error, setError] = useState(null);
const [copied, setCopied] = useState(false);
const handleCreate = async (e) => {
setError(null);
e.preventDefault();
const { apiKey: newApiKey, error } =
await BrowserExtensionApiKey.generateKey();
if (!!newApiKey) {
const fullApiKey = `${fullApiUrl()}|${newApiKey}`;
setApiKey(fullApiKey);
onSuccess();
window.postMessage(
{ type: POPUP_BROWSER_EXTENSION_EVENT, apiKey: fullApiKey },
"*"
);
}
setError(error);
};
const copyApiKey = () => {
if (!apiKey) return false;
window.navigator.clipboard.writeText(apiKey);
setCopied(true);
};
useEffect(() => {
function resetStatus() {
if (!copied) return false;
setTimeout(() => {
setCopied(false);
}, 3000);
}
resetStatus();
}, [copied]);
return (
<div className="relative w-[500px] max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<h3 className="text-xl font-semibold text-white">
New Browser Extension API Key
</h3>
<button
onClick={closeModal}
type="button"
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border border-none cursor-pointer"
>
<X className="text-gray-300 text-lg" />
</button>
</div>
<form onSubmit={handleCreate}>
<div className="p-6 space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
{apiKey && (
<input
type="text"
defaultValue={apiKey}
disabled={true}
className="rounded-lg px-4 py-2 text-white bg-zinc-900 border border-gray-500/50 border-none"
/>
)}
{isMultiUser && (
<p className="text-yellow-300 text-xs md:text-sm font-semibold">
Warning: You are in multi-user mode, this API key will allow
access to all workspaces associated with your account. Please
share it cautiously.
</p>
)}
<p className="text-white text-xs md:text-sm">
After clicking "Create API Key", AnythingLLM will attempt to
connect to your browser extension automatically.
</p>
<p className="text-white text-xs md:text-sm">
If you see "Connected to AnythingLLM" in the extension, the
connection was successful. If not, please copy the connection
string and paste it into the extension manually.
</p>
</div>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
{!apiKey ? (
<>
<button
onClick={closeModal}
type="button"
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300 border-none"
>
Cancel
</button>
<button
type="submit"
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800 border-none"
>
Create API Key
</button>
</>
) : (
<button
onClick={copyApiKey}
type="button"
disabled={copied}
className="w-full transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800 text-center justify-center border-none cursor-pointer"
>
{copied ? "API Key Copied!" : "Copy API Key"}
</button>
)}
</div>
</form>
</div>
</div>
);
}

View File

@ -0,0 +1,133 @@
import { useEffect, useState } from "react";
import Sidebar from "@/components/SettingsSidebar";
import { isMobile } from "react-device-detect";
import * as Skeleton from "react-loading-skeleton";
import "react-loading-skeleton/dist/skeleton.css";
import { PlusCircle } from "@phosphor-icons/react";
import BrowserExtensionApiKey from "@/models/browserExtensionApiKey";
import BrowserExtensionApiKeyRow from "./BrowserExtensionApiKeyRow";
import CTAButton from "@/components/lib/CTAButton";
import NewBrowserExtensionApiKeyModal from "./NewBrowserExtensionApiKeyModal";
import ModalWrapper from "@/components/ModalWrapper";
import { useModal } from "@/hooks/useModal";
import { fullApiUrl } from "@/utils/constants";
export default function BrowserExtensionApiKeys() {
const [loading, setLoading] = useState(true);
const [apiKeys, setApiKeys] = useState([]);
const [error, setError] = useState(null);
const { isOpen, openModal, closeModal } = useModal();
const [isMultiUser, setIsMultiUser] = useState(false);
useEffect(() => {
fetchExistingKeys();
}, []);
const fetchExistingKeys = async () => {
const result = await BrowserExtensionApiKey.getAll();
if (result.success) {
setApiKeys(result.apiKeys);
setIsMultiUser(result.apiKeys.some((key) => key.user !== null));
} else {
setError(result.error || "Failed to fetch API keys");
}
setLoading(false);
};
const removeApiKey = (id) => {
setApiKeys((prevKeys) => prevKeys.filter((apiKey) => apiKey.id !== id));
};
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-lg leading-6 font-bold text-white">
Browser Extension API Keys
</p>
</div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
Manage API keys for browser extensions connecting to your
AnythingLLM instance.
</p>
</div>
<div className="w-full justify-end flex">
<CTAButton onClick={openModal} className="mt-3 mr-0 -mb-6 z-10">
<PlusCircle className="h-4 w-4" weight="bold" />
Generate New API Key
</CTAButton>
</div>
{loading ? (
<Skeleton.default
height="80vh"
width="100%"
highlightColor="#3D4147"
baseColor="#2C2F35"
count={1}
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
containerClassName="flex w-full"
/>
) : error ? (
<div className="text-red-500 mt-6">Error: {error}</div>
) : (
<table className="w-full text-sm text-left rounded-lg mt-6">
<thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg">
Extension Connection String
</th>
{isMultiUser && (
<th scope="col" className="px-6 py-3">
Created By
</th>
)}
<th scope="col" className="px-6 py-3">
Created At
</th>
<th scope="col" className="px-6 py-3 rounded-tr-lg">
Actions
</th>
</tr>
</thead>
<tbody>
{apiKeys.length === 0 ? (
<tr className="bg-transparent text-white text-opacity-80 text-sm font-medium">
<td
colSpan={isMultiUser ? "4" : "3"}
className="px-6 py-4 text-center"
>
No API keys found
</td>
</tr>
) : (
apiKeys.map((apiKey) => (
<BrowserExtensionApiKeyRow
key={apiKey.id}
apiKey={apiKey}
removeApiKey={removeApiKey}
connectionString={`${fullApiUrl()}|${apiKey.key}`}
isMultiUser={isMultiUser}
/>
))
)}
</tbody>
</table>
)}
</div>
</div>
<ModalWrapper isOpen={isOpen}>
<NewBrowserExtensionApiKeyModal
closeModal={closeModal}
onSuccess={fetchExistingKeys}
isMultiUser={isMultiUser}
/>
</ModalWrapper>
</div>
);
}

View File

@ -250,6 +250,25 @@ export const PermittedDomains = ({ defaultValue = [] }) => {
setDomains(validDomains); setDomains(validDomains);
}; };
const handleBlur = (event) => {
const currentInput = event.target.value;
if (!currentInput) return;
const validDomains = [...domains, currentInput].map((input) => {
let url = input;
if (!url.includes("http://") && !url.includes("https://"))
url = `https://${url}`;
try {
new URL(url);
return url;
} catch {
return null;
}
});
event.target.value = "";
setDomains(validDomains);
};
return ( return (
<div> <div>
<div className="flex flex-col mb-2"> <div className="flex flex-col mb-2">
@ -270,6 +289,7 @@ export const PermittedDomains = ({ defaultValue = [] }) => {
<TagsInput <TagsInput
value={domains} value={domains}
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur}
placeholder="https://mysite.com, https://anythingllm.com" placeholder="https://mysite.com, https://anythingllm.com"
classNames={{ classNames={{
tag: "bg-blue-300/10 text-zinc-800 m-1", tag: "bg-blue-300/10 text-zinc-800 m-1",

View File

@ -5,7 +5,6 @@ import PasswordModal, { usePasswordModal } from "@/components/Modals/Password";
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
import { FullScreenLoader } from "@/components/Preloader"; import { FullScreenLoader } from "@/components/Preloader";
import UserMenu from "@/components/UserMenu"; import UserMenu from "@/components/UserMenu";
import { FineTuningAlert } from "../FineTuning/Banner";
export default function Main() { export default function Main() {
const { loading, requiresAuth, mode } = usePasswordModal(); const { loading, requiresAuth, mode } = usePasswordModal();
@ -23,7 +22,6 @@ export default function Main() {
<DefaultChatContainer /> <DefaultChatContainer />
</div> </div>
</UserMenu> </UserMenu>
<FineTuningAlert />
</> </>
); );
} }

View File

@ -6,7 +6,6 @@ import Workspace from "@/models/workspace";
import PasswordModal, { usePasswordModal } from "@/components/Modals/Password"; import PasswordModal, { usePasswordModal } from "@/components/Modals/Password";
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
import { FullScreenLoader } from "@/components/Preloader"; import { FullScreenLoader } from "@/components/Preloader";
import { FineTuningAlert } from "../FineTuning/Banner";
export default function WorkspaceChat() { export default function WorkspaceChat() {
const { loading, requiresAuth, mode } = usePasswordModal(); const { loading, requiresAuth, mode } = usePasswordModal();
@ -50,7 +49,6 @@ function ShowWorkspaceChat() {
{!isMobile && <Sidebar />} {!isMobile && <Sidebar />}
<WorkspaceChatContainer loading={loading} workspace={workspace} /> <WorkspaceChatContainer loading={loading} workspace={workspace} />
</div> </div>
<FineTuningAlert />
</> </>
); );
} }

View File

@ -41,3 +41,5 @@ export function fullApiUrl() {
if (API_BASE !== "/api") return API_BASE; if (API_BASE !== "/api") return API_BASE;
return `${window.location.origin}/api`; return `${window.location.origin}/api`;
} }
export const POPUP_BROWSER_EXTENSION_EVENT = "NEW_BROWSER_EXTENSION_CONNECTION";

View File

@ -138,6 +138,9 @@ export default {
embedChats: () => { embedChats: () => {
return `/settings/embed-chats`; return `/settings/embed-chats`;
}, },
browserExtension: () => {
return `/settings/browser-extension`;
},
experimental: () => { experimental: () => {
return `/settings/beta-features`; return `/settings/beta-features`;
}, },

View File

@ -483,14 +483,6 @@
"@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14" "@jridgewell/sourcemap-codec" "^1.4.14"
"@metamask/jazzicon@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@metamask/jazzicon/-/jazzicon-2.0.0.tgz#5615528e91c0fc5c9d79202d1f0954a7922525a0"
integrity sha512-7M+WSZWKcQAo0LEhErKf1z+D3YX0tEDAcGvcKbDyvDg34uvgeKR00mFNIYwAhdAS9t8YXxhxZgsrRBBg6X8UQg==
dependencies:
color "^0.11.3"
mersenne-twister "^1.1.0"
"@microsoft/fetch-event-source@^2.0.1": "@microsoft/fetch-event-source@^2.0.1":
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz#9ceecc94b49fbaa15666e38ae8587f64acce007d" resolved "https://registry.yarnpkg.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz#9ceecc94b49fbaa15666e38ae8587f64acce007d"
@ -1097,11 +1089,6 @@ cliui@^8.0.1:
strip-ansi "^6.0.1" strip-ansi "^6.0.1"
wrap-ansi "^7.0.0" wrap-ansi "^7.0.0"
clone@^1.0.2:
version "1.0.4"
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
clsx@^1.1.1: clsx@^1.1.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
@ -1112,7 +1099,7 @@ clsx@^2.0.0:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
color-convert@^1.3.0, color-convert@^1.9.0: color-convert@^1.9.0:
version "1.9.3" version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@ -1131,27 +1118,11 @@ color-name@1.1.3:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
color-name@^1.0.0, color-name@~1.1.4: color-name@~1.1.4:
version "1.1.4" version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-string@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991"
integrity sha512-sz29j1bmSDfoAxKIEU6zwoIZXN6BrFbAMIhfYCNyiZXBDuU/aiHlN84lp/xDzL2ubyFhLDobHIlU1X70XRrMDA==
dependencies:
color-name "^1.0.0"
color@^0.11.3:
version "0.11.4"
resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764"
integrity sha512-Ajpjd8asqZ6EdxQeqGzU5WBhhTfJ/0cA4Wlbre7e5vXfmDSmda7Ov6jeKoru+b0vHcb1CqvuroTHp5zIWzhVMA==
dependencies:
clone "^1.0.2"
color-convert "^1.3.0"
color-string "^0.3.0"
commander@^4.0.0: commander@^4.0.0:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
@ -2545,11 +2516,6 @@ merge2@^1.3.0:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
mersenne-twister@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mersenne-twister/-/mersenne-twister-1.1.0.tgz#f916618ee43d7179efcf641bec4531eb9670978a"
integrity sha512-mUYWsMKNrm4lfygPkL3OfGzOPTR2DBlTkBNHM//F6hGp8cLThY897crAlk3/Jo17LEOOjQUrNAx6DvgO77QJkA==
micromatch@^4.0.4, micromatch@^4.0.5: micromatch@^4.0.4, micromatch@^4.0.5:
version "4.0.7" version "4.0.7"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5"

View File

@ -10,7 +10,7 @@
"node": ">=18" "node": ">=18"
}, },
"scripts": { "scripts": {
"lint": "cd server && yarn lint && cd ../frontend && yarn lint && cd ../embed && yarn lint && cd ../collector && yarn lint", "lint": "cd server && yarn lint && cd ../frontend && yarn lint && cd ../collector && yarn lint",
"setup": "cd server && yarn && cd ../collector && yarn && cd ../frontend && yarn && cd .. && yarn setup:envs && yarn prisma:setup && echo \"Please run yarn dev:server, yarn dev:collector, and yarn dev:frontend in separate terminal tabs.\"", "setup": "cd server && yarn && cd ../collector && yarn && cd ../frontend && yarn && cd .. && yarn setup:envs && yarn prisma:setup && echo \"Please run yarn dev:server, yarn dev:collector, and yarn dev:frontend in separate terminal tabs.\"",
"setup:envs": "cp -n ./frontend/.env.example ./frontend/.env && cp -n ./server/.env.example ./server/.env.development && cp -n ./collector/.env.example ./collector/.env && cp -n ./docker/.env.example ./docker/.env && echo \"All ENV files copied!\n\"", "setup:envs": "cp -n ./frontend/.env.example ./frontend/.env && cp -n ./server/.env.example ./server/.env.development && cp -n ./collector/.env.example ./collector/.env && cp -n ./docker/.env.example ./docker/.env && echo \"All ENV files copied!\n\"",
"dev:server": "cd server && yarn dev", "dev:server": "cd server && yarn dev",

View File

@ -241,6 +241,10 @@ TTS_PROVIDER="native"
# AGENT_GSE_KEY= # AGENT_GSE_KEY=
# AGENT_GSE_CTX= # AGENT_GSE_CTX=
#------ SearchApi.io ----------- https://www.searchapi.io/
# AGENT_SEARCHAPI_API_KEY=
# AGENT_SEARCHAPI_ENGINE=google
#------ Serper.dev ----------- https://serper.dev/ #------ Serper.dev ----------- https://serper.dev/
# AGENT_SERPER_DEV_KEY= # AGENT_SERPER_DEV_KEY=

1
server/.gitignore vendored
View File

@ -8,6 +8,7 @@ storage/tmp/*
storage/vector-cache/*.json storage/vector-cache/*.json
storage/exports storage/exports
storage/imports storage/imports
storage/plugins/agent-skills/*
!storage/documents/DOCUMENTS.md !storage/documents/DOCUMENTS.md
logs/server.log logs/server.log
*.db *.db

Some files were not shown because too many files have changed in this diff Show More