mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-19 12:40:09 +01:00
Merge branch 'master' of github.com:Mintplex-Labs/anything-llm into render
This commit is contained in:
commit
393772c4a5
@ -22,7 +22,7 @@
|
||||
// Terraform support
|
||||
"ghcr.io/devcontainers/features/terraform:1": {},
|
||||
// Just a wrap to install needed packages
|
||||
"ghcr.io/rocker-org/devcontainer-features/apt-packages:1": {
|
||||
"ghcr.io/devcontainers-contrib/features/apt-packages:1": {
|
||||
// Dependencies copied from ../docker/Dockerfile plus some dev stuff
|
||||
"packages": [
|
||||
"build-essential",
|
||||
|
@ -10,3 +10,7 @@ frontend/bundleinspector.html
|
||||
|
||||
#server
|
||||
server/swagger/openapi.json
|
||||
|
||||
#embed
|
||||
**/static/**
|
||||
embed/src/utils/chat/hljs.js
|
||||
|
@ -17,7 +17,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "*.config.js",
|
||||
"files": ["*.config.js"],
|
||||
"options": {
|
||||
"semi": false,
|
||||
"parser": "flow",
|
||||
|
@ -29,7 +29,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<b>English</b> · <a href='/README.zh-CN.md'>简体中文</a>
|
||||
<b>English</b> · <a href='./locales/README.zh-CN.md'>简体中文</a> · <a href='./locales/README.ja-JP.md'>日本語</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@ -123,7 +123,7 @@ Some cool features of AnythingLLM
|
||||
- [Pinecone](https://pinecone.io)
|
||||
- [Chroma](https://trychroma.com)
|
||||
- [Weaviate](https://weaviate.io)
|
||||
- [QDrant](https://qdrant.tech)
|
||||
- [Qdrant](https://qdrant.tech)
|
||||
- [Milvus](https://milvus.io)
|
||||
- [Zilliz](https://zilliz.com)
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
||||
"scripts": {
|
||||
"dev": "NODE_ENV=development nodemon --ignore hotdir --ignore storage --trace-warnings index.js",
|
||||
"start": "NODE_ENV=production node index.js",
|
||||
"lint": "yarn prettier --write ./processSingleFile ./processLink ./utils index.js"
|
||||
"lint": "yarn prettier --ignore-path ../.prettierignore --write ./processSingleFile ./processLink ./utils index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@googleapis/youtube": "^9.0.0",
|
||||
|
@ -14,7 +14,11 @@ class RepoLoader {
|
||||
#validGithubUrl() {
|
||||
const UrlPattern = require("url-pattern");
|
||||
const pattern = new UrlPattern(
|
||||
"https\\://github.com/(:author)/(:project(*))"
|
||||
"https\\://github.com/(:author)/(:project(*))",
|
||||
{
|
||||
// fixes project names with special characters (.github)
|
||||
segmentValueCharset: "a-zA-Z0-9-._~%/+",
|
||||
}
|
||||
);
|
||||
const match = pattern.match(this.repo);
|
||||
if (!match) return false;
|
||||
|
@ -23,6 +23,7 @@ class MimeDetector {
|
||||
{
|
||||
"text/plain": [
|
||||
"ts",
|
||||
"tsx",
|
||||
"py",
|
||||
"opts",
|
||||
"lock",
|
||||
@ -35,6 +36,7 @@ class MimeDetector {
|
||||
"js",
|
||||
"lua",
|
||||
"pas",
|
||||
"r",
|
||||
],
|
||||
},
|
||||
true
|
||||
|
@ -128,6 +128,12 @@ GID='1000'
|
||||
# VOYAGEAI_API_KEY=
|
||||
# EMBEDDING_MODEL_PREF='voyage-large-2-instruct'
|
||||
|
||||
# EMBEDDING_ENGINE='litellm'
|
||||
# EMBEDDING_MODEL_PREF='text-embedding-ada-002'
|
||||
# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192
|
||||
# LITE_LLM_BASE_PATH='http://127.0.0.1:4000'
|
||||
# LITE_LLM_API_KEY='sk-123abc'
|
||||
|
||||
###########################################
|
||||
######## Vector Database Selection ########
|
||||
###########################################
|
||||
@ -233,3 +239,9 @@ GID='1000'
|
||||
|
||||
#------ Serper.dev ----------- https://serper.dev/
|
||||
# AGENT_SERPER_DEV_KEY=
|
||||
|
||||
#------ Bing Search ----------- https://portal.azure.com/
|
||||
# AGENT_BING_SEARCH_API_KEY=
|
||||
|
||||
#------ Serply.io ----------- https://serply.io/
|
||||
# AGENT_SERPLY_API_KEY=
|
||||
|
@ -86,6 +86,49 @@ mintplexlabs/anythingllm;
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> Docker Compose</td>
|
||||
<td>
|
||||
version: '3.8'
|
||||
services:
|
||||
anythingllm:
|
||||
image: mintplexlabs/anythingllm
|
||||
container_name: anythingllm
|
||||
ports:
|
||||
- "3001:3001"
|
||||
cap_add:
|
||||
- SYS_ADMIN
|
||||
environment:
|
||||
# Adjust for your environemnt
|
||||
- STORAGE_DIR=/app/server/storage
|
||||
- JWT_SECRET="make this a large list of random numbers and letters 20+"
|
||||
- LLM_PROVIDER=ollama
|
||||
- OLLAMA_BASE_PATH=http://127.0.0.1:11434
|
||||
- OLLAMA_MODEL_PREF=llama2
|
||||
- OLLAMA_MODEL_TOKEN_LIMIT=4096
|
||||
- EMBEDDING_ENGINE=ollama
|
||||
- EMBEDDING_BASE_PATH=http://127.0.0.1:11434
|
||||
- EMBEDDING_MODEL_PREF=nomic-embed-text:latest
|
||||
- EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192
|
||||
- VECTOR_DB=lancedb
|
||||
- WHISPER_PROVIDER=local
|
||||
- TTS_PROVIDER=native
|
||||
- PASSWORDMINCHAR=8
|
||||
- AGENT_SERPER_DEV_KEY="SERPER DEV API KEY"
|
||||
- AGENT_SERPLY_API_KEY="Serply.io API KEY"
|
||||
volumes:
|
||||
- anythingllm_storage:/app/server/storage
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
anythingllm_storage:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: /path/on/local/disk
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
Go to `http://localhost:3001` and you are now using AnythingLLM! All your data and progress will persist between
|
||||
|
@ -1,9 +0,0 @@
|
||||
# defaults
|
||||
**/.git
|
||||
**/.svn
|
||||
**/.hg
|
||||
**/node_modules
|
||||
|
||||
**/dist
|
||||
**/static/**
|
||||
src/utils/chat/hljs.js
|
@ -4,9 +4,7 @@
|
||||
"target": "esnext",
|
||||
"jsx": "react",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
}
|
||||
}
|
||||
"@/*": ["./src/*"],
|
||||
},
|
||||
},
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "anythingllm-embedded-chat",
|
||||
"private": false,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "nodemon -e js,jsx,css --watch src --exec \"yarn run dev:preview\"",
|
||||
@ -8,7 +9,7 @@
|
||||
"dev:build": "vite build && cat src/static/tailwind@3.4.1.js >> dist/anythingllm-chat-widget.js",
|
||||
"build": "vite build && cat src/static/tailwind@3.4.1.js >> dist/anythingllm-chat-widget.js && npx terser --compress -o dist/anythingllm-chat-widget.min.js -- dist/anythingllm-chat-widget.js",
|
||||
"build:publish": "yarn build && mkdir -p ../frontend/public/embed && cp -r dist/anythingllm-chat-widget.min.js ../frontend/public/embed/anythingllm-chat-widget.min.js",
|
||||
"lint": "yarn prettier --write ./src"
|
||||
"lint": "yarn prettier --ignore-path ../.prettierignore --write ./src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
|
@ -38,7 +38,7 @@ export default defineConfig({
|
||||
rollupOptions: {
|
||||
external: [
|
||||
// Reduces transformation time by 50% and we don't even use this variant, so we can ignore.
|
||||
/@phosphor-icons\/react\/dist\/ssr/,
|
||||
/@phosphor-icons\/react\/dist\/ssr/
|
||||
]
|
||||
},
|
||||
commonjsOptions: {
|
||||
@ -51,7 +51,7 @@ export default defineConfig({
|
||||
emptyOutDir: true,
|
||||
inlineDynamicImports: true,
|
||||
assetsDir: "",
|
||||
sourcemap: 'inline',
|
||||
sourcemap: "inline"
|
||||
},
|
||||
optimizeDeps: {
|
||||
esbuildOptions: {
|
||||
@ -60,5 +60,5 @@ export default defineConfig({
|
||||
},
|
||||
plugins: []
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
@ -4,9 +4,7 @@
|
||||
"target": "esnext",
|
||||
"jsx": "react",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@
|
||||
"start": "vite --open",
|
||||
"dev": "NODE_ENV=development vite --debug --host=0.0.0.0",
|
||||
"build": "vite build",
|
||||
"lint": "yarn prettier --write ./src",
|
||||
"lint": "yarn prettier --ignore-path ../.prettierignore --write ./src",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -19,6 +19,7 @@
|
||||
"file-saver": "^2.0.5",
|
||||
"he": "^1.2.0",
|
||||
"highlight.js": "^11.9.0",
|
||||
"js-levenshtein": "^1.1.6",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"markdown-it": "^13.0.1",
|
||||
"pluralize": "^8.0.0",
|
||||
|
@ -1,27 +1,41 @@
|
||||
import { createContext, useEffect, useState } from "react";
|
||||
import AnythingLLM from "./media/logo/anything-llm.png";
|
||||
import DefaultLoginLogo from "./media/illustrations/login-logo.svg";
|
||||
import System from "./models/system";
|
||||
|
||||
export const LogoContext = createContext();
|
||||
|
||||
export function LogoProvider({ children }) {
|
||||
const [logo, setLogo] = useState("");
|
||||
const [loginLogo, setLoginLogo] = useState("");
|
||||
const [isCustomLogo, setIsCustomLogo] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchInstanceLogo() {
|
||||
try {
|
||||
const logoURL = await System.fetchLogo();
|
||||
logoURL ? setLogo(logoURL) : setLogo(AnythingLLM);
|
||||
const { isCustomLogo, logoURL } = await System.fetchLogo();
|
||||
if (logoURL) {
|
||||
setLogo(logoURL);
|
||||
setLoginLogo(isCustomLogo ? logoURL : DefaultLoginLogo);
|
||||
setIsCustomLogo(isCustomLogo);
|
||||
} else {
|
||||
setLogo(AnythingLLM);
|
||||
setLoginLogo(DefaultLoginLogo);
|
||||
setIsCustomLogo(false);
|
||||
}
|
||||
} catch (err) {
|
||||
setLogo(AnythingLLM);
|
||||
setLoginLogo(DefaultLoginLogo);
|
||||
setIsCustomLogo(false);
|
||||
console.error("Failed to fetch logo:", err);
|
||||
}
|
||||
}
|
||||
|
||||
fetchInstanceLogo();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LogoContext.Provider value={{ logo, setLogo }}>
|
||||
<LogoContext.Provider value={{ logo, setLogo, loginLogo, isCustomLogo }}>
|
||||
{children}
|
||||
</LogoContext.Provider>
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import Jazzicon from "../UserIcon";
|
||||
import UserIcon from "../UserIcon";
|
||||
import { userFromStorage } from "@/utils/request";
|
||||
import { AI_BACKGROUND_COLOR, USER_BACKGROUND_COLOR } from "@/utils/constants";
|
||||
|
||||
@ -11,8 +11,7 @@ export default function ChatBubble({ message, type, popMsg }) {
|
||||
<div className={`flex justify-center items-end w-full ${backgroundColor}`}>
|
||||
<div className={`py-8 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}>
|
||||
<div className="flex gap-x-5">
|
||||
<Jazzicon
|
||||
size={36}
|
||||
<UserIcon
|
||||
user={{ uid: isUser ? userFromStorage()?.username : "system" }}
|
||||
role={type}
|
||||
/>
|
||||
|
@ -13,7 +13,7 @@ import { isMobile } from "react-device-detect";
|
||||
import { SidebarMobileHeader } from "../Sidebar";
|
||||
import ChatBubble from "../ChatBubble";
|
||||
import System from "@/models/system";
|
||||
import Jazzicon from "../UserIcon";
|
||||
import UserIcon from "../UserIcon";
|
||||
import { userFromStorage } from "@/utils/request";
|
||||
import { AI_BACKGROUND_COLOR, USER_BACKGROUND_COLOR } from "@/utils/constants";
|
||||
import useUser from "@/hooks/useUser";
|
||||
@ -46,7 +46,7 @@ export default function DefaultChatContainer() {
|
||||
className={`pt-10 pb-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
|
||||
>
|
||||
<div className="flex gap-x-5">
|
||||
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
|
||||
<UserIcon user={{ uid: "system" }} role={"assistant"} />
|
||||
|
||||
<span
|
||||
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||
@ -70,7 +70,7 @@ export default function DefaultChatContainer() {
|
||||
className={`pb-4 pt-2 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
|
||||
>
|
||||
<div className="flex gap-x-5">
|
||||
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
|
||||
<UserIcon user={{ uid: "system" }} role={"assistant"} />
|
||||
|
||||
<span
|
||||
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||
@ -93,7 +93,7 @@ export default function DefaultChatContainer() {
|
||||
className={`pt-2 pb-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
|
||||
>
|
||||
<div className="flex gap-x-5">
|
||||
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
|
||||
<UserIcon user={{ uid: "system" }} role={"assistant"} />
|
||||
<div>
|
||||
<span
|
||||
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||
@ -127,8 +127,7 @@ export default function DefaultChatContainer() {
|
||||
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
|
||||
>
|
||||
<div className="flex gap-x-5">
|
||||
<Jazzicon
|
||||
size={36}
|
||||
<UserIcon
|
||||
user={{ uid: userFromStorage()?.username }}
|
||||
role={"user"}
|
||||
/>
|
||||
@ -151,7 +150,7 @@ export default function DefaultChatContainer() {
|
||||
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
|
||||
>
|
||||
<div className="flex gap-x-5">
|
||||
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
|
||||
<UserIcon user={{ uid: "system" }} role={"assistant"} />
|
||||
<div>
|
||||
<span
|
||||
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||
@ -188,8 +187,7 @@ export default function DefaultChatContainer() {
|
||||
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
|
||||
>
|
||||
<div className="flex gap-x-5">
|
||||
<Jazzicon
|
||||
size={36}
|
||||
<UserIcon
|
||||
user={{ uid: userFromStorage()?.username }}
|
||||
role={"user"}
|
||||
/>
|
||||
@ -213,7 +211,7 @@ export default function DefaultChatContainer() {
|
||||
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
|
||||
>
|
||||
<div className="flex gap-x-5">
|
||||
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
|
||||
<UserIcon user={{ uid: "system" }} role={"assistant"} />
|
||||
|
||||
<span
|
||||
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||
@ -251,8 +249,7 @@ export default function DefaultChatContainer() {
|
||||
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
|
||||
>
|
||||
<div className="flex gap-x-5">
|
||||
<Jazzicon
|
||||
size={36}
|
||||
<UserIcon
|
||||
user={{ uid: userFromStorage()?.username }}
|
||||
role={"user"}
|
||||
/>
|
||||
@ -275,7 +272,7 @@ export default function DefaultChatContainer() {
|
||||
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
|
||||
>
|
||||
<div className="flex gap-x-5">
|
||||
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
|
||||
<UserIcon user={{ uid: "system" }} role={"assistant"} />
|
||||
<div>
|
||||
<span
|
||||
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||
|
@ -0,0 +1,186 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import System from "@/models/system";
|
||||
import { Warning } from "@phosphor-icons/react";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
|
||||
export default function LiteLLMOptions({ settings }) {
|
||||
const [basePathValue, setBasePathValue] = useState(settings?.LiteLLMBasePath);
|
||||
const [basePath, setBasePath] = useState(settings?.LiteLLMBasePath);
|
||||
const [apiKeyValue, setApiKeyValue] = useState(settings?.LiteLLMAPIKey);
|
||||
const [apiKey, setApiKey] = useState(settings?.LiteLLMAPIKey);
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-y-4">
|
||||
<div className="w-full flex items-center gap-4">
|
||||
<div className="flex flex-col w-60">
|
||||
<label className="text-white text-sm font-semibold block mb-4">
|
||||
Base URL
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
name="LiteLLMBasePath"
|
||||
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||
placeholder="http://127.0.0.1:4000"
|
||||
defaultValue={settings?.LiteLLMBasePath}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
onChange={(e) => setBasePathValue(e.target.value)}
|
||||
onBlur={() => setBasePath(basePathValue)}
|
||||
/>
|
||||
</div>
|
||||
<LiteLLMModelSelection
|
||||
settings={settings}
|
||||
basePath={basePath}
|
||||
apiKey={apiKey}
|
||||
/>
|
||||
<div className="flex flex-col w-60">
|
||||
<label className="text-white text-sm font-semibold block mb-4">
|
||||
Max embedding chunk length
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="EmbeddingModelMaxChunkLength"
|
||||
className="bg-zinc-900 text-white placeholder-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||
placeholder="8192"
|
||||
min={1}
|
||||
onScroll={(e) => e.target.blur()}
|
||||
defaultValue={settings?.EmbeddingModelMaxChunkLength}
|
||||
required={false}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex items-center gap-4">
|
||||
<div className="flex flex-col w-60">
|
||||
<div className="flex flex-col gap-y-1 mb-4">
|
||||
<label className="text-white text-sm font-semibold flex items-center gap-x-2">
|
||||
API Key <p className="!text-xs !italic !font-thin">optional</p>
|
||||
</label>
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
name="LiteLLMAPIKey"
|
||||
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||
placeholder="sk-mysecretkey"
|
||||
defaultValue={settings?.LiteLLMAPIKey ? "*".repeat(20) : ""}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
onChange={(e) => setApiKeyValue(e.target.value)}
|
||||
onBlur={() => setApiKey(apiKeyValue)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LiteLLMModelSelection({ settings, basePath = null, apiKey = null }) {
|
||||
const [customModels, setCustomModels] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function findCustomModels() {
|
||||
if (!basePath) {
|
||||
setCustomModels([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const { models } = await System.customModels(
|
||||
"litellm",
|
||||
typeof apiKey === "boolean" ? null : apiKey,
|
||||
basePath
|
||||
);
|
||||
setCustomModels(models || []);
|
||||
setLoading(false);
|
||||
}
|
||||
findCustomModels();
|
||||
}, [basePath, apiKey]);
|
||||
|
||||
if (loading || customModels.length == 0) {
|
||||
return (
|
||||
<div className="flex flex-col w-60">
|
||||
<label className="text-white text-sm font-semibold block mb-4">
|
||||
Embedding Model Selection
|
||||
</label>
|
||||
<select
|
||||
name="EmbeddingModelPref"
|
||||
disabled={true}
|
||||
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
|
||||
>
|
||||
<option disabled={true} selected={true}>
|
||||
{basePath?.includes("/v1")
|
||||
? "-- loading available models --"
|
||||
: "-- waiting for URL --"}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-60">
|
||||
<div className="flex items-center">
|
||||
<label className="text-white text-sm font-semibold block mb-4">
|
||||
Embedding Model Selection
|
||||
</label>
|
||||
<EmbeddingModelTooltip />
|
||||
</div>
|
||||
<select
|
||||
name="EmbeddingModelPref"
|
||||
required={true}
|
||||
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
|
||||
>
|
||||
{customModels.length > 0 && (
|
||||
<optgroup label="Your loaded models">
|
||||
{customModels.map((model) => {
|
||||
return (
|
||||
<option
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
selected={settings.EmbeddingModelPref === model.id}
|
||||
>
|
||||
{model.id}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</optgroup>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmbeddingModelTooltip() {
|
||||
return (
|
||||
<div className="flex items-center justify-center -mt-3 ml-1">
|
||||
<Warning
|
||||
size={14}
|
||||
className="ml-1 text-orange-500 cursor-pointer"
|
||||
data-tooltip-id="model-tooltip"
|
||||
data-tooltip-place="right"
|
||||
/>
|
||||
<Tooltip
|
||||
delayHide={300}
|
||||
id="model-tooltip"
|
||||
className="max-w-xs"
|
||||
clickable={true}
|
||||
>
|
||||
<p className="text-sm">
|
||||
Be sure to select a valid embedding model. Chat models are not
|
||||
embedding models. See{" "}
|
||||
<a
|
||||
href="https://litellm.vercel.app/docs/embedding/supported_embedding"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
this page
|
||||
</a>{" "}
|
||||
for more information.
|
||||
</p>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -32,6 +32,7 @@ export default function GeminiLLMOptions({ settings }) {
|
||||
>
|
||||
{[
|
||||
"gemini-pro",
|
||||
"gemini-1.0-pro",
|
||||
"gemini-1.5-pro-latest",
|
||||
"gemini-1.5-flash-latest",
|
||||
].map((model) => {
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
export default function ModalWrapper({ children, isOpen }) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
return createPortal(
|
||||
<div className="bg-black/60 backdrop-blur-sm fixed top-0 left-0 outline-none w-screen h-screen flex items-center justify-center z-30">
|
||||
{children}
|
||||
</div>
|
||||
</div>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,90 @@
|
||||
import React, { useState } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import Document from "@/models/document";
|
||||
|
||||
export default function NewFolderModal({ closeModal, files, setFiles }) {
|
||||
const [error, setError] = useState(null);
|
||||
const [folderName, setFolderName] = useState("");
|
||||
|
||||
const handleCreate = async (e) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
if (folderName.trim() !== "") {
|
||||
const newFolder = {
|
||||
name: folderName,
|
||||
type: "folder",
|
||||
items: [],
|
||||
};
|
||||
const { success } = await Document.createFolder(folderName);
|
||||
if (success) {
|
||||
setFiles({
|
||||
...files,
|
||||
items: [...files.items, newFolder],
|
||||
});
|
||||
closeModal();
|
||||
} else {
|
||||
setError("Failed to create folder");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full max-w-xl 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">
|
||||
Create New Folder
|
||||
</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"
|
||||
data-modal-hide="staticModal"
|
||||
>
|
||||
<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">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="folderName"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Folder Name
|
||||
</label>
|
||||
<input
|
||||
name="folderName"
|
||||
type="text"
|
||||
className="bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||
placeholder="Enter folder name"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
value={folderName}
|
||||
onChange={(e) => setFolderName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</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">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
|
||||
>
|
||||
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"
|
||||
>
|
||||
Create Folder
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -3,11 +3,15 @@ import PreLoader from "@/components/Preloader";
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import FolderRow from "./FolderRow";
|
||||
import System from "@/models/system";
|
||||
import { Plus, Trash } from "@phosphor-icons/react";
|
||||
import { MagnifyingGlass, Plus, Trash } from "@phosphor-icons/react";
|
||||
import Document from "@/models/document";
|
||||
import showToast from "@/utils/toast";
|
||||
import FolderSelectionPopup from "./FolderSelectionPopup";
|
||||
import MoveToFolderIcon from "./MoveToFolderIcon";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import NewFolderModal from "./NewFolderModal";
|
||||
import debounce from "lodash.debounce";
|
||||
import { filterFileSearchResults } from "./utils";
|
||||
|
||||
function Directory({
|
||||
files,
|
||||
@ -24,9 +28,13 @@ function Directory({
|
||||
loadingMessage,
|
||||
}) {
|
||||
const [amountSelected, setAmountSelected] = useState(0);
|
||||
const [newFolderName, setNewFolderName] = useState("");
|
||||
const [showNewFolderInput, setShowNewFolderInput] = useState(false);
|
||||
const [showFolderSelection, setShowFolderSelection] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const {
|
||||
isOpen: isFolderModalOpen,
|
||||
openModal: openFolderModal,
|
||||
closeModal: closeFolderModal,
|
||||
} = useModal();
|
||||
|
||||
useEffect(() => {
|
||||
setAmountSelected(Object.keys(selectedItems).length);
|
||||
@ -121,32 +129,6 @@ function Directory({
|
||||
return !!selectedItems[id];
|
||||
};
|
||||
|
||||
const createNewFolder = () => {
|
||||
setShowNewFolderInput(true);
|
||||
};
|
||||
|
||||
const confirmNewFolder = async () => {
|
||||
if (newFolderName.trim() !== "") {
|
||||
const newFolder = {
|
||||
name: newFolderName,
|
||||
type: "folder",
|
||||
items: [],
|
||||
};
|
||||
|
||||
// If folder failed to create - silently fail.
|
||||
const { success } = await Document.createFolder(newFolderName);
|
||||
if (success) {
|
||||
setFiles({
|
||||
...files,
|
||||
items: [...files.items, newFolder],
|
||||
});
|
||||
}
|
||||
|
||||
setNewFolderName("");
|
||||
setShowNewFolderInput(false);
|
||||
}
|
||||
};
|
||||
|
||||
const moveToFolder = async (folder) => {
|
||||
const toMove = [];
|
||||
for (const itemId of Object.keys(selectedItems)) {
|
||||
@ -183,40 +165,39 @@ function Directory({
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleSearch = debounce((e) => {
|
||||
const searchValue = e.target.value;
|
||||
setSearchTerm(searchValue);
|
||||
}, 500);
|
||||
|
||||
const filteredFiles = filterFileSearchResults(files, searchTerm);
|
||||
return (
|
||||
<div className="px-8 pb-8">
|
||||
<div className="flex flex-col gap-y-6">
|
||||
<div className="flex items-center justify-between w-[560px] px-5 relative">
|
||||
<h3 className="text-white text-base font-bold">My Documents</h3>
|
||||
{showNewFolderInput ? (
|
||||
<div className="flex items-center gap-x-2 z-50">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Folder name"
|
||||
value={newFolderName}
|
||||
onChange={(e) => setNewFolderName(e.target.value)}
|
||||
className="bg-zinc-900 text-white placeholder-white/20 text-sm rounded-md p-2.5 w-[150px] h-[32px]"
|
||||
type="search"
|
||||
placeholder="Search for document"
|
||||
onChange={handleSearch}
|
||||
className="search-input bg-zinc-900 text-white placeholder-white/40 text-sm rounded-lg pl-9 pr-2.5 py-2 w-[250px] h-[32px]"
|
||||
/>
|
||||
<MagnifyingGlass
|
||||
size={14}
|
||||
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white"
|
||||
weight="bold"
|
||||
/>
|
||||
<div className="flex gap-x-2">
|
||||
<button
|
||||
onClick={confirmNewFolder}
|
||||
className="text-sky-400 rounded-md text-sm font-bold hover:text-sky-500"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="flex items-center gap-x-2 cursor-pointer px-[14px] py-[7px] -mr-[14px] rounded-lg hover:bg-[#222628]/60"
|
||||
onClick={createNewFolder}
|
||||
className="flex items-center gap-x-2 cursor-pointer px-[14px] py-[7px] -mr-[14px] rounded-lg hover:bg-[#222628]/60 z-20 relative"
|
||||
onClick={openFolderModal}
|
||||
>
|
||||
<Plus size={18} weight="bold" color="#D3D4D4" />
|
||||
<div className="text-[#D3D4D4] text-xs font-bold leading-[18px]">
|
||||
New Folder
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="relative w-[560px] h-[310px] bg-zinc-900 rounded-2xl overflow-hidden">
|
||||
@ -234,8 +215,8 @@ function Directory({
|
||||
{loadingMessage}
|
||||
</p>
|
||||
</div>
|
||||
) : files.items ? (
|
||||
files.items.map(
|
||||
) : filteredFiles.length > 0 ? (
|
||||
filteredFiles.map(
|
||||
(item, index) =>
|
||||
item.type === "folder" && (
|
||||
<FolderRow
|
||||
@ -302,6 +283,7 @@ function Directory({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<UploadFile
|
||||
workspace={workspace}
|
||||
fetchKeys={fetchKeys}
|
||||
@ -309,6 +291,15 @@ function Directory({
|
||||
setLoadingMessage={setLoadingMessage}
|
||||
/>
|
||||
</div>
|
||||
{isFolderModalOpen && (
|
||||
<div className="bg-black/60 backdrop-blur-sm fixed top-0 left-0 outline-none w-screen h-screen flex items-center justify-center z-30">
|
||||
<NewFolderModal
|
||||
closeModal={closeFolderModal}
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
import strDistance from "js-levenshtein";
|
||||
|
||||
const LEVENSHTEIN_MIN = 8;
|
||||
|
||||
// Regular expression pattern to match the v4 UUID and the ending .json
|
||||
const uuidPattern =
|
||||
/-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/;
|
||||
const jsonPattern = /\.json$/;
|
||||
|
||||
// Function to strip UUID v4 and JSON from file names as that will impact search results.
|
||||
const stripUuidAndJsonFromString = (input = "") => {
|
||||
return input
|
||||
?.replace(uuidPattern, "") // remove v4 uuid
|
||||
?.replace(jsonPattern, "") // remove trailing .json
|
||||
?.replace("-", " "); // turn slugged names into spaces
|
||||
};
|
||||
|
||||
export function filterFileSearchResults(files = [], searchTerm = "") {
|
||||
if (!searchTerm) return files?.items || [];
|
||||
|
||||
const searchResult = [];
|
||||
for (const folder of files?.items) {
|
||||
// If folder is a good match then add all its children
|
||||
if (strDistance(folder.name, searchTerm) <= LEVENSHTEIN_MIN) {
|
||||
searchResult.push(folder);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise check children for good results
|
||||
const fileSearchResults = [];
|
||||
for (const file of folder?.items) {
|
||||
if (
|
||||
strDistance(stripUuidAndJsonFromString(file.name), searchTerm) <=
|
||||
LEVENSHTEIN_MIN
|
||||
) {
|
||||
fileSearchResults.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
if (fileSearchResults.length > 0) {
|
||||
searchResult.push({
|
||||
...folder,
|
||||
items: fileSearchResults,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
}
|
@ -168,6 +168,7 @@ export default function MultiUserAuth() {
|
||||
const [token, setToken] = useState(null);
|
||||
const [showRecoveryForm, setShowRecoveryForm] = useState(false);
|
||||
const [showResetPasswordForm, setShowResetPasswordForm] = useState(false);
|
||||
const [customAppName, setCustomAppName] = useState(null);
|
||||
|
||||
const {
|
||||
isOpen: isRecoveryCodeModalOpen,
|
||||
@ -250,6 +251,15 @@ export default function MultiUserAuth() {
|
||||
}
|
||||
}, [downloadComplete, user, token]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCustomAppName = async () => {
|
||||
const { appName } = await System.fetchCustomAppName();
|
||||
setCustomAppName(appName || "");
|
||||
setLoading(false);
|
||||
};
|
||||
fetchCustomAppName();
|
||||
}, []);
|
||||
|
||||
if (showRecoveryForm) {
|
||||
return (
|
||||
<RecoveryForm
|
||||
@ -272,11 +282,11 @@ export default function MultiUserAuth() {
|
||||
Welcome to
|
||||
</h3>
|
||||
<p className="text-4xl md:text-2xl font-bold bg-gradient-to-r from-[#75D6FF] via-[#FFFFFF] to-[#FFFFFF] bg-clip-text text-transparent">
|
||||
AnythingLLM
|
||||
{customAppName || "AnythingLLM"}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-white/90 text-center">
|
||||
Sign in to your AnythingLLM account.
|
||||
Sign in to your {customAppName || "AnythingLLM"} account.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import System from "../../../models/system";
|
||||
import { AUTH_TOKEN } from "../../../utils/constants";
|
||||
import useLogo from "../../../hooks/useLogo";
|
||||
import paths from "../../../utils/paths";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
@ -10,10 +9,10 @@ import RecoveryCodeModal from "@/components/Modals/DisplayRecoveryCodeModal";
|
||||
export default function SingleUserAuth() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const { logo: _initLogo } = useLogo();
|
||||
const [recoveryCodes, setRecoveryCodes] = useState([]);
|
||||
const [downloadComplete, setDownloadComplete] = useState(false);
|
||||
const [token, setToken] = useState(null);
|
||||
const [customAppName, setCustomAppName] = useState(null);
|
||||
|
||||
const {
|
||||
isOpen: isRecoveryCodeModalOpen,
|
||||
@ -57,6 +56,15 @@ export default function SingleUserAuth() {
|
||||
}
|
||||
}, [downloadComplete, token]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCustomAppName = async () => {
|
||||
const { appName } = await System.fetchCustomAppName();
|
||||
setCustomAppName(appName || "");
|
||||
setLoading(false);
|
||||
};
|
||||
fetchCustomAppName();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handleLogin}>
|
||||
@ -68,11 +76,11 @@ export default function SingleUserAuth() {
|
||||
Welcome to
|
||||
</h3>
|
||||
<p className="text-4xl md:text-2xl font-bold bg-gradient-to-r from-[#75D6FF] via-[#FFFFFF] to-[#FFFFFF] bg-clip-text text-transparent">
|
||||
AnythingLLM
|
||||
{customAppName || "AnythingLLM"}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-white/90 text-center">
|
||||
Sign in to your AnythingLLM instance.
|
||||
Sign in to your {customAppName || "AnythingLLM"} instance.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,10 +9,9 @@ import {
|
||||
} from "../../../utils/constants";
|
||||
import useLogo from "../../../hooks/useLogo";
|
||||
import illustration from "@/media/illustrations/login-illustration.svg";
|
||||
import loginLogo from "@/media/illustrations/login-logo.svg";
|
||||
|
||||
export default function PasswordModal({ mode = "single" }) {
|
||||
const { logo: _initLogo } = useLogo();
|
||||
const { loginLogo } = useLogo();
|
||||
return (
|
||||
<div className="fixed top-0 left-0 right-0 z-50 w-full overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] h-full bg-[#25272C] flex flex-col md:flex-row items-center justify-center">
|
||||
<div
|
||||
@ -34,13 +33,14 @@ export default function PasswordModal({ mode = "single" }) {
|
||||
alt="login illustration"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center h-full w-full md:w-1/2 z-50 relative">
|
||||
<div className="flex flex-col items-center justify-center h-full w-full md:w-1/2 z-50 relative -mt-20">
|
||||
<img
|
||||
src={loginLogo}
|
||||
className={`mb-8 w-[84px] h-[84px] absolute ${
|
||||
mode === "single" ? "md:top-50" : "md:top-36"
|
||||
} top-44 z-30`}
|
||||
alt="logo"
|
||||
alt="Logo"
|
||||
className={`hidden relative md:flex rounded-2xl w-fit m-4 z-30 ${
|
||||
mode === "single" ? "md:top-2" : "md:top-12"
|
||||
} absolute max-h-[65px] md:bg-login-gradient md:shadow-[0_4px_14px_rgba(0,0,0,0.25)]`}
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
{mode === "single" ? <SingleUserAuth /> : <MultiUserAuth />}
|
||||
</div>
|
||||
|
@ -111,10 +111,12 @@ export default function SettingsSidebar() {
|
||||
{/* Primary Body */}
|
||||
<div className="h-full flex flex-col w-full justify-between pt-4 overflow-y-scroll no-scroll">
|
||||
<div className="h-auto md:sidebar-items md:dark:sidebar-items">
|
||||
<div className=" flex flex-col gap-y-4 pb-8 overflow-y-scroll no-scroll">
|
||||
<div className="flex flex-col gap-y-4 pb-[60px] overflow-y-scroll no-scroll">
|
||||
<SidebarOptions user={user} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bottom-2 left-0 right-0 pt-2 bg-sidebar bg-opacity-80 backdrop-filter backdrop-blur-md">
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
@ -139,22 +141,21 @@ export default function SettingsSidebar() {
|
||||
</Link>
|
||||
<div
|
||||
ref={sidebarRef}
|
||||
style={{ height: "calc(100% - 76px)" }}
|
||||
className="transition-all duration-500 relative m-[16px] rounded-[16px] bg-sidebar border-2 border-outline min-w-[250px] p-[10px]"
|
||||
className="transition-all duration-500 relative m-[16px] rounded-[16px] bg-sidebar border-2 border-outline min-w-[250px] p-[10px] h-[calc(100%-76px)]"
|
||||
>
|
||||
<div className="w-full h-full flex flex-col overflow-x-hidden items-between min-w-[235px]">
|
||||
<div className="text-white text-opacity-60 text-sm font-medium uppercase mt-[4px] mb-0 ml-2">
|
||||
Instance Settings
|
||||
</div>
|
||||
<div className="relative h-full flex flex-col w-full justify-between pt-[10px] overflow-y-scroll no-scroll">
|
||||
<div className="relative h-[calc(100%-60px)] flex flex-col w-full justify-between pt-[10px] overflow-y-scroll no-scroll">
|
||||
<div className="h-auto sidebar-items">
|
||||
<div className="flex flex-col gap-y-2 h-full pb-8 overflow-y-scroll no-scroll">
|
||||
<div className="flex flex-col gap-y-2 pb-[60px] overflow-y-scroll no-scroll">
|
||||
<SidebarOptions user={user} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<Footer />
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 right-0 pt-4 pb-3 rounded-b-[16px] bg-sidebar bg-opacity-80 backdrop-filter backdrop-blur-md z-10">
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -27,7 +27,6 @@ export default function ThreadItem({
|
||||
const { slug } = useParams();
|
||||
const optionsContainer = useRef(null);
|
||||
const [showOptions, setShowOptions] = useState(false);
|
||||
const [name, setName] = useState(thread.name);
|
||||
const linkTo = !thread.slug
|
||||
? paths.workspace.chat(slug)
|
||||
: paths.workspace.thread(slug, thread.slug);
|
||||
@ -97,7 +96,7 @@ export default function ThreadItem({
|
||||
isActive ? "font-medium text-white" : "text-slate-400"
|
||||
}`}
|
||||
>
|
||||
{truncate(name, 25)}
|
||||
{truncate(thread.name, 25)}
|
||||
</p>
|
||||
</a>
|
||||
)}
|
||||
@ -133,7 +132,6 @@ export default function ThreadItem({
|
||||
workspace={workspace}
|
||||
thread={thread}
|
||||
onRemove={onRemove}
|
||||
onRename={setName}
|
||||
close={() => setShowOptions(false)}
|
||||
/>
|
||||
)}
|
||||
@ -144,14 +142,7 @@ export default function ThreadItem({
|
||||
);
|
||||
}
|
||||
|
||||
function OptionsMenu({
|
||||
containerRef,
|
||||
workspace,
|
||||
thread,
|
||||
onRename,
|
||||
onRemove,
|
||||
close,
|
||||
}) {
|
||||
function OptionsMenu({ containerRef, workspace, thread, onRemove, close }) {
|
||||
const menuRef = useRef(null);
|
||||
|
||||
// Ref menu options
|
||||
@ -208,7 +199,7 @@ function OptionsMenu({
|
||||
return;
|
||||
}
|
||||
|
||||
onRename(name);
|
||||
thread.name = name;
|
||||
close();
|
||||
};
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { Plus, CircleNotch, Trash } from "@phosphor-icons/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import ThreadItem from "./ThreadItem";
|
||||
import { useParams } from "react-router-dom";
|
||||
export const THREAD_RENAME_EVENT = "renameThread";
|
||||
|
||||
export default function ThreadContainer({ workspace }) {
|
||||
const { threadSlug = null } = useParams();
|
||||
@ -12,6 +13,26 @@ export default function ThreadContainer({ workspace }) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [ctrlPressed, setCtrlPressed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const chatHandler = (event) => {
|
||||
const { threadSlug, newName } = event.detail;
|
||||
setThreads((prevThreads) =>
|
||||
prevThreads.map((thread) => {
|
||||
if (thread.slug === threadSlug) {
|
||||
return { ...thread, name: newName };
|
||||
}
|
||||
return thread;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
window.addEventListener(THREAD_RENAME_EVENT, chatHandler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(THREAD_RENAME_EVENT, chatHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchThreads() {
|
||||
if (!workspace.slug) return;
|
||||
@ -22,11 +43,17 @@ export default function ThreadContainer({ workspace }) {
|
||||
fetchThreads();
|
||||
}, [workspace.slug]);
|
||||
|
||||
// Enable toggling of meta-key (ctrl on win and cmd/fn on others)
|
||||
// Enable toggling of bulk-deletion by holding meta-key (ctrl on win and cmd/fn on others)
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event) => {
|
||||
if (["Control", "Meta"].includes(event.key)) {
|
||||
setCtrlPressed((prev) => !prev);
|
||||
setCtrlPressed(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyUp = (event) => {
|
||||
if (["Control", "Meta"].includes(event.key)) {
|
||||
setCtrlPressed(false);
|
||||
// when toggling, unset bulk progress so
|
||||
// previously marked threads that were never deleted
|
||||
// come back to life.
|
||||
@ -37,9 +64,13 @@ export default function ThreadContainer({ workspace }) {
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
window.addEventListener("keyup", handleKeyUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
window.removeEventListener("keyup", handleKeyUp);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -56,7 +87,6 @@ export default function ThreadContainer({ workspace }) {
|
||||
const slugs = threads.filter((t) => t.deleted === true).map((t) => t.slug);
|
||||
await Workspace.threads.deleteBulk(workspace.slug, slugs);
|
||||
setThreads((prev) => prev.filter((t) => !t.deleted));
|
||||
setCtrlPressed(false);
|
||||
};
|
||||
|
||||
function removeThread(threadId) {
|
||||
@ -89,6 +119,7 @@ export default function ThreadContainer({ workspace }) {
|
||||
)
|
||||
? threads.findIndex((thread) => thread?.slug === threadSlug) + 1
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col" role="list" aria-label="Threads">
|
||||
<ThreadItem
|
||||
|
@ -4,7 +4,7 @@ import "react-loading-skeleton/dist/skeleton.css";
|
||||
import Workspace from "@/models/workspace";
|
||||
import ManageWorkspace, {
|
||||
useManageWorkspaceModal,
|
||||
} from "../../Modals/MangeWorkspace";
|
||||
} from "../../Modals/ManageWorkspace";
|
||||
import paths from "@/utils/paths";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { GearSix, SquaresFour, UploadSimple } from "@phosphor-icons/react";
|
||||
|
@ -32,18 +32,17 @@ export default function Sidebar() {
|
||||
<img
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
className="rounded max-h-[24px]"
|
||||
style={{ objectFit: "contain" }}
|
||||
className="rounded max-h-[24px] object-contain"
|
||||
/>
|
||||
</Link>
|
||||
<div
|
||||
ref={sidebarRef}
|
||||
style={{ height: "calc(100% - 76px)" }}
|
||||
className="relative m-[16px] rounded-[16px] bg-sidebar border-2 border-outline min-w-[250px] p-[10px]"
|
||||
className="relative m-[16px] rounded-[16px] bg-sidebar border-2 border-outline min-w-[250px] p-[10px] h-[calc(100%-76px)]"
|
||||
>
|
||||
<div className="flex flex-col h-full overflow-x-hidden">
|
||||
<div className="flex-grow flex flex-col min-w-[235px]">
|
||||
<div className="flex flex-col gap-y-2 pb-8 overflow-y-scroll no-scroll">
|
||||
<div className="relative h-[calc(100%-60px)] flex flex-col w-full justify-between pt-[10px] overflow-y-scroll no-scroll">
|
||||
<div className="flex flex-col gap-y-2 pb-[60px] overflow-y-scroll no-scroll">
|
||||
<div className="flex gap-x-2 items-center justify-between">
|
||||
{(!user || user?.role !== "default") && (
|
||||
<button
|
||||
@ -59,7 +58,8 @@ export default function Sidebar() {
|
||||
</div>
|
||||
<ActiveWorkspaces />
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow justify-end mb-2">
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 right-0 pt-4 pb-3 rounded-b-[16px] bg-sidebar bg-opacity-80 backdrop-filter backdrop-blur-md z-10">
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
@ -156,12 +156,9 @@ export function SidebarMobileHeader() {
|
||||
</div>
|
||||
|
||||
{/* Primary Body */}
|
||||
<div className="h-full flex flex-col w-full justify-between pt-4 overflow-y-hidden ">
|
||||
<div className="h-full flex flex-col w-full justify-between pt-4 ">
|
||||
<div className="h-auto md:sidebar-items">
|
||||
<div
|
||||
style={{ height: "calc(100vw - -3rem)" }}
|
||||
className=" flex flex-col gap-y-4 pb-8 overflow-y-scroll no-scroll"
|
||||
>
|
||||
<div className=" flex flex-col gap-y-4 overflow-y-scroll no-scroll pb-[60px]">
|
||||
<div className="flex gap-x-2 items-center justify-between">
|
||||
{(!user || user?.role !== "default") && (
|
||||
<button
|
||||
@ -178,7 +175,7 @@ export function SidebarMobileHeader() {
|
||||
<ActiveWorkspaces />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="z-99 absolute bottom-0 left-0 right-0 pt-2 pb-6 rounded-br-[26px] bg-sidebar bg-opacity-80 backdrop-filter backdrop-blur-md">
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@ import React, { useRef, useEffect } from "react";
|
||||
import JAZZ from "@metamask/jazzicon";
|
||||
import usePfp from "../../hooks/usePfp";
|
||||
|
||||
export default function Jazzicon({ size = 10, user, role }) {
|
||||
export default function UserIcon({ size = 36, user, role }) {
|
||||
const { pfp } = usePfp();
|
||||
const divRef = useRef(null);
|
||||
const seed = user?.uid
|
||||
|
BIN
frontend/src/components/UserIcon/workspace.png
Normal file
BIN
frontend/src/components/UserIcon/workspace.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,126 @@
|
||||
import { AI_BACKGROUND_COLOR, USER_BACKGROUND_COLOR } from "@/utils/constants";
|
||||
import { Pencil } from "@phosphor-icons/react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
const EDIT_EVENT = "toggle-message-edit";
|
||||
|
||||
export function useEditMessage({ chatId, role }) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
function onEditEvent(e) {
|
||||
if (e.detail.chatId !== chatId || e.detail.role !== role) {
|
||||
setIsEditing(false);
|
||||
return false;
|
||||
}
|
||||
setIsEditing((prev) => !prev);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function listenForEdits() {
|
||||
if (!chatId || !role) return;
|
||||
window.addEventListener(EDIT_EVENT, onEditEvent);
|
||||
}
|
||||
listenForEdits();
|
||||
return () => {
|
||||
window.removeEventListener(EDIT_EVENT, onEditEvent);
|
||||
};
|
||||
}, [chatId, role]);
|
||||
|
||||
return { isEditing, setIsEditing };
|
||||
}
|
||||
|
||||
export function EditMessageAction({ chatId = null, role, isEditing }) {
|
||||
function handleEditClick() {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(EDIT_EVENT, { detail: { chatId, role } })
|
||||
);
|
||||
}
|
||||
|
||||
if (!chatId || isEditing) return null;
|
||||
return (
|
||||
<div
|
||||
className={`mt-3 relative ${
|
||||
role === "user" && !isEditing ? "opacity-0" : ""
|
||||
} group-hover:opacity-100 transition-all duration-300`}
|
||||
>
|
||||
<button
|
||||
onClick={handleEditClick}
|
||||
data-tooltip-id="edit-input-text"
|
||||
data-tooltip-content={`Edit ${
|
||||
role === "user" ? "Prompt" : "Response"
|
||||
} `}
|
||||
className="border-none text-zinc-300"
|
||||
aria-label={`Edit ${role === "user" ? "Prompt" : "Response"}`}
|
||||
>
|
||||
<Pencil size={18} className="mb-1" />
|
||||
</button>
|
||||
<Tooltip
|
||||
id="edit-input-text"
|
||||
place="bottom"
|
||||
delayShow={300}
|
||||
className="tooltip !text-xs"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function EditMessageForm({
|
||||
role,
|
||||
chatId,
|
||||
message,
|
||||
adjustTextArea,
|
||||
saveChanges,
|
||||
}) {
|
||||
const formRef = useRef(null);
|
||||
function handleSaveMessage(e) {
|
||||
e.preventDefault();
|
||||
const form = new FormData(e.target);
|
||||
const editedMessage = form.get("editedMessage");
|
||||
saveChanges({ editedMessage, chatId, role });
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(EDIT_EVENT, { detail: { chatId, role } })
|
||||
);
|
||||
}
|
||||
|
||||
function cancelEdits() {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(EDIT_EVENT, { detail: { chatId, role } })
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!formRef || !formRef.current) return;
|
||||
formRef.current.focus();
|
||||
adjustTextArea({ target: formRef.current });
|
||||
}, [formRef]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSaveMessage} className="flex flex-col w-full">
|
||||
<textarea
|
||||
ref={formRef}
|
||||
name="editedMessage"
|
||||
className={`w-full rounded ${
|
||||
role === "user" ? USER_BACKGROUND_COLOR : AI_BACKGROUND_COLOR
|
||||
} border border-white/20 active:outline-none focus:outline-none focus:ring-0 pr-16 pl-1.5 pt-1.5 resize-y`}
|
||||
defaultValue={message}
|
||||
onChange={adjustTextArea}
|
||||
/>
|
||||
<div className="mt-3 flex justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
className="px-2 py-1 bg-gray-200 text-gray-700 font-medium rounded-md mr-2 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
Save & Submit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="px-2 py-1 bg-historical-msg-system text-white font-medium rounded-md hover:bg-historical-msg-user/90 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2"
|
||||
onClick={cancelEdits}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
@ -2,14 +2,15 @@ import React, { memo, useState } from "react";
|
||||
import useCopyText from "@/hooks/useCopyText";
|
||||
import {
|
||||
Check,
|
||||
ClipboardText,
|
||||
ThumbsUp,
|
||||
ThumbsDown,
|
||||
ArrowsClockwise,
|
||||
Copy,
|
||||
} from "@phosphor-icons/react";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
import Workspace from "@/models/workspace";
|
||||
import TTSMessage from "./TTSButton";
|
||||
import { EditMessageAction } from "./EditMessage";
|
||||
|
||||
const Actions = ({
|
||||
message,
|
||||
@ -18,9 +19,10 @@ const Actions = ({
|
||||
slug,
|
||||
isLastMessage,
|
||||
regenerateMessage,
|
||||
isEditing,
|
||||
role,
|
||||
}) => {
|
||||
const [selectedFeedback, setSelectedFeedback] = useState(feedbackScore);
|
||||
|
||||
const handleFeedback = async (newFeedback) => {
|
||||
const updatedFeedback =
|
||||
selectedFeedback === newFeedback ? null : newFeedback;
|
||||
@ -32,14 +34,15 @@ const Actions = ({
|
||||
<div className="flex w-full justify-between items-center">
|
||||
<div className="flex justify-start items-center gap-x-4">
|
||||
<CopyMessage message={message} />
|
||||
{isLastMessage && (
|
||||
<EditMessageAction chatId={chatId} role={role} isEditing={isEditing} />
|
||||
{isLastMessage && !isEditing && (
|
||||
<RegenerateMessage
|
||||
regenerateMessage={regenerateMessage}
|
||||
slug={slug}
|
||||
chatId={chatId}
|
||||
/>
|
||||
)}
|
||||
{chatId && (
|
||||
{chatId && role !== "user" && !isEditing && (
|
||||
<>
|
||||
<FeedbackButton
|
||||
isSelected={selectedFeedback === true}
|
||||
@ -111,7 +114,7 @@ function CopyMessage({ message }) {
|
||||
{copied ? (
|
||||
<Check size={18} className="mb-1" />
|
||||
) : (
|
||||
<ClipboardText size={18} className="mb-1" />
|
||||
<Copy size={18} className="mb-1" />
|
||||
)}
|
||||
</button>
|
||||
<Tooltip
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { memo } from "react";
|
||||
import { Warning } from "@phosphor-icons/react";
|
||||
import Jazzicon from "../../../../UserIcon";
|
||||
import UserIcon from "../../../../UserIcon";
|
||||
import Actions from "./Actions";
|
||||
import renderMarkdown from "@/utils/chat/markdown";
|
||||
import { userFromStorage } from "@/utils/request";
|
||||
@ -8,6 +8,7 @@ import Citations from "../Citation";
|
||||
import { AI_BACKGROUND_COLOR, USER_BACKGROUND_COLOR } from "@/utils/constants";
|
||||
import { v4 } from "uuid";
|
||||
import createDOMPurify from "dompurify";
|
||||
import { EditMessageForm, useEditMessage } from "./Actions/EditMessage";
|
||||
|
||||
const DOMPurify = createDOMPurify(window);
|
||||
const HistoricalMessage = ({
|
||||
@ -21,7 +22,16 @@ const HistoricalMessage = ({
|
||||
chatId = null,
|
||||
isLastMessage = false,
|
||||
regenerateMessage,
|
||||
saveEditedMessage,
|
||||
}) => {
|
||||
const { isEditing } = useEditMessage({ chatId, role });
|
||||
const adjustTextArea = (event) => {
|
||||
const element = event.target;
|
||||
element.style.height = "auto";
|
||||
element.style.height = element.scrollHeight + "px";
|
||||
};
|
||||
|
||||
if (!!error) {
|
||||
return (
|
||||
<div
|
||||
key={uuid}
|
||||
@ -29,12 +39,11 @@ const HistoricalMessage = ({
|
||||
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-[800px] flex-col">
|
||||
<div className="flex gap-x-5">
|
||||
<ProfileImage role={role} workspace={workspace} />
|
||||
{error ? (
|
||||
<div className="p-2 rounded-lg bg-red-50 text-red-500">
|
||||
<span className={`inline-block `}>
|
||||
<span className="inline-block">
|
||||
<Warning className="h-4 w-4 mb-1 inline-block" /> Could not
|
||||
respond to message.
|
||||
</span>
|
||||
@ -42,6 +51,30 @@ const HistoricalMessage = ({
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={uuid}
|
||||
className={`flex justify-center items-end w-full group ${
|
||||
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="flex gap-x-5">
|
||||
<ProfileImage role={role} workspace={workspace} />
|
||||
{isEditing ? (
|
||||
<EditMessageForm
|
||||
role={role}
|
||||
chatId={chatId}
|
||||
message={message}
|
||||
adjustTextArea={adjustTextArea}
|
||||
saveChanges={saveEditedMessage}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className={`flex flex-col gap-y-1`}
|
||||
@ -51,7 +84,6 @@ const HistoricalMessage = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{role === "assistant" && !error && (
|
||||
<div className="flex gap-x-5">
|
||||
<div className="relative w-[35px] h-[35px] rounded-full flex-shrink-0 overflow-hidden" />
|
||||
<Actions
|
||||
@ -61,9 +93,10 @@ const HistoricalMessage = ({
|
||||
slug={workspace?.slug}
|
||||
isLastMessage={isLastMessage}
|
||||
regenerateMessage={regenerateMessage}
|
||||
isEditing={isEditing}
|
||||
role={role}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{role === "assistant" && <Citations sources={sources} />}
|
||||
</div>
|
||||
</div>
|
||||
@ -84,8 +117,7 @@ function ProfileImage({ role, workspace }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Jazzicon
|
||||
size={36}
|
||||
<UserIcon
|
||||
user={{
|
||||
uid: role === "user" ? userFromStorage()?.username : workspace.slug,
|
||||
}}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { memo } from "react";
|
||||
import { Warning } from "@phosphor-icons/react";
|
||||
import Jazzicon from "../../../../UserIcon";
|
||||
import UserIcon from "../../../../UserIcon";
|
||||
import renderMarkdown from "@/utils/chat/markdown";
|
||||
import Citations from "../Citation";
|
||||
|
||||
@ -84,7 +84,7 @@ export function WorkspaceProfileImage({ workspace }) {
|
||||
);
|
||||
}
|
||||
|
||||
return <Jazzicon size={36} user={{ uid: workspace.slug }} role="assistant" />;
|
||||
return <UserIcon user={{ uid: workspace.slug }} role="assistant" />;
|
||||
}
|
||||
|
||||
export default memo(PromptReply);
|
||||
|
@ -1,20 +1,24 @@
|
||||
import HistoricalMessage from "./HistoricalMessage";
|
||||
import PromptReply from "./PromptReply";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useManageWorkspaceModal } from "../../../Modals/MangeWorkspace";
|
||||
import ManageWorkspace from "../../../Modals/MangeWorkspace";
|
||||
import { useManageWorkspaceModal } from "../../../Modals/ManageWorkspace";
|
||||
import ManageWorkspace from "../../../Modals/ManageWorkspace";
|
||||
import { ArrowDown } from "@phosphor-icons/react";
|
||||
import debounce from "lodash.debounce";
|
||||
import useUser from "@/hooks/useUser";
|
||||
import Chartable from "./Chartable";
|
||||
import Workspace from "@/models/workspace";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
export default function ChatHistory({
|
||||
history = [],
|
||||
workspace,
|
||||
sendCommand,
|
||||
updateHistory,
|
||||
regenerateAssistantMessage,
|
||||
}) {
|
||||
const { user } = useUser();
|
||||
const { threadSlug = null } = useParams();
|
||||
const { showing, showModal, hideModal } = useManageWorkspaceModal();
|
||||
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||
const chatHistoryRef = useRef(null);
|
||||
@ -87,6 +91,46 @@ export default function ChatHistory({
|
||||
sendCommand(`${heading} ${message}`, true);
|
||||
};
|
||||
|
||||
const saveEditedMessage = async ({ editedMessage, chatId, role }) => {
|
||||
if (!editedMessage) return; // Don't save empty edits.
|
||||
|
||||
// if the edit was a user message, we will auto-regenerate the response and delete all
|
||||
// messages post modified message
|
||||
if (role === "user") {
|
||||
// remove all messages after the edited message
|
||||
// technically there are two chatIds per-message pair, this will split the first.
|
||||
const updatedHistory = history.slice(
|
||||
0,
|
||||
history.findIndex((msg) => msg.chatId === chatId) + 1
|
||||
);
|
||||
|
||||
// update last message in history to edited message
|
||||
updatedHistory[updatedHistory.length - 1].content = editedMessage;
|
||||
// remove all edited messages after the edited message in backend
|
||||
await Workspace.deleteEditedChats(workspace.slug, threadSlug, chatId);
|
||||
sendCommand(editedMessage, true, updatedHistory);
|
||||
return;
|
||||
}
|
||||
|
||||
// If role is an assistant we simply want to update the comment and save on the backend as an edit.
|
||||
if (role === "assistant") {
|
||||
const updatedHistory = [...history];
|
||||
const targetIdx = history.findIndex(
|
||||
(msg) => msg.chatId === chatId && msg.role === role
|
||||
);
|
||||
if (targetIdx < 0) return;
|
||||
updatedHistory[targetIdx].content = editedMessage;
|
||||
updateHistory(updatedHistory);
|
||||
await Workspace.updateChatResponse(
|
||||
workspace.slug,
|
||||
threadSlug,
|
||||
chatId,
|
||||
editedMessage
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if (history.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col h-full md:mt-0 pb-44 md:pb-40 w-full justify-end items-center">
|
||||
@ -172,6 +216,7 @@ export default function ChatHistory({
|
||||
error={props.error}
|
||||
regenerateMessage={regenerateAssistantMessage}
|
||||
isLastMessage={isLastBotReply}
|
||||
saveEditedMessage={saveEditedMessage}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -38,7 +38,6 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
if (!message || message === "") return false;
|
||||
|
||||
const prevChatHistory = [
|
||||
...chatHistory,
|
||||
{ content: message, role: "user" },
|
||||
@ -240,6 +239,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
history={chatHistory}
|
||||
workspace={workspace}
|
||||
sendCommand={sendCommand}
|
||||
updateHistory={setChatHistory}
|
||||
regenerateAssistantMessage={regenerateAssistantMessage}
|
||||
/>
|
||||
<PromptInput
|
||||
|
@ -22,6 +22,7 @@ export default function WorkspaceChat({ loading, workspace }) {
|
||||
const chatHistory = threadSlug
|
||||
? await Workspace.threads.chatHistory(workspace.slug, threadSlug)
|
||||
: await Workspace.chatHistory(workspace.slug);
|
||||
|
||||
setHistory(chatHistory);
|
||||
setLoadingHistory(false);
|
||||
}
|
||||
|
@ -10,7 +10,12 @@ export const DISABLED_PROVIDERS = [
|
||||
];
|
||||
const PROVIDER_DEFAULT_MODELS = {
|
||||
openai: [],
|
||||
gemini: ["gemini-pro", "gemini-1.5-pro-latest", "gemini-1.5-flash-latest"],
|
||||
gemini: [
|
||||
"gemini-pro",
|
||||
"gemini-1.0-pro",
|
||||
"gemini-1.5-pro-latest",
|
||||
"gemini-1.5-flash-latest",
|
||||
],
|
||||
anthropic: [
|
||||
"claude-instant-1.2",
|
||||
"claude-2.0",
|
||||
|
@ -2,6 +2,6 @@ import { useContext } from "react";
|
||||
import { LogoContext } from "../LogoContext";
|
||||
|
||||
export default function useLogo() {
|
||||
const { logo, setLogo } = useContext(LogoContext);
|
||||
return { logo, setLogo };
|
||||
const { logo, setLogo, loginLogo, isCustomLogo } = useContext(LogoContext);
|
||||
return { logo, setLogo, loginLogo, isCustomLogo };
|
||||
}
|
||||
|
@ -742,3 +742,7 @@ does not extend the close button beyond the viewport. */
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.search-input::-webkit-search-cancel-button {
|
||||
filter: grayscale(100%) invert(1) brightness(100) opacity(0.5);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ const System = {
|
||||
cacheKeys: {
|
||||
footerIcons: "anythingllm_footer_links",
|
||||
supportEmail: "anythingllm_support_email",
|
||||
customAppName: "anythingllm_custom_app_name",
|
||||
},
|
||||
ping: async function () {
|
||||
return await fetch(`${API_BASE}/ping`)
|
||||
@ -305,19 +306,58 @@ const System = {
|
||||
);
|
||||
return { email: supportEmail, error: null };
|
||||
},
|
||||
|
||||
fetchCustomAppName: async function () {
|
||||
const cache = window.localStorage.getItem(this.cacheKeys.customAppName);
|
||||
const { appName, lastFetched } = cache
|
||||
? safeJsonParse(cache, { appName: "", lastFetched: 0 })
|
||||
: { appName: "", lastFetched: 0 };
|
||||
|
||||
if (!!appName && Date.now() - lastFetched < 3_600_000)
|
||||
return { appName: appName, error: null };
|
||||
|
||||
const { customAppName, error } = await fetch(
|
||||
`${API_BASE}/system/custom-app-name`,
|
||||
{
|
||||
method: "GET",
|
||||
cache: "no-cache",
|
||||
headers: baseHeaders(),
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
return { customAppName: "", error: e.message };
|
||||
});
|
||||
|
||||
if (!customAppName || !!error) {
|
||||
window.localStorage.removeItem(this.cacheKeys.customAppName);
|
||||
return { appName: "", error: null };
|
||||
}
|
||||
|
||||
window.localStorage.setItem(
|
||||
this.cacheKeys.customAppName,
|
||||
JSON.stringify({ appName: customAppName, lastFetched: Date.now() })
|
||||
);
|
||||
return { appName: customAppName, error: null };
|
||||
},
|
||||
fetchLogo: async function () {
|
||||
return await fetch(`${API_BASE}/system/logo`, {
|
||||
method: "GET",
|
||||
cache: "no-cache",
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.ok && res.status !== 204) return res.blob();
|
||||
.then(async (res) => {
|
||||
if (res.ok && res.status !== 204) {
|
||||
const isCustomLogo = res.headers.get("X-Is-Custom-Logo") === "true";
|
||||
const blob = await res.blob();
|
||||
const logoURL = URL.createObjectURL(blob);
|
||||
return { isCustomLogo, logoURL };
|
||||
}
|
||||
throw new Error("Failed to fetch logo!");
|
||||
})
|
||||
.then((blob) => URL.createObjectURL(blob))
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
return null;
|
||||
return { isCustomLogo: false, logoURL: null };
|
||||
});
|
||||
},
|
||||
fetchPfp: async function (id) {
|
||||
|
@ -90,6 +90,26 @@ const Workspace = {
|
||||
return false;
|
||||
});
|
||||
},
|
||||
deleteEditedChats: async function (slug = "", threadSlug = "", startingId) {
|
||||
if (!!threadSlug)
|
||||
return this.threads._deleteEditedChats(slug, threadSlug, startingId);
|
||||
return this._deleteEditedChats(slug, startingId);
|
||||
},
|
||||
updateChatResponse: async function (
|
||||
slug = "",
|
||||
threadSlug = "",
|
||||
chatId,
|
||||
newText
|
||||
) {
|
||||
if (!!threadSlug)
|
||||
return this.threads._updateChatResponse(
|
||||
slug,
|
||||
threadSlug,
|
||||
chatId,
|
||||
newText
|
||||
);
|
||||
return this._updateChatResponse(slug, chatId, newText);
|
||||
},
|
||||
streamChat: async function ({ slug }, message, handleChat) {
|
||||
const ctrl = new AbortController();
|
||||
|
||||
@ -287,8 +307,6 @@ const Workspace = {
|
||||
return null;
|
||||
});
|
||||
},
|
||||
threads: WorkspaceThread,
|
||||
|
||||
uploadPfp: async function (formData, slug) {
|
||||
return await fetch(`${API_BASE}/workspace/${slug}/upload-pfp`, {
|
||||
method: "POST",
|
||||
@ -336,6 +354,37 @@ const Workspace = {
|
||||
return { success: false, error: e.message };
|
||||
});
|
||||
},
|
||||
_updateChatResponse: async function (slug = "", chatId, newText) {
|
||||
return await fetch(`${API_BASE}/workspace/${slug}/update-chat`, {
|
||||
method: "POST",
|
||||
headers: baseHeaders(),
|
||||
body: JSON.stringify({ chatId, newText }),
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.ok) return true;
|
||||
throw new Error("Failed to update chat.");
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
return false;
|
||||
});
|
||||
},
|
||||
_deleteEditedChats: async function (slug = "", startingId) {
|
||||
return await fetch(`${API_BASE}/workspace/${slug}/delete-edited-chats`, {
|
||||
method: "DELETE",
|
||||
headers: baseHeaders(),
|
||||
body: JSON.stringify({ startingId }),
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.ok) return true;
|
||||
throw new Error("Failed to delete chats.");
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
return false;
|
||||
});
|
||||
},
|
||||
threads: WorkspaceThread,
|
||||
};
|
||||
|
||||
export default Workspace;
|
||||
|
@ -163,6 +163,51 @@ const WorkspaceThread = {
|
||||
}
|
||||
);
|
||||
},
|
||||
_deleteEditedChats: async function (
|
||||
workspaceSlug = "",
|
||||
threadSlug = "",
|
||||
startingId
|
||||
) {
|
||||
return await fetch(
|
||||
`${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}/delete-edited-chats`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: baseHeaders(),
|
||||
body: JSON.stringify({ startingId }),
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.ok) return true;
|
||||
throw new Error("Failed to delete chats.");
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
return false;
|
||||
});
|
||||
},
|
||||
_updateChatResponse: async function (
|
||||
workspaceSlug = "",
|
||||
threadSlug = "",
|
||||
chatId,
|
||||
newText
|
||||
) {
|
||||
return await fetch(
|
||||
`${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}/update-chat`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: baseHeaders(),
|
||||
body: JSON.stringify({ chatId, newText }),
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.ok) return true;
|
||||
throw new Error("Failed to update chat.");
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
return false;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default WorkspaceThread;
|
||||
|
@ -0,0 +1,100 @@
|
||||
import Admin from "@/models/admin";
|
||||
import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function CustomAppName() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [customAppName, setCustomAppName] = useState("");
|
||||
const [originalAppName, setOriginalAppName] = useState("");
|
||||
const [canCustomize, setCanCustomize] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchInitialParams = async () => {
|
||||
const settings = await System.keys();
|
||||
if (!settings?.MultiUserMode && !settings?.RequiresAuth) {
|
||||
setCanCustomize(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const { appName } = await System.fetchCustomAppName();
|
||||
setCustomAppName(appName || "");
|
||||
setOriginalAppName(appName || "");
|
||||
setCanCustomize(true);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchInitialParams();
|
||||
}, []);
|
||||
|
||||
const updateCustomAppName = async (e, newValue = null) => {
|
||||
e.preventDefault();
|
||||
let custom_app_name = newValue;
|
||||
if (newValue === null) {
|
||||
const form = new FormData(e.target);
|
||||
custom_app_name = form.get("customAppName");
|
||||
}
|
||||
const { success, error } = await Admin.updateSystemPreferences({
|
||||
custom_app_name,
|
||||
});
|
||||
if (!success) {
|
||||
showToast(`Failed to update custom app name: ${error}`, "error");
|
||||
return;
|
||||
} else {
|
||||
showToast("Successfully updated custom app name.", "success");
|
||||
window.localStorage.removeItem(System.cacheKeys.customAppName);
|
||||
setCustomAppName(custom_app_name);
|
||||
setOriginalAppName(custom_app_name);
|
||||
setHasChanges(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
setCustomAppName(e.target.value);
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
if (!canCustomize || loading) return null;
|
||||
|
||||
return (
|
||||
<form className="mb-6" onSubmit={updateCustomAppName}>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<h2 className="text-base leading-6 font-bold text-white">
|
||||
Custom App Name
|
||||
</h2>
|
||||
<p className="text-xs leading-[18px] font-base text-white/60">
|
||||
Set a custom app name that is displayed on the login page.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<input
|
||||
name="customAppName"
|
||||
type="text"
|
||||
className="bg-zinc-900 mt-3 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 max-w-[275px] placeholder:text-white/20"
|
||||
placeholder="AnythingLLM"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
onChange={handleChange}
|
||||
value={customAppName}
|
||||
/>
|
||||
{originalAppName !== "" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => updateCustomAppName(e, "")}
|
||||
className="mt-4 text-white text-base font-medium hover:text-opacity-60"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{hasChanges && (
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all mt-6 w-fit duration-300 border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
@ -2,7 +2,6 @@ import useLogo from "@/hooks/useLogo";
|
||||
import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import AnythingLLM from "@/media/logo/anything-llm.png";
|
||||
import { Plus } from "@phosphor-icons/react";
|
||||
|
||||
export default function CustomLogo() {
|
||||
@ -36,7 +35,7 @@ export default function CustomLogo() {
|
||||
return;
|
||||
}
|
||||
|
||||
const logoURL = await System.fetchLogo();
|
||||
const { logoURL } = await System.fetchLogo();
|
||||
_setLogo(logoURL);
|
||||
|
||||
showToast("Image uploaded successfully.", "success");
|
||||
@ -51,13 +50,13 @@ export default function CustomLogo() {
|
||||
if (!success) {
|
||||
console.error("Failed to remove logo:", error);
|
||||
showToast(`Failed to remove logo: ${error}`, "error");
|
||||
const logoURL = await System.fetchLogo();
|
||||
const { logoURL } = await System.fetchLogo();
|
||||
setLogo(logoURL);
|
||||
setIsDefaultLogo(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const logoURL = await System.fetchLogo();
|
||||
const { logoURL } = await System.fetchLogo();
|
||||
_setLogo(logoURL);
|
||||
|
||||
showToast("Image successfully removed.", "success");
|
||||
|
@ -4,6 +4,7 @@ import FooterCustomization from "./FooterCustomization";
|
||||
import SupportEmail from "./SupportEmail";
|
||||
import CustomLogo from "./CustomLogo";
|
||||
import CustomMessages from "./CustomMessages";
|
||||
import CustomAppName from "./CustomAppName";
|
||||
|
||||
export default function Appearance() {
|
||||
return (
|
||||
@ -25,6 +26,7 @@ export default function Appearance() {
|
||||
</p>
|
||||
</div>
|
||||
<CustomLogo />
|
||||
<CustomAppName />
|
||||
<CustomMessages />
|
||||
<FooterCustomization />
|
||||
<SupportEmail />
|
||||
|
@ -11,6 +11,7 @@ import OllamaLogo from "@/media/llmprovider/ollama.png";
|
||||
import LMStudioLogo from "@/media/llmprovider/lmstudio.png";
|
||||
import CohereLogo from "@/media/llmprovider/cohere.png";
|
||||
import VoyageAiLogo from "@/media/embeddingprovider/voyageai.png";
|
||||
import LiteLLMLogo from "@/media/llmprovider/litellm.png";
|
||||
|
||||
import PreLoader from "@/components/Preloader";
|
||||
import ChangeWarningModal from "@/components/ChangeWarning";
|
||||
@ -22,6 +23,7 @@ import OllamaEmbeddingOptions from "@/components/EmbeddingSelection/OllamaOption
|
||||
import LMStudioEmbeddingOptions from "@/components/EmbeddingSelection/LMStudioOptions";
|
||||
import CohereEmbeddingOptions from "@/components/EmbeddingSelection/CohereOptions";
|
||||
import VoyageAiOptions from "@/components/EmbeddingSelection/VoyageAiOptions";
|
||||
import LiteLLMOptions from "@/components/EmbeddingSelection/LiteLLMOptions";
|
||||
|
||||
import EmbedderItem from "@/components/EmbeddingSelection/EmbedderItem";
|
||||
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
|
||||
@ -88,6 +90,13 @@ const EMBEDDERS = [
|
||||
options: (settings) => <VoyageAiOptions settings={settings} />,
|
||||
description: "Run powerful embedding models from Voyage AI.",
|
||||
},
|
||||
{
|
||||
name: "LiteLLM",
|
||||
value: "litellm",
|
||||
logo: LiteLLMLogo,
|
||||
options: (settings) => <LiteLLMOptions settings={settings} />,
|
||||
description: "Run powerful embedding models from LiteLLM.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function GeneralEmbeddingPreference() {
|
||||
|
@ -301,6 +301,13 @@ export const EMBEDDING_ENGINE_PRIVACY = {
|
||||
],
|
||||
logo: VoyageAiLogo,
|
||||
},
|
||||
litellm: {
|
||||
name: "LiteLLM",
|
||||
description: [
|
||||
"Your document text is only accessible on the server running LiteLLM and to the providers you configured in LiteLLM.",
|
||||
],
|
||||
logo: LiteLLMLogo,
|
||||
},
|
||||
};
|
||||
|
||||
export default function DataHandling({ setHeader, setForwardBtn, setBackBtn }) {
|
||||
|
@ -9,7 +9,7 @@ export const DB_LOGOS = {
|
||||
"sql-server": MSSQLLogo,
|
||||
};
|
||||
|
||||
export default function DBConnection({ connection, onRemove }) {
|
||||
export default function DBConnection({ connection, onRemove, setHasChanges }) {
|
||||
const { database_id, engine } = connection;
|
||||
function removeConfirmation() {
|
||||
if (
|
||||
@ -20,6 +20,7 @@ export default function DBConnection({ connection, onRemove }) {
|
||||
return false;
|
||||
}
|
||||
onRemove(database_id);
|
||||
setHasChanges(true);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -9,6 +9,7 @@ export default function AgentSQLConnectorSelection({
|
||||
settings,
|
||||
toggleSkill,
|
||||
enabled = false,
|
||||
setHasChanges,
|
||||
}) {
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
const [connections, setConnections] = useState(
|
||||
@ -72,6 +73,7 @@ export default function AgentSQLConnectorSelection({
|
||||
})
|
||||
);
|
||||
}}
|
||||
setHasChanges={setHasChanges}
|
||||
/>
|
||||
))}
|
||||
<button
|
||||
|
@ -6,6 +6,7 @@ export function GoogleSearchOptions({ settings }) {
|
||||
<a
|
||||
href="https://programmablesearchengine.google.com/controlpanel/create"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-300 underline"
|
||||
>
|
||||
from Google here.
|
||||
@ -57,6 +58,7 @@ export function SerperDotDevOptions({ settings }) {
|
||||
<a
|
||||
href="https://serper.dev"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-300 underline"
|
||||
>
|
||||
from Serper.dev.
|
||||
@ -82,3 +84,101 @@ export function SerperDotDevOptions({ settings }) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function BingSearchOptions({ settings }) {
|
||||
return (
|
||||
<>
|
||||
<p className="text-sm text-white/60 my-2">
|
||||
You can get a Bing Web Search API subscription key{" "}
|
||||
<a
|
||||
href="https://portal.azure.com/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-300 underline"
|
||||
>
|
||||
from the Azure portal.
|
||||
</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-4">
|
||||
API Key
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="env::AgentBingSearchApiKey"
|
||||
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||
placeholder="Bing Web Search API Key"
|
||||
defaultValue={settings?.AgentBingSearchApiKey ? "*".repeat(20) : ""}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-white/60 my-2">
|
||||
To set up a Bing Web Search API subscription:
|
||||
</p>
|
||||
<ol className="list-decimal text-sm text-white/60 ml-6">
|
||||
<li>
|
||||
Go to the Azure portal:{" "}
|
||||
<a
|
||||
href="https://portal.azure.com/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-300 underline"
|
||||
>
|
||||
https://portal.azure.com/
|
||||
</a>
|
||||
</li>
|
||||
<li>Create a new Azure account or sign in with an existing one.</li>
|
||||
<li>
|
||||
Navigate to the "Create a resource" section and search for "Bing
|
||||
Search v7".
|
||||
</li>
|
||||
<li>
|
||||
Select the "Bing Search v7" resource and create a new subscription.
|
||||
</li>
|
||||
<li>
|
||||
Choose the pricing tier that suits your needs (free tier available).
|
||||
</li>
|
||||
<li>Obtain the API key for your Bing Web Search subscription.</li>
|
||||
</ol>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function SerplySearchOptions({ settings }) {
|
||||
return (
|
||||
<>
|
||||
<p className="text-sm text-white/60 my-2">
|
||||
You can get a free API key{" "}
|
||||
<a
|
||||
href="https://serply.io"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-300 underline"
|
||||
>
|
||||
from Serply.io.
|
||||
</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-4">
|
||||
API Key
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="env::AgentSerplyApiKey"
|
||||
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||
placeholder="Serply API Key"
|
||||
defaultValue={settings?.AgentSerplyApiKey ? "*".repeat(20) : ""}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 73 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
@ -2,11 +2,15 @@ import React, { useEffect, useRef, useState } from "react";
|
||||
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
|
||||
import GoogleSearchIcon from "./icons/google.png";
|
||||
import SerperDotDevIcon from "./icons/serper.png";
|
||||
import BingSearchIcon from "./icons/bing.png";
|
||||
import SerplySearchIcon from "./icons/serply.png";
|
||||
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
|
||||
import SearchProviderItem from "./SearchProviderItem";
|
||||
import {
|
||||
SerperDotDevOptions,
|
||||
GoogleSearchOptions,
|
||||
BingSearchOptions,
|
||||
SerplySearchOptions,
|
||||
} from "./SearchProviderOptions";
|
||||
|
||||
const SEARCH_PROVIDERS = [
|
||||
@ -34,6 +38,22 @@ const SEARCH_PROVIDERS = [
|
||||
description:
|
||||
"Serper.dev web-search. Free account with a 2,500 calls, but then paid.",
|
||||
},
|
||||
{
|
||||
name: "Bing Search",
|
||||
value: "bing-search",
|
||||
logo: BingSearchIcon,
|
||||
options: (settings) => <BingSearchOptions settings={settings} />,
|
||||
description:
|
||||
"Web search powered by the Bing Search API. Free for 1000 queries per month.",
|
||||
},
|
||||
{
|
||||
name: "Serply.io",
|
||||
value: "serply-engine",
|
||||
logo: SerplySearchIcon,
|
||||
options: (settings) => <SerplySearchOptions settings={settings} />,
|
||||
description:
|
||||
"Serply.io web-search. Free account with a 100 calls/month forever.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function AgentWebSearchSelection({
|
||||
|
@ -100,6 +100,7 @@ export default function WorkspaceAgentConfiguration({ workspace }) {
|
||||
skills={agentSkills}
|
||||
toggleAgentSkill={toggleAgentSkill}
|
||||
settings={settings}
|
||||
setHasChanges={setHasChanges}
|
||||
/>
|
||||
{hasChanges && (
|
||||
<button
|
||||
@ -143,7 +144,12 @@ function LoadingSkeleton() {
|
||||
);
|
||||
}
|
||||
|
||||
function AvailableAgentSkills({ skills, settings, toggleAgentSkill }) {
|
||||
function AvailableAgentSkills({
|
||||
skills,
|
||||
settings,
|
||||
toggleAgentSkill,
|
||||
setHasChanges,
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col mb-8">
|
||||
@ -211,6 +217,7 @@ function AvailableAgentSkills({ skills, settings, toggleAgentSkill }) {
|
||||
settings={settings}
|
||||
toggleSkill={toggleAgentSkill}
|
||||
enabled={skills.includes("sql-agent")}
|
||||
setHasChanges={setHasChanges}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { THREAD_RENAME_EVENT } from "@/components/Sidebar/ActiveWorkspaces/ThreadContainer";
|
||||
export const ABORT_STREAM_EVENT = "abort-chat-stream";
|
||||
|
||||
// For handling of chat responses in the frontend by their various types.
|
||||
@ -108,13 +109,10 @@ export default function handleChat(
|
||||
} else if (type === "finalizeResponseStream") {
|
||||
const chatIdx = _chatHistory.findIndex((chat) => chat.uuid === uuid);
|
||||
if (chatIdx !== -1) {
|
||||
const existingHistory = { ..._chatHistory[chatIdx] };
|
||||
const updatedHistory = {
|
||||
...existingHistory,
|
||||
chatId, // finalize response stream only has some specific keys for data. we are explicitly listing them here.
|
||||
};
|
||||
_chatHistory[chatIdx] = updatedHistory;
|
||||
_chatHistory[chatIdx - 1] = { ..._chatHistory[chatIdx - 1], chatId }; // update prompt with chatID
|
||||
_chatHistory[chatIdx] = { ..._chatHistory[chatIdx], chatId }; // update response with chatID
|
||||
}
|
||||
|
||||
setChatHistory([..._chatHistory]);
|
||||
setLoadingResponse(false);
|
||||
} else if (type === "stopGeneration") {
|
||||
@ -139,6 +137,21 @@ export default function handleChat(
|
||||
// Chat was reset, keep reset message and clear everything else.
|
||||
setChatHistory([_chatHistory.pop()]);
|
||||
}
|
||||
|
||||
// If thread was updated automatically based on chat prompt
|
||||
// then we can handle the updating of the thread here.
|
||||
if (action === "rename_thread") {
|
||||
if (!!chatResult?.thread?.slug && chatResult.thread.name) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(THREAD_RENAME_EVENT, {
|
||||
detail: {
|
||||
threadSlug: chatResult.thread.slug,
|
||||
newName: chatResult.thread.name,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function chatPrompt(workspace) {
|
||||
|
@ -51,7 +51,7 @@ export default defineConfig({
|
||||
rollupOptions: {
|
||||
external: [
|
||||
// Reduces transformation time by 50% and we don't even use this variant, so we can ignore.
|
||||
/@phosphor-icons\/react\/dist\/ssr/,
|
||||
/@phosphor-icons\/react\/dist\/ssr/
|
||||
]
|
||||
},
|
||||
commonjsOptions: {
|
||||
|
@ -2260,6 +2260,11 @@ jiti@^1.19.1:
|
||||
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d"
|
||||
integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==
|
||||
|
||||
js-levenshtein@^1.1.6:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
|
||||
integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
|
235
locales/README.ja-JP.md
Normal file
235
locales/README.ja-JP.md
Normal file
@ -0,0 +1,235 @@
|
||||
<a name="readme-top"></a>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://useanything.com"><img src="https://github.com/Mintplex-Labs/anything-llm/blob/master/images/wordmark.png?raw=true" alt="AnythingLLM logo"></a>
|
||||
</p>
|
||||
|
||||
<div align='center'>
|
||||
<a href="https://trendshift.io/repositories/2415" target="_blank"><img src="https://trendshift.io/api/badge/repositories/2415" alt="Mintplex-Labs%2Fanything-llm | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<b>AnythingLLM:</b> あなたが探していたオールインワンAIアプリ。<br />
|
||||
ドキュメントとチャットし、AIエージェントを使用し、高度にカスタマイズ可能で、複数ユーザー対応、面倒な設定は不要です。
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/6UyHPeGZAC" target="_blank">
|
||||
<img src="https://img.shields.io/badge/chat-mintplex_labs-blue.svg?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAH1UExURQAAAP////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////r6+ubn5+7u7/3+/v39/enq6urq6/v7+97f39rb26eoqT1BQ0pOT4+Rkuzs7cnKykZKS0NHSHl8fdzd3ejo6UxPUUBDRdzc3RwgIh8jJSAkJm5xcvHx8aanqB4iJFBTVezt7V5hYlJVVuLj43p9fiImKCMnKZKUlaaoqSElJ21wcfT09O3u7uvr6zE0Nr6/wCUpK5qcnf7+/nh7fEdKTHx+f0tPUOTl5aipqiouMGtubz5CRDQ4OsTGxufn515hY7a3uH1/gXBydIOFhlVYWvX29qaoqCQoKs7Pz/Pz87/AwUtOUNfY2dHR0mhrbOvr7E5RUy8zNXR2d/f39+Xl5UZJSx0hIzQ3Odra2/z8/GlsbaGjpERHSezs7L/BwScrLTQ4Odna2zM3Obm7u3x/gKSmp9jZ2T1AQu/v71pdXkVISr2+vygsLiInKTg7PaOlpisvMcXGxzk8PldaXPLy8u7u7rm6u7S1tsDBwvj4+MPExbe4ueXm5s/Q0Kyf7ewAAAAodFJOUwAABClsrNjx/QM2l9/7lhmI6jTB/kA1GgKJN+nea6vy/MLZQYeVKK3rVA5tAAAAAWJLR0QB/wIt3gAAAAd0SU1FB+cKBAAmMZBHjXIAAAISSURBVDjLY2CAAkYmZhZWNnYODnY2VhZmJkYGVMDIycXNw6sBBbw8fFycyEoYGfkFBDVQgKAAPyMjQl5IWEQDDYgIC8FUMDKKsmlgAWyiEBWMjGJY5YEqxMAqGMWFNXAAYXGgAkYJSQ2cQFKCkYFRShq3AmkpRgYJbghbU0tbB0Tr6ukbgGhDI10gySfBwCwDUWBsYmpmDqQtLK2sbTQ0bO3sHYA8GWYGWWj4WTs6Obu4ami4OTm7exhqeHp5+4DCVJZBDmqdr7ufn3+ArkZgkJ+fU3CIRmgYWFiOARYGvo5OQUHhEUAFTkF+kVHRsLBgkIeyYmLjwoOc4hMSk5JTnINS06DC8gwcEEZ6RqZGlpOfc3ZObl5+gZ+TR2ERWFyBQQFMF5eklmqUpQb5+ReU61ZUOvkFVVXXQBSAraitq29o1GiKcfLzc29u0mjxBzq0tQ0kww5xZHtHUGeXhkZhdxBYgZ4d0LI6c4gjwd7siQQraOp1AivQ6CuAKZCDBBRQQQNQgUb/BGf3cqCCiZOcnCe3QQIKHNRTpk6bDgpZjRkzg3pBQTBrdtCcuZCgluAD0vPmL1gIdvSixUuWgqNs2YJ+DUhkEYxuggkGmOQUcckrioPTJCOXEnZ5JS5YslbGnuyVERlDDFvGEUPOWvwqaH6RVkHKeuDMK6SKnHlVhTgx8jeTmqy6Eij7K6nLqiGyPwChsa1MUrnq1wAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMy0xMC0wNFQwMDozODo0OSswMDowMB9V0a8AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjMtMTAtMDRUMDA6Mzg6NDkrMDA6MDBuCGkTAAAAKHRFWHRkYXRlOnRpbWVzdGFtcAAyMDIzLTEwLTA0VDAwOjM4OjQ5KzAwOjAwOR1IzAAAAABJRU5ErkJggg==" alt="Discord">
|
||||
</a> |
|
||||
<a href="https://github.com/Mintplex-Labs/anything-llm/blob/master/LICENSE" target="_blank">
|
||||
<img src="https://img.shields.io/static/v1?label=license&message=MIT&color=white" alt="ライセンス">
|
||||
</a> |
|
||||
<a href="https://docs.useanything.com" target="_blank">
|
||||
ドキュメント
|
||||
</a> |
|
||||
<a href="https://my.mintplexlabs.com/aio-checkout?product=anythingllm" target="_blank">
|
||||
ホストされたインスタンス
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href='../README.md'>English</a> · <a href='./README.zh-CN.md'>简体中文</a> · <b>日本語</b>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
👉 デスクトップ用AnythingLLM(Mac、Windows、Linux対応)!<a href="https://useanything.com/download" target="_blank">今すぐダウンロード</a>
|
||||
</p>
|
||||
|
||||
これは、任意のドキュメント、リソース、またはコンテンツの断片を、チャット中にLLMが参照として使用できるコンテキストに変換できるフルスタックアプリケーションです。このアプリケーションを使用すると、使用するLLMまたはベクトルデータベースを選択し、マルチユーザー管理と権限をサポートできます。
|
||||
|
||||
![チャット](https://github.com/Mintplex-Labs/anything-llm/assets/16845892/cfc5f47c-bd91-4067-986c-f3f49621a859)
|
||||
|
||||
<details>
|
||||
<summary><kbd>デモを見る!</kbd></summary>
|
||||
|
||||
[![ビデオを見る](/images/youtube.png)](https://youtu.be/f95rGD9trL0)
|
||||
|
||||
</details>
|
||||
|
||||
### 製品概要
|
||||
|
||||
AnythingLLMは、市販のLLMや人気のあるオープンソースLLM、およびベクトルDBソリューションを使用して、妥協のないプライベートChatGPTを構築できるフルスタックアプリケーションです。ローカルで実行することも、リモートでホストすることもでき、提供されたドキュメントと知的にチャットできます。
|
||||
|
||||
AnythingLLMは、ドキュメントを`ワークスペース`と呼ばれるオブジェクトに分割します。ワークスペースはスレッドのように機能しますが、ドキュメントのコンテナ化が追加されています。ワークスペースはドキュメントを共有できますが、互いに通信することはないため、各ワークスペースのコンテキストをクリーンに保つことができます。
|
||||
|
||||
AnythingLLMのいくつかのクールな機能
|
||||
|
||||
- **マルチユーザーインスタンスのサポートと権限付与**
|
||||
- ワークスペース内のエージェント(ウェブを閲覧、コードを実行など)
|
||||
- [ウェブサイト用のカスタム埋め込み可能なチャットウィジェット](./embed/README.md)
|
||||
- 複数のドキュメントタイプのサポート(PDF、TXT、DOCXなど)
|
||||
- シンプルなUIからベクトルデータベース内のドキュメントを管理
|
||||
- 2つのチャットモード`会話`と`クエリ`。会話は以前の質問と修正を保持します。クエリはドキュメントに対するシンプルなQAです
|
||||
- チャット中の引用
|
||||
- 100%クラウドデプロイメント対応。
|
||||
- 「独自のLLMを持参」モデル。
|
||||
- 大規模なドキュメントを管理するための非常に効率的なコスト削減策。巨大なドキュメントやトランスクリプトを埋め込むために一度以上支払うことはありません。他のドキュメントチャットボットソリューションよりも90%コスト効率が良いです。
|
||||
- カスタム統合のための完全な開発者API!
|
||||
|
||||
### サポートされているLLM、埋め込みモデル、音声モデル、およびベクトルデータベース
|
||||
|
||||
**言語学習モデル:**
|
||||
|
||||
- [llama.cpp互換の任意のオープンソースモデル](/server/storage/models/README.md#text-generation-llm-selection)
|
||||
- [OpenAI](https://openai.com)
|
||||
- [OpenAI (汎用)](https://openai.com)
|
||||
- [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service)
|
||||
- [Anthropic](https://www.anthropic.com/)
|
||||
- [Google Gemini Pro](https://ai.google.dev/)
|
||||
- [Hugging Face (チャットモデル)](https://huggingface.co/)
|
||||
- [Ollama (チャットモデル)](https://ollama.ai/)
|
||||
- [LM Studio (すべてのモデル)](https://lmstudio.ai)
|
||||
- [LocalAi (すべてのモデル)](https://localai.io/)
|
||||
- [Together AI (チャットモデル)](https://www.together.ai/)
|
||||
- [Perplexity (チャットモデル)](https://www.perplexity.ai/)
|
||||
- [OpenRouter (チャットモデル)](https://openrouter.ai/)
|
||||
- [Mistral](https://mistral.ai/)
|
||||
- [Groq](https://groq.com/)
|
||||
- [Cohere](https://cohere.com/)
|
||||
- [KoboldCPP](https://github.com/LostRuins/koboldcpp)
|
||||
|
||||
**埋め込みモデル:**
|
||||
|
||||
- [AnythingLLMネイティブ埋め込み](/server/storage/models/README.md)(デフォルト)
|
||||
- [OpenAI](https://openai.com)
|
||||
- [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service)
|
||||
- [LocalAi (すべて)](https://localai.io/)
|
||||
- [Ollama (すべて)](https://ollama.ai/)
|
||||
- [LM Studio (すべて)](https://lmstudio.ai)
|
||||
- [Cohere](https://cohere.com/)
|
||||
|
||||
**音声変換モデル:**
|
||||
|
||||
- [AnythingLLM内蔵](https://github.com/Mintplex-Labs/anything-llm/tree/master/server/storage/models#audiovideo-transcription)(デフォルト)
|
||||
- [OpenAI](https://openai.com/)
|
||||
|
||||
**TTS(テキストから音声へ)サポート:**
|
||||
|
||||
- ネイティブブラウザ内蔵(デフォルト)
|
||||
- [OpenAI TTS](https://platform.openai.com/docs/guides/text-to-speech/voice-options)
|
||||
- [ElevenLabs](https://elevenlabs.io/)
|
||||
|
||||
**STT(音声からテキストへ)サポート:**
|
||||
|
||||
- ネイティブブラウザ内蔵(デフォルト)
|
||||
|
||||
**ベクトルデータベース:**
|
||||
|
||||
- [LanceDB](https://github.com/lancedb/lancedb)(デフォルト)
|
||||
- [Astra DB](https://www.datastax.com/products/datastax-astra)
|
||||
- [Pinecone](https://pinecone.io)
|
||||
- [Chroma](https://trychroma.com)
|
||||
- [Weaviate](https://weaviate.io)
|
||||
- [QDrant](https://qdrant.tech)
|
||||
- [Milvus](https://milvus.io)
|
||||
- [Zilliz](https://zilliz.com)
|
||||
|
||||
### 技術概要
|
||||
|
||||
このモノレポは、主に3つのセクションで構成されています:
|
||||
|
||||
- `frontend`: LLMが使用できるすべてのコンテンツを簡単に作成および管理できるviteJS + Reactフロントエンド。
|
||||
- `server`: すべてのインタラクションを処理し、すべてのベクトルDB管理およびLLMインタラクションを行うNodeJS expressサーバー。
|
||||
- `collector`: UIからドキュメントを処理および解析するNodeJS expressサーバー。
|
||||
- `docker`: Dockerの指示およびビルドプロセス + ソースからのビルド情報。
|
||||
- `embed`: [埋め込みウィジェット](./embed/README.md)の生成に特化したコード。
|
||||
|
||||
## 🛳 セルフホスティング
|
||||
|
||||
Mintplex Labsおよびコミュニティは、AnythingLLMをローカルで実行できる多数のデプロイメント方法、スクリプト、テンプレートを維持しています。以下の表を参照して、お好みの環境でのデプロイ方法を読むか、自動デプロイを行ってください。
|
||||
| Docker | AWS | GCP | Digital Ocean | Render.com |
|
||||
|----------------------------------------|----:|-----|---------------|------------|
|
||||
| [![Docker上でデプロイ][docker-btn]][docker-deploy] | [![AWS上でデプロイ][aws-btn]][aws-deploy] | [![GCP上でデプロイ][gcp-btn]][gcp-deploy] | [![DigitalOcean上でデプロイ][do-btn]][do-deploy] | [![Render.com上でデプロイ][render-btn]][render-deploy] |
|
||||
|
||||
| Railway |
|
||||
| --------------------------------------------------- |
|
||||
| [![Railway上でデプロイ][railway-btn]][railway-deploy] |
|
||||
|
||||
[Dockerを使用せずに本番環境のAnythingLLMインスタンスを設定する →](./BARE_METAL.md)
|
||||
|
||||
## 開発環境のセットアップ方法
|
||||
|
||||
- `yarn setup` 各アプリケーションセクションに必要な`.env`ファイルを入力します(リポジトリのルートから)。
|
||||
- 次に進む前にこれらを入力してください。`server/.env.development`が入力されていないと正しく動作しません。
|
||||
- `yarn dev:server` ローカルでサーバーを起動します(リポジトリのルートから)。
|
||||
- `yarn dev:frontend` ローカルでフロントエンドを起動します(リポジトリのルートから)。
|
||||
- `yarn dev:collector` ドキュメントコレクターを実行します(リポジトリのルートから)。
|
||||
|
||||
[ドキュメントについて学ぶ](./server/storage/documents/DOCUMENTS.md)
|
||||
|
||||
[ベクトルキャッシュについて学ぶ](./server/storage/vector-cache/VECTOR_CACHE.md)
|
||||
|
||||
## 貢献する方法
|
||||
|
||||
- issueを作成する
|
||||
- `<issue number>-<short name>`の形式のブランチ名でPRを作成する
|
||||
- マージしましょう
|
||||
|
||||
## テレメトリーとプライバシー
|
||||
|
||||
Mintplex Labs Inc.によって開発されたAnythingLLMには、匿名の使用情報を収集するテレメトリー機能が含まれています。
|
||||
|
||||
<details>
|
||||
<summary><kbd>AnythingLLMのテレメトリーとプライバシーについての詳細</kbd></summary>
|
||||
|
||||
### なぜ?
|
||||
|
||||
この情報を使用して、AnythingLLMの使用方法を理解し、新機能とバグ修正の優先順位を決定し、AnythingLLMのパフォーマンスと安定性を向上させるのに役立てます。
|
||||
|
||||
### オプトアウト
|
||||
|
||||
サーバーまたはdockerの.env設定で`DISABLE_TELEMETRY`を「true」に設定して、テレメトリーからオプトアウトします。アプリ内でも、サイドバー > `プライバシー`に移動してテレメトリーを無効にすることができます。
|
||||
|
||||
### 明示的に追跡するもの
|
||||
|
||||
製品およびロードマップの意思決定に役立つ使用詳細のみを追跡します。具体的には:
|
||||
|
||||
- インストールのタイプ(Dockerまたはデスクトップ)
|
||||
- ドキュメントが追加または削除されたとき。ドキュメントについての情報はありません。イベントが発生したことのみを知ります。これにより、使用状況を把握できます。
|
||||
- 使用中のベクトルデータベースのタイプ。どのベクトルデータベースプロバイダーが最も使用されているかを知り、更新があったときに優先して変更を行います。
|
||||
- 使用中のLLMのタイプ。最も人気のある選択肢を知り、更新があったときに優先して変更を行います。
|
||||
- チャットが送信された。これは最も一般的な「イベント」であり、すべてのインストールでのこのプロジェクトの日常的な「アクティビティ」についてのアイデアを提供します。再び、イベントのみが送信され、チャット自体の性質や内容に関する情報はありません。
|
||||
|
||||
これらの主張を検証するには、`Telemetry.sendTelemetry`が呼び出されるすべての場所を見つけてください。また、これらのイベントは出力ログに書き込まれるため、送信された具体的なデータも確認できます。IPアドレスやその他の識別情報は収集されません。テレメトリープロバイダーは[PostHog](https://posthog.com/)です。
|
||||
|
||||
[ソースコード内のすべてのテレメトリーイベントを表示](https://github.com/search?q=repo%3AMintplex-Labs%2Fanything-llm%20.sendTelemetry\(&type=code)
|
||||
|
||||
</details>
|
||||
|
||||
## 🔗 その他の製品
|
||||
|
||||
- **[VectorAdmin][vector-admin]**:ベクトルデータベースを管理するためのオールインワンGUIおよびツールスイート。
|
||||
- **[OpenAI Assistant Swarm][assistant-swarm]**:単一のエージェントから指揮できるOpenAIアシスタントの軍隊に、ライブラリ全体を変換します。
|
||||
|
||||
<div align="right">
|
||||
|
||||
[![][back-to-top]](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
Copyright © 2024 [Mintplex Labs][profile-link]。<br />
|
||||
このプロジェクトは[MIT](./LICENSE)ライセンスの下でライセンスされています。
|
||||
|
||||
<!-- LINK GROUP -->
|
||||
|
||||
[back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-222628?style=flat-square
|
||||
[profile-link]: https://github.com/mintplex-labs
|
||||
[vector-admin]: https://github.com/mintplex-labs/vector-admin
|
||||
[assistant-swarm]: https://github.com/Mintplex-Labs/openai-assistant-swarm
|
||||
[docker-btn]: ./images/deployBtns/docker.png
|
||||
[docker-deploy]: ./docker/HOW_TO_USE_DOCKER.md
|
||||
[aws-btn]: ./images/deployBtns/aws.png
|
||||
[aws-deploy]: ./cloud-deployments/aws/cloudformation/DEPLOY.md
|
||||
[gcp-btn]: https://deploy.cloud.run/button.svg
|
||||
[gcp-deploy]: ./cloud-deployments/gcp/deployment/DEPLOY.md
|
||||
[do-btn]: https://www.deploytodo.com/do-btn-blue.svg
|
||||
[do-deploy]: ./cloud-deployments/digitalocean/terraform/DEPLOY.md
|
||||
[render-btn]: https://render.com/images/deploy-to-render-button.svg
|
||||
[render-deploy]: https://render.com/deploy?repo=https://github.com/Mintplex-Labs/anything-llm&branch=render
|
||||
[render-btn]: https://render.com/images/deploy-to-render-button.svg
|
||||
[render-deploy]: https://render.com/deploy?repo=https://github.com/Mintplex-Labs/anything-llm&branch=render
|
||||
[railway-btn]: https://railway.app/button.svg
|
||||
[railway-deploy]: https://railway.app/template/HNSCS1?referralCode=WFgJkn
|
@ -25,7 +25,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href='/README.md'>English</a> · <b>简体中文</b>
|
||||
<a href='../README.md'>English</a> · <b>简体中文</b> · <a href='./README.ja-JP.md'>简体中文</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
@ -125,6 +125,12 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea
|
||||
# VOYAGEAI_API_KEY=
|
||||
# EMBEDDING_MODEL_PREF='voyage-large-2-instruct'
|
||||
|
||||
# EMBEDDING_ENGINE='litellm'
|
||||
# EMBEDDING_MODEL_PREF='text-embedding-ada-002'
|
||||
# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192
|
||||
# LITE_LLM_BASE_PATH='http://127.0.0.1:4000'
|
||||
# LITE_LLM_API_KEY='sk-123abc'
|
||||
|
||||
###########################################
|
||||
######## Vector Database Selection ########
|
||||
###########################################
|
||||
@ -229,3 +235,9 @@ TTS_PROVIDER="native"
|
||||
|
||||
#------ Serper.dev ----------- https://serper.dev/
|
||||
# AGENT_SERPER_DEV_KEY=
|
||||
|
||||
#------ Bing Search ----------- https://portal.azure.com/
|
||||
# AGENT_BING_SEARCH_API_KEY=
|
||||
|
||||
#------ Serply.io ----------- https://serply.io/
|
||||
# AGENT_SERPLY_API_KEY=
|
||||
|
@ -355,6 +355,9 @@ function adminEndpoints(app) {
|
||||
?.value,
|
||||
[]
|
||||
) || [],
|
||||
custom_app_name:
|
||||
(await SystemSettings.get({ label: "custom_app_name" }))?.value ||
|
||||
null,
|
||||
};
|
||||
response.status(200).json({ settings });
|
||||
} catch (e) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
const { EventLogs } = require("../../../models/eventLogs");
|
||||
const { SystemSettings } = require("../../../models/systemSettings");
|
||||
const { purgeDocument } = require("../../../utils/files/purgeDocument");
|
||||
const { getVectorDbClass } = require("../../../utils/helpers");
|
||||
const {
|
||||
prepareWorkspaceChatsForExport,
|
||||
@ -206,6 +207,72 @@ function apiSystemEndpoints(app) {
|
||||
}
|
||||
}
|
||||
);
|
||||
app.delete(
|
||||
"/v1/system/remove-documents",
|
||||
[validApiKey],
|
||||
async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['System Settings']
|
||||
#swagger.description = 'Permanently remove documents from the system.'
|
||||
#swagger.requestBody = {
|
||||
description: 'Array of document names to be removed permanently.',
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
names: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
example: [
|
||||
"custom-documents/file.txt-fc4beeeb-e436-454d-8bb4-e5b8979cb48f.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
description: 'Documents removed successfully.',
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
success: true,
|
||||
message: 'Documents removed successfully'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
description: 'Forbidden',
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
#swagger.responses[500] = {
|
||||
description: 'Internal Server Error'
|
||||
}
|
||||
*/
|
||||
try {
|
||||
const { names } = reqBody(request);
|
||||
for await (const name of names) await purgeDocument(name);
|
||||
response
|
||||
.status(200)
|
||||
.json({ success: true, message: "Documents removed successfully" })
|
||||
.end();
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { apiSystemEndpoints };
|
||||
|
@ -15,6 +15,8 @@ const {
|
||||
validWorkspaceSlug,
|
||||
} = require("../utils/middleware/validWorkspace");
|
||||
const { writeResponseChunk } = require("../utils/helpers/chat/responses");
|
||||
const { WorkspaceThread } = require("../models/workspaceThread");
|
||||
const truncate = require("truncate");
|
||||
|
||||
function chatEndpoints(app) {
|
||||
if (!app) return;
|
||||
@ -196,6 +198,24 @@ function chatEndpoints(app) {
|
||||
user,
|
||||
thread
|
||||
);
|
||||
|
||||
// If thread was renamed emit event to frontend via special `action` response.
|
||||
await WorkspaceThread.autoRenameThread({
|
||||
thread,
|
||||
workspace,
|
||||
user,
|
||||
newName: truncate(message, 22),
|
||||
onRename: (thread) => {
|
||||
writeResponseChunk(response, {
|
||||
action: "rename_thread",
|
||||
thread: {
|
||||
slug: thread.slug,
|
||||
name: thread.name,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await Telemetry.sendTelemetry("sent_chat", {
|
||||
multiUserMode: multiUserMode(response),
|
||||
LLMSelection: process.env.LLM_PROVIDER || "openai",
|
||||
|
@ -531,17 +531,24 @@ function systemEndpoints(app) {
|
||||
const defaultFilename = getDefaultFilename();
|
||||
const logoPath = await determineLogoFilepath(defaultFilename);
|
||||
const { found, buffer, size, mime } = fetchLogo(logoPath);
|
||||
|
||||
if (!found) {
|
||||
response.sendStatus(204).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const currentLogoFilename = await SystemSettings.currentLogoFilename();
|
||||
response.writeHead(200, {
|
||||
"Access-Control-Expose-Headers":
|
||||
"Content-Disposition,X-Is-Custom-Logo,Content-Type,Content-Length",
|
||||
"Content-Type": mime || "image/png",
|
||||
"Content-Disposition": `attachment; filename=${path.basename(
|
||||
logoPath
|
||||
)}`,
|
||||
"Content-Length": size,
|
||||
"X-Is-Custom-Logo":
|
||||
currentLogoFilename !== null &&
|
||||
currentLogoFilename !== defaultFilename,
|
||||
});
|
||||
response.end(Buffer.from(buffer, "base64"));
|
||||
return;
|
||||
@ -578,6 +585,22 @@ function systemEndpoints(app) {
|
||||
}
|
||||
});
|
||||
|
||||
// No middleware protection in order to get this on the login page
|
||||
app.get("/system/custom-app-name", async (_, response) => {
|
||||
try {
|
||||
const customAppName =
|
||||
(
|
||||
await SystemSettings.get({
|
||||
label: "custom_app_name",
|
||||
})
|
||||
)?.value ?? null;
|
||||
response.status(200).json({ customAppName: customAppName });
|
||||
} catch (error) {
|
||||
console.error("Error fetching custom app name:", error);
|
||||
response.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get(
|
||||
"/system/pfp/:id",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.all])],
|
||||
|
@ -1,4 +1,9 @@
|
||||
const { multiUserMode, userFromSession, reqBody } = require("../utils/http");
|
||||
const {
|
||||
multiUserMode,
|
||||
userFromSession,
|
||||
reqBody,
|
||||
safeJsonParse,
|
||||
} = require("../utils/http");
|
||||
const { validatedRequest } = require("../utils/middleware/validatedRequest");
|
||||
const { Telemetry } = require("../models/telemetry");
|
||||
const {
|
||||
@ -168,6 +173,77 @@ function workspaceThreadEndpoints(app) {
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.delete(
|
||||
"/workspace/:slug/thread/:threadSlug/delete-edited-chats",
|
||||
[
|
||||
validatedRequest,
|
||||
flexUserRoleValid([ROLES.all]),
|
||||
validWorkspaceAndThreadSlug,
|
||||
],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const { startingId } = reqBody(request);
|
||||
const user = await userFromSession(request, response);
|
||||
const workspace = response.locals.workspace;
|
||||
const thread = response.locals.thread;
|
||||
|
||||
await WorkspaceChats.delete({
|
||||
workspaceId: Number(workspace.id),
|
||||
thread_id: Number(thread.id),
|
||||
user_id: user?.id,
|
||||
id: { gte: Number(startingId) },
|
||||
});
|
||||
|
||||
response.sendStatus(200).end();
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/workspace/:slug/thread/:threadSlug/update-chat",
|
||||
[
|
||||
validatedRequest,
|
||||
flexUserRoleValid([ROLES.all]),
|
||||
validWorkspaceAndThreadSlug,
|
||||
],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const { chatId, newText = null } = reqBody(request);
|
||||
if (!newText || !String(newText).trim())
|
||||
throw new Error("Cannot save empty response");
|
||||
|
||||
const user = await userFromSession(request, response);
|
||||
const workspace = response.locals.workspace;
|
||||
const thread = response.locals.thread;
|
||||
const existingChat = await WorkspaceChats.get({
|
||||
workspaceId: workspace.id,
|
||||
thread_id: thread.id,
|
||||
user_id: user?.id,
|
||||
id: Number(chatId),
|
||||
});
|
||||
if (!existingChat) throw new Error("Invalid chat.");
|
||||
|
||||
const chatResponse = safeJsonParse(existingChat.response, null);
|
||||
if (!chatResponse) throw new Error("Failed to parse chat response");
|
||||
|
||||
await WorkspaceChats._update(existingChat.id, {
|
||||
response: JSON.stringify({
|
||||
...chatResponse,
|
||||
text: String(newText),
|
||||
}),
|
||||
});
|
||||
|
||||
response.sendStatus(200).end();
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { workspaceThreadEndpoints };
|
||||
|
@ -380,7 +380,6 @@ function workspaceEndpoints(app) {
|
||||
const history = multiUserMode(response)
|
||||
? await WorkspaceChats.forWorkspaceByUser(workspace.id, user.id)
|
||||
: await WorkspaceChats.forWorkspace(workspace.id);
|
||||
|
||||
response.status(200).json({ history: convertToChatHistory(history) });
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
@ -420,6 +419,67 @@ function workspaceEndpoints(app) {
|
||||
}
|
||||
);
|
||||
|
||||
app.delete(
|
||||
"/workspace/:slug/delete-edited-chats",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const { startingId } = reqBody(request);
|
||||
const user = await userFromSession(request, response);
|
||||
const workspace = response.locals.workspace;
|
||||
|
||||
await WorkspaceChats.delete({
|
||||
workspaceId: workspace.id,
|
||||
thread_id: null,
|
||||
user_id: user?.id,
|
||||
id: { gte: Number(startingId) },
|
||||
});
|
||||
|
||||
response.sendStatus(200).end();
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/workspace/:slug/update-chat",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const { chatId, newText = null } = reqBody(request);
|
||||
if (!newText || !String(newText).trim())
|
||||
throw new Error("Cannot save empty response");
|
||||
|
||||
const user = await userFromSession(request, response);
|
||||
const workspace = response.locals.workspace;
|
||||
const existingChat = await WorkspaceChats.get({
|
||||
workspaceId: workspace.id,
|
||||
thread_id: null,
|
||||
user_id: user?.id,
|
||||
id: Number(chatId),
|
||||
});
|
||||
if (!existingChat) throw new Error("Invalid chat.");
|
||||
|
||||
const chatResponse = safeJsonParse(existingChat.response, null);
|
||||
if (!chatResponse) throw new Error("Failed to parse chat response");
|
||||
|
||||
await WorkspaceChats._update(existingChat.id, {
|
||||
response: JSON.stringify({
|
||||
...chatResponse,
|
||||
text: String(newText),
|
||||
}),
|
||||
});
|
||||
|
||||
response.sendStatus(200).end();
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/workspace/:slug/chat-feedback/:chatId",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
|
||||
|
@ -32,6 +32,7 @@ const SystemSettings = {
|
||||
"agent_search_provider",
|
||||
"default_agent_skills",
|
||||
"agent_sql_connections",
|
||||
"custom_app_name",
|
||||
],
|
||||
validations: {
|
||||
footer_data: (updates) => {
|
||||
@ -74,7 +75,14 @@ const SystemSettings = {
|
||||
agent_search_provider: (update) => {
|
||||
try {
|
||||
if (update === "none") return null;
|
||||
if (!["google-search-engine", "serper-dot-dev"].includes(update))
|
||||
if (
|
||||
![
|
||||
"google-search-engine",
|
||||
"serper-dot-dev",
|
||||
"bing-search",
|
||||
"serply-engine",
|
||||
].includes(update)
|
||||
)
|
||||
throw new Error("Invalid SERP provider.");
|
||||
return String(update);
|
||||
} catch (e) {
|
||||
@ -175,6 +183,8 @@ const SystemSettings = {
|
||||
AgentGoogleSearchEngineId: process.env.AGENT_GSE_CTX || null,
|
||||
AgentGoogleSearchEngineKey: process.env.AGENT_GSE_KEY || null,
|
||||
AgentSerperApiKey: process.env.AGENT_SERPER_DEV_KEY || null,
|
||||
AgentBingSearchApiKey: process.env.AGENT_BING_SEARCH_API_KEY || null,
|
||||
AgentSerplyApiKey: process.env.AGENT_SERPLY_API_KEY || null,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -220,6 +220,24 @@ const WorkspaceChats = {
|
||||
console.error(error.message);
|
||||
}
|
||||
},
|
||||
|
||||
// Explicit update of settings + key validations.
|
||||
// Only use this method when directly setting a key value
|
||||
// that takes no user input for the keys being modified.
|
||||
_update: async function (id = null, data = {}) {
|
||||
if (!id) throw new Error("No workspace chat id provided for update");
|
||||
|
||||
try {
|
||||
await prisma.workspace_chats.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { WorkspaceChats };
|
||||
|
@ -2,13 +2,14 @@ const prisma = require("../utils/prisma");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
|
||||
const WorkspaceThread = {
|
||||
defaultName: "Thread",
|
||||
writable: ["name"],
|
||||
|
||||
new: async function (workspace, userId = null) {
|
||||
try {
|
||||
const thread = await prisma.workspace_threads.create({
|
||||
data: {
|
||||
name: "New thread",
|
||||
name: this.defaultName,
|
||||
slug: uuidv4(),
|
||||
user_id: userId ? Number(userId) : null,
|
||||
workspace_id: workspace.id,
|
||||
@ -84,6 +85,32 @@ const WorkspaceThread = {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
// Will fire on first message (included or not) for a thread and rename the thread with the newName prop.
|
||||
autoRenameThread: async function ({
|
||||
workspace = null,
|
||||
thread = null,
|
||||
user = null,
|
||||
newName = null,
|
||||
onRename = null,
|
||||
}) {
|
||||
if (!workspace || !thread || !newName) return false;
|
||||
if (thread.name !== this.defaultName) return false; // don't rename if already named.
|
||||
|
||||
const { WorkspaceChats } = require("./workspaceChats");
|
||||
const chatCount = await WorkspaceChats.count({
|
||||
workspaceId: workspace.id,
|
||||
user_id: user?.id || null,
|
||||
thread_id: thread.id,
|
||||
});
|
||||
if (chatCount !== 1) return { renamed: false, thread };
|
||||
const { thread: updatedThread } = await this.update(thread, {
|
||||
name: newName,
|
||||
});
|
||||
|
||||
onRename?.(updatedThread);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { WorkspaceThread };
|
||||
|
@ -12,7 +12,7 @@
|
||||
"scripts": {
|
||||
"dev": "NODE_ENV=development nodemon --ignore documents --ignore vector-cache --ignore storage --ignore swagger --trace-warnings index.js",
|
||||
"start": "NODE_ENV=production node index.js",
|
||||
"lint": "yarn prettier --write ./endpoints ./models ./utils index.js",
|
||||
"lint": "yarn prettier --ignore-path ../.prettierignore --write ./endpoints ./models ./utils index.js",
|
||||
"swagger": "node ./swagger/init.js",
|
||||
"sqlite:migrate": "cd ./utils/prisma && node migrateFromSqlite.js"
|
||||
},
|
||||
@ -32,7 +32,7 @@
|
||||
"@langchain/textsplitters": "0.0.0",
|
||||
"@pinecone-database/pinecone": "^2.0.1",
|
||||
"@prisma/client": "5.3.1",
|
||||
"@qdrant/js-client-rest": "^1.4.0",
|
||||
"@qdrant/js-client-rest": "^1.9.0",
|
||||
"@xenova/transformers": "^2.14.0",
|
||||
"@zilliz/milvus2-sdk-node": "^2.3.5",
|
||||
"archiver": "^5.3.1",
|
||||
@ -75,6 +75,7 @@
|
||||
"sqlite3": "^5.1.6",
|
||||
"swagger-autogen": "^2.23.5",
|
||||
"swagger-ui-express": "^5.0.0",
|
||||
"truncate": "^3.0.0",
|
||||
"url-pattern": "^1.0.3",
|
||||
"uuid": "^9.0.0",
|
||||
"uuid-apikey": "^1.5.3",
|
||||
|
@ -2241,6 +2241,71 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/system/remove-documents": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"System Settings"
|
||||
],
|
||||
"description": "Permanently remove documents from the system.",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Documents removed successfully.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"example": {
|
||||
"success": true,
|
||||
"message": "Documents removed successfully"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidAPIKey"
|
||||
}
|
||||
},
|
||||
"application/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"description": "Array of document names to be removed permanently.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"names": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": [
|
||||
"custom-documents/file.txt-fc4beeeb-e436-454d-8bb4-e5b8979cb48f.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
|
@ -91,6 +91,10 @@ class GeminiLLM {
|
||||
switch (this.model) {
|
||||
case "gemini-pro":
|
||||
return 30_720;
|
||||
case "gemini-1.0-pro":
|
||||
return 30_720;
|
||||
case "gemini-1.5-flash-latest":
|
||||
return 1_048_576;
|
||||
case "gemini-1.5-pro-latest":
|
||||
return 1_048_576;
|
||||
default:
|
||||
@ -101,6 +105,7 @@ class GeminiLLM {
|
||||
isValidChatCompletionModel(modelName = "") {
|
||||
const validModels = [
|
||||
"gemini-pro",
|
||||
"gemini-1.0-pro",
|
||||
"gemini-1.5-pro-latest",
|
||||
"gemini-1.5-flash-latest",
|
||||
];
|
||||
|
93
server/utils/EmbeddingEngines/liteLLM/index.js
Normal file
93
server/utils/EmbeddingEngines/liteLLM/index.js
Normal file
@ -0,0 +1,93 @@
|
||||
const { toChunks, maximumChunkLength } = require("../../helpers");
|
||||
|
||||
class LiteLLMEmbedder {
|
||||
constructor() {
|
||||
const { OpenAI: OpenAIApi } = require("openai");
|
||||
if (!process.env.LITE_LLM_BASE_PATH)
|
||||
throw new Error(
|
||||
"LiteLLM must have a valid base path to use for the api."
|
||||
);
|
||||
this.basePath = process.env.LITE_LLM_BASE_PATH;
|
||||
this.openai = new OpenAIApi({
|
||||
baseURL: this.basePath,
|
||||
apiKey: process.env.LITE_LLM_API_KEY ?? null,
|
||||
});
|
||||
this.model = process.env.EMBEDDING_MODEL_PREF || "text-embedding-ada-002";
|
||||
|
||||
// Limit of how many strings we can process in a single pass to stay with resource or network limits
|
||||
this.maxConcurrentChunks = 500;
|
||||
this.embeddingMaxChunkLength = maximumChunkLength();
|
||||
}
|
||||
|
||||
async embedTextInput(textInput) {
|
||||
const result = await this.embedChunks(
|
||||
Array.isArray(textInput) ? textInput : [textInput]
|
||||
);
|
||||
return result?.[0] || [];
|
||||
}
|
||||
|
||||
async embedChunks(textChunks = []) {
|
||||
// Because there is a hard POST limit on how many chunks can be sent at once to LiteLLM (~8mb)
|
||||
// we concurrently execute each max batch of text chunks possible.
|
||||
// Refer to constructor maxConcurrentChunks for more info.
|
||||
const embeddingRequests = [];
|
||||
for (const chunk of toChunks(textChunks, this.maxConcurrentChunks)) {
|
||||
embeddingRequests.push(
|
||||
new Promise((resolve) => {
|
||||
this.openai.embeddings
|
||||
.create({
|
||||
model: this.model,
|
||||
input: chunk,
|
||||
})
|
||||
.then((result) => {
|
||||
resolve({ data: result?.data, error: null });
|
||||
})
|
||||
.catch((e) => {
|
||||
e.type =
|
||||
e?.response?.data?.error?.code ||
|
||||
e?.response?.status ||
|
||||
"failed_to_embed";
|
||||
e.message = e?.response?.data?.error?.message || e.message;
|
||||
resolve({ data: [], error: e });
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const { data = [], error = null } = await Promise.all(
|
||||
embeddingRequests
|
||||
).then((results) => {
|
||||
// If any errors were returned from LiteLLM abort the entire sequence because the embeddings
|
||||
// will be incomplete.
|
||||
const errors = results
|
||||
.filter((res) => !!res.error)
|
||||
.map((res) => res.error)
|
||||
.flat();
|
||||
if (errors.length > 0) {
|
||||
let uniqueErrors = new Set();
|
||||
errors.map((error) =>
|
||||
uniqueErrors.add(`[${error.type}]: ${error.message}`)
|
||||
);
|
||||
|
||||
return {
|
||||
data: [],
|
||||
error: Array.from(uniqueErrors).join(", "),
|
||||
};
|
||||
}
|
||||
return {
|
||||
data: results.map((res) => res?.data || []).flat(),
|
||||
error: null,
|
||||
};
|
||||
});
|
||||
|
||||
if (!!error) throw new Error(`LiteLLM Failed to embed: ${error}`);
|
||||
return data.length > 0 &&
|
||||
data.every((embd) => embd.hasOwnProperty("embedding"))
|
||||
? data.map((embd) => embd.embedding)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
LiteLLMEmbedder,
|
||||
};
|
@ -41,6 +41,7 @@ class AIbitat {
|
||||
...rest,
|
||||
};
|
||||
this.provider = this.defaultProvider.provider;
|
||||
this.model = this.defaultProvider.model;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -154,11 +154,12 @@ const docSummarizer = {
|
||||
this.controller.abort();
|
||||
});
|
||||
|
||||
return await summarizeContent(
|
||||
this.super.provider,
|
||||
this.controller.signal,
|
||||
document.content
|
||||
);
|
||||
return await summarizeContent({
|
||||
provider: this.super.provider,
|
||||
model: this.super.model,
|
||||
controllerSignal: this.controller.signal,
|
||||
content: document.content,
|
||||
});
|
||||
} catch (error) {
|
||||
this.super.handlerProps.log(
|
||||
`document-summarizer.summarizeDoc raised an error. ${error.message}`
|
||||
|
@ -65,6 +65,12 @@ const webBrowsing = {
|
||||
case "serper-dot-dev":
|
||||
engine = "_serperDotDev";
|
||||
break;
|
||||
case "bing-search":
|
||||
engine = "_bingWebSearch";
|
||||
break;
|
||||
case "serply-engine":
|
||||
engine = "_serplyEngine";
|
||||
break;
|
||||
default:
|
||||
engine = "_googleSearchEngine";
|
||||
}
|
||||
@ -168,6 +174,123 @@ const webBrowsing = {
|
||||
});
|
||||
});
|
||||
|
||||
if (data.length === 0)
|
||||
return `No information was found online for the search query.`;
|
||||
return JSON.stringify(data);
|
||||
},
|
||||
_bingWebSearch: async function (query) {
|
||||
if (!process.env.AGENT_BING_SEARCH_API_KEY) {
|
||||
this.super.introspect(
|
||||
`${this.caller}: I can't use Bing Web Search because the user has not defined the required API key.\nVisit: https://portal.azure.com/ to create the API key.`
|
||||
);
|
||||
return `Search is disabled and no content was found. This functionality is disabled because the user has not set it up yet.`;
|
||||
}
|
||||
|
||||
const searchURL = new URL(
|
||||
"https://api.bing.microsoft.com/v7.0/search"
|
||||
);
|
||||
searchURL.searchParams.append("q", query);
|
||||
|
||||
this.super.introspect(
|
||||
`${this.caller}: Using Bing Web Search to search for "${
|
||||
query.length > 100 ? `${query.slice(0, 100)}...` : query
|
||||
}"`
|
||||
);
|
||||
|
||||
const searchResponse = await fetch(searchURL, {
|
||||
headers: {
|
||||
"Ocp-Apim-Subscription-Key":
|
||||
process.env.AGENT_BING_SEARCH_API_KEY,
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
const searchResults = data.webPages?.value || [];
|
||||
return searchResults.map((result) => ({
|
||||
title: result.name,
|
||||
link: result.url,
|
||||
snippet: result.snippet,
|
||||
}));
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
return [];
|
||||
});
|
||||
|
||||
if (searchResponse.length === 0)
|
||||
return `No information was found online for the search query.`;
|
||||
return JSON.stringify(searchResponse);
|
||||
},
|
||||
_serplyEngine: async function (
|
||||
query,
|
||||
language = "en",
|
||||
hl = "us",
|
||||
limit = 100,
|
||||
device_type = "desktop",
|
||||
proxy_location = "US"
|
||||
) {
|
||||
// query (str): The query to search for
|
||||
// hl (str): Host Language code to display results in (reference https://developers.google.com/custom-search/docs/xml_results?hl=en#wsInterfaceLanguages)
|
||||
// limit (int): The maximum number of results to return [10-100, defaults to 100]
|
||||
// device_type: get results based on desktop/mobile (defaults to desktop)
|
||||
|
||||
if (!process.env.AGENT_SERPLY_API_KEY) {
|
||||
this.super.introspect(
|
||||
`${this.caller}: I can't use Serply.io searching because the user has not defined the required API key.\nVisit: https://serply.io to create the API key for free.`
|
||||
);
|
||||
return `Search is disabled and no content was found. This functionality is disabled because the user has not set it up yet.`;
|
||||
}
|
||||
|
||||
this.super.introspect(
|
||||
`${this.caller}: Using Serply to search for "${
|
||||
query.length > 100 ? `${query.slice(0, 100)}...` : query
|
||||
}"`
|
||||
);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
q: query,
|
||||
language: language,
|
||||
hl,
|
||||
gl: proxy_location.toUpperCase(),
|
||||
});
|
||||
const url = `https://api.serply.io/v1/search/${params.toString()}`;
|
||||
const { response, error } = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-API-KEY": process.env.AGENT_SERPLY_API_KEY,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "anything-llm",
|
||||
"X-Proxy-Location": proxy_location,
|
||||
"X-User-Agent": device_type,
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data?.message === "Unauthorized") {
|
||||
return {
|
||||
response: null,
|
||||
error:
|
||||
"Unauthorized. Please double check your AGENT_SERPLY_API_KEY",
|
||||
};
|
||||
}
|
||||
return { response: data, error: null };
|
||||
})
|
||||
.catch((e) => {
|
||||
return { response: null, error: e.message };
|
||||
});
|
||||
if (error)
|
||||
return `There was an error searching for content. ${error}`;
|
||||
|
||||
const data = [];
|
||||
response.results?.forEach((searchResult) => {
|
||||
const { title, link, description } = searchResult;
|
||||
data.push({
|
||||
title,
|
||||
link,
|
||||
snippet: description,
|
||||
});
|
||||
});
|
||||
|
||||
if (data.length === 0)
|
||||
return `No information was found online for the search query.`;
|
||||
return JSON.stringify(data);
|
||||
|
@ -90,11 +90,13 @@ const webScraping = {
|
||||
);
|
||||
this.controller.abort();
|
||||
});
|
||||
return summarizeContent(
|
||||
this.super.provider,
|
||||
this.controller.signal,
|
||||
content
|
||||
);
|
||||
|
||||
return summarizeContent({
|
||||
provider: this.super.provider,
|
||||
model: this.super.model,
|
||||
controllerSignal: this.controller.signal,
|
||||
content,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
@ -2,8 +2,19 @@
|
||||
* A service that provides an AI client to create a completion.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} LangChainModelConfig
|
||||
* @property {(string|null)} baseURL - Override the default base URL process.env for this provider
|
||||
* @property {(string|null)} apiKey - Override the default process.env for this provider
|
||||
* @property {(number|null)} temperature - Override the default temperature
|
||||
* @property {(string|null)} model - Overrides model used for provider.
|
||||
*/
|
||||
|
||||
const { ChatOpenAI } = require("@langchain/openai");
|
||||
const { ChatAnthropic } = require("@langchain/anthropic");
|
||||
const { ChatOllama } = require("@langchain/community/chat_models/ollama");
|
||||
const { toValidNumber } = require("../../../http");
|
||||
|
||||
const DEFAULT_WORKSPACE_PROMPT =
|
||||
"You are a helpful ai assistant who can assist the user and use tools available to help answer the users prompts and questions.";
|
||||
|
||||
@ -27,8 +38,15 @@ class Provider {
|
||||
return this._client;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} provider - the string key of the provider LLM being loaded.
|
||||
* @param {LangChainModelConfig} config - Config to be used to override default connection object.
|
||||
* @returns
|
||||
*/
|
||||
static LangChainChatModel(provider = "openai", config = {}) {
|
||||
switch (provider) {
|
||||
// Cloud models
|
||||
case "openai":
|
||||
return new ChatOpenAI({
|
||||
apiKey: process.env.OPEN_AI_KEY,
|
||||
@ -39,11 +57,108 @@ class Provider {
|
||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||
...config,
|
||||
});
|
||||
default:
|
||||
case "groq":
|
||||
return new ChatOpenAI({
|
||||
apiKey: process.env.OPEN_AI_KEY,
|
||||
configuration: {
|
||||
baseURL: "https://api.groq.com/openai/v1",
|
||||
},
|
||||
apiKey: process.env.GROQ_API_KEY,
|
||||
...config,
|
||||
});
|
||||
case "mistral":
|
||||
return new ChatOpenAI({
|
||||
configuration: {
|
||||
baseURL: "https://api.mistral.ai/v1",
|
||||
},
|
||||
apiKey: process.env.MISTRAL_API_KEY ?? null,
|
||||
...config,
|
||||
});
|
||||
case "openrouter":
|
||||
return new ChatOpenAI({
|
||||
configuration: {
|
||||
baseURL: "https://openrouter.ai/api/v1",
|
||||
defaultHeaders: {
|
||||
"HTTP-Referer": "https://useanything.com",
|
||||
"X-Title": "AnythingLLM",
|
||||
},
|
||||
},
|
||||
apiKey: process.env.OPENROUTER_API_KEY ?? null,
|
||||
...config,
|
||||
});
|
||||
case "perplexity":
|
||||
return new ChatOpenAI({
|
||||
configuration: {
|
||||
baseURL: "https://api.perplexity.ai",
|
||||
},
|
||||
apiKey: process.env.PERPLEXITY_API_KEY ?? null,
|
||||
...config,
|
||||
});
|
||||
case "togetherai":
|
||||
return new ChatOpenAI({
|
||||
configuration: {
|
||||
baseURL: "https://api.together.xyz/v1",
|
||||
},
|
||||
apiKey: process.env.TOGETHER_AI_API_KEY ?? null,
|
||||
...config,
|
||||
});
|
||||
case "generic-openai":
|
||||
return new ChatOpenAI({
|
||||
configuration: {
|
||||
baseURL: process.env.GENERIC_OPEN_AI_BASE_PATH,
|
||||
},
|
||||
apiKey: process.env.GENERIC_OPEN_AI_API_KEY,
|
||||
maxTokens: toValidNumber(
|
||||
process.env.GENERIC_OPEN_AI_MAX_TOKENS,
|
||||
1024
|
||||
),
|
||||
...config,
|
||||
});
|
||||
|
||||
// OSS Model Runners
|
||||
// case "anythingllm_ollama":
|
||||
// return new ChatOllama({
|
||||
// baseUrl: process.env.PLACEHOLDER,
|
||||
// ...config,
|
||||
// });
|
||||
case "ollama":
|
||||
return new ChatOllama({
|
||||
baseUrl: process.env.OLLAMA_BASE_PATH,
|
||||
...config,
|
||||
});
|
||||
case "lmstudio":
|
||||
return new ChatOpenAI({
|
||||
configuration: {
|
||||
baseURL: process.env.LMSTUDIO_BASE_PATH?.replace(/\/+$/, ""),
|
||||
},
|
||||
apiKey: "not-used", // Needs to be specified or else will assume OpenAI
|
||||
...config,
|
||||
});
|
||||
case "koboldcpp":
|
||||
return new ChatOpenAI({
|
||||
configuration: {
|
||||
baseURL: process.env.KOBOLD_CPP_BASE_PATH,
|
||||
},
|
||||
apiKey: "not-used",
|
||||
...config,
|
||||
});
|
||||
case "localai":
|
||||
return new ChatOpenAI({
|
||||
configuration: {
|
||||
baseURL: process.env.LOCAL_AI_BASE_PATH,
|
||||
},
|
||||
apiKey: process.env.LOCAL_AI_API_KEY ?? "not-used",
|
||||
...config,
|
||||
});
|
||||
case "textgenwebui":
|
||||
return new ChatOpenAI({
|
||||
configuration: {
|
||||
baseURL: process.env.TEXT_GEN_WEB_UI_BASE_PATH,
|
||||
},
|
||||
apiKey: process.env.TEXT_GEN_WEB_UI_API_KEY ?? "not-used",
|
||||
...config,
|
||||
});
|
||||
default:
|
||||
throw new Error(`Unsupported provider ${provider} for this task.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,28 +1,52 @@
|
||||
const OpenAI = require("openai");
|
||||
const Provider = require("./ai-provider.js");
|
||||
const { RetryError } = require("../error.js");
|
||||
const InheritMultiple = require("./helpers/classes.js");
|
||||
const UnTooled = require("./helpers/untooled.js");
|
||||
|
||||
/**
|
||||
* The agent provider for the Groq provider.
|
||||
* Using OpenAI tool calling with groq really sucks right now
|
||||
* its just fast and bad. We should probably migrate this to Untooled to improve
|
||||
* coherence.
|
||||
* The agent provider for the GroqAI provider.
|
||||
* We wrap Groq in UnTooled because its tool-calling built in is quite bad and wasteful.
|
||||
*/
|
||||
class GroqProvider extends Provider {
|
||||
class GroqProvider extends InheritMultiple([Provider, UnTooled]) {
|
||||
model;
|
||||
|
||||
constructor(config = {}) {
|
||||
const { model = "llama3-8b-8192" } = config;
|
||||
super();
|
||||
const client = new OpenAI({
|
||||
baseURL: "https://api.groq.com/openai/v1",
|
||||
apiKey: process.env.GROQ_API_KEY,
|
||||
maxRetries: 3,
|
||||
});
|
||||
super(client);
|
||||
|
||||
this._client = client;
|
||||
this.model = model;
|
||||
this.verbose = true;
|
||||
}
|
||||
|
||||
get client() {
|
||||
return this._client;
|
||||
}
|
||||
|
||||
async #handleFunctionCallChat({ messages = [] }) {
|
||||
return await this.client.chat.completions
|
||||
.create({
|
||||
model: this.model,
|
||||
temperature: 0,
|
||||
messages,
|
||||
})
|
||||
.then((result) => {
|
||||
if (!result.hasOwnProperty("choices"))
|
||||
throw new Error("GroqAI chat: No results!");
|
||||
if (result.choices.length === 0)
|
||||
throw new Error("GroqAI chat: No results length!");
|
||||
return result.choices[0].message.content;
|
||||
})
|
||||
.catch((_) => {
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a completion based on the received messages.
|
||||
*
|
||||
@ -32,68 +56,49 @@ class GroqProvider extends Provider {
|
||||
*/
|
||||
async complete(messages, functions = null) {
|
||||
try {
|
||||
const response = await this.client.chat.completions.create({
|
||||
model: this.model,
|
||||
// stream: true,
|
||||
let completion;
|
||||
if (functions.length > 0) {
|
||||
const { toolCall, text } = await this.functionCall(
|
||||
messages,
|
||||
...(Array.isArray(functions) && functions?.length > 0
|
||||
? { functions }
|
||||
: {}),
|
||||
});
|
||||
|
||||
// Right now, we only support one completion,
|
||||
// so we just take the first one in the list
|
||||
const completion = response.choices[0].message;
|
||||
const cost = this.getCost(response.usage);
|
||||
// treat function calls
|
||||
if (completion.function_call) {
|
||||
let functionArgs = {};
|
||||
try {
|
||||
functionArgs = JSON.parse(completion.function_call.arguments);
|
||||
} catch (error) {
|
||||
// call the complete function again in case it gets a json error
|
||||
return this.complete(
|
||||
[
|
||||
...messages,
|
||||
{
|
||||
role: "function",
|
||||
name: completion.function_call.name,
|
||||
function_call: completion.function_call,
|
||||
content: error?.message,
|
||||
},
|
||||
],
|
||||
functions
|
||||
functions,
|
||||
this.#handleFunctionCallChat.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
// console.log(completion, { functionArgs })
|
||||
if (toolCall !== null) {
|
||||
this.providerLog(`Valid tool call found - running ${toolCall.name}.`);
|
||||
this.deduplicator.trackRun(toolCall.name, toolCall.arguments);
|
||||
return {
|
||||
result: null,
|
||||
functionCall: {
|
||||
name: completion.function_call.name,
|
||||
arguments: functionArgs,
|
||||
name: toolCall.name,
|
||||
arguments: toolCall.arguments,
|
||||
},
|
||||
cost,
|
||||
cost: 0,
|
||||
};
|
||||
}
|
||||
completion = { content: text };
|
||||
}
|
||||
|
||||
if (!completion?.content) {
|
||||
this.providerLog(
|
||||
"Will assume chat completion without tool call inputs."
|
||||
);
|
||||
const response = await this.client.chat.completions.create({
|
||||
model: this.model,
|
||||
messages: this.cleanMsgs(messages),
|
||||
});
|
||||
completion = response.choices[0].message;
|
||||
}
|
||||
|
||||
// The UnTooled class inherited Deduplicator is mostly useful to prevent the agent
|
||||
// from calling the exact same function over and over in a loop within a single chat exchange
|
||||
// _but_ we should enable it to call previously used tools in a new chat interaction.
|
||||
this.deduplicator.reset("runs");
|
||||
return {
|
||||
result: completion.content,
|
||||
cost,
|
||||
cost: 0,
|
||||
};
|
||||
} catch (error) {
|
||||
// If invalid Auth error we need to abort because no amount of waiting
|
||||
// will make auth better.
|
||||
if (error instanceof OpenAI.AuthenticationError) throw error;
|
||||
|
||||
if (
|
||||
error instanceof OpenAI.RateLimitError ||
|
||||
error instanceof OpenAI.InternalServerError ||
|
||||
error instanceof OpenAI.APIError // Also will catch AuthenticationError!!!
|
||||
) {
|
||||
throw new RetryError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -103,7 +108,7 @@ class GroqProvider extends Provider {
|
||||
*
|
||||
* @param _usage The completion to get the cost for.
|
||||
* @returns The cost of the completion.
|
||||
* Stubbed since Groq has no cost basis.
|
||||
* Stubbed since LMStudio has no cost basis.
|
||||
*/
|
||||
getCost(_usage) {
|
||||
return 0;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user