merge with master

This commit is contained in:
timothycarambat 2024-06-07 10:08:21 -07:00
commit 2e9875c501
120 changed files with 2433 additions and 442 deletions

View File

@ -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",

View File

@ -10,3 +10,7 @@ frontend/bundleinspector.html
#server
server/swagger/openapi.json
#embed
**/static/**
embed/src/utils/chat/hljs.js

View File

@ -17,7 +17,7 @@
}
},
{
"files": "*.config.js",
"files": ["*.config.js"],
"options": {
"semi": false,
"parser": "flow",

View File

@ -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)

View File

@ -0,0 +1,214 @@
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: anything-llm-volume
annotations:
pv.beta.kubernetes.io/uid: "1000"
pv.beta.kubernetes.io/gid: "1000"
spec:
storageClassName: gp2
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
awsElasticBlockStore:
# This is the volume UUID from AWS EC2 EBS Volumes list.
volumeID: "{{ anythingllm_awsElasticBlockStore_volumeID }}"
fsType: ext4
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- us-east-1c
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: anything-llm-volume-claim
namespace: "{{ namespace }}"
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: anything-llm
namespace: "{{ namespace }}"
labels:
anything-llm: "true"
spec:
selector:
matchLabels:
k8s-app: anything-llm
replicas: 1
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 0%
maxUnavailable: 100%
template:
metadata:
labels:
anything-llm: "true"
k8s-app: anything-llm
app.kubernetes.io/name: anything-llm
app.kubernetes.io/part-of: anything-llm
annotations:
prometheus.io/scrape: "true"
prometheus.io/path: /metrics
prometheus.io/port: "9090"
spec:
serviceAccountName: "default"
terminationGracePeriodSeconds: 10
securityContext:
fsGroup: 1000
runAsNonRoot: true
runAsGroup: 1000
runAsUser: 1000
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- us-east-1c
containers:
- name: anything-llm
resources:
limits:
memory: "1Gi"
cpu: "500m"
requests:
memory: "512Mi"
cpu: "250m"
imagePullPolicy: IfNotPresent
image: "mintplexlabs/anythingllm:render"
securityContext:
allowPrivilegeEscalation: true
capabilities:
add:
- SYS_ADMIN
runAsNonRoot: true
runAsGroup: 1000
runAsUser: 1000
command:
# Specify a command to override the Dockerfile's ENTRYPOINT.
- /bin/bash
- -c
- |
set -x -e
sleep 3
echo "AWS_REGION: $AWS_REGION"
echo "SERVER_PORT: $SERVER_PORT"
echo "NODE_ENV: $NODE_ENV"
echo "STORAGE_DIR: $STORAGE_DIR"
{
cd /app/server/ &&
npx prisma generate --schema=./prisma/schema.prisma &&
npx prisma migrate deploy --schema=./prisma/schema.prisma &&
node /app/server/index.js
echo "Server process exited with status $?"
} &
{
node /app/collector/index.js
echo "Collector process exited with status $?"
} &
wait -n
exit $?
readinessProbe:
httpGet:
path: /v1/api/health
port: 8888
initialDelaySeconds: 15
periodSeconds: 5
successThreshold: 2
livenessProbe:
httpGet:
path: /v1/api/health
port: 8888
initialDelaySeconds: 15
periodSeconds: 5
failureThreshold: 3
env:
- name: AWS_REGION
value: "{{ aws_region }}"
- name: AWS_ACCESS_KEY_ID
value: "{{ aws_access_id }}"
- name: AWS_SECRET_ACCESS_KEY
value: "{{ aws_access_secret }}"
- name: SERVER_PORT
value: "3001"
- name: JWT_SECRET
value: "my-random-string-for-seeding" # Please generate random string at least 12 chars long.
- name: STORAGE_DIR
value: "/storage"
- name: NODE_ENV
value: "production"
- name: UID
value: "1000"
- name: GID
value: "1000"
volumeMounts:
- name: anything-llm-server-storage-volume-mount
mountPath: /storage
volumes:
- name: anything-llm-server-storage-volume-mount
persistentVolumeClaim:
claimName: anything-llm-volume-claim
---
# This serves the UI and the backend.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: anything-llm-ingress
namespace: "{{ namespace }}"
annotations:
external-dns.alpha.kubernetes.io/hostname: "{{ namespace }}-chat.{{ base_domain }}"
kubernetes.io/ingress.class: "internal-ingress"
nginx.ingress.kubernetes.io/rewrite-target: /
ingress.kubernetes.io/ssl-redirect: "false"
spec:
rules:
- host: "{{ namespace }}-chat.{{ base_domain }}"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: anything-llm-svc
port:
number: 3001
tls: # < placing a host in the TLS config will indicate a cert should be created
- hosts:
- "{{ namespace }}-chat.{{ base_domain }}"
secretName: letsencrypt-prod
---
apiVersion: v1
kind: Service
metadata:
labels:
kubernetes.io/name: anything-llm
name: anything-llm-svc
namespace: "{{ namespace }}"
spec:
ports:
# "port" is external port, and "targetPort" is internal.
- port: 3301
targetPort: 3001
name: traffic
- port: 9090
targetPort: 9090
name: metrics
selector:
k8s-app: anything-llm

View File

@ -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",

View File

@ -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;

View File

@ -23,6 +23,7 @@ class MimeDetector {
{
"text/plain": [
"ts",
"tsx",
"py",
"opts",
"lock",
@ -35,6 +36,7 @@ class MimeDetector {
"js",
"lua",
"pas",
"r",
],
},
true

View File

@ -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 ########
###########################################
@ -232,4 +238,7 @@ GID='1000'
# AGENT_GSE_CTX=
#------ Serper.dev ----------- https://serper.dev/
# AGENT_SERPER_DEV_KEY=
# AGENT_SERPER_DEV_KEY=
#------ Bing Search ----------- https://portal.azure.com/
# AGENT_BING_SEARCH_API_KEY=

View File

@ -1,9 +0,0 @@
# defaults
**/.git
**/.svn
**/.hg
**/node_modules
**/dist
**/static/**
src/utils/chat/hljs.js

View File

@ -4,9 +4,7 @@
"target": "esnext",
"jsx": "react",
"paths": {
"@/*": [
"./src/*"
],
}
}
}
"@/*": ["./src/*"],
},
},
}

View File

@ -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",

View File

@ -37,7 +37,10 @@ export default function App() {
return (
<>
<Head />
<div className={`fixed inset-0 z-50 ${isChatOpen ? "block" : "hidden"}`}>
<div
id="anything-llm-embed-chat-container"
className={`fixed inset-0 z-50 ${isChatOpen ? "block" : "hidden"}`}
>
<div
className={`${windowHeight} ${windowWidth} h-full w-full bg-white fixed bottom-0 right-0 mb-4 md:mr-4 rounded-2xl border border-gray-300 shadow-[0_4px_14px_rgba(0,0,0,0.25)] ${positionClasses[position]}`}
id="anything-llm-chat"
@ -53,6 +56,7 @@ export default function App() {
</div>
{!isChatOpen && (
<div
id="anything-llm-embed-chat-button-container"
className={`fixed bottom-0 ${positionClasses[position]} mb-4 z-50`}
>
<OpenButton

View File

@ -23,6 +23,7 @@ export default function OpenButton({ settings, isOpen, toggleOpen }) {
: CHAT_ICONS.plus;
return (
<button
id="anything-llm-embed-chat-button"
onClick={toggleOpen}
className={`flex items-center justify-center p-4 rounded-full bg-[${settings.buttonColor}] text-white text-2xl`}
aria-label="Toggle Menu"

View File

@ -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: []
}
},
}
})

View File

@ -4,9 +4,7 @@
"target": "esnext",
"jsx": "react",
"paths": {
"@/*": [
"./src/*"
],
"@/*": ["./src/*"]
}
}
}
}

View File

@ -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": {
@ -66,4 +66,4 @@
"tailwindcss": "^3.3.1",
"vite": "^4.3.0"
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -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>
);

View File

@ -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";
@ -9,12 +9,9 @@ export default function ChatBubble({ message, type, popMsg }) {
return (
<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-[800px] flex-col`}
>
<div className={`py-8 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}>
<div className="flex gap-x-5">
<Jazzicon
size={36}
<UserIcon
user={{ uid: isUser ? userFromStorage()?.username : "system" }}
role={type}
/>

View File

@ -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";
@ -43,10 +43,10 @@ export default function DefaultChatContainer() {
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR} md:mt-0 mt-[40px]`}
>
<div
className={`pt-10 pb-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
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`}
@ -67,10 +67,10 @@ export default function DefaultChatContainer() {
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`}
>
<div
className={`pb-4 pt-2 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
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`}
@ -90,10 +90,10 @@ export default function DefaultChatContainer() {
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`}
>
<div
className={`pt-2 pb-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
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`}
@ -124,11 +124,10 @@ export default function DefaultChatContainer() {
className={`flex justify-center items-end w-full ${USER_BACKGROUND_COLOR}`}
>
<div
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
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"}
/>
@ -148,10 +147,10 @@ export default function DefaultChatContainer() {
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`}
>
<div
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
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`}
@ -185,11 +184,10 @@ export default function DefaultChatContainer() {
className={`flex justify-center items-end w-full ${USER_BACKGROUND_COLOR}`}
>
<div
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
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"}
/>
@ -210,10 +208,10 @@ export default function DefaultChatContainer() {
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`}
>
<div
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
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`}
@ -248,11 +246,10 @@ export default function DefaultChatContainer() {
className={`flex justify-center items-end w-full ${USER_BACKGROUND_COLOR}`}
>
<div
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
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"}
/>
@ -272,10 +269,10 @@ export default function DefaultChatContainer() {
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`}
>
<div
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
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`}

View File

@ -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>
);
}

View File

@ -108,10 +108,13 @@ export default function Footer() {
rel="noreferrer"
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
{React.createElement(ICON_COMPONENTS[item.icon], {
weight: "fill",
className: "h-5 w-5",
})}
{React.createElement(
ICON_COMPONENTS?.[item.icon] ?? ICON_COMPONENTS.Info,
{
weight: "fill",
className: "h-5 w-5",
}
)}
</a>
))}
{!isMobile && <SettingsButton />}

View File

@ -30,7 +30,12 @@ export default function GeminiLLMOptions({ settings }) {
required={true}
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
{["gemini-pro", "gemini-1.5-pro-latest"].map((model) => {
{[
"gemini-pro",
"gemini-1.0-pro",
"gemini-1.5-pro-latest",
"gemini-1.5-flash-latest",
].map((model) => {
return (
<option key={model} value={model}>
{model}

View File

@ -170,6 +170,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,
@ -252,6 +253,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
@ -274,11 +284,12 @@ export default function MultiUserAuth() {
{t("login.multi-user.welcome")}
</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">
{t("login.sign-in")}
{t("login.sign-in.start")} {customAppName || "AnythingLLM"}{" "}
{t("login.sign-in.end")}
</p>
</div>
</div>

View File

@ -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";
@ -12,10 +11,10 @@ export default function SingleUserAuth() {
const { t } = useTranslation();
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,
@ -59,6 +58,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}>
@ -70,11 +78,12 @@ export default function SingleUserAuth() {
{t("login.multi-user.welcome")}
</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">
{t("login.sign-in")}
{t("login.sign-in.start")} {customAppName || "AnythingLLM"}{" "}
{t("login.sign-in.end")}
</p>
</div>
</div>

View File

@ -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>

View File

@ -87,7 +87,7 @@ export default function SettingsSidebar() {
/>
<div
ref={sidebarRef}
className="h-[100vh] fixed top-0 left-0 rounded-r-[26px] bg-sidebar w-[80%] p-[18px] "
className="h-[100vh] fixed top-0 left-0 rounded-r-[26px] bg-sidebar w-[80%] p-[18px]"
>
<div className="w-full h-full flex flex-col overflow-x-hidden items-between">
{/* Header Information */}
@ -111,12 +111,14 @@ export default function SettingsSidebar() {
</div>
{/* Primary Body */}
<div className="h-full flex flex-col w-full justify-between pt-4 overflow-y-scroll no-scroll ">
<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} t={t} />
</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>
@ -141,22 +143,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">
{t("settings.title")}
</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} t={t} />
</div>
</div>
<div className="mb-2">
<Footer />
</div>
</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-50">
<Footer />
</div>
</div>
</div>

View File

@ -22,11 +22,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 +43,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 +66,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 +98,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

View File

@ -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";

View File

@ -32,34 +32,34 @@ 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="flex gap-x-2 items-center justify-between">
{(!user || user?.role !== "default") && (
<button
onClick={showNewWsModal}
className="flex flex-grow w-[75%] h-[44px] gap-x-2 py-[5px] px-2.5 mb-2 bg-white rounded-[8px] text-sidebar justify-center items-center hover:bg-opacity-80 transition-all duration-300"
>
<Plus size={18} weight="bold" />
<p className="text-sidebar text-sm font-semibold">
New Workspace
</p>
</button>
)}
<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
onClick={showNewWsModal}
className="flex flex-grow w-[75%] h-[44px] gap-x-2 py-[5px] px-2.5 mb-2 bg-white rounded-[8px] text-sidebar justify-center items-center hover:bg-opacity-80 transition-all duration-300"
>
<Plus size={18} weight="bold" />
<p className="text-sidebar text-sm font-semibold">
New Workspace
</p>
</button>
)}
</div>
<ActiveWorkspaces />
</div>
<ActiveWorkspaces />
</div>
<div className="flex flex-col flex-grow justify-end mb-2">
<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-30">
<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>

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -368,7 +368,7 @@ export function Chartable({ props, workspace }) {
if (!!props.chatId) {
return (
<div className="flex justify-center items-end w-full">
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
<div className="flex gap-x-5">
<WorkspaceProfileImage workspace={workspace} />
<div className="relative w-full">
@ -389,7 +389,7 @@ export function Chartable({ props, workspace }) {
return (
<div className="flex justify-center items-end w-full">
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
<div className="relative w-full">
<DownloadGraph onClick={handleDownload} />
<div ref={ref}>{renderChart()}</div>

View File

@ -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>
);
}

View File

@ -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

View File

@ -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,22 +22,28 @@ const HistoricalMessage = ({
chatId = null,
isLastMessage = false,
regenerateMessage,
saveEditedMessage,
}) => {
return (
<div
key={uuid}
className={`flex justify-center items-end w-full ${
role === "user" ? USER_BACKGROUND_COLOR : AI_BACKGROUND_COLOR
}`}
>
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
className={`py-8 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
key={uuid}
className={`flex justify-center items-end w-full ${
role === "user" ? USER_BACKGROUND_COLOR : AI_BACKGROUND_COLOR
}`}
>
<div className="flex gap-x-5">
<ProfileImage role={role} workspace={workspace} />
{error ? (
<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} />
<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>
@ -44,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`}
@ -53,19 +84,19 @@ 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
message={message}
feedbackScore={feedbackScore}
chatId={chatId}
slug={workspace?.slug}
isLastMessage={isLastMessage}
regenerateMessage={regenerateMessage}
/>
</div>
)}
<div className="flex gap-x-5">
<div className="relative w-[35px] h-[35px] rounded-full flex-shrink-0 overflow-hidden" />
<Actions
message={message}
feedbackScore={feedbackScore}
chatId={chatId}
slug={workspace?.slug}
isLastMessage={isLastMessage}
regenerateMessage={regenerateMessage}
isEditing={isEditing}
role={role}
/>
</div>
{role === "assistant" && <Citations sources={sources} />}
</div>
</div>
@ -86,8 +117,7 @@ function ProfileImage({ role, workspace }) {
}
return (
<Jazzicon
size={36}
<UserIcon
user={{
uid: role === "user" ? userFromStorage()?.username : workspace.slug,
}}

View File

@ -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";
@ -21,7 +21,7 @@ const PromptReply = ({
<div
className={`flex justify-center items-end w-full ${assistantBackgroundColor}`}
>
<div className="py-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
<div className="py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
<div className="flex gap-x-5">
<WorkspaceProfileImage workspace={workspace} />
<div className="mt-3 ml-5 dot-falling"></div>
@ -36,7 +36,7 @@ const PromptReply = ({
<div
className={`flex justify-center items-end w-full ${assistantBackgroundColor}`}
>
<div className="py-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
<div className="py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
<div className="flex gap-x-5">
<WorkspaceProfileImage workspace={workspace} />
<span
@ -57,7 +57,7 @@ const PromptReply = ({
key={uuid}
className={`flex justify-center items-end w-full ${assistantBackgroundColor}`}
>
<div className="py-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
<div className="py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
<div className="flex gap-x-5">
<WorkspaceProfileImage workspace={workspace} />
<span
@ -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);

View File

@ -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}
/>
);
})}
@ -198,7 +243,7 @@ export default function ChatHistory({
function StatusResponse({ props }) {
return (
<div className="flex justify-center items-end w-full">
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
<div className="flex gap-x-5">
<span
className={`text-xs inline-block p-2 rounded-lg text-white/60 font-mono whitespace-pre-line`}

View File

@ -240,6 +240,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
history={chatHistory}
workspace={workspace}
sendCommand={sendCommand}
updateHistory={setChatHistory}
regenerateAssistantMessage={regenerateAssistantMessage}
/>
<PromptInput

View File

@ -8,7 +8,7 @@ export default function LoadingChat() {
return (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll"
className="p-4 transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<Skeleton.default
height="100px"
@ -16,7 +16,7 @@ export default function LoadingChat() {
highlightColor={highlightColor}
baseColor={baseColor}
count={1}
className="max-w-full md:max-w-[75%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
className="max-w-full md:max-w-[80%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
containerClassName="flex justify-start"
/>
<Skeleton.default
@ -25,7 +25,7 @@ export default function LoadingChat() {
baseColor={baseColor}
highlightColor={highlightColor}
count={1}
className="max-w-full md:max-w-[75%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
className="max-w-full md:max-w-[80%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
containerClassName="flex justify-end"
/>
<Skeleton.default
@ -34,7 +34,7 @@ export default function LoadingChat() {
baseColor={baseColor}
highlightColor={highlightColor}
count={1}
className="max-w-full md:max-w-[75%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
className="max-w-full md:max-w-[80%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
containerClassName="flex justify-start"
/>
<Skeleton.default
@ -43,7 +43,7 @@ export default function LoadingChat() {
baseColor={baseColor}
highlightColor={highlightColor}
count={1}
className="max-w-full md:max-w-[75%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
className="max-w-full md:max-w-[80%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
containerClassName="flex justify-end"
/>
<Skeleton.default
@ -52,7 +52,7 @@ export default function LoadingChat() {
baseColor={baseColor}
highlightColor={highlightColor}
count={1}
className="max-w-full md:max-w-[75%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
className="max-w-full md:max-w-[80%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
containerClassName="flex justify-start"
/>
</div>

View File

@ -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);
}

View File

@ -10,7 +10,12 @@ export const DISABLED_PROVIDERS = [
];
const PROVIDER_DEFAULT_MODELS = {
openai: [],
gemini: ["gemini-pro", "gemini-1.5-pro-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",

View File

@ -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 };
}

View File

@ -40,7 +40,10 @@ const TRANSLATIONS = {
"forgot-pass": "Forgot password",
reset: "Reset",
},
"sign-in": "Sign in to your AnythingLLM account.",
"sign-in": {
start: "Sign in to your",
end: "account.",
},
},
// Workspace Settings menu items

View File

@ -41,7 +41,10 @@ const TRANSLATIONS = {
"forgot-pass": "忘记密码",
reset: "重置",
},
"sign-in": "登录",
"sign-in": {
start: null,
end: null,
},
},
// Workspace Settings menu items

View File

@ -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) {

View File

@ -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;

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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";
import { useTranslation } from "react-i18next";
@ -37,7 +36,7 @@ export default function CustomLogo() {
return;
}
const logoURL = await System.fetchLogo();
const { logoURL } = await System.fetchLogo();
_setLogo(logoURL);
showToast("Image uploaded successfully.", "success");
@ -52,13 +51,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");

View File

@ -5,6 +5,7 @@ import SupportEmail from "./SupportEmail";
import CustomLogo from "./CustomLogo";
import CustomMessages from "./CustomMessages";
import { useTranslation } from "react-i18next";
import CustomAppName from "./CustomAppName";
export default function Appearance() {
const { t } = useTranslation();
@ -27,6 +28,7 @@ export default function Appearance() {
</p>
</div>
<CustomLogo />
<CustomAppName />
<CustomMessages />
<FooterCustomization />
<SupportEmail />

View File

@ -7,7 +7,7 @@ import useQuery from "@/hooks/useQuery";
import ChatRow from "./ChatRow";
import showToast from "@/utils/toast";
import System from "@/models/system";
import { CaretDown, Download } from "@phosphor-icons/react";
import { CaretDown, Download, Trash } from "@phosphor-icons/react";
import { saveAs } from "file-saver";
import { useTranslation } from "react-i18next";
@ -50,6 +50,13 @@ export default function WorkspaceChats() {
const [showMenu, setShowMenu] = useState(false);
const menuRef = useRef();
const openMenuButton = useRef();
const query = useQuery();
const [loading, setLoading] = useState(true);
const [chats, setChats] = useState([]);
const [offset, setOffset] = useState(Number(query.get("offset") || 0));
const [canNext, setCanNext] = useState(false);
const { t } = useTranslation();
const handleDumpChats = async (exportType) => {
const chats = await System.exportChats(exportType);
if (!!chats) {
@ -63,10 +70,21 @@ export default function WorkspaceChats() {
}
};
const handleClearAllChats = async () => {
if (
!window.confirm(
`Are you sure you want to clear all chats?\n\nThis action is irreversible.`
)
)
return false;
await System.deleteChat(-1);
setChats([]);
showToast("Cleared all chats.", "success");
};
const toggleMenu = () => {
setShowMenu(!showMenu);
};
const { t } = useTranslation();
useEffect(() => {
function handleClickOutside(event) {
@ -85,6 +103,16 @@ export default function WorkspaceChats() {
};
}, []);
useEffect(() => {
async function fetchChats() {
const { chats: _chats, hasPages = false } = await System.chats(offset);
setChats(_chats);
setCanNext(hasPages);
setLoading(false);
}
fetchChats();
}, [offset]);
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
@ -102,7 +130,7 @@ export default function WorkspaceChats() {
<button
ref={openMenuButton}
onClick={toggleMenu}
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
className="flex items-center gap-x-2 px-4 py-1 rounded-lg bg-[#46C8FF] hover:text-white text-xs font-semibold hover:bg-[#2C2F36] shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit"
>
<Download size={18} weight="bold" />
{t("recorded.export")}
@ -130,26 +158,44 @@ export default function WorkspaceChats() {
</div>
</div>
</div>
{chats.length > 0 && (
<button
onClick={handleClearAllChats}
className="flex items-center gap-x-2 px-4 py-1 border hover:border-transparent border-white/40 text-white/40 rounded-lg bg-transparent hover:text-white text-xs font-semibold hover:bg-red-500 shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit"
>
<Trash size={18} weight="bold" />
Clear Chats
</button>
)}
</div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
{t("recorded.description")}
</p>
</div>
<ChatsContainer />
<ChatsContainer
loading={loading}
chats={chats}
setChats={setChats}
offset={offset}
setOffset={setOffset}
canNext={canNext}
t={t}
/>
</div>
</div>
</div>
);
}
function ChatsContainer() {
const query = useQuery();
const [loading, setLoading] = useState(true);
const [chats, setChats] = useState([]);
const [offset, setOffset] = useState(Number(query.get("offset") || 0));
const [canNext, setCanNext] = useState(false);
const { t } = useTranslation();
function ChatsContainer({
loading,
chats,
setChats,
offset,
setOffset,
canNext,
t,
}) {
const handlePrevious = () => {
setOffset(Math.max(offset - 1, 0));
};
@ -157,20 +203,11 @@ function ChatsContainer() {
setOffset(offset + 1);
};
const handleDeleteChat = (chatId) => {
const handleDeleteChat = async (chatId) => {
await System.deleteChat(chatId);
setChats((prevChats) => prevChats.filter((chat) => chat.id !== chatId));
};
useEffect(() => {
async function fetchChats() {
const { chats: _chats, hasPages = false } = await System.chats(offset);
setChats(_chats);
setCanNext(hasPages);
setLoading(false);
}
fetchChats();
}, [offset]);
if (loading) {
return (
<Skeleton.default

View File

@ -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";
@ -89,6 +91,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() {

View File

@ -393,16 +393,17 @@ export default function GeneralLLMPreference() {
>
<div className="flex gap-x-4 items-center">
<img
src={selectedLLMObject.logo}
alt={`${selectedLLMObject.name} logo`}
src={selectedLLMObject?.logo || AnythingLLMIcon}
alt={`${selectedLLMObject?.name} logo`}
className="w-10 h-10 rounded-md"
/>
<div className="flex flex-col text-left">
<div className="text-sm font-semibold text-white">
{selectedLLMObject.name}
{selectedLLMObject?.name || "None selected"}
</div>
<div className="mt-1 text-xs text-[#D2D5DB]">
{selectedLLMObject.description}
{selectedLLMObject?.description ||
"You need to select an LLM"}
</div>
</div>
</div>

View File

@ -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 }) {

View File

@ -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,66 @@ 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>
</>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

@ -2,11 +2,13 @@ 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 { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
import SearchProviderItem from "./SearchProviderItem";
import {
SerperDotDevOptions,
GoogleSearchOptions,
BingSearchOptions,
} from "./SearchProviderOptions";
import { useTranslation } from "react-i18next";
@ -35,6 +37,14 @@ 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.",
},
];
export default function AgentWebSearchSelection({

View File

@ -108,13 +108,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") {

View File

@ -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: {

235
locales/README.ja-JP.md Normal file
View 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">
👉 デスクトップ用AnythingLLMMac、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

View File

@ -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">

View File

@ -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 ########
###########################################
@ -228,4 +234,7 @@ TTS_PROVIDER="native"
# AGENT_GSE_CTX=
#------ Serper.dev ----------- https://serper.dev/
# AGENT_SERPER_DEV_KEY=
# AGENT_SERPER_DEV_KEY=
#------ Bing Search ----------- https://portal.azure.com/
# AGENT_BING_SEARCH_API_KEY=

View File

@ -33,10 +33,7 @@ function adminEndpoints(app) {
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (_request, response) => {
try {
const users = (await User.where()).map((user) => {
const { password, ...rest } = user;
return rest;
});
const users = await User.where();
response.status(200).json({ users });
} catch (e) {
console.error(e);
@ -358,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) {

View File

@ -73,10 +73,7 @@ function apiAdminEndpoints(app) {
return;
}
const users = (await User.where()).map((user) => {
const { password, ...rest } = user;
return rest;
});
const users = await User.where();
response.status(200).json({ users });
} catch (e) {
console.error(e);

View File

@ -5,6 +5,7 @@ const {
viewLocalFiles,
findDocumentInDocuments,
normalizePath,
isWithin,
} = require("../../../utils/files");
const { reqBody } = require("../../../utils/http");
const { EventLogs } = require("../../../models/eventLogs");
@ -603,6 +604,8 @@ function apiDocumentEndpoints(app) {
try {
const { name } = reqBody(request);
const storagePath = path.join(documentsPath, normalizePath(name));
if (!isWithin(path.resolve(documentsPath), path.resolve(storagePath)))
throw new Error("Invalid path name");
if (fs.existsSync(storagePath)) {
response.status(500).json({

View File

@ -206,6 +206,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 };

View File

@ -1,5 +1,5 @@
const { Document } = require("../models/documents");
const { normalizePath, documentsPath } = require("../utils/files");
const { normalizePath, documentsPath, isWithin } = require("../utils/files");
const { reqBody } = require("../utils/http");
const {
flexUserRoleValid,
@ -18,6 +18,8 @@ function documentEndpoints(app) {
try {
const { name } = reqBody(request);
const storagePath = path.join(documentsPath, normalizePath(name));
if (!isWithin(path.resolve(documentsPath), path.resolve(storagePath)))
throw new Error("Invalid folder name.");
if (fs.existsSync(storagePath)) {
response.status(500).json({

View File

@ -1,7 +1,7 @@
process.env.NODE_ENV === "development"
? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` })
: require("dotenv").config();
const { viewLocalFiles, normalizePath } = require("../utils/files");
const { viewLocalFiles, normalizePath, isWithin } = require("../utils/files");
const { purgeDocument, purgeFolder } = require("../utils/files/purgeDocument");
const { getVectorDbClass } = require("../utils/helpers");
const { updateENV, dumpENV } = require("../utils/helpers/updateENV");
@ -110,7 +110,7 @@ function systemEndpoints(app) {
if (await SystemSettings.isMultiUserMode()) {
const { username, password } = reqBody(request);
const existingUser = await User.get({ username: String(username) });
const existingUser = await User._get({ username: String(username) });
if (!existingUser) {
await EventLogs.logEvent(
@ -188,7 +188,7 @@ function systemEndpoints(app) {
// Return recovery codes to frontend
response.status(200).json({
valid: true,
user: existingUser,
user: User.filterFields(existingUser),
token: makeJWT(
{ id: existingUser.id, username: existingUser.username },
"30d"
@ -201,7 +201,7 @@ function systemEndpoints(app) {
response.status(200).json({
valid: true,
user: existingUser,
user: User.filterFields(existingUser),
token: makeJWT(
{ id: existingUser.id, username: existingUser.username },
"30d"
@ -436,14 +436,22 @@ function systemEndpoints(app) {
return;
}
let error = null;
const { usePassword, newPassword } = reqBody(request);
const { error } = await updateENV(
{
AuthToken: usePassword ? newPassword : "",
JWTSecret: usePassword ? v4() : "",
},
true
);
if (!usePassword) {
// Password is being disabled so directly unset everything to bypass validation.
process.env.AUTH_TOKEN = "";
process.env.JWT_SECRET = "";
} else {
error = await updateENV(
{
AuthToken: newPassword,
JWTSecret: v4(),
},
true
)?.error;
}
if (process.env.NODE_ENV === "production") await dumpENV();
response.status(200).json({ success: !error, error });
} catch (e) {
@ -518,17 +526,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;
@ -565,6 +580,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])],
@ -614,11 +645,13 @@ function systemEndpoints(app) {
const userRecord = await User.get({ id: user.id });
const oldPfpFilename = userRecord.pfpFilename;
if (oldPfpFilename) {
const storagePath = path.join(__dirname, "../storage/assets/pfp");
const oldPfpPath = path.join(
__dirname,
`../storage/assets/pfp/${normalizePath(userRecord.pfpFilename)}`
storagePath,
normalizePath(userRecord.pfpFilename)
);
if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))
throw new Error("Invalid path name");
if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
}
@ -647,13 +680,14 @@ function systemEndpoints(app) {
const userRecord = await User.get({ id: user.id });
const oldPfpFilename = userRecord.pfpFilename;
console.log("oldPfpFilename", oldPfpFilename);
if (oldPfpFilename) {
const storagePath = path.join(__dirname, "../storage/assets/pfp");
const oldPfpPath = path.join(
__dirname,
`../storage/assets/pfp/${normalizePath(oldPfpFilename)}`
storagePath,
normalizePath(oldPfpFilename)
);
if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))
throw new Error("Invalid path name");
if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
}
@ -976,7 +1010,9 @@ function systemEndpoints(app) {
async (request, response) => {
try {
const { id } = request.params;
await WorkspaceChats.delete({ id: Number(id) });
Number(id) === -1
? await WorkspaceChats.delete({}, true)
: await WorkspaceChats.delete({ id: Number(id) });
response.json({ success: true, error: null });
} catch (e) {
console.error(e);
@ -1024,7 +1060,7 @@ function systemEndpoints(app) {
const updates = {};
if (username) {
updates.username = String(username);
updates.username = User.validations.username(String(username));
}
if (password) {
updates.password = String(password);

View File

@ -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 };

View File

@ -6,7 +6,7 @@ const {
userFromSession,
safeJsonParse,
} = require("../utils/http");
const { normalizePath } = require("../utils/files");
const { normalizePath, isWithin } = require("../utils/files");
const { Workspace } = require("../models/workspace");
const { Document } = require("../models/documents");
const { DocumentVectors } = require("../models/vectors");
@ -111,39 +111,45 @@ function workspaceEndpoints(app) {
handleFileUpload,
],
async function (request, response) {
const Collector = new CollectorApi();
const { originalname } = request.file;
const processingOnline = await Collector.online();
try {
const Collector = new CollectorApi();
const { originalname } = request.file;
const processingOnline = await Collector.online();
if (!processingOnline) {
response
.status(500)
.json({
success: false,
error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`,
})
.end();
return;
if (!processingOnline) {
response
.status(500)
.json({
success: false,
error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`,
})
.end();
return;
}
const { success, reason } =
await Collector.processDocument(originalname);
if (!success) {
response.status(500).json({ success: false, error: reason }).end();
return;
}
Collector.log(
`Document ${originalname} uploaded processed and successfully. It is now available in documents.`
);
await Telemetry.sendTelemetry("document_uploaded");
await EventLogs.logEvent(
"document_uploaded",
{
documentName: originalname,
},
response.locals?.user?.id
);
response.status(200).json({ success: true, error: null });
} catch (e) {
console.log(e.message, e);
response.sendStatus(500).end();
}
const { success, reason } = await Collector.processDocument(originalname);
if (!success) {
response.status(500).json({ success: false, error: reason }).end();
return;
}
Collector.log(
`Document ${originalname} uploaded processed and successfully. It is now available in documents.`
);
await Telemetry.sendTelemetry("document_uploaded");
await EventLogs.logEvent(
"document_uploaded",
{
documentName: originalname,
},
response.locals?.user?.id
);
response.status(200).json({ success: true, error: null });
}
);
@ -151,37 +157,42 @@ function workspaceEndpoints(app) {
"/workspace/:slug/upload-link",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => {
const Collector = new CollectorApi();
const { link = "" } = reqBody(request);
const processingOnline = await Collector.online();
try {
const Collector = new CollectorApi();
const { link = "" } = reqBody(request);
const processingOnline = await Collector.online();
if (!processingOnline) {
response
.status(500)
.json({
success: false,
error: `Document processing API is not online. Link ${link} will not be processed automatically.`,
})
.end();
return;
if (!processingOnline) {
response
.status(500)
.json({
success: false,
error: `Document processing API is not online. Link ${link} will not be processed automatically.`,
})
.end();
return;
}
const { success, reason } = await Collector.processLink(link);
if (!success) {
response.status(500).json({ success: false, error: reason }).end();
return;
}
Collector.log(
`Link ${link} uploaded processed and successfully. It is now available in documents.`
);
await Telemetry.sendTelemetry("link_uploaded");
await EventLogs.logEvent(
"link_uploaded",
{ link },
response.locals?.user?.id
);
response.status(200).json({ success: true, error: null });
} catch (e) {
console.log(e.message, e);
response.sendStatus(500).end();
}
const { success, reason } = await Collector.processLink(link);
if (!success) {
response.status(500).json({ success: false, error: reason }).end();
return;
}
Collector.log(
`Link ${link} uploaded processed and successfully. It is now available in documents.`
);
await Telemetry.sendTelemetry("link_uploaded");
await EventLogs.logEvent(
"link_uploaded",
{ link },
response.locals?.user?.id
);
response.status(200).json({ success: true, error: null });
}
);
@ -369,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);
@ -409,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],
@ -618,13 +689,13 @@ function workspaceEndpoints(app) {
const oldPfpFilename = workspaceRecord.pfpFilename;
if (oldPfpFilename) {
const storagePath = path.join(__dirname, "../storage/assets/pfp");
const oldPfpPath = path.join(
__dirname,
`../storage/assets/pfp/${normalizePath(
workspaceRecord.pfpFilename
)}`
storagePath,
normalizePath(workspaceRecord.pfpFilename)
);
if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))
throw new Error("Invalid path name");
if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
}
@ -659,11 +730,13 @@ function workspaceEndpoints(app) {
const oldPfpFilename = workspaceRecord.pfpFilename;
if (oldPfpFilename) {
const storagePath = path.join(__dirname, "../storage/assets/pfp");
const oldPfpPath = path.join(
__dirname,
`../storage/assets/pfp/${normalizePath(oldPfpFilename)}`
storagePath,
normalizePath(oldPfpFilename)
);
if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))
throw new Error("Invalid path name");
if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
}

View File

@ -27,6 +27,7 @@ const SystemSettings = {
"agent_search_provider",
"default_agent_skills",
"agent_sql_connections",
"custom_app_name",
],
validations: {
footer_data: (updates) => {
@ -69,7 +70,11 @@ 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"].includes(
update
)
)
throw new Error("Invalid SERP provider.");
return String(update);
} catch (e) {
@ -170,6 +175,7 @@ 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,
};
},

View File

@ -10,6 +10,20 @@ const User = {
"role",
"suspended",
],
validations: {
username: (newValue = "") => {
try {
if (String(newValue).length > 100)
throw new Error("Username cannot be longer than 100 characters");
if (String(newValue).length < 2)
throw new Error("Username must be at least 2 characters");
return String(newValue);
} catch (e) {
throw new Error(e.message);
}
},
},
// validations for the above writable fields.
castColumnValue: function (key, value) {
switch (key) {
@ -19,6 +33,12 @@ const User = {
return String(value);
}
},
filterFields: function (user = {}) {
const { password, ...rest } = user;
return { ...rest };
},
create: async function ({ username, password, role = "default" }) {
const passwordCheck = this.checkPasswordComplexity(password);
if (!passwordCheck.checkedOK) {
@ -30,12 +50,12 @@ const User = {
const hashedPassword = bcrypt.hashSync(password, 10);
const user = await prisma.users.create({
data: {
username,
username: this.validations.username(username),
password: hashedPassword,
role,
role: String(role),
},
});
return { user, error: null };
return { user: this.filterFields(user), error: null };
} catch (error) {
console.error("FAILED TO CREATE USER.", error.message);
return { user: null, error: error.message };
@ -69,7 +89,13 @@ const User = {
// and force-casts to the proper type;
Object.entries(updates).forEach(([key, value]) => {
if (this.writable.includes(key)) {
updates[key] = this.castColumnValue(key, value);
if (this.validations.hasOwnProperty(key)) {
updates[key] = this.validations[key](
this.castColumnValue(key, value)
);
} else {
updates[key] = this.castColumnValue(key, value);
}
return;
}
delete updates[key];
@ -127,6 +153,17 @@ const User = {
},
get: async function (clause = {}) {
try {
const user = await prisma.users.findFirst({ where: clause });
return user ? this.filterFields({ ...user }) : null;
} catch (error) {
console.error(error.message);
return null;
}
},
// Returns user object with all fields
_get: async function (clause = {}) {
try {
const user = await prisma.users.findFirst({ where: clause });
return user ? { ...user } : null;
@ -162,7 +199,7 @@ const User = {
where: clause,
...(limit !== null ? { take: limit } : {}),
});
return users;
return users.map((usr) => this.filterFields(usr));
} catch (error) {
console.error(error.message);
return [];

View File

@ -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 };

View File

@ -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",

View File

@ -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": {

View File

@ -17,8 +17,12 @@ class GeminiLLM {
this.gemini = genAI.getGenerativeModel(
{ model: this.model },
{
// Gemini-1.5-pro is only available on the v1beta API.
apiVersion: this.model === "gemini-1.5-pro-latest" ? "v1beta" : "v1",
// Gemini-1.5-pro and Gemini-1.5-flash are only available on the v1beta API.
apiVersion:
this.model === "gemini-1.5-pro-latest" ||
this.model === "gemini-1.5-flash-latest"
? "v1beta"
: "v1",
}
);
this.limits = {
@ -87,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:
@ -95,7 +103,12 @@ class GeminiLLM {
}
isValidChatCompletionModel(modelName = "") {
const validModels = ["gemini-pro", "gemini-1.5-pro-latest"];
const validModels = [
"gemini-pro",
"gemini-1.0-pro",
"gemini-1.5-pro-latest",
"gemini-1.5-flash-latest",
];
return validModels.includes(modelName);
}

View File

@ -97,7 +97,7 @@ class GenericOpenAiLLM {
max_tokens: this.maxTokens,
})
.catch((e) => {
throw new Error(e.response.data.error.message);
throw new Error(e.message);
});
if (!result.hasOwnProperty("choices") || result.choices.length === 0)

View File

@ -103,7 +103,7 @@ class GroqLLM {
temperature,
})
.catch((e) => {
throw new Error(e.response.data.error.message);
throw new Error(e.message);
});
if (!result.hasOwnProperty("choices") || result.choices.length === 0)

View File

@ -92,7 +92,7 @@ class KoboldCPPLLM {
temperature,
})
.catch((e) => {
throw new Error(e.response.data.error.message);
throw new Error(e.message);
});
if (!result.hasOwnProperty("choices") || result.choices.length === 0)

View File

@ -1,7 +1,6 @@
const { NativeEmbedder } = require("../../EmbeddingEngines/native");
const {
writeResponseChunk,
clientAbortedHandler,
handleDefaultStreamResponseV2,
} = require("../../helpers/chat/responses");
class LiteLLM {
@ -93,7 +92,7 @@ class LiteLLM {
max_tokens: parseInt(this.maxTokens), // LiteLLM requires int
})
.catch((e) => {
throw new Error(e.response.data.error.message);
throw new Error(e.message);
});
if (!result.hasOwnProperty("choices") || result.choices.length === 0)
@ -113,45 +112,7 @@ class LiteLLM {
}
handleStream(response, stream, responseProps) {
const { uuid = uuidv4(), sources = [] } = responseProps;
return new Promise(async (resolve) => {
let fullText = "";
const handleAbort = () => clientAbortedHandler(resolve, fullText);
response.on("close", handleAbort);
for await (const chunk of stream) {
const message = chunk?.choices?.[0];
const token = message?.delta?.content;
if (token) {
fullText += token;
writeResponseChunk(response, {
uuid,
sources: [],
type: "textResponseChunk",
textResponse: token,
close: false,
error: false,
});
}
// LiteLLM does not give a finish reason in stream until the final chunk
if (message.finish_reason || message.finish_reason === "stop") {
writeResponseChunk(response, {
uuid,
sources,
type: "textResponseChunk",
textResponse: "",
close: true,
error: false,
});
response.removeListener("close", handleAbort);
resolve(fullText);
}
}
});
return handleDefaultStreamResponseV2(response, stream, responseProps);
}
// Simple wrapper for dynamic embedder & normalize interface for all LLM implementations

View File

@ -130,7 +130,7 @@ class OpenAiLLM {
temperature,
})
.catch((e) => {
throw new Error(e.response.data.error.message);
throw new Error(e.message);
});
if (!result.hasOwnProperty("choices") || result.choices.length === 0)

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