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

This commit is contained in:
timothycarambat 2024-08-12 11:59:46 -07:00
commit 8fc547e78a
61 changed files with 3064 additions and 600 deletions

View File

@ -78,8 +78,8 @@
}, },
"updateContentCommand": "cd server && yarn && cd ../collector && PUPPETEER_DOWNLOAD_BASE_URL=https://storage.googleapis.com/chrome-for-testing-public yarn && cd ../frontend && yarn && cd .. && yarn setup:envs && yarn prisma:setup && echo \"Please run yarn dev:server, yarn dev:collector, and yarn dev:frontend in separate terminal tabs.\"", "updateContentCommand": "cd server && yarn && cd ../collector && PUPPETEER_DOWNLOAD_BASE_URL=https://storage.googleapis.com/chrome-for-testing-public yarn && cd ../frontend && yarn && cd .. && yarn setup:envs && yarn prisma:setup && echo \"Please run yarn dev:server, yarn dev:collector, and yarn dev:frontend in separate terminal tabs.\"",
// Use 'postCreateCommand' to run commands after the container is created. // Use 'postCreateCommand' to run commands after the container is created.
// This configures VITE for github codespaces // This configures VITE for github codespaces and installs gh cli
"postCreateCommand": "if [ \"${CODESPACES}\" = \"true\" ]; then echo 'VITE_API_BASE=\"https://$CODESPACE_NAME-3001.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN/api\"' > ./frontend/.env; fi", "postCreateCommand": "if [ \"${CODESPACES}\" = \"true\" ]; then echo 'VITE_API_BASE=\"https://$CODESPACE_NAME-3001.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN/api\"' > ./frontend/.env && (type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) && sudo mkdir -p -m 755 /etc/apt/keyrings && wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null && sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg && echo \"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main\" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null && sudo apt update && sudo apt install gh -y; fi",
"portsAttributes": { "portsAttributes": {
"3001": { "3001": {
"label": "Backend", "label": "Backend",

View File

@ -0,0 +1,115 @@
name: Publish AnythingLLM Docker image on Release (amd64 & arm64)
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true
on:
release:
types: [published]
jobs:
push_multi_platform_to_registries:
name: Push Docker multi-platform image to multiple registries
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Check if DockerHub build needed
shell: bash
run: |
# Check if the secret for USERNAME is set (don't even check for the password)
if [[ -z "${{ secrets.DOCKER_USERNAME }}" ]]; then
echo "DockerHub build not needed"
echo "enabled=false" >> $GITHUB_OUTPUT
else
echo "DockerHub build needed"
echo "enabled=true" >> $GITHUB_OUTPUT
fi
id: dockerhub
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
# Only login to the Docker Hub if the repo is mintplex/anythingllm, to allow for forks to build on GHCR
if: steps.dockerhub.outputs.enabled == 'true'
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: |
${{ steps.dockerhub.outputs.enabled == 'true' && 'mintplexlabs/anythingllm' || '' }}
ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push multi-platform Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/Dockerfile
push: true
sbom: true
provenance: mode=max
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# For Docker scout there are some intermediary reported CVEs which exists outside
# of execution content or are unreachable by an attacker but exist in image.
# We create VEX files for these so they don't show in scout summary.
- name: Collect known and verified CVE exceptions
id: cve-list
run: |
# Collect CVEs from filenames in vex folder
CVE_NAMES=""
for file in ./docker/vex/*.vex.json; do
[ -e "$file" ] || continue
filename=$(basename "$file")
stripped_filename=${filename%.vex.json}
CVE_NAMES+=" $stripped_filename"
done
echo "CVE_EXCEPTIONS=$CVE_NAMES" >> $GITHUB_OUTPUT
shell: bash
# About VEX attestations https://docs.docker.com/scout/explore/exceptions/
# Justifications https://github.com/openvex/spec/blob/main/OPENVEX-SPEC.md#status-justifications
- name: Add VEX attestations
env:
CVE_EXCEPTIONS: ${{ steps.cve-list.outputs.CVE_EXCEPTIONS }}
run: |
echo $CVE_EXCEPTIONS
curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s --
for cve in $CVE_EXCEPTIONS; do
for tag in "${{ join(fromJSON(steps.meta.outputs.json).tags, ' ') }}"; do
echo "Attaching VEX exception $cve to $tag"
docker scout attestation add \
--file "./docker/vex/$cve.vex.json" \
--predicate-type https://openvex.dev/ns/v0.2.0 \
$tag
done
done
shell: bash

View File

@ -6,7 +6,7 @@ concurrency:
on: on:
push: push:
branches: ['558-multi-modal-support'] # put your current branch to create a build. Core team only. branches: ['pipertts-support'] # put your current branch to create a build. Core team only.
paths-ignore: paths-ignore:
- '**.md' - '**.md'
- 'cloud-deployments/*' - 'cloud-deployments/*'

2
.vscode/tasks.json vendored
View File

@ -46,7 +46,7 @@
} }
} }
}, },
"command": "cd ${workspaceFolder}/server/ && yarn dev", "command": "if [ \"${CODESPACES}\" = \"true\" ]; then while ! gh codespace ports -c $CODESPACE_NAME | grep 3001; do sleep 1; done; gh codespace ports visibility 3001:public -c $CODESPACE_NAME; fi & cd ${workspaceFolder}/server/ && yarn dev",
"runOptions": { "runOptions": {
"instanceLimit": 1, "instanceLimit": 1,
"reevaluateOnRerun": true "reevaluateOnRerun": true

View File

@ -110,6 +110,7 @@ AnythingLLM divides your documents into objects called `workspaces`. A Workspace
**TTS (text-to-speech) support:** **TTS (text-to-speech) support:**
- Native Browser Built-in (default) - Native Browser Built-in (default)
- [PiperTTSLocal - runs in browser](https://github.com/rhasspy/piper)
- [OpenAI TTS](https://platform.openai.com/docs/guides/text-to-speech/voice-options) - [OpenAI TTS](https://platform.openai.com/docs/guides/text-to-speech/voice-options)
- [ElevenLabs](https://elevenlabs.io/) - [ElevenLabs](https://elevenlabs.io/)

View File

@ -9,7 +9,9 @@ sudo systemctl enable docker
sudo systemctl start docker sudo systemctl start docker
mkdir -p /home/anythingllm mkdir -p /home/anythingllm
touch /home/anythingllm/.env cat <<EOF >/home/anythingllm/.env
${env_content}
EOF
sudo docker pull mintplexlabs/anythingllm sudo docker pull mintplexlabs/anythingllm
sudo docker run -d -p 3001:3001 --cap-add SYS_ADMIN -v /home/anythingllm:/app/server/storage -v /home/anythingllm/.env:/app/server/.env -e STORAGE_DIR="/app/server/storage" mintplexlabs/anythingllm sudo docker run -d -p 3001:3001 --cap-add SYS_ADMIN -v /home/anythingllm:/app/server/storage -v /home/anythingllm/.env:/app/server/.env -e STORAGE_DIR="/app/server/storage" mintplexlabs/anythingllm

View File

@ -223,10 +223,6 @@ class GitLabRepoLoader {
const objects = Array.isArray(data) const objects = Array.isArray(data)
? data.filter((item) => item.type === "blob") ? data.filter((item) => item.type === "blob")
: []; // only get files, not paths or submodules : []; // only get files, not paths or submodules
if (objects.length === 0) {
fetching = false;
break;
}
// Apply ignore path rules to found objects. If any rules match it is an invalid file path. // Apply ignore path rules to found objects. If any rules match it is an invalid file path.
console.log( console.log(

View File

@ -9,34 +9,36 @@ const { tokenizeString } = require("../../tokenizer");
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
async function discoverLinks(startUrl, depth = 1, maxLinks = 20) { async function discoverLinks(startUrl, maxDepth = 1, maxLinks = 20) {
const baseUrl = new URL(startUrl); const baseUrl = new URL(startUrl);
const discoveredLinks = new Set(); const discoveredLinks = new Set([startUrl]);
const pendingLinks = [startUrl]; let queue = [[startUrl, 0]]; // [url, currentDepth]
let currentLevel = 0; const scrapedUrls = new Set();
depth = depth < 1 ? 1 : depth;
maxLinks = maxLinks < 1 ? 1 : maxLinks;
// Check depth and if there are any links left to scrape for (let currentDepth = 0; currentDepth < maxDepth; currentDepth++) {
while (currentLevel < depth && pendingLinks.length > 0) { const levelSize = queue.length;
const newLinks = await getPageLinks(pendingLinks[0], baseUrl); const nextQueue = [];
pendingLinks.shift();
for (let i = 0; i < levelSize && discoveredLinks.size < maxLinks; i++) {
const [currentUrl, urlDepth] = queue[i];
if (!scrapedUrls.has(currentUrl)) {
scrapedUrls.add(currentUrl);
const newLinks = await getPageLinks(currentUrl, baseUrl);
for (const link of newLinks) { for (const link of newLinks) {
if (!discoveredLinks.has(link)) { if (!discoveredLinks.has(link) && discoveredLinks.size < maxLinks) {
discoveredLinks.add(link); discoveredLinks.add(link);
pendingLinks.push(link); if (urlDepth + 1 < maxDepth) {
nextQueue.push([link, urlDepth + 1]);
}
}
} }
// Exit out if we reach maxLinks
if (discoveredLinks.size >= maxLinks) {
return Array.from(discoveredLinks).slice(0, maxLinks);
} }
} }
if (pendingLinks.length === 0) { queue = nextQueue;
currentLevel++; if (queue.length === 0 || discoveredLinks.size >= maxLinks) break;
}
} }
return Array.from(discoveredLinks); return Array.from(discoveredLinks);

View File

@ -47,10 +47,12 @@ class YoutubeTranscript {
let transcript = ""; let transcript = "";
const chunks = transcriptXML.getElementsByTagName("text"); const chunks = transcriptXML.getElementsByTagName("text");
for (const chunk of chunks) { for (const chunk of chunks) {
transcript += chunk.textContent; // Add space after each text chunk
transcript += chunk.textContent + " ";
} }
return transcript; // Trim extra whitespace
return transcript.trim().replace(/\s+/g, " ");
} catch (e) { } catch (e) {
throw new YoutubeTranscriptError(e); throw new YoutubeTranscriptError(e);
} }

View File

@ -37,6 +37,7 @@ class MimeDetector {
"lua", "lua",
"pas", "pas",
"r", "r",
"go",
], ],
}, },
true true

View File

@ -13,6 +13,7 @@
"dependencies": { "dependencies": {
"@metamask/jazzicon": "^2.0.0", "@metamask/jazzicon": "^2.0.0",
"@microsoft/fetch-event-source": "^2.0.1", "@microsoft/fetch-event-source": "^2.0.1",
"@mintplex-labs/piper-tts-web": "^1.0.4",
"@phosphor-icons/react": "^2.1.7", "@phosphor-icons/react": "^2.1.7",
"@tremor/react": "^3.15.1", "@tremor/react": "^3.15.1",
"dompurify": "^3.0.8", "dompurify": "^3.0.8",
@ -24,7 +25,9 @@
"js-levenshtein": "^1.1.6", "js-levenshtein": "^1.1.6",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"markdown-it-katex": "^2.0.3",
"moment": "^2.30.1", "moment": "^2.30.1",
"onnxruntime-web": "^1.18.0",
"pluralize": "^8.0.0", "pluralize": "^8.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-device-detect": "^2.2.2", "react-device-detect": "^2.2.2",

View File

@ -0,0 +1,220 @@
import { useState, useEffect, useRef } from "react";
import PiperTTSClient from "@/utils/piperTTS";
import { titleCase } from "text-case";
import { humanFileSize } from "@/utils/numbers";
import showToast from "@/utils/toast";
import { CircleNotch, PauseCircle, PlayCircle } from "@phosphor-icons/react";
export default function PiperTTSOptions({ settings }) {
return (
<>
<p className="text-sm font-base text-white text-opacity-60 mb-4">
All PiperTTS models will run in your browser locally. This can be
resource intensive on lower-end devices.
</p>
<div className="flex gap-x-4 items-center">
<PiperTTSModelSelection settings={settings} />
</div>
</>
);
}
function voicesByLanguage(voices = []) {
const voicesByLanguage = voices.reduce((acc, voice) => {
const langName = voice?.language?.name_english ?? "Unlisted";
acc[langName] = acc[langName] || [];
acc[langName].push(voice);
return acc;
}, {});
return Object.entries(voicesByLanguage);
}
function voiceDisplayName(voice) {
const { is_stored, name, quality, files } = voice;
const onnxFileKey = Object.keys(files).find((key) => key.endsWith(".onnx"));
const fileSize = files?.[onnxFileKey]?.size_bytes || 0;
return `${is_stored ? "✔ " : ""}${titleCase(name)}-${quality === "low" ? "Low" : "HQ"} (${humanFileSize(fileSize)})`;
}
function PiperTTSModelSelection({ settings }) {
const [loading, setLoading] = useState(true);
const [voices, setVoices] = useState([]);
const [selectedVoice, setSelectedVoice] = useState(
settings?.TTSPiperTTSVoiceModel
);
function flushVoices() {
PiperTTSClient.flush()
.then(() =>
showToast("All voices flushed from browser storage", "info", {
clear: true,
})
)
.catch((e) => console.error(e));
}
useEffect(() => {
PiperTTSClient.voices()
.then((voices) => {
if (voices?.length !== 0) return setVoices(voices);
throw new Error("Could not fetch voices from web worker.");
})
.catch((e) => {
console.error(e);
})
.finally(() => setLoading(false));
}, []);
if (loading) {
return (
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
Voice Model Selection
</label>
<select
name="TTSPiperTTSVoiceModel"
value=""
disabled={true}
className="border-none bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
<option value="" disabled={true}>
-- loading available models --
</option>
</select>
</div>
);
}
return (
<div className="flex flex-col w-fit">
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
Voice Model Selection
</label>
<div className="flex items-center w-fit gap-x-4 mb-2">
<select
name="TTSPiperTTSVoiceModel"
required={true}
onChange={(e) => setSelectedVoice(e.target.value)}
value={selectedVoice}
className="border-none flex-shrink-0 bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
{voicesByLanguage(voices).map(([lang, voices]) => {
return (
<optgroup key={lang} label={lang}>
{voices.map((voice) => (
<option key={voice.key} value={voice.key}>
{voiceDisplayName(voice)}
</option>
))}
</optgroup>
);
})}
</select>
<DemoVoiceSample voiceId={selectedVoice} />
</div>
<p className="text-xs text-white/40">
The "✔" indicates this model is already stored locally and does not
need to be downloaded when run.
</p>
</div>
{!!voices.find((voice) => voice.is_stored) && (
<button
type="button"
onClick={flushVoices}
className="w-fit border-none hover:text-white hover:underline text-white/40 text-sm my-4"
>
Flush voice cache
</button>
)}
</div>
);
}
function DemoVoiceSample({ voiceId }) {
const playerRef = useRef(null);
const [speaking, setSpeaking] = useState(false);
const [loading, setLoading] = useState(false);
const [audioSrc, setAudioSrc] = useState(null);
async function speakMessage(e) {
e.preventDefault();
if (speaking) {
playerRef?.current?.pause();
return;
}
try {
if (!audioSrc) {
setLoading(true);
const client = new PiperTTSClient({ voiceId });
const blobUrl = await client.getAudioBlobForText(
"Hello, welcome to AnythingLLM!"
);
setAudioSrc(blobUrl);
setLoading(false);
client.worker?.terminate();
PiperTTSClient._instance = null;
} else {
playerRef.current.play();
}
} catch (e) {
console.error(e);
setLoading(false);
setSpeaking(false);
}
}
useEffect(() => {
function setupPlayer() {
if (!playerRef?.current) return;
playerRef.current.addEventListener("play", () => {
setSpeaking(true);
});
playerRef.current.addEventListener("pause", () => {
playerRef.current.currentTime = 0;
setSpeaking(false);
setAudioSrc(null);
});
}
setupPlayer();
}, []);
return (
<button
type="button"
onClick={speakMessage}
disabled={loading}
className="border-none text-zinc-300 flex items-center gap-x-1"
>
{speaking ? (
<>
<PauseCircle size={20} className="flex-shrink-0" />
<p className="text-sm flex-shrink-0">Stop demo</p>
</>
) : (
<>
{loading ? (
<>
<CircleNotch size={20} className="animate-spin flex-shrink-0" />
<p className="text-sm flex-shrink-0">Loading voice</p>
</>
) : (
<>
<PlayCircle size={20} className="flex-shrink-0" />
<p className="text-sm flex-shrink-0">Play sample</p>
</>
)}
</>
)}
<audio
ref={playerRef}
hidden={true}
src={audioSrc}
autoPlay={true}
controls={false}
/>
</button>
);
}

View File

@ -7,6 +7,7 @@ import { Plus, X } from "@phosphor-icons/react";
export default function AccountModal({ user, hideModal }) { export default function AccountModal({ user, hideModal }) {
const { pfp, setPfp } = usePfp(); const { pfp, setPfp } = usePfp();
const handleFileUpload = async (event) => { const handleFileUpload = async (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
if (!file) return false; if (!file) return false;
@ -133,6 +134,10 @@ export default function AccountModal({ user, hideModal }) {
required required
autoComplete="off" autoComplete="off"
/> />
<p className="mt-2 text-xs text-white/60">
Username must be only contain lowercase letters, numbers,
underscores, and hyphens with no spaces
</p>
</div> </div>
<div> <div>
<label <label
@ -143,10 +148,14 @@ export default function AccountModal({ user, hideModal }) {
</label> </label>
<input <input
name="password" name="password"
type="password" type="text"
className="bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" className="bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder={`${user.username}'s new password`} placeholder={`${user.username}'s new password`}
minLength={8}
/> />
<p className="mt-2 text-xs text-white/60">
Password must be at least 8 characters long
</p>
</div> </div>
<LanguagePreference /> <LanguagePreference />
</div> </div>

View File

@ -1,9 +1,11 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import NativeTTSMessage from "./native"; import NativeTTSMessage from "./native";
import AsyncTTSMessage from "./asyncTts"; import AsyncTTSMessage from "./asyncTts";
import PiperTTSMessage from "./piperTTS";
import System from "@/models/system"; import System from "@/models/system";
export default function TTSMessage({ slug, chatId, message }) { export default function TTSMessage({ slug, chatId, message }) {
const [settings, setSettings] = useState({});
const [provider, setProvider] = useState("native"); const [provider, setProvider] = useState("native");
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -11,13 +13,26 @@ export default function TTSMessage({ slug, chatId, message }) {
async function getSettings() { async function getSettings() {
const _settings = await System.keys(); const _settings = await System.keys();
setProvider(_settings?.TextToSpeechProvider ?? "native"); setProvider(_settings?.TextToSpeechProvider ?? "native");
setSettings(_settings);
setLoading(false); setLoading(false);
} }
getSettings(); getSettings();
}, []); }, []);
if (!chatId || loading) return null; if (!chatId || loading) return null;
if (provider !== "native")
switch (provider) {
case "openai":
case "elevenlabs":
return <AsyncTTSMessage slug={slug} chatId={chatId} />; return <AsyncTTSMessage slug={slug} chatId={chatId} />;
case "piper_local":
return (
<PiperTTSMessage
voiceId={settings?.TTSPiperTTSVoiceModel}
message={message}
/>
);
default:
return <NativeTTSMessage message={message} />; return <NativeTTSMessage message={message} />;
} }
}

View File

@ -0,0 +1,91 @@
import { useEffect, useState, useRef } from "react";
import { SpeakerHigh, PauseCircle, CircleNotch } from "@phosphor-icons/react";
import { Tooltip } from "react-tooltip";
import PiperTTSClient from "@/utils/piperTTS";
export default function PiperTTS({ voiceId = null, message }) {
const playerRef = useRef(null);
const [speaking, setSpeaking] = useState(false);
const [loading, setLoading] = useState(false);
const [audioSrc, setAudioSrc] = useState(null);
async function speakMessage(e) {
e.preventDefault();
if (speaking) {
playerRef?.current?.pause();
return;
}
try {
if (!audioSrc) {
setLoading(true);
const client = new PiperTTSClient({ voiceId });
const blobUrl = await client.getAudioBlobForText(message);
setAudioSrc(blobUrl);
setLoading(false);
} else {
playerRef.current.play();
}
} catch (e) {
console.error(e);
setLoading(false);
setSpeaking(false);
}
}
useEffect(() => {
function setupPlayer() {
if (!playerRef?.current) return;
playerRef.current.addEventListener("play", () => {
setSpeaking(true);
});
playerRef.current.addEventListener("pause", () => {
playerRef.current.currentTime = 0;
setSpeaking(false);
});
}
setupPlayer();
}, []);
return (
<div className="mt-3 relative">
<button
type="button"
onClick={speakMessage}
disabled={loading}
data-tooltip-id="message-to-speech"
data-tooltip-content={
speaking ? "Pause TTS speech of message" : "TTS Speak message"
}
className="border-none text-zinc-300"
aria-label={speaking ? "Pause speech" : "Speak message"}
>
{speaking ? (
<PauseCircle size={18} className="mb-1" />
) : (
<>
{loading ? (
<CircleNotch size={18} className="mb-1 animate-spin" />
) : (
<SpeakerHigh size={18} className="mb-1" />
)}
</>
)}
<audio
ref={playerRef}
hidden={true}
src={audioSrc}
autoPlay={true}
controls={false}
/>
</button>
<Tooltip
id="message-to-speech"
place="bottom"
delayShow={300}
className="tooltip !text-xs"
/>
</div>
);
}

View File

@ -1,3 +1,4 @@
import useUser from "@/hooks/useUser";
import { PaperclipHorizontal } from "@phosphor-icons/react"; import { PaperclipHorizontal } from "@phosphor-icons/react";
import { Tooltip } from "react-tooltip"; import { Tooltip } from "react-tooltip";
@ -6,6 +7,9 @@ import { Tooltip } from "react-tooltip";
* @returns * @returns
*/ */
export default function AttachItem() { export default function AttachItem() {
const { user } = useUser();
if (!!user && user.role === "default") return null;
return ( return (
<> <>
<button <button

View File

@ -1,4 +1,4 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import { X } from "@phosphor-icons/react"; import { X } from "@phosphor-icons/react";
import ModalWrapper from "@/components/ModalWrapper"; import ModalWrapper from "@/components/ModalWrapper";
import { CMD_REGEX } from "."; import { CMD_REGEX } from ".";
@ -10,9 +10,15 @@ export default function EditPresetModal({
onDelete, onDelete,
preset, preset,
}) { }) {
const [command, setCommand] = useState(preset?.command?.slice(1) || ""); const [command, setCommand] = useState("");
const [deleting, setDeleting] = useState(false); const [deleting, setDeleting] = useState(false);
useEffect(() => {
if (preset && isOpen) {
setCommand(preset.command?.slice(1) || "");
}
}, [preset, isOpen]);
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
const form = new FormData(e.target); const form = new FormData(e.target);

View File

@ -62,13 +62,18 @@ export default function SlashPresets({ setShowing, sendCommand }) {
} }
fetchPresets(); fetchPresets();
closeEditModal(); closeEditModalAndResetPreset();
}; };
const handleDeletePreset = async (presetId) => { const handleDeletePreset = async (presetId) => {
await System.deleteSlashCommandPreset(presetId); await System.deleteSlashCommandPreset(presetId);
fetchPresets(); fetchPresets();
closeEditModalAndResetPreset();
};
const closeEditModalAndResetPreset = () => {
closeEditModal(); closeEditModal();
setSelectedPreset(null);
}; };
return ( return (
@ -116,7 +121,7 @@ export default function SlashPresets({ setShowing, sendCommand }) {
{selectedPreset && ( {selectedPreset && (
<EditPresetModal <EditPresetModal
isOpen={isEditModalOpen} isOpen={isEditModalOpen}
onClose={closeEditModal} onClose={closeEditModalAndResetPreset}
onSave={handleUpdatePreset} onSave={handleUpdatePreset}
onDelete={handleDeletePreset} onDelete={handleDeletePreset}
preset={selectedPreset} preset={selectedPreset}

View File

@ -1,10 +1,11 @@
import { useEffect } from "react"; import { useEffect, useCallback } from "react";
import { Microphone } from "@phosphor-icons/react"; import { Microphone } from "@phosphor-icons/react";
import { Tooltip } from "react-tooltip"; import { Tooltip } from "react-tooltip";
import _regeneratorRuntime from "regenerator-runtime"; import _regeneratorRuntime from "regenerator-runtime";
import SpeechRecognition, { import SpeechRecognition, {
useSpeechRecognition, useSpeechRecognition,
} from "react-speech-recognition"; } from "react-speech-recognition";
import { PROMPT_INPUT_EVENT } from "../../PromptInput";
let timeout; let timeout;
const SILENCE_INTERVAL = 3_200; // wait in seconds of silence before closing. const SILENCE_INTERVAL = 3_200; // wait in seconds of silence before closing.
@ -45,15 +46,49 @@ export default function SpeechToText({ sendCommand }) {
clearTimeout(timeout); clearTimeout(timeout);
} }
const handleKeyPress = useCallback(
(event) => {
if (event.ctrlKey && event.keyCode === 77) {
if (listening) {
endTTSSession();
} else {
startSTTSession();
}
}
},
[listening, endTTSSession, startSTTSession]
);
function handlePromptUpdate(e) {
if (!e?.detail && timeout) {
endTTSSession();
clearTimeout(timeout);
}
}
useEffect(() => { useEffect(() => {
if (transcript?.length > 0) { document.addEventListener("keydown", handleKeyPress);
return () => {
document.removeEventListener("keydown", handleKeyPress);
};
}, [handleKeyPress]);
useEffect(() => {
if (!!window)
window.addEventListener(PROMPT_INPUT_EVENT, handlePromptUpdate);
return () =>
window?.removeEventListener(PROMPT_INPUT_EVENT, handlePromptUpdate);
}, []);
useEffect(() => {
if (transcript?.length > 0 && listening) {
sendCommand(transcript, false); sendCommand(transcript, false);
clearTimeout(timeout); clearTimeout(timeout);
timeout = setTimeout(() => { timeout = setTimeout(() => {
endTTSSession(); endTTSSession();
}, SILENCE_INTERVAL); }, SILENCE_INTERVAL);
} }
}, [transcript]); }, [transcript, listening]);
if (!browserSupportsSpeechRecognition) return null; if (!browserSupportsSpeechRecognition) return null;
return ( return (
@ -69,7 +104,9 @@ export default function SpeechToText({ sendCommand }) {
> >
<Microphone <Microphone
weight="fill" weight="fill"
className="w-6 h-6 pointer-events-none text-white" className={`w-6 h-6 pointer-events-none text-white overflow-hidden rounded-full ${
listening ? "animate-pulse-glow" : ""
}`}
/> />
<Tooltip <Tooltip
id="tooltip-text-size-btn" id="tooltip-text-size-btn"

View File

@ -112,7 +112,7 @@ export default function PromptInput({
className="flex flex-col gap-y-1 rounded-t-lg md:w-3/4 w-full mx-auto max-w-xl items-center" className="flex flex-col gap-y-1 rounded-t-lg md:w-3/4 w-full mx-auto max-w-xl items-center"
> >
<div className="flex items-center rounded-lg md:mb-4"> <div className="flex items-center rounded-lg md:mb-4">
<div className="w-[635px] bg-main-gradient shadow-2xl border border-white/50 rounded-2xl flex flex-col px-4 overflow-hidden"> <div className="w-[95vw] md:w-[635px] bg-main-gradient shadow-2xl border border-white/50 rounded-2xl flex flex-col px-4 overflow-hidden">
<AttachmentManager attachments={attachments} /> <AttachmentManager attachments={attachments} />
<div className="flex items-center w-full border-b-2 border-gray-500/50"> <div className="flex items-center w-full border-b-2 border-gray-500/50">
<textarea <textarea

View File

@ -14,6 +14,9 @@ import handleSocketResponse, {
AGENT_SESSION_START, AGENT_SESSION_START,
} from "@/utils/chat/agent"; } from "@/utils/chat/agent";
import DnDFileUploaderWrapper from "./DnDWrapper"; import DnDFileUploaderWrapper from "./DnDWrapper";
import SpeechRecognition, {
useSpeechRecognition,
} from "react-speech-recognition";
export default function ChatContainer({ workspace, knownHistory = [] }) { export default function ChatContainer({ workspace, knownHistory = [] }) {
const { threadSlug = null } = useParams(); const { threadSlug = null } = useParams();
@ -29,6 +32,10 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
setMessage(event.target.value); setMessage(event.target.value);
}; };
const { listening, resetTranscript } = useSpeechRecognition({
clearTranscriptOnListen: true,
});
// Emit an update to the state of the prompt input without directly // Emit an update to the state of the prompt input without directly
// passing a prop in so that it does not re-render constantly. // passing a prop in so that it does not re-render constantly.
function setMessageEmit(messageContent = "") { function setMessageEmit(messageContent = "") {
@ -57,11 +64,20 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
}, },
]; ];
if (listening) {
// Stop the mic if the send button is clicked
endTTSSession();
}
setChatHistory(prevChatHistory); setChatHistory(prevChatHistory);
setMessageEmit(""); setMessageEmit("");
setLoadingResponse(true); setLoadingResponse(true);
}; };
function endTTSSession() {
SpeechRecognition.stopListening();
resetTranscript();
}
const regenerateAssistantMessage = (chatId) => { const regenerateAssistantMessage = (chatId) => {
const updatedHistory = chatHistory.slice(0, -1); const updatedHistory = chatHistory.slice(0, -1);
const lastUserMessage = updatedHistory.slice(-1)[0]; const lastUserMessage = updatedHistory.slice(-1)[0];

View File

@ -794,3 +794,12 @@ does not extend the close button beyond the viewport. */
top: -3.5rem; top: -3.5rem;
} }
} }
/* Math/Katex formatting to prevent duplication of content on screen */
.katex-html[aria-hidden="true"] {
display: none;
}
.katex-mathml {
font-size: 20px;
}

View File

@ -0,0 +1,482 @@
const TRANSLATIONS = {
common: {
"workspaces-name": "Name der Arbeitsbereiche",
error: "Fehler",
success: "Erfolg",
user: "Benutzer",
selection: "Modellauswahl",
saving: "Speichern...",
save: "Änderungen speichern",
previous: "Vorherige Seite",
next: "Nächste Seite",
},
settings: {
title: "Instanzeinstellungen",
system: "Allgemeine Einstellungen",
invites: "Einladungen",
users: "Benutzer",
workspaces: "Arbeitsbereiche",
"workspace-chats": "Arbeitsbereich-Chats",
customization: "Anpassung",
"api-keys": "Entwickler-API",
llm: "LLM",
transcription: "Transkription",
embedder: "Einbettung",
"text-splitting": "Textsplitting & Chunking",
"voice-speech": "Sprache & Sprachausgabe",
"vector-database": "Vektordatenbank",
embeds: "Chat-Einbettung",
"embed-chats": "Chat-Einbettungsverlauf",
security: "Sicherheit",
"event-logs": "Ereignisprotokolle",
privacy: "Datenschutz & Datenverarbeitung",
"ai-providers": "KI-Anbieter",
"agent-skills": "Agentenfähigkeiten",
admin: "Administrator",
tools: "Werkzeuge",
"experimental-features": "Experimentelle Funktionen",
contact: "Support kontaktieren",
},
login: {
"multi-user": {
welcome: "Willkommen bei",
"placeholder-username": "Benutzername",
"placeholder-password": "Passwort",
login: "Anmelden",
validating: "Überprüfung...",
"forgot-pass": "Passwort vergessen",
reset: "Zurücksetzen",
},
"sign-in": {
start: "Melden Sie sich bei Ihrem",
end: "Konto an.",
},
"password-reset": {
title: "Passwort zurücksetzen",
description:
"Geben Sie die erforderlichen Informationen unten ein, um Ihr Passwort zurückzusetzen.",
"recovery-codes": "Wiederherstellungscodes",
"recovery-code": "Wiederherstellungscode {{index}}",
"back-to-login": "Zurück zur Anmeldung",
},
},
welcomeMessage: {
part1:
"Willkommen bei AnythingLLM, AnythingLLM ist ein Open-Source-KI-Tool von Mintplex Labs, das alles in einen trainierten Chatbot verwandelt, den Sie abfragen und mit dem Sie chatten können. AnythingLLM ist eine BYOK-Software (Bring-Your-Own-Keys), daher gibt es keine Abonnements, Gebühren oder Kosten für diese Software außerhalb der Dienste, die Sie damit nutzen möchten.",
part2:
"AnythingLLM ist der einfachste Weg, leistungsstarke KI-Produkte wie OpenAI, GPT-4, LangChain, PineconeDB, ChromaDB und andere Dienste in einem übersichtlichen Paket ohne Aufwand zusammenzufassen, um Ihre Produktivität um das 100-fache zu steigern.",
part3:
"AnythingLLM kann vollständig lokal auf Ihrem Computer mit geringem Overhead laufen - Sie werden nicht einmal bemerken, dass es da ist! Keine GPU erforderlich. Cloud- und On-Premises-Installation ist ebenfalls verfügbar.\nDas KI-Tool-Ökosystem wird jeden Tag leistungsfähiger. AnythingLLM macht es einfach, es zu nutzen.",
githubIssue: "Erstellen Sie ein Problem auf Github",
user1: "Wie fange ich an?!",
part4:
'Es ist einfach. Alle Sammlungen sind in Behältern organisiert, die wir "Arbeitsbereiche" nennen. Arbeitsbereiche sind Behälter für Dateien, Dokumente, Bilder, PDFs und andere Dateien, die in etwas umgewandelt werden, das LLMs verstehen und in Gesprächen verwenden können.\n\nSie können jederzeit Dateien hinzufügen und entfernen.',
createWorkspace: "Erstellen Sie Ihren ersten Arbeitsbereich",
user2:
"Ist das so eine Art KI-Dropbox oder so? Was ist mit dem Chatten? Es ist doch ein Chatbot, oder?",
part5:
"AnythingLLM ist mehr als eine intelligentere Dropbox.\n\nAnythingLLM bietet zwei Möglichkeiten, mit Ihren Daten zu sprechen:\n\n<i>Abfrage:</i> Ihre Chats geben Daten oder Schlussfolgerungen zurück, die in den Dokumenten Ihres Arbeitsbereichs gefunden wurden, auf die es Zugriff hat. Je mehr Dokumente Sie dem Arbeitsbereich hinzufügen, desto intelligenter wird er! \n\n<i>Konversation:</i> Ihre Dokumente + Ihr laufender Chat-Verlauf tragen gleichzeitig zum LLM-Wissen bei. Großartig für das Anhängen von Echtzeit-Textinformationen oder Korrekturen und Missverständnissen, die das LLM haben könnte. \n\nSie können zwischen beiden Modi wechseln \n<i>mitten im Chatten!</i>",
user3: "Wow, das klingt erstaunlich, lass es mich gleich ausprobieren!",
part6: "Viel Spaß!",
starOnGithub: "Stern auf GitHub",
contact: "Kontaktieren Sie Mintplex Labs",
},
"new-workspace": {
title: "Neuer Arbeitsbereich",
placeholder: "Mein Arbeitsbereich",
},
"workspaces—settings": {
general: "Allgemeine Einstellungen",
chat: "Chat-Einstellungen",
vector: "Vektordatenbank",
members: "Mitglieder",
agent: "Agentenkonfiguration",
},
general: {
vector: {
title: "Vektoranzahl",
description: "Gesamtanzahl der Vektoren in Ihrer Vektordatenbank.",
},
names: {
description: "Dies ändert nur den Anzeigenamen Ihres Arbeitsbereichs.",
},
message: {
title: "Vorgeschlagene Chat-Nachrichten",
description:
"Passen Sie die Nachrichten an, die Ihren Arbeitsbereich-Benutzern vorgeschlagen werden.",
add: "Neue Nachricht hinzufügen",
save: "Nachrichten speichern",
heading: "Erkläre mir",
body: "die Vorteile von AnythingLLM",
},
pfp: {
title: "Assistent-Profilbild",
description:
"Passen Sie das Profilbild des Assistenten für diesen Arbeitsbereich an.",
image: "Arbeitsbereich-Bild",
remove: "Arbeitsbereich-Bild entfernen",
},
delete: {
title: "Arbeitsbereich löschen",
description:
"Löschen Sie diesen Arbeitsbereich und alle seine Daten. Dies löscht den Arbeitsbereich für alle Benutzer.",
delete: "Arbeitsbereich löschen",
deleting: "Arbeitsbereich wird gelöscht...",
"confirm-start": "Sie sind dabei, Ihren gesamten",
"confirm-end":
"Arbeitsbereich zu löschen. Dies entfernt alle Vektoreinbettungen in Ihrer Vektordatenbank.\n\nDie ursprünglichen Quelldateien bleiben unberührt. Diese Aktion ist irreversibel.",
},
},
chat: {
llm: {
title: "Arbeitsbereich-LLM-Anbieter",
description:
"Der spezifische LLM-Anbieter und das Modell, das für diesen Arbeitsbereich verwendet wird. Standardmäßig wird der System-LLM-Anbieter und dessen Einstellungen verwendet.",
search: "Durchsuchen Sie alle LLM-Anbieter",
},
model: {
title: "Arbeitsbereich-Chat-Modell",
description:
"Das spezifische Chat-Modell, das für diesen Arbeitsbereich verwendet wird. Wenn leer, wird die System-LLM-Präferenz verwendet.",
wait: "-- warte auf Modelle --",
},
mode: {
title: "Chat-Modus",
chat: {
title: "Chat",
"desc-start": "wird Antworten mit dem allgemeinen Wissen des LLM",
and: "und",
"desc-end": "gefundenem Dokumentenkontext liefern.",
},
query: {
title: "Abfrage",
"desc-start": "wird Antworten",
only: "nur",
"desc-end": "liefern, wenn Dokumentenkontext gefunden wird.",
},
},
history: {
title: "Chat-Verlauf",
"desc-start":
"Die Anzahl der vorherigen Chats, die in das Kurzzeitgedächtnis der Antwort einbezogen werden.",
recommend: "Empfohlen 20. ",
"desc-end":
"Alles über 45 führt wahrscheinlich zu kontinuierlichen Chat-Ausfällen, abhängig von der Nachrichtengröße.",
},
prompt: {
title: "Prompt",
description:
"Der Prompt, der in diesem Arbeitsbereich verwendet wird. Definieren Sie den Kontext und die Anweisungen für die KI, um eine Antwort zu generieren. Sie sollten einen sorgfältig formulierten Prompt bereitstellen, damit die KI eine relevante und genaue Antwort generieren kann.",
},
refusal: {
title: "Abfragemodus-Ablehnungsantwort",
"desc-start": "Wenn im",
query: "Abfrage",
"desc-end":
"modus, möchten Sie vielleicht eine benutzerdefinierte Ablehnungsantwort zurückgeben, wenn kein Kontext gefunden wird.",
},
temperature: {
title: "LLM-Temperatur",
"desc-start":
'Diese Einstellung steuert, wie "kreativ" Ihre LLM-Antworten sein werden.',
"desc-end":
"Je höher die Zahl, desto kreativer. Bei einigen Modellen kann dies zu unverständlichen Antworten führen, wenn sie zu hoch eingestellt ist.",
hint: "Die meisten LLMs haben verschiedene akzeptable Bereiche gültiger Werte. Konsultieren Sie Ihren LLM-Anbieter für diese Informationen.",
},
},
"vector-workspace": {
identifier: "Vektordatenbank-Identifikator",
snippets: {
title: "Maximale Kontext-Snippets",
description:
"Diese Einstellung steuert die maximale Anzahl von Kontext-Snippets, die pro Chat oder Abfrage an das LLM gesendet werden.",
recommend: "Empfohlen: 4",
},
doc: {
title: "Dokumentähnlichkeitsschwelle",
description:
"Der minimale Ähnlichkeitswert, der erforderlich ist, damit eine Quelle als relevant für den Chat betrachtet wird. Je höher die Zahl, desto ähnlicher muss die Quelle dem Chat sein.",
zero: "Keine Einschränkung",
low: "Niedrig (Ähnlichkeitswert ≥ .25)",
medium: "Mittel (Ähnlichkeitswert ≥ .50)",
high: "Hoch (Ähnlichkeitswert ≥ .75)",
},
reset: {
reset: "Vektordatenbank zurücksetzen",
resetting: "Vektoren werden gelöscht...",
confirm:
"Sie sind dabei, die Vektordatenbank dieses Arbeitsbereichs zurückzusetzen. Dies entfernt alle derzeit eingebetteten Vektoreinbettungen.\n\nDie ursprünglichen Quelldateien bleiben unberührt. Diese Aktion ist irreversibel.",
error:
"Die Arbeitsbereich-Vektordatenbank konnte nicht zurückgesetzt werden!",
success: "Die Arbeitsbereich-Vektordatenbank wurde zurückgesetzt!",
},
},
agent: {
"performance-warning":
"Die Leistung von LLMs, die Werkzeugaufrufe nicht explizit unterstützen, hängt stark von den Fähigkeiten und der Genauigkeit des Modells ab. Einige Fähigkeiten können eingeschränkt oder nicht funktionsfähig sein.",
provider: {
title: "Arbeitsbereich-Agent LLM-Anbieter",
description:
"Der spezifische LLM-Anbieter und das Modell, das für den @agent-Agenten dieses Arbeitsbereichs verwendet wird.",
},
mode: {
chat: {
title: "Arbeitsbereich-Agent Chat-Modell",
description:
"Das spezifische Chat-Modell, das für den @agent-Agenten dieses Arbeitsbereichs verwendet wird.",
},
title: "Arbeitsbereich-Agent-Modell",
description:
"Das spezifische LLM-Modell, das für den @agent-Agenten dieses Arbeitsbereichs verwendet wird.",
wait: "-- warte auf Modelle --",
},
skill: {
title: "Standard-Agentenfähigkeiten",
description:
"Verbessern Sie die natürlichen Fähigkeiten des Standard-Agenten mit diesen vorgefertigten Fähigkeiten. Diese Einrichtung gilt für alle Arbeitsbereiche.",
rag: {
title: "RAG & Langzeitgedächtnis",
description:
'Erlauben Sie dem Agenten, Ihre lokalen Dokumente zu nutzen, um eine Abfrage zu beantworten oder bitten Sie den Agenten, Inhalte für den Langzeitabruf zu "merken".',
},
view: {
title: "Dokumente anzeigen & zusammenfassen",
description:
"Erlauben Sie dem Agenten, den Inhalt der aktuell eingebetteten Arbeitsbereichsdateien aufzulisten und zusammenzufassen.",
},
scrape: {
title: "Websites durchsuchen",
description:
"Erlauben Sie dem Agenten, Websites zu besuchen und deren Inhalt zu extrahieren.",
},
generate: {
title: "Diagramme generieren",
description:
"Aktivieren Sie den Standard-Agenten, um verschiedene Arten von Diagrammen aus bereitgestellten oder im Chat gegebenen Daten zu generieren.",
},
save: {
title: "Dateien generieren & im Browser speichern",
description:
"Aktivieren Sie den Standard-Agenten, um Dateien zu generieren und zu schreiben, die gespeichert und in Ihrem Browser heruntergeladen werden können.",
},
web: {
title: "Live-Websuche und -Browsing",
"desc-start":
"Ermöglichen Sie Ihrem Agenten, das Web zu durchsuchen, um Ihre Fragen zu beantworten, indem Sie eine Verbindung zu einem Websuche-Anbieter (SERP) herstellen.",
"desc-end":
"Die Websuche während Agentensitzungen funktioniert erst, wenn dies eingerichtet ist.",
},
},
},
recorded: {
title: "Arbeitsbereich-Chats",
description:
"Dies sind alle aufgezeichneten Chats und Nachrichten, die von Benutzern gesendet wurden, geordnet nach ihrem Erstellungsdatum.",
export: "Exportieren",
table: {
id: "Id",
by: "Gesendet von",
workspace: "Arbeitsbereich",
prompt: "Prompt",
response: "Antwort",
at: "Gesendet am",
},
},
appearance: {
title: "Erscheinungsbild",
description: "Passen Sie die Erscheinungseinstellungen Ihrer Plattform an.",
logo: {
title: "Logo anpassen",
description:
"Laden Sie Ihr eigenes Logo hoch, um Ihren Chatbot zu personalisieren.",
add: "Benutzerdefiniertes Logo hinzufügen",
recommended: "Empfohlene Größe: 800 x 200",
remove: "Entfernen",
replace: "Ersetzen",
},
message: {
title: "Nachrichten anpassen",
description:
"Passen Sie die automatischen Nachrichten an, die Ihren Benutzern angezeigt werden.",
new: "Neu",
system: "System",
user: "Benutzer",
message: "Nachricht",
assistant: "AnythingLLM Chat-Assistent",
"double-click": "Doppelklicken zum Bearbeiten...",
save: "Nachrichten speichern",
},
icons: {
title: "Benutzerdefinierte Fußzeilen-Icons",
description:
"Passen Sie die Fußzeilen-Icons an, die am unteren Rand der Seitenleiste angezeigt werden.",
icon: "Icon",
link: "Link",
},
},
api: {
title: "API-Schlüssel",
description:
"API-Schlüssel ermöglichen es dem Besitzer, programmatisch auf diese AnythingLLM-Instanz zuzugreifen und sie zu verwalten.",
link: "Lesen Sie die API-Dokumentation",
generate: "Neuen API-Schlüssel generieren",
table: {
key: "API-Schlüssel",
by: "Erstellt von",
created: "Erstellt",
},
},
llm: {
title: "LLM-Präferenz",
description:
"Dies sind die Anmeldeinformationen und Einstellungen für Ihren bevorzugten LLM-Chat- und Einbettungsanbieter. Es ist wichtig, dass diese Schlüssel aktuell und korrekt sind, sonst wird AnythingLLM nicht richtig funktionieren.",
provider: "LLM-Anbieter",
},
transcription: {
title: "Transkriptionsmodell-Präferenz",
description:
"Dies sind die Anmeldeinformationen und Einstellungen für Ihren bevorzugten Transkriptionsmodellanbieter. Es ist wichtig, dass diese Schlüssel aktuell und korrekt sind, sonst werden Mediendateien und Audio nicht transkribiert.",
provider: "Transkriptionsanbieter",
"warn-start":
"Die Verwendung des lokalen Whisper-Modells auf Maschinen mit begrenztem RAM oder CPU kann AnythingLLM bei der Verarbeitung von Mediendateien zum Stillstand bringen.",
"warn-recommend":
"Wir empfehlen mindestens 2 GB RAM und das Hochladen von Dateien <10 MB.",
"warn-end":
"Das eingebaute Modell wird bei der ersten Verwendung automatisch heruntergeladen.",
},
embedding: {
title: "Einbettungspräferenz",
"desc-start":
"Bei der Verwendung eines LLM, das keine native Unterstützung für eine Einbettungs-Engine bietet, müssen Sie möglicherweise zusätzlich Anmeldeinformationen für die Texteinbettung angeben.",
"desc-end":
"Einbettung ist der Prozess, Text in Vektoren umzuwandeln. Diese Anmeldeinformationen sind erforderlich, um Ihre Dateien und Prompts in ein Format umzuwandeln, das AnythingLLM zur Verarbeitung verwenden kann.",
provider: {
title: "Einbettungsanbieter",
description:
"Bei Verwendung der nativen Einbettungs-Engine von AnythingLLM ist keine Einrichtung erforderlich.",
},
},
text: {
title: "Textsplitting & Chunking-Präferenzen",
"desc-start":
"Manchmal möchten Sie vielleicht die Standardmethode ändern, wie neue Dokumente gesplittet und gechunkt werden, bevor sie in Ihre Vektordatenbank eingefügt werden.",
"desc-end":
"Sie sollten diese Einstellung nur ändern, wenn Sie verstehen, wie Textsplitting funktioniert und welche Nebenwirkungen es hat.",
"warn-start": "Änderungen hier gelten nur für",
"warn-center": "neu eingebettete Dokumente",
"warn-end": ", nicht für bestehende Dokumente.",
size: {
title: "Textchunk-Größe",
description:
"Dies ist die maximale Länge der Zeichen, die in einem einzelnen Vektor vorhanden sein können.",
recommend: "Die maximale Länge des Einbettungsmodells beträgt",
},
overlap: {
title: "Textchunk-Überlappung",
description:
"Dies ist die maximale Überlappung von Zeichen, die während des Chunkings zwischen zwei benachbarten Textchunks auftritt.",
},
},
vector: {
title: "Vektordatenbank",
description:
"Dies sind die Anmeldeinformationen und Einstellungen für die Funktionsweise Ihrer AnythingLLM-Instanz. Es ist wichtig, dass diese Schlüssel aktuell und korrekt sind.",
provider: {
title: "Vektordatenbankanbieter",
description: "Für LanceDB ist keine Konfiguration erforderlich.",
},
},
embeddable: {
title: "Einbettbare Chat-Widgets",
description:
"Einbettbare Chat-Widgets sind öffentlich zugängliche Chat-Schnittstellen, die an einen einzelnen Arbeitsbereich gebunden sind. Diese ermöglichen es Ihnen, Arbeitsbereiche zu erstellen, die Sie dann weltweit veröffentlichen können.",
create: "Einbettung erstellen",
table: {
workspace: "Arbeitsbereich",
chats: "Gesendete Chats",
Active: "Aktive Domains",
},
},
"embed-chats": {
title: "Eingebettete Chats",
description:
"Dies sind alle aufgezeichneten Chats und Nachrichten von jeder Einbettung, die Sie veröffentlicht haben.",
table: {
embed: "Einbettung",
sender: "Absender",
message: "Nachricht",
response: "Antwort",
at: "Gesendet am",
},
},
multi: {
title: "Mehrbenutzer-Modus",
description:
"Richten Sie Ihre Instanz ein, um Ihr Team zu unterstützen, indem Sie den Mehrbenutzer-Modus aktivieren.",
enable: {
"is-enable": "Mehrbenutzer-Modus ist aktiviert",
enable: "Mehrbenutzer-Modus aktivieren",
description:
"Standardmäßig sind Sie der einzige Administrator. Als Administrator müssen Sie Konten für alle neuen Benutzer oder Administratoren erstellen. Verlieren Sie Ihr Passwort nicht, da nur ein Administrator-Benutzer Passwörter zurücksetzen kann.",
username: "Administrator-Kontoname",
password: "Administrator-Kontopasswort",
},
password: {
title: "Passwortschutz",
description:
"Schützen Sie Ihre AnythingLLM-Instanz mit einem Passwort. Wenn Sie dieses vergessen, gibt es keine Wiederherstellungsmethode, also stellen Sie sicher, dass Sie dieses Passwort speichern.",
},
instance: {
title: "Instanz mit Passwort schützen",
description:
"Standardmäßig sind Sie der einzige Administrator. Als Administrator müssen Sie Konten für alle neuen Benutzer oder Administratoren erstellen. Verlieren Sie Ihr Passwort nicht, da nur ein Administrator-Benutzer Passwörter zurücksetzen kann.",
password: "Instanz-Passwort",
},
},
event: {
title: "Ereignisprotokolle",
description:
"Sehen Sie alle Aktionen und Ereignisse, die auf dieser Instanz zur Überwachung stattfinden.",
clear: "Ereignisprotokolle löschen",
table: {
type: "Ereignistyp",
user: "Benutzer",
occurred: "Aufgetreten am",
},
},
privacy: {
title: "Datenschutz & Datenverarbeitung",
description:
"Dies ist Ihre Konfiguration dafür, wie verbundene Drittanbieter und AnythingLLM Ihre Daten behandeln.",
llm: "LLM-Auswahl",
embedding: "Einbettungspräferenz",
vector: "Vektordatenbank",
anonymous: "Anonyme Telemetrie aktiviert",
},
};
export default TRANSLATIONS;

View File

@ -0,0 +1,496 @@
const TRANSLATIONS = {
common: {
"workspaces-name": "Nome dos Workspaces",
error: "erro",
success: "sucesso",
user: "Usuário",
selection: "Seleção de Modelo",
saving: "Salvando...",
save: "Salvar alterações",
previous: "Página Anterior",
next: "Próxima Página",
},
// Setting Sidebar menu items.
settings: {
title: "Configurações da Instância",
system: "Configurações Gerais",
invites: "Convites",
users: "Usuários",
workspaces: "Workspaces",
"workspace-chats": "Chats do Workspace",
customization: "Customização",
"api-keys": "API para Desenvolvedores",
llm: "LLM",
transcription: "Transcrição",
embedder: "Incorporador",
"text-splitting": "Divisor de Texto e Fragmentação",
"voice-speech": "Voz e Fala",
"vector-database": "Banco de Dados Vetorial",
embeds: "Incorporar Chat",
"embed-chats": "Histórico de Chats Incorporados",
security: "Segurança",
"event-logs": "Logs de Eventos",
privacy: "Privacidade e Dados",
"ai-providers": "Provedores de IA",
"agent-skills": "Habilidades do Agente",
admin: "Admin",
tools: "Ferramentas",
"experimental-features": "Recursos Experimentais",
contact: "Contato com Suporte",
},
// Page Definitions
login: {
"multi-user": {
welcome: "Bem-vindo ao",
"placeholder-username": "Nome de Usuário",
"placeholder-password": "Senha",
login: "Entrar",
validating: "Validando...",
"forgot-pass": "Esqueceu a senha",
reset: "Redefinir",
},
"sign-in": {
start: "Faça login na sua",
end: "conta.",
},
"password-reset": {
title: "Redefinição de Senha",
description:
"Forneça as informações necessárias abaixo para redefinir sua senha.",
"recovery-codes": "Códigos de Recuperação",
"recovery-code": "Código de Recuperação {{index}}",
"back-to-login": "Voltar ao Login",
},
},
welcomeMessage: {
part1:
"Bem-vindo ao AnythingLLM, AnythingLLM é uma ferramenta de IA de código aberto da Mintplex Labs que transforma qualquer coisa em um chatbot treinado que você pode consultar e conversar. AnythingLLM é um software BYOK (bring-your-own-keys | traga suas próprias chaves), portanto, não há assinatura, taxa ou cobranças para este software fora dos serviços que você deseja usar com ele.",
part2:
"AnythingLLM é a maneira mais fácil de reunir produtos de IA poderosos como OpenAi, GPT-4, LangChain, PineconeDB, ChromaDB e outros serviços em um pacote organizado sem complicações para aumentar sua produtividade em 100x.",
part3:
"AnythingLLM pode ser executado totalmente localmente em sua máquina com pouca sobrecarga que você nem perceberá que está lá! Não é necessário GPU. A instalação em nuvem e localmente também está disponível.\nO ecossistema de ferramentas de IA fica mais poderoso a cada dia. AnythingLLM facilita o uso.",
githubIssue: "Criar uma issue no Github",
user1: "Como eu começo?!",
part4:
'É simples. Todas as coleções são organizadas em grupos que chamamos de "Workspaces". Workspaces são grupos de arquivos, documentos, imagens, PDFs e outros arquivos que serão transformados em algo que os LLMs podem entender e usar em conversas.\n\nVocê pode adicionar e remover arquivos a qualquer momento.',
createWorkspace: "Crie seu primeiro workspace",
user2:
"Isso é como um Dropbox de IA ou algo assim? E quanto a conversar? Não é um chatbot?",
part5:
"AnythingLLM é mais do que um Dropbox mais inteligente.\n\nAnythingLLM oferece duas maneiras de conversar com seus dados:\n\n<i>Consulta:</i> Seus chats retornarão dados ou inferências encontradas com os documentos em seu workspace ao qual tem acesso. Adicionar mais documentos ao Workspace o torna mais inteligente!\n\n<i>Conversacional:</i> Seus documentos + seu histórico de chat em andamento contribuem para o conhecimento do LLM ao mesmo tempo. Ótimo para adicionar informações em tempo real baseadas em texto ou correções e mal-entendidos que o LLM possa ter.\n\nVocê pode alternar entre qualquer modo \n<i>no meio da conversa!</i>",
user3: "Uau, isso soa incrível, deixe-me experimentar já!",
part6: "Divirta-se!",
starOnGithub: "Dar estrela no GitHub",
contact: "Contato Mintplex Labs",
},
"new-workspace": {
title: "Novo Workspace",
placeholder: "Meu Workspace",
},
// Workspace Settings menu items
"workspaces—settings": {
general: "Configurações Gerais",
chat: "Configurações de Chat",
vector: "Banco de Dados Vetorial",
members: "Membros",
agent: "Configuração do Agente",
},
// General Appearance
general: {
vector: {
title: "Contagem de Vetores",
description: "Número total de vetores no seu banco de dados vetorial.",
},
names: {
description: "Isso mudará apenas o nome de exibição do seu workspace.",
},
message: {
title: "Mensagens de Chat Sugeridas",
description:
"Personalize as mensagens que serão sugeridas aos usuários do seu workspace.",
add: "Adicionar nova mensagem",
save: "Salvar Mensagens",
heading: "Explique para mim",
body: "os benefícios do AnythingLLM",
},
pfp: {
title: "Imagem de Perfil do Assistente",
description:
"Personalize a imagem de perfil do assistente para este workspace.",
image: "Imagem do Workspace",
remove: "Remover Imagem do Workspace",
},
delete: {
title: "Excluir Workspace",
description:
"Excluir este workspace e todos os seus dados. Isso excluirá o workspace para todos os usuários.",
delete: "Excluir Workspace",
deleting: "Excluindo Workspace...",
"confirm-start": "Você está prestes a excluir todo o seu",
"confirm-end":
"workspace. Isso removerá todas as incorporações vetoriais no seu banco de dados vetorial.\n\nOs arquivos de origem originais permanecerão intactos. Esta ação é irreversível.",
},
},
// Chat Settings
chat: {
llm: {
title: "Provedor de LLM do Workspace",
description:
"O provedor e modelo específico de LLM que será usado para este workspace. Por padrão, usa o provedor e as configurações do sistema LLM.",
search: "Pesquisar todos os provedores de LLM",
},
model: {
title: "Modelo de Chat do Workspace",
description:
"O modelo de chat específico que será usado para este workspace. Se vazio, usará a preferência do LLM do sistema.",
wait: "-- aguardando modelos --",
},
mode: {
title: "Modo de Chat",
chat: {
title: "Chat",
"desc-start": "fornecerá respostas com o conhecimento geral do LLM",
and: "e",
"desc-end": "contexto do documento encontrado.",
},
query: {
title: "Consulta",
"desc-start": "fornecerá respostas",
only: "somente",
"desc-end": "se o contexto do documento for encontrado.",
},
},
history: {
title: "Histórico de Chat",
"desc-start":
"O número de chats anteriores que serão incluídos na memória de curto prazo da resposta.",
recommend: "Recomendado: 20. ",
"desc-end":
"Qualquer coisa acima de 45 provavelmente levará a falhas contínuas de chat dependendo do tamanho da mensagem.",
},
prompt: {
title: "Prompt",
description:
"O prompt que será usado neste workspace. Defina o contexto e as instruções para que a IA gere uma resposta. Você deve fornecer um prompt cuidadosamente elaborado para que a IA possa gerar uma resposta relevante e precisa.",
},
refusal: {
title: "Resposta de Recusa no Modo de Consulta",
"desc-start": "Quando estiver no modo",
query: "consulta",
"desc-end":
", você pode querer retornar uma resposta de recusa personalizada quando nenhum contexto for encontrado.",
},
temperature: {
title: "Temperatura do LLM",
"desc-start":
'Esta configuração controla o quão "criativas" serão as respostas do seu LLM.',
"desc-end":
"Quanto maior o número, mais criativa será a resposta. Para alguns modelos, isso pode levar a respostas incoerentes quando configurado muito alto.",
hint: "A maioria dos LLMs tem vários intervalos aceitáveis de valores válidos. Consulte seu provedor de LLM para essa informação.",
},
},
// Vector Database
"vector-workspace": {
identifier: "Identificador do Banco de Dados Vetorial",
snippets: {
title: "Máximo de Trechos de Contexto",
description:
"Esta configuração controla a quantidade máxima de trechos de contexto que será enviada ao LLM por chat ou consulta.",
recommend: "Recomendado: 4",
},
doc: {
title: "Limite de Similaridade de Documentos",
description:
"A pontuação mínima de similaridade necessária para que uma fonte seja considerada relacionada ao chat. Quanto maior o número, mais semelhante a fonte deve ser ao chat.",
zero: "Sem restrição",
low: "Baixo (pontuação de similaridade ≥ 0,25)",
medium: "Médio (pontuação de similaridade ≥ 0,50)",
high: "Alto (pontuação de similaridade ≥ 0,75)",
},
reset: {
reset: "Redefinir Banco de Dados Vetorial",
resetting: "Limpando vetores...",
confirm:
"Você está prestes a redefinir o banco de dados vetorial deste workspace. Isso removerá todas as incorporações vetoriais atualmente embutidas.\n\nOs arquivos de origem originais permanecerão intactos. Esta ação é irreversível.",
error: "O banco de dados vetorial do workspace não pôde ser redefinido!",
success:
"O banco de dados vetorial do workspace foi redefinido com sucesso!",
},
},
// Agent Configuration
agent: {
"performance-warning":
"O desempenho dos LLMs que não suportam explicitamente a chamada de ferramentas depende muito das capacidades e da precisão do modelo. Algumas habilidades podem ser limitadas ou não funcionais.",
provider: {
title: "Provedor de LLM do Agente do Workspace",
description:
"O provedor e modelo específico de LLM que será usado para o agente @agent deste workspace.",
},
mode: {
chat: {
title: "Modelo de Chat do Agente do Workspace",
description:
"O modelo de chat específico que será usado para o agente @agent deste workspace.",
},
title: "Modelo do Agente do Workspace",
description:
"O modelo de LLM específico que será usado para o agente @agent deste workspace.",
wait: "-- aguardando modelos --",
},
skill: {
title: "Habilidades padrão do agente",
description:
"Melhore as habilidades naturais do agente padrão com essas habilidades pré-construídas. Esta configuração se aplica a todos os workspaces.",
rag: {
title: "RAG e memória de longo prazo",
description:
'Permitir que o agente utilize seus documentos locais para responder a uma consulta ou pedir ao agente para "lembrar" peças de conteúdo para recuperação de memória de longo prazo.',
},
view: {
title: "Visualizar e resumir documentos",
description:
"Permitir que o agente liste e resuma o conteúdo dos arquivos do workspace atualmente incorporados.",
},
scrape: {
title: "Raspagem de sites",
description:
"Permitir que o agente visite e raspe o conteúdo de sites.",
},
generate: {
title: "Gerar gráficos",
description:
"Habilitar o agente padrão para gerar vários tipos de gráficos a partir dos dados fornecidos ou dados no chat.",
},
save: {
title: "Gerar e salvar arquivos no navegador",
description:
"Habilitar o agente padrão para gerar e gravar arquivos que podem ser salvos e baixados no seu navegador.",
},
web: {
title: "Pesquisa e navegação na web ao vivo",
"desc-start":
"Permitir que seu agente pesquise na web para responder suas perguntas conectando-se a um provedor de pesquisa na web (SERP).",
"desc-end":
"A pesquisa na web durante as sessões do agente não funcionará até que isso seja configurado.",
},
},
},
// Workspace Chats
recorded: {
title: "Chats do Workspace",
description:
"Estes são todos os chats e mensagens gravados que foram enviados pelos usuários ordenados por data de criação.",
export: "Exportar",
table: {
id: "Id",
by: "Enviado Por",
workspace: "Workspace",
prompt: "Prompt",
response: "Resposta",
at: "Enviado Em",
},
},
// Appearance
appearance: {
title: "Aparência",
description: "Personalize as configurações de aparência da sua plataforma.",
logo: {
title: "Personalizar Logo",
description:
"Envie seu logotipo personalizado para tornar seu chatbot seu.",
add: "Adicionar um logotipo personalizado",
recommended: "Tamanho recomendado: 800 x 200",
remove: "Remover",
replace: "Substituir",
},
message: {
title: "Personalizar Mensagens",
description:
"Personalize as mensagens automáticas exibidas aos seus usuários.",
new: "Novo",
system: "sistema",
user: "usuário",
message: "mensagem",
assistant: "Assistente de Chat AnythingLLM",
"double-click": "Clique duas vezes para editar...",
save: "Salvar Mensagens",
},
icons: {
title: "Ícones de Rodapé Personalizados",
description:
"Personalize os ícones de rodapé exibidos na parte inferior da barra lateral.",
icon: "Ícone",
link: "Link",
},
},
// API Keys
api: {
title: "Chaves API",
description:
"As chaves API permitem que o titular acesse e gerencie programaticamente esta instância do AnythingLLM.",
link: "Leia a documentação da API",
generate: "Gerar Nova Chave API",
table: {
key: "Chave API",
by: "Criado Por",
created: "Criado",
},
},
llm: {
title: "Preferência de LLM",
description:
"Estas são as credenciais e configurações para seu provedor preferido de chat e incorporação de LLM. É importante que essas chaves estejam atualizadas e corretas, caso contrário, o AnythingLLM não funcionará corretamente.",
provider: "Provedor de LLM",
},
transcription: {
title: "Preferência de Modelo de Transcrição",
description:
"Estas são as credenciais e configurações para seu provedor preferido de modelo de transcrição. É importante que essas chaves estejam atualizadas e corretas, caso contrário, os arquivos de mídia e áudio não serão transcritos.",
provider: "Provedor de Transcrição",
"warn-start":
"Usar o modelo whisper local em máquinas com RAM ou CPU limitados pode travar o AnythingLLM ao processar arquivos de mídia.",
"warn-recommend":
"Recomendamos pelo menos 2GB de RAM e upload de arquivos <10Mb.",
"warn-end":
"O modelo embutido será baixado automaticamente no primeiro uso.",
},
embedding: {
title: "Preferência de Incorporação",
"desc-start":
"Ao usar um LLM que não suporta nativamente um mecanismo de incorporação - pode ser necessário especificar adicionalmente as credenciais para incorporação de texto.",
"desc-end":
"A incorporação é o processo de transformar texto em vetores. Essas credenciais são necessárias para transformar seus arquivos e prompts em um formato que o AnythingLLM possa usar para processar.",
provider: {
title: "Provedor de Incorporação",
description:
"Não é necessária configuração ao usar o mecanismo de incorporação nativo do AnythingLLM.",
},
},
text: {
title: "Preferências de Divisão e Fragmentação de Texto",
"desc-start":
"Às vezes, você pode querer alterar a maneira padrão como novos documentos são divididos e fragmentados antes de serem inseridos em seu banco de dados de vetores.",
"desc-end":
"Você só deve modificar esta configuração se entender como a divisão de texto funciona e seus efeitos colaterais.",
"warn-start": "As alterações aqui se aplicarão apenas a",
"warn-center": "documentos recém-incorporados",
"warn-end": ", não documentos existentes.",
size: {
title: "Tamanho do Fragmento de Texto",
description:
"Este é o comprimento máximo de caracteres que pode estar presente em um único vetor.",
recommend: "O comprimento máximo do modelo de incorporação é",
},
overlap: {
title: "Sobreposição de Fragmento de Texto",
description:
"Esta é a sobreposição máxima de caracteres que ocorre durante a fragmentação entre dois fragmentos de texto adjacentes.",
},
},
// Vector Database
vector: {
title: "Banco de Dados Vetorial",
description:
"Estas são as credenciais e configurações de como sua instância do AnythingLLM funcionará. É importante que essas chaves estejam atualizadas e corretas.",
provider: {
title: "Provedor de Banco de Dados Vetorial",
description: "Não há configuração necessária para o LanceDB.",
},
},
// Embeddable Chat Widgets
embeddable: {
title: "Widgets de Chat Incorporáveis",
description:
"Os widgets de chat incorporáveis são interfaces de chat públicas vinculadas a um único workspace. Eles permitem que você construa workspaces que você pode publicar para o mundo.",
create: "Criar incorporação",
table: {
workspace: "Workspace",
chats: "Chats Enviados",
Active: "Domínios Ativos",
},
},
"embed-chats": {
title: "Incorporar Chats",
description:
"Estes são todos os chats e mensagens registrados de qualquer incorporação que você publicou.",
table: {
embed: "Incorporação",
sender: "Remetente",
message: "Mensagem",
response: "Resposta",
at: "Enviado Em",
},
},
multi: {
title: "Modo Multiusuário",
description:
"Configure sua instância para suportar sua equipe ativando o Modo Multiusuário.",
enable: {
"is-enable": "Modo Multiusuário está Ativado",
enable: "Ativar Modo Multiusuário",
description:
"Por padrão, você será o único administrador. Como administrador, você precisará criar contas para todos os novos usuários ou administradores. Não perca sua senha, pois apenas um usuário Administrador pode redefinir senhas.",
username: "Nome de usuário da conta de Administrador",
password: "Senha da conta de Administrador",
},
password: {
title: "Proteção por Senha",
description:
"Proteja sua instância do AnythingLLM com uma senha. Se você esquecer esta senha, não há método de recuperação, então certifique-se de salvar esta senha.",
},
instance: {
title: "Proteger Instância com Senha",
description:
"Por padrão, você será o único administrador. Como administrador, você precisará criar contas para todos os novos usuários ou administradores. Não perca sua senha, pois apenas um usuário Administrador pode redefinir senhas.",
password: "Senha da instância",
},
},
// Event Logs
event: {
title: "Logs de Eventos",
description:
"Veja todas as ações e eventos acontecendo nesta instância para monitoramento.",
clear: "Limpar Logs de Eventos",
table: {
type: "Tipo de Evento",
user: "Usuário",
occurred: "Ocorreu Em",
},
},
// Privacy & Data-Handling
privacy: {
title: "Privacidade e Tratamento de Dados",
description:
"Esta é a sua configuração de como os provedores de terceiros conectados e o AnythingLLM tratam seus dados.",
llm: "Seleção de LLM",
embedding: "Preferência de Incorporação",
vector: "Banco de Dados Vetorial",
anonymous: "Telemetria Anônima Ativada",
},
};
export default TRANSLATIONS;

View File

@ -19,30 +19,38 @@ import Korean from "./ko/common.js";
import Spanish from "./es/common.js"; import Spanish from "./es/common.js";
import French from "./fr/common.js"; import French from "./fr/common.js";
import Mandarin from "./zh/common.js"; import Mandarin from "./zh/common.js";
import German from "./de/common.js";
import Russian from "./ru/common.js"; import Russian from "./ru/common.js";
import Italian from "./it/common.js"; import Italian from "./it/common.js";
import Portuguese from "./pt_BR/common.js";
export const defaultNS = "common"; export const defaultNS = "common";
export const resources = { export const resources = {
en: { en: {
common: English, common: English,
}, },
ko: {
common: Korean,
},
zh: { zh: {
common: Mandarin, common: Mandarin,
}, },
es: { es: {
common: Spanish, common: Spanish,
}, },
de: {
common: German,
},
fr: { fr: {
common: French, common: French,
}, },
ko: {
common: Korean,
},
ru: { ru: {
common: Russian, common: Russian,
}, },
it: { it: {
common: Italian, common: Italian,
}, },
pt: {
common: Portuguese,
},
}; };

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -7,6 +7,7 @@ import { RoleHintDisplay } from "..";
export default function NewUserModal({ closeModal }) { export default function NewUserModal({ closeModal }) {
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [role, setRole] = useState("default"); const [role, setRole] = useState("default");
const handleCreate = async (e) => { const handleCreate = async (e) => {
setError(null); setError(null);
e.preventDefault(); e.preventDefault();
@ -54,7 +55,18 @@ export default function NewUserModal({ closeModal }) {
minLength={2} minLength={2}
required={true} required={true}
autoComplete="off" autoComplete="off"
pattern="^[a-z0-9_-]+$"
onInvalid={(e) =>
e.target.setCustomValidity(
"Username must be only contain lowercase letters, numbers, underscores, and hyphens with no spaces"
)
}
onChange={(e) => e.target.setCustomValidity("")}
/> />
<p className="mt-2 text-xs text-white/60">
Username must be only contain lowercase letters, numbers,
underscores, and hyphens with no spaces
</p>
</div> </div>
<div> <div>
<label <label
@ -70,7 +82,11 @@ export default function NewUserModal({ closeModal }) {
placeholder="User's initial password" placeholder="User's initial password"
required={true} required={true}
autoComplete="off" autoComplete="off"
minLength={8}
/> />
<p className="mt-2 text-xs text-white/60">
Password must be at least 8 characters long
</p>
</div> </div>
<div> <div>
<label <label
@ -84,7 +100,7 @@ export default function NewUserModal({ closeModal }) {
required={true} required={true}
defaultValue={"default"} defaultValue={"default"}
onChange={(e) => setRole(e.target.value)} onChange={(e) => setRole(e.target.value)}
className="rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border-gray-500 focus:ring-blue-500 focus:border-blue-500" className="rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border-gray-500 focus:ring-blue-500 focus:border-blue-500 w-full"
> >
<option value="default">Default</option> <option value="default">Default</option>
<option value="manager">Manager</option> <option value="manager">Manager</option>

View File

@ -52,11 +52,15 @@ export default function EditUserModal({ currentUser, user, closeModal }) {
type="text" type="text"
className="bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" className="bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="User's username" placeholder="User's username"
minLength={2}
defaultValue={user.username} defaultValue={user.username}
minLength={2}
required={true} required={true}
autoComplete="off" autoComplete="off"
/> />
<p className="mt-2 text-xs text-white/60">
Username must be only contain lowercase letters, numbers,
underscores, and hyphens with no spaces
</p>
</div> </div>
<div> <div>
<label <label
@ -71,7 +75,11 @@ export default function EditUserModal({ currentUser, user, closeModal }) {
className="bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" className="bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder={`${user.username}'s new password`} placeholder={`${user.username}'s new password`}
autoComplete="off" autoComplete="off"
minLength={8}
/> />
<p className="mt-2 text-xs text-white/60">
Password must be at least 8 characters long
</p>
</div> </div>
<div> <div>
<label <label
@ -85,7 +93,7 @@ export default function EditUserModal({ currentUser, user, closeModal }) {
required={true} required={true}
defaultValue={user.role} defaultValue={user.role}
onChange={(e) => setRole(e.target.value)} onChange={(e) => setRole(e.target.value)}
className="rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border-gray-500 focus:ring-blue-500 focus:border-blue-500" className="rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border-gray-500 focus:ring-blue-500 focus:border-blue-500 w-full"
> >
<option value="default">Default</option> <option value="default">Default</option>
<option value="manager">Manager</option> <option value="manager">Manager</option>

View File

@ -1,9 +1,11 @@
import FineTuning from "@/models/experimental/fineTuning"; import FineTuning from "@/models/experimental/fineTuning";
import { dollarFormat } from "@/utils/numbers"; import { dollarFormat } from "@/utils/numbers";
import showToast from "@/utils/toast"; import showToast from "@/utils/toast";
import { CheckCircle } from "@phosphor-icons/react"; import { Check } from "@phosphor-icons/react";
import { useState } from "react"; import { useState, useEffect } from "react";
import FineTuningSteps from "../index"; import FineTuningSteps from "../index";
import CTAButton from "@/components/lib/CTAButton";
import Workspace from "@/models/workspace";
/** /**
* @param {{settings: import("../index").OrderSettings}} param0 * @param {{settings: import("../index").OrderSettings}} param0
@ -11,6 +13,18 @@ import FineTuningSteps from "../index";
*/ */
export default function Confirmation({ settings, setSettings, setStep }) { export default function Confirmation({ settings, setSettings, setStep }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [workspaces, setWorkspaces] = useState([]);
useEffect(() => {
Workspace.all()
.then((fetchedWorkspaces) => {
setWorkspaces(fetchedWorkspaces);
})
.catch(() => {
showToast("Failed to fetch workspaces", "error");
});
}, []);
async function handleCheckout() { async function handleCheckout() {
setLoading(true); setLoading(true);
const data = await FineTuning.createOrder({ const data = await FineTuning.createOrder({
@ -40,107 +54,124 @@ export default function Confirmation({ settings, setSettings, setStep }) {
setStep(FineTuningSteps.confirmation.next()); setStep(FineTuningSteps.confirmation.next());
} }
const getWorkspaceName = (slug) => {
const workspace = workspaces.find((ws) => ws.slug === slug);
return workspace ? workspace.name : slug;
};
return ( return (
<div className="flex-[2] flex flex-col gap-y-[18px] mt-10"> <div className="flex-[2] flex flex-col gap-y-[18px] mt-10">
<div className="bg-[#303237] text-white rounded-xl flex-1 p-4 flex flex-col justify-between"> <div className="bg-[#303237] text-white rounded-xl flex-1 p-6">
<div className="w-full flex flex-col gap-y-4"> <div className="w-full flex flex-col gap-y-3 max-w-[700px]">
<h2 className="text-xl text-white font-semibold">Confirm & Submit</h2> <h2 className="text-base text-white font-semibold">
<p> Confirm & Submit
</h2>
<p className="text-white/80 text-sm">
Below are your fine-tuning order details. If you have any questions Below are your fine-tuning order details. If you have any questions
before or after ordering your fine-tune you can{" "} before or after ordering your fine-tune you can checkout the{" "}
<a <a
href="https://docs.useanything.com/fine-tuning/overview" href="https://docs.useanything.com/fine-tuning/overview"
target="_blank" target="_blank"
className="underline" rel="noreferrer"
className="underline text-sky-400"
> >
checkout the fine-tuning FAQ fine-tuning FAQ
</a>{" "} </a>{" "}
or email{" "} or email{" "}
<a className="underline" href="mailto:team@mintplexlabs.com"> <a
className="underline text-sky-400"
href="mailto:team@mintplexlabs.com"
>
team@mintplexlabs.com team@mintplexlabs.com
</a> </a>
. .
</p> </p>
<div className="p-2 bg-zinc-800 text-white font-mono flex flex-col gap-y-2 h-full rounded-lg"> <div className="p-4 bg-zinc-900 text-white flex flex-col gap-y-2 rounded-lg mt-4">
<div className="flex items-center gap-x-1 text-sm"> <div className="flex flex-col gap-y-3 text-sm">
<p className="">Contact e-mail:</p> <div className="flex items-start gap-x-1">
<p className="font-thin">{settings.email}</p> <p className="w-1/3">Contact e-mail:</p>
<p className="text-white/80 w-2/3">{settings.email}</p>
</div> </div>
<div className="flex items-center gap-x-1 text-sm"> <div className="flex items-start gap-x-1">
<p className="">Base LLM:</p> <p className="w-1/3">Base LLM:</p>
<p className="font-thin">{settings.baseModel}</p> <p className="text-white/80 w-2/3">{settings.baseModel}</p>
</div> </div>
<div className="flex items-center gap-x-1 text-sm"> <div className="flex items-start gap-x-1">
<p className="">Output model name:</p> <p className="w-1/3">Output model name:</p>
<p className="font-thin">"{settings.modelName}"</p> <p className="text-white/80 w-2/3">"{settings.modelName}"</p>
</div> </div>
<div className="flex flex-col gap-y-1 text-sm"> <div className="flex items-start gap-x-1">
<div className="flex items-center gap-x-1"> <p className="w-1/3">Training on workspaces:</p>
<p className="">Training on workspaces:</p> <div className="text-white/80 w-2/3 flex flex-wrap gap-1">
{settings.trainingData.slugs.map((slug, i) => { {settings.trainingData.slugs.map((slug, i) => (
return ( <span
<p key={slug} className="font-thin"> key={slug}
"{slug}" className={`rounded-full bg-white/10 px-2 py-0.5 h-[20px] text-xs font-medium text-white shadow-sm`}
{i !== settings.trainingData.slugs.length - 1 ? "," : ""} >
{getWorkspaceName(slug)}
</span>
))}
</div>
</div>
<div className="flex items-start gap-x-1">
<p className="w-1/3">Training data:</p>
<p className="text-white/80 w-2/3">
{settings.trainingData.feedback === true
? "Training on positive-feedback chats only"
: "Training on all chats"}
</p> </p>
);
})}
</div> </div>
{settings.trainingData.feedback === true ? (
<p className="underline">
training on <b>positive-feedback chats only</b>.
</p>
) : (
<p className="underline">
training on <b>all chats</b>.
</p>
)}
</div> </div>
<br /> <div className="mt-4">
<div className="flex items-center gap-x-1 text-sm"> <ul className="flex flex-col gap-y-1 text-sm">
<CheckCircle className="text-green-300" /> <li className="flex items-center gap-x-2">
<p className="font-thin">Agreed to Terms and Conditions</p> <Check className="text-white" size={12} weight="bold" />
</div> <p className="text-white/80">
<div className="flex items-center gap-x-1 text-sm"> Agreed to Terms and Conditions
<CheckCircle className="text-green-300" /> </p>
<p className="font-thin">Understand privacy & data handling</p> </li>
</div> <li className="flex items-center gap-x-2">
<div className="flex items-center gap-x-1 text-sm"> <Check className="text-white" size={12} weight="bold" />
<CheckCircle className="text-green-300" /> <p className="text-white/80">
<p className="font-thin">Agreed to Fulfillment terms</p> Understand privacy & data handling
</p>
</li>
<li className="flex items-center gap-x-2">
<Check className="text-white" size={12} weight="bold" />
<p className="text-white/80">Agreed to Fulfillment terms</p>
</li>
</ul>
</div> </div>
<div> <div className="mt-4 border-white/40 pt-2">
<div className="flex items-center gap-x-1 text-lg border-t-[2px] border-white/40 pt-2 mb-0"> <div className="flex items-center gap-x-1 text-lg mb-0">
<p className="">Total one-time cost:</p> <p className="">Total one-time cost:</p>
<p className="font-thin"> <p className="text-white/80">
{dollarFormat(settings.tuningInfo.pricing.usd)} {dollarFormat(settings.tuningInfo.pricing.usd)}
<sup>*</sup> <sup>*</sup>
</p> </p>
</div> </div>
<p className="m-0 p-0 text-xs text-white/60 font-mono"> <p className="m-0 p-0 text-xs text-white/60">
<sup>*</sup> price does not include any coupons, incentives, or <sup>*</sup> price does not include any coupons, incentives, or
discounts you can apply at checkout. discounts you can apply at checkout.
</p> </p>
</div> </div>
</div> </div>
<p> <p className="text-xs text-white/80 mt-4">
Once you proceed to checkout, if you do not complete this purchase Once you proceed to checkout, if you do not complete this purchase
your data will be deleted from our servers within 1 hour of your data will be deleted from our servers within 1 hour of
abandonment of the creation of the checkout in accordance to our abandonment of the creation of the checkout in accordance to our
privacy and data handling policy. privacy and data handling policy.
</p> </p>
</div> <CTAButton
<button
disabled={loading} disabled={loading}
onClick={handleCheckout} onClick={handleCheckout}
type="button" className="text-dark-text w-full mt-[18px] h-[34px] hover:bg-accent"
className="mt-8 w-full py-2 text-center text-black bg-white hover:bg-white/80 border-none rounded-lg"
> >
{loading ? <>Generating order...</> : <>Start Training &rarr;</>} {loading ? "Generating order..." : "Start Training →"}
</button> </CTAButton>
</div>
</div> </div>
</div> </div>
); );

View File

@ -1,8 +1,14 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import FineTuning from "@/models/experimental/fineTuning"; import FineTuning from "@/models/experimental/fineTuning";
import Workspace from "@/models/workspace"; import Workspace from "@/models/workspace";
import { CheckCircle, Warning, X } from "@phosphor-icons/react"; import {
CheckCircle,
Warning,
X,
MagnifyingGlass,
} from "@phosphor-icons/react";
import FineTuningSteps from ".."; import FineTuningSteps from "..";
import CTAButton from "@/components/lib/CTAButton";
export default function DataUpload({ setSettings, setStep }) { export default function DataUpload({ setSettings, setStep }) {
const [workspaces, setWorkspaces] = useState([]); const [workspaces, setWorkspaces] = useState([]);
@ -41,34 +47,29 @@ export default function DataUpload({ setSettings, setStep }) {
return ( return (
<div className="flex-[2] flex flex-col gap-y-[18px] mt-10"> <div className="flex-[2] flex flex-col gap-y-[18px] mt-10">
<div className="bg-[#303237] text-white rounded-xl flex-1 p-4"> <div className="bg-[#303237] text-white rounded-xl flex-1 p-6">
<form <div className="w-full flex flex-col gap-y-3 max-w-[700px]">
onSubmit={handleSubmit} <h2 className="text-base text-white font-semibold">
className="flex flex-col justify-between h-full" Select your training dataset
>
<div className="w-full flex flex-col gap-y-4">
<h2 className="text-xl text-white font-semibold">
Select your training dataset.
</h2> </h2>
<p> <p className="text-white/80 text-sm">
This is the data your model will be trained and tuned on. This is This is the data your model will be trained and tuned on. This is a
a critical step and you should always train on the exact critical step and you should always train on the exact information
information you want the model to inherit. By default, AnythingLLM you want the model to inherit. By default, AnythingLLM will use all
will use all chats, but you can filter chats by workspace and even chats, but you can filter chats by workspace and even limit training
limit training to chats which users have left a positive feedback to chats which users have left a positive feedback indication on
indication on (thumbs up). (thumbs up).
</p> </p>
<div className="flex flex-col pr-10"> <form onSubmit={handleSubmit} className="flex flex-col gap-y-6 mt-4">
<div className="flex flex-col gap-y-1 mb-4"> <div className="flex flex-col">
<label className="text-white text-sm font-bold"> <label className="text-white text-sm font-semibold block mb-3">
Only use positive responses Only use positive responses
</label> </label>
<p className="text-xs font-normal text-white/80"> <p className="text-xs font-normal text-white/80 mb-2">
Enabling this toggle will filter your dataset to only use Enabling this toggle will filter your dataset to only use
"positive" responses that were marked during chatting. "positive" responses that were marked during chatting.
</p> </p>
</div>
<label className="relative inline-flex cursor-pointer items-center w-fit"> <label className="relative inline-flex cursor-pointer items-center w-fit">
<input <input
type="checkbox" type="checkbox"
@ -84,36 +85,35 @@ export default function DataUpload({ setSettings, setStep }) {
</label> </label>
</div> </div>
<div className="flex flex-col pr-10"> <div className="flex flex-col">
<div className="flex flex-col gap-y-1 mb-4"> <label className="text-white text-sm font-semibold block mb-3">
<label className="text-white text-sm font-bold">
Selected Workspaces Selected Workspaces
</label> </label>
<p className="text-xs font-normal text-white/80"> <p className="text-xs font-normal text-white/80 mb-2">
You training data will be limited to these workspaces. Your training data will be limited to these workspaces.
</p> </p>
</div>
<WorkspaceSelector <WorkspaceSelector
workspaces={workspaces} workspaces={workspaces}
selectedWorkspaces={dataFilters.workspaces} selectedWorkspaces={dataFilters.workspaces}
setDataFilters={setDataFilters} setDataFilters={setDataFilters}
/> />
</div> </div>
<DatasetSummary <DatasetSummary
workspaces={dataFilters.workspaces} workspaces={dataFilters.workspaces}
feedback={dataFilters.feedback} feedback={dataFilters.feedback}
/> />
</div>
<button <CTAButton
type="submit" type="submit"
className="mt-20 w-full py-2 text-center text-white hover:bg-primary-button border-none rounded-lg" className="text-dark-text w-full mt-[18px] h-[34px] hover:bg-accent"
> >
Proceed to Confirmation &rarr; Proceed to Confirmation &rarr;
</button> </CTAButton>
</form> </form>
</div> </div>
</div> </div>
</div>
); );
} }
@ -155,33 +155,9 @@ function WorkspaceSelector({
} }
return ( return (
<div className="flex flex-col items-center"> <div className="flex flex-col gap-y-2">
<div className="w-full h-fit"> <div className="min-w-[150px] max-w-[300px] h-[32px] p-[10px] rounded-lg flex items-center bg-dark-highlight mt-1">
<div className="w-full relative z-1"> <MagnifyingGlass size={16} className="text-white" />
<div className="p-1 flex border border-white/40 bg-zinc-800 rounded">
<div className="flex flex-auto flex-wrap">
{selectedWorkspaces.map((workspace) => {
return (
<div
key={workspace.slug}
className="flex gap-x-1 justify-center items-center m-1 font-medium py-1 px-2 bg-zinc-800 rounded-full text-white/40 bg-white/10 border border-white/40 "
>
<div className="text-xs font-normal text-white leading-none max-w-full flex-initial">
{workspace.name}
</div>
<div className="flex flex-auto flex-row-reverse">
<button
onClick={() => handleRemoveWorkspace(workspace)}
type="button"
className="hover:text-red-500"
>
<X size={14} weight="bold" />
</button>
</div>
</div>
);
})}
<div className="flex-1">
<input <input
value={query} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
@ -192,9 +168,31 @@ function WorkspaceSelector({
}, 500) }, 500)
} }
placeholder="Enter a workspace name" placeholder="Enter a workspace name"
className="w-[200px] bg-transparent p-1 px-2 appearance-none outline-none h-full w-full text-white" className="bg-transparent p-1 px-2 appearance-none outline-none h-full w-full text-white text-xs placeholder:`text-white/50`"
/> />
</div> </div>
<div className="flex flex-col items-center -ml-2">
<div className="w-full h-fit">
<div className="w-full relative z-1">
<div className="p-2 flex rounded-lg">
<div className="flex flex-wrap gap-2 w-full">
{selectedWorkspaces.map((workspace) => {
return (
<div
key={workspace.slug}
className="flex items-center justify-between rounded-full h-[20px] bg-white/10 px-2 py-1 text-xs font-medium text-white shadow-sm"
>
<span className="truncate mr-1">{workspace.name}</span>
<button
onClick={() => handleRemoveWorkspace(workspace)}
type="button"
className="hover:text-red-500 flex-shrink-0"
>
<X size={10} weight="bold" />
</button>
</div>
);
})}
</div> </div>
</div> </div>
</div> </div>
@ -211,6 +209,7 @@ function WorkspaceSelector({
)} )}
</div> </div>
</div> </div>
</div>
); );
} }
@ -221,7 +220,7 @@ function WorkspaceSuggestions({
}) { }) {
if (availableWorkspaces.length === 0) { if (availableWorkspaces.length === 0) {
return ( return (
<div className="w-full mt-[2px] bg-zinc-800 border border-white/40 top-[45px] h-40 rounded-lg p-2 text-sm"> <div className="w-full mt-[2px] bg-zinc-900 top-[45px] h-40 rounded-lg p-2 text-sm">
<p className="text-center text-white/40"> <p className="text-center text-white/40">
no workspaces available to select. no workspaces available to select.
</p> </p>
@ -239,7 +238,7 @@ function WorkspaceSuggestions({
: availableWorkspaces; : availableWorkspaces;
return ( return (
<div className="w-full mt-[2px] bg-zinc-800 border border-white/40 top-[45px] h-40 rounded-lg p-2 text-sm flex flex-col gap-y-1 justify-start overflow-y-scroll"> <div className="w-full mt-[2px] bg-zinc-900 top-[45px] h-40 rounded-lg p-2 text-sm flex flex-col gap-y-1 justify-start overflow-y-scroll">
{filteredWorkspace.map((workspace) => { {filteredWorkspace.map((workspace) => {
return ( return (
<button <button
@ -271,19 +270,19 @@ function DatasetSummary({ workspaces = [], feedback = null }) {
}, [workspaces, feedback]); }, [workspaces, feedback]);
return ( return (
<div className="bg-zinc-800 text-white/80 p-2 rounded-lg font-mono"> <div className="bg-zinc-900 text-white/80 p-4 rounded-lg text-sm">
<p>Training dataset size: {stats.count ?? "Unknown"}</p> <p>Training dataset size: {stats.count ?? "Unknown"}</p>
{stats.count < stats.recommendedMin ? ( {stats.count < stats.recommendedMin ? (
<div className="flex items-center gap-x-1 text-red-500 text-sm p-1 rounded-lg bg-red-500/20 w-fit my-2"> <div className="flex items-center gap-x-1 text-red-500 text-sm p-2 rounded-lg bg-red-500/20 w-fit my-2">
<Warning /> <Warning size={14} />
<p> <p>
Your dataset is below the recommended minimum of{" "} Your dataset is below the recommended minimum of{" "}
{stats.recommendedMin}! You may see no impact from a fine-tune. {stats.recommendedMin}! You may see no impact from a fine-tune.
</p> </p>
</div> </div>
) : ( ) : (
<div className="flex items-center gap-x-1 text-green-500 text-sm p-1 rounded-lg bg-green-500/20 w-fit my-2"> <div className="flex items-center gap-x-1 text-green-500 text-sm p-2 rounded-lg bg-green-500/20 w-fit my-2">
<CheckCircle /> <CheckCircle size={14} />
<p> <p>
Your dataset is large enough that you should see good results from a Your dataset is large enough that you should see good results from a
fine-tune. fine-tune.

View File

@ -1,3 +1,4 @@
import CTAButton from "@/components/lib/CTAButton";
import FineTuningSteps from ".."; import FineTuningSteps from "..";
export default function Fulfillment({ setSettings, setStep }) { export default function Fulfillment({ setSettings, setStep }) {
@ -10,34 +11,29 @@ export default function Fulfillment({ setSettings, setStep }) {
return ( return (
<div className="flex-[2] flex flex-col gap-y-[18px] mt-10"> <div className="flex-[2] flex flex-col gap-y-[18px] mt-10">
<div className="bg-[#303237] text-white rounded-xl flex-1 p-4"> <div className="bg-[#303237] text-white rounded-xl flex-1 p-6">
<div className="w-full flex flex-col gap-y-4"> <div className="w-full flex flex-col gap-y-3 max-w-[700px]">
<h2 className="text-xl text-white font-semibold"> <h2 className="text-base text-white font-semibold">
Fulfillment Policy Fulfillment Policy
</h2> </h2>
<p> <p className="text-white/80 text-sm">
Fulfillment of a fine-tune model is straight-forward. We do not host Fulfillment of a fine-tune model is straight-forward. We do not host
your model. We provide you a download link to run the model in a your model. We provide you a download link to run the model in a
standard format where ever you run local LLMs standard format where ever you run local LLMs
</p> </p>
<div className="flex flex-col gap-y-2 text-white/75 text-sm border p-2 border-white rounded-lg font-mono h-[60vh] overflow-y-scroll"> <div className="flex flex-col gap-y-2 text-white/80 text-xs font-semibold rounded-lg p-4 h-[60vh] overflow-y-auto bg-dark-text mt-2">
<h1 class="text-white/80 text-lg font-semibold"> <div className="text-xs text-white">
Fulfillment Terms <h1>Fulfillment Terms</h1>
</h1> <p>Last updated: July 15, 2024</p>
<p> </div>
<strong>Last updated: July 15, 2024</strong> <p className="text-white/80">
</p>
<p>
These fulfillment terms outline the agreement between Mintplex These fulfillment terms outline the agreement between Mintplex
Labs Inc. (Company, we, us, or our) and the customer Labs Inc. ("Company," "we," "us," or "our") and the customer
regarding the creation and delivery of fine-tuned models. regarding the creation and delivery of fine-tuned models.
</p> </p>
<h2 class="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">Delivery of Model</h2>
Delivery of Model <p className="text-white/80">
</h2>
<p>
Upon completion of a fine-tuning job, we will deliver a download Upon completion of a fine-tuning job, we will deliver a download
link to a .gguf model file suitable for LLM text inferencing. The link to a .gguf model file suitable for LLM text inferencing. The
customer acknowledges that this exchange is strictly transactional customer acknowledges that this exchange is strictly transactional
@ -45,17 +41,15 @@ export default function Fulfillment({ setSettings, setStep }) {
is considered concluded and will be ineligible for a refund. is considered concluded and will be ineligible for a refund.
</p> </p>
<h2 class="text-white/80 text-base font-semibold">Support</h2> <h2 className="text-white mt-4">Support</h2>
<p> <p className="text-white/80">
Please note that the delivery of the model does not include any Please note that the delivery of the model does not include any
dedicated support. Customers are encouraged to refer to available dedicated support. Customers are encouraged to refer to available
documentation and resources for guidance on using the model. documentation and resources for guidance on using the model.
</p> </p>
<h2 class="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">Requesting Download Links</h2>
Requesting Download Links <p className="text-white/80">
</h2>
<p>
Customers may request refreshed download links from Customers may request refreshed download links from
my.mintplexlabs.com as long as the model is retained in our cloud my.mintplexlabs.com as long as the model is retained in our cloud
storage. We will retain a model in our storage for a maximum of 3 storage. We will retain a model in our storage for a maximum of 3
@ -63,10 +57,8 @@ export default function Fulfillment({ setSettings, setStep }) {
links are valid for 24 hours. links are valid for 24 hours.
</p> </p>
<h2 class="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">Cancellation and Refunds</h2>
Cancellation and Refunds <p className="text-white/80">
</h2>
<p>
Mintplex Labs Inc. reserves the right to cancel any fine-tuning Mintplex Labs Inc. reserves the right to cancel any fine-tuning
job at our discretion. In the event of a cancellation, a refund job at our discretion. In the event of a cancellation, a refund
may be issued. Additionally, we reserve the right to deny a may be issued. Additionally, we reserve the right to deny a
@ -74,22 +66,22 @@ export default function Fulfillment({ setSettings, setStep }) {
cause or notice to the Customer. cause or notice to the Customer.
</p> </p>
<h2 class="text-white/80 text-base font-semibold">No Guarantees</h2> <h2 className="text-white mt-4">No Guarantees</h2>
<p> <p className="text-white/80">
Mintplex Labs Inc. makes <strong>NO GUARANTEES</strong> regarding Mintplex Labs Inc. makes <strong>NO GUARANTEES</strong> regarding
the resulting model's output, functionality, speed, or the resulting model's output, functionality, speed, or
compatibility with your tools, infrastructure and devices. Refund compatibility with your tools, infrastructure and devices. Refund
requests of this nature are not eligible for refunds. requests of this nature are not eligible for refunds.
</p> </p>
<p> <p className="text-white/80">
Models are delivered and accepted in "As-Is" condition. All Models are delivered and accepted in "As-Is" condition. All
delivered model and output files are deemed final and delivered model and output files are deemed final and
non-refundable for any reason after training is complete and a non-refundable for any reason after training is complete and a
model has been generated. model has been generated.
</p> </p>
<h2 class="text-white/80 text-base font-semibold">Payment Terms</h2> <h2 className="text-white mt-4">Payment Terms</h2>
<p> <p className="text-white/80">
All payments are required prior to the commencement of the All payments are required prior to the commencement of the
fine-tuning process. Customers are responsible for ensuring that fine-tuning process. Customers are responsible for ensuring that
valid payment information is provided. Checkout sessions not valid payment information is provided. Checkout sessions not
@ -97,32 +89,36 @@ export default function Fulfillment({ setSettings, setStep }) {
abandoned and will be deleted from our system. abandoned and will be deleted from our system.
</p> </p>
<h2 class="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">
Denial of Service for Payment Reasons Denial of Service for Payment Reasons
</h2> </h2>
<p> <p className="text-white/80">
Mintplex Labs Inc. reserves the right to deny service to any Mintplex Labs Inc. reserves the right to deny service to any
customer with an outstanding balance or invalid payment customer with an outstanding balance or invalid payment
information. If any discrepancies arise regarding payment or information. If any discrepancies arise regarding payment or
usage, we may suspend services until the matter is resolved. usage, we may suspend services until the matter is resolved.
</p> </p>
<h2 class="text-white/80 text-base font-semibold">Contact</h2> <h2 className="text-white mt-4">Contact</h2>
<p> <p className="text-white/80">
For any questions related to payment or fulfillment of services, For any questions related to payment or fulfillment of services,
please contact us at{" "} please contact us at{" "}
<a href="mailto:team@mintplexlabs.com">team@mintplexlabs.com</a>. <a
href="mailto:team@mintplexlabs.com"
className="text-blue-400 hover:underline"
>
team@mintplexlabs.com
</a>
.
</p> </p>
</div> </div>
</div> <CTAButton
className="text-dark-text w-full mt-[18px] h-[34px] hover:bg-accent"
<button
onClick={handleAccept} onClick={handleAccept}
type="button"
className="mt-8 w-full py-2 text-center text-white hover:bg-primary-button border-none rounded-lg"
> >
Agree and continue &rarr; Agree and continue &rarr;
</button> </CTAButton>
</div>
</div> </div>
</div> </div>
); );

View File

@ -1,5 +1,6 @@
import { CheckCircle, XCircle } from "@phosphor-icons/react"; import { Check, X } from "@phosphor-icons/react";
import FineTuningSteps from ".."; import FineTuningSteps from "..";
import CTAButton from "@/components/lib/CTAButton";
export default function Introduction({ setSettings, setStep }) { export default function Introduction({ setSettings, setStep }) {
const handleAccept = () => { const handleAccept = () => {
@ -11,12 +12,12 @@ export default function Introduction({ setSettings, setStep }) {
return ( return (
<div className="flex-[2] flex flex-col gap-y-[18px] mt-10"> <div className="flex-[2] flex flex-col gap-y-[18px] mt-10">
<div className="bg-[#303237] text-white rounded-xl flex-1 p-4"> <div className="bg-[#303237] text-white rounded-xl flex-1 p-6">
<div className="w-full flex flex-col gap-y-4"> <div className="w-full flex flex-col gap-y-2 max-w-[700px]">
<h2 className="text-xl text-white font-semibold"> <h2 className="text-base text-white font-semibold">
What is a "Fine-Tuned" model? What is a "Fine-Tuned" model?
</h2> </h2>
<div className="flex flex-col gap-y-2 text-white/80"> <div className="flex flex-col gap-y-[25px] text-white/80 text-sm">
<p> <p>
Fine-tuned models are basically "customized" Fine-tuned models are basically "customized"
Language-Learning-Models (LLMs). These can be based on popular Language-Learning-Models (LLMs). These can be based on popular
@ -36,8 +37,8 @@ export default function Introduction({ setSettings, setStep }) {
</p> </p>
</div> </div>
<div className="flex flex-col gap-y-2 text-white/80"> <div className="flex flex-col gap-y-2 text-white/80 text-sm mt-4">
<h3 className="text-lg text-white font-semibold"> <h3 className="text-base text-white font-semibold">
When should I get a fine-tuned model? When should I get a fine-tuned model?
</h3> </h3>
<p> <p>
@ -45,65 +46,65 @@ export default function Introduction({ setSettings, setStep }) {
following following
</p> </p>
<ul className="flex flex-col gap-y-1"> <ul className="flex flex-col gap-y-1">
<li className="flex items-center gap-x-1"> <li className="flex items-center gap-x-2">
<CheckCircle className="text-green-300" /> Setting the style, <Check className="text-white" size={12} weight="bold" /> Setting
tone, format, or other qualitative aspects without prompting the style, tone, format, or other qualitative aspects without
prompting
</li> </li>
<li className="flex items-center gap-x-1"> <li className="flex items-center gap-x-2">
<CheckCircle className="text-green-300" /> Improving reliability <Check className="text-white" size={12} weight="bold" />{" "}
at producing a desired output Improving reliability at producing a desired output
</li> </li>
<li className="flex items-center gap-x-1"> <li className="flex items-center gap-x-2">
<CheckCircle className="text-green-300" /> Correcting failures <Check className="text-white" size={12} weight="bold" />{" "}
to follow complex prompts, citations, or lack of background Correcting failures to follow complex prompts, citations, or
knowledge lack of background knowledge
</li> </li>
<li className="flex items-center gap-x-1"> <li className="flex items-center gap-x-2">
<CheckCircle className="text-green-300" /> You want to run this <Check className="text-white" size={12} weight="bold" /> You
model privately or offline want to run this model privately or offline
</li> </li>
</ul> </ul>
</div> </div>
<div className="flex flex-col gap-y-2 text-white/80"> <div className="flex flex-col gap-y-2 text-white/80 text-sm mt-4">
<h3 className="text-lg text-white font-semibold"> <h3 className="text-base text-white font-semibold">
What are fine-tunes bad for? What are fine-tunes bad for?
</h3> </h3>
<p> <p>
Fine-tuned models powerful, but they are not the "silver bullet" Fine-tuned models are powerful, but they are not the "silver
to any issues you have with RAG currently. Some notable bullet" to any issues you have with RAG currently. Some notable
limitations are limitations are
</p> </p>
<ul> <ul>
<li className="flex items-center gap-x-1"> <li className="flex items-center gap-x-1">
<XCircle className="text-red-300" /> You need perfect recall of <X className="text-white" size={12} weight="bold" /> You need
some piece of literature or reference document perfect recall of some piece of literature or reference document
</li> </li>
<li className="flex items-center gap-x-1"> <li className="flex items-center gap-x-1">
<XCircle className="text-red-300" /> You want your model to have <X className="text-white" size={12} weight="bold" /> You want
perfect memory or recollection your model to have perfect memory or recollection
</li> </li>
</ul> </ul>
</div> </div>
<div className="flex flex-col gap-y-2 text-white/80"> <div className="flex flex-col gap-y-2 text-white/80 text-sm">
<p> <p>
In summary, if you are getting good results with RAG currently, In summary, if you are getting good results with RAG currently,
creating a fine-tune can squeeze <b>even more performance</b> out creating a fine-tune can squeeze <b>even more performance</b> out
of a model. Fine-Tunes are are for improving response quality and of a model. Fine-Tunes are for improving response quality and
general responses, but they are <b>not for knowledge recall</b> - general responses, but they are <b>not for knowledge recall</b> -
that is what RAG is for! Together, it is a powerful combination. that is what RAG is for! Together, it is a powerful combination.
</p> </p>
</div> </div>
</div> <CTAButton
className="text-dark-text w-full mt-[18px] h-[34px] hover:bg-accent"
<button
onClick={handleAccept} onClick={handleAccept}
type="button" text="Create fine-tune model &rarr;"
className="mt-8 w-full py-2 text-center text-white hover:bg-primary-button border-none rounded-lg"
> >
Start a fine-tune &rarr; Create a fine-tune model &rarr;
</button> </CTAButton>
</div>
</div> </div>
</div> </div>
); );

View File

@ -2,9 +2,11 @@ import FineTuning from "@/models/experimental/fineTuning";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import FineTuningSteps from ".."; import FineTuningSteps from "..";
import { CircleNotch } from "@phosphor-icons/react"; import { CircleNotch } from "@phosphor-icons/react";
import CTAButton from "@/components/lib/CTAButton";
export default function OrderDetails({ setSettings, setStep }) { export default function OrderDetails({ setSettings, setStep }) {
const [info, setInfo] = useState({}); const [info, setInfo] = useState({});
useEffect(() => { useEffect(() => {
FineTuning.info() FineTuning.info()
.then((res) => { .then((res) => {
@ -32,33 +34,30 @@ export default function OrderDetails({ setSettings, setStep }) {
return ( return (
<div className="flex-[2] flex flex-col gap-y-[18px] mt-10"> <div className="flex-[2] flex flex-col gap-y-[18px] mt-10">
<div className="bg-[#303237] text-white rounded-xl flex-1 p-4"> <div className="bg-[#303237] text-white rounded-xl flex-1 p-6">
<form onSubmit={handleSubmit}> <div className="w-full flex flex-col gap-y-3 max-w-[700px]">
<div className="w-full flex flex-col gap-y-4"> <h2 className="text-base text-white font-semibold">
<h2 className="text-xl text-white font-semibold">
Time to create your fine tune! Time to create your fine tune!
</h2> </h2>
<p> <p className="text-white/80 text-sm">
Creating a model is quite simple. Currently we have a limited base Creating a model is quite simple. Currently we have a limited base
model selection, however in the future we plan to expand support model selection, however in the future we plan to expand support to
to many more foundational models. many more foundational models.
</p> </p>
<form onSubmit={handleSubmit} className="flex flex-col gap-y-6 mt-4">
<div className="flex flex-col pr-10"> <div className="flex flex-col">
<div className="flex flex-col gap-y-1 mb-4"> <label className="text-white text-sm font-semibold block mb-3">
<label className="text-white text-sm font-bold">
Account e-mail Account e-mail
</label> </label>
<p className="text-xs font-normal text-white/80"> <p className="text-xs font-normal text-white/80 mb-2">
This e-mail is where you will receive all order information This e-mail is where you will receive all order information and
and updates. This e-mail <b>must be accurate</b> or else we updates. This e-mail <b>must be accurate</b> or else we won't be
won't be able to contact you with your fine-tuned model! able to contact you with your fine-tuned model!
</p> </p>
</div>
<input <input
type="email" type="email"
name="email" name="email"
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5" className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full max-w-[200px] p-2.5"
placeholder="jdoe@example.com" placeholder="jdoe@example.com"
required={true} required={true}
autoComplete="off" autoComplete="off"
@ -66,33 +65,29 @@ export default function OrderDetails({ setSettings, setStep }) {
/> />
</div> </div>
<div className="flex flex-col pr-10"> <div className="flex flex-col">
<div className="flex flex-col gap-y-1 mb-4"> <label className="text-white text-sm font-semibold block mb-3">
<label className="text-white text-sm font-bold">
Preferred Base Model Preferred Base Model
</label> </label>
<p className="text-xs font-normal text-white/80"> <p className="text-xs font-normal text-white/80 mb-2">
This is the foundational model your fine-tune will be based This is the foundational model your fine-tune will be based on.
on. We recommend Llama 3 8B. We recommend Llama 3 8B.
</p> </p>
</div>
{info.hasOwnProperty("availableBaseModels") ? ( {info.hasOwnProperty("availableBaseModels") ? (
<select <select
name="baseModel" name="baseModel"
required={true} required={true}
className="border-none bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-fit p-2.5" className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full max-w-[200px] p-2.5"
> >
<option disabled="true" selected="true" value=""> <option disabled value="">
-- select a base model -- -- select a base model --
</option> </option>
<optgroup label="Available base models"> <optgroup label="Available base models">
{(info?.availableBaseModels || []).map((model) => { {(info?.availableBaseModels || []).map((model) => (
return (
<option key={model} value={model}> <option key={model} value={model}>
{model} {model}
</option> </option>
); ))}
})}
</optgroup> </optgroup>
</select> </select>
) : ( ) : (
@ -103,36 +98,35 @@ export default function OrderDetails({ setSettings, setStep }) {
)} )}
</div> </div>
<div className="flex flex-col pr-10"> <div className="flex flex-col">
<div className="flex flex-col gap-y-1 mb-4"> <label className="text-white text-sm font-semibold block mb-3">
<label className="text-white text-sm font-bold">
Model name Model name
</label> </label>
<p className="text-xs font-normal text-white/80"> <p className="text-xs font-normal text-white/80 mb-2">
What would you like to call your model? This has no impact on What would you like to call your model? This has no impact on
its output or training and is only used for how we communicate its output or training and is only used for how we communicate
with you about the model. with you about the model.
</p> </p>
</div>
<input <input
type="text" type="text"
name="modelName" name="modelName"
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5" className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full max-w-[200px] p-2.5"
placeholder="My really cool model!" placeholder="My really cool model!"
required={true} required={true}
autoComplete="off" autoComplete="off"
spellCheck={false} spellCheck={false}
/> />
</div> </div>
</div>
<button <CTAButton
type="submit" type="submit"
className="mt-8 w-full py-2 text-center text-white hover:bg-primary-button border-none rounded-lg" className="text-dark-text w-full mt-[18px] h-[34px] hover:bg-accent"
> >
Proceed to data selection &rarr; Proceed to data selection &rarr;
</button> </CTAButton>
</form> </form>
</div> </div>
</div> </div>
</div>
); );
} }

View File

@ -1,13 +1,15 @@
import CTAButton from "@/components/lib/CTAButton";
import paths from "@/utils/paths";
export default function OrderPlaced({ settings }) { export default function OrderPlaced({ settings }) {
return ( return (
<div className="flex-[2] flex flex-col gap-y-[18px] mt-10"> <div className="flex-[2] flex flex-col gap-y-[18px] mt-10">
<div className="bg-[#303237] text-white rounded-xl flex-1 p-4"> <div className="bg-[#303237] text-white rounded-xl flex-1 p-6">
<div className="w-full flex flex-col gap-y-4"> <div className="w-full flex flex-col gap-y-2 max-w-[700px]">
<h2 className="text-xl text-white font-semibold"> <h2 className="text-base text-white font-semibold">
Your order is placed! Your order is placed!
</h2> </h2>
<div className="flex flex-col gap-y-[25px] text-white/80 text-xs">
<div className="">
<p> <p>
Your fine-tune will begin once payment is complete. If the payment Your fine-tune will begin once payment is complete. If the payment
window did not automatically open - your checkout link is below. window did not automatically open - your checkout link is below.
@ -15,54 +17,66 @@ export default function OrderPlaced({ settings }) {
<a <a
href={settings.checkoutUrl} href={settings.checkoutUrl}
target="_blank" target="_blank"
className="text-xs font-mono text-white/60 underline" rel="noreferrer"
className="text-sky-400 hover:underline hover:cursor-pointer"
> >
{new URL(settings.checkoutUrl).origin} {new URL(settings.checkoutUrl).origin}
</a> </a>
<p className="text-xs font-mono text-white/80"> <p className="text-xs text-white/80">
Your fine-tune does not begin until this payment is completed. Your fine-tune does not begin until this payment is completed.
</p> </p>
</div>
<div className=""> <div className="flex flex-col gap-y-2">
<p className="font-mono text-white/80 text-sm"> <p className="text-white/80 font-medium">
Reference: {settings.jobId} Reference: <span className="font-normal">{settings.jobId}</span>
</p> </p>
<p className="text-xs font-mono text-white/80"> <p className="text-xs text-white/80">
This reference id is how we will communicate with you about your This reference id is how we will communicate with you about your
fine-tune training status. <b>Save this reference id.</b> fine-tune training status. <b>Save this reference id.</b>
</p> </p>
</div> </div>
<div className=""> <div className="flex flex-col gap-y-2">
<p className="font-mono text-white/80 text-sm"> <p className="text-white/80 font-medium">
Contact: {settings.email} Contact: <span className="font-normal">{settings.email}</span>
</p> </p>
<p className="text-xs font-mono text-white/80"> <p className="text-xs text-white/80">
Check the email above for order confirmation, status updates, and Check the email above for order confirmation, status updates,
more. Mintplex Labs will only contact you about your order via and more. Mintplex Labs will only contact you about your order
email. via email.
</p> </p>
</div> </div>
<div className="font-mono text-white/80 text-sm flex items-center gap-x-2"> <div className="flex flex-col items-left gap-x-4 text-xs">
<a <a
href="https://docs.useanything.com/fine-tuning/overview" href="https://docs.useanything.com/fine-tuning/overview"
target="_blank" target="_blank"
className="underline" rel="noreferrer"
className="text-sky-400 hover:underline hover:cursor-pointer"
> >
Documentation Documentation
</a> </a>
<a href="mailto:team@mintplexlabs.com" className="underline"> <a
href="mailto:team@mintplexlabs.com"
className="text-sky-400 hover:underline hover:cursor-pointer"
>
Contact support Contact support
</a> </a>
</div> </div>
<p className="text-xs font-mono text-white/80"> <p className="text-xs text-white/80">
You can close this window or navigate away once you see the You can close this window or navigate away once you see the
confirmation email in your inbox. confirmation email in your inbox.
</p> </p>
</div> </div>
<CTAButton
className="text-dark-text w-full mt-[18px] h-[34px] hover:bg-accent"
onClick={() => (window.location.href = paths.home())}
>
Finish
</CTAButton>
</div>
</div> </div>
</div> </div>
); );

View File

@ -1,3 +1,4 @@
import CTAButton from "@/components/lib/CTAButton";
import FineTuningSteps from ".."; import FineTuningSteps from "..";
export default function PrivacyHandling({ setSettings, setStep }) { export default function PrivacyHandling({ setSettings, setStep }) {
@ -10,12 +11,12 @@ export default function PrivacyHandling({ setSettings, setStep }) {
return ( return (
<div className="flex-[2] flex flex-col gap-y-[18px] mt-10"> <div className="flex-[2] flex flex-col gap-y-[18px] mt-10">
<div className="bg-[#303237] text-white rounded-xl flex-1 p-4"> <div className="bg-[#303237] text-white rounded-xl flex-1 p-6">
<div className="w-full flex flex-col gap-y-4"> <div className="w-full flex flex-col gap-y-3 max-w-[700px]">
<h2 className="text-xl text-white font-semibold"> <h2 className="text-base text-white font-semibold">
Data Handling Policy & Privacy Data Handling Policy & Privacy
</h2> </h2>
<p> <p className="text-white/80 text-sm">
Please accept the terms and conditions to continue with creation and Please accept the terms and conditions to continue with creation and
ordering of a fine-tune model. We take the handling of your data ordering of a fine-tune model. We take the handling of your data
very seriously and will only use your uploaded data for training the very seriously and will only use your uploaded data for training the
@ -23,18 +24,14 @@ export default function PrivacyHandling({ setSettings, setStep }) {
completed, or canceled your information is automatically and completed, or canceled your information is automatically and
permanently deleted. permanently deleted.
</p> </p>
<div className="flex flex-col gap-y-2 text-white/75 text-sm border p-2 border-white rounded-lg font-mono h-[60vh] overflow-y-scroll"> <div className="flex flex-col gap-y-2 text-white/75 text-xs font-semibold rounded-lg p-4 h-[60vh] overflow-y-auto bg-dark-text mt-2">
<h1 class="text-white/80 text-lg font-semibold">Privacy Policy</h1> <div className="text-xs">
<h1 className="text-white/80">Privacy Policy</h1>
<p> <p>Mintplex Labs Inc.</p>
<strong>Mintplex Labs Inc.</strong>
</p>
<p>Effective Date: July 15, 2024</p> <p>Effective Date: July 15, 2024</p>
</div>
<h2 class="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">1. Introduction</h2>
1. Introduction <p className="text-white/80">
</h2>
<p>
Welcome to Mintplex Labs Inc. ("we", "our", "us"). We are Welcome to Mintplex Labs Inc. ("we", "our", "us"). We are
committed to protecting your privacy and ensuring the security of committed to protecting your privacy and ensuring the security of
your personal information. This Privacy Policy describes how we your personal information. This Privacy Policy describes how we
@ -42,45 +39,41 @@ export default function PrivacyHandling({ setSettings, setStep }) {
services. services.
</p> </p>
<h2 class="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">2. Information We Collect</h2>
2. Information We Collect <p className="text-white/80">
</h2>
<p>
When you place an order with us for tuning and large language When you place an order with us for tuning and large language
model (LLM) fulfillment, we collect certain personal information model (LLM) fulfillment, we collect certain personal information
from you, including but not limited to: from you, including but not limited to:
</p> </p>
<ul> <ul className="list-disc pl-5 text-white/80">
<li>Email address</li> <li>Email address</li>
<li>Payment information</li> <li>Payment information</li>
<li>Uploaded training data</li> <li>Uploaded training data</li>
</ul> </ul>
<h2 class="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">3. Use of Information</h2>
3. Use of Information <p className="text-white/80">
</h2> We use the information we collect for the following purposes:
<p>We use the information we collect for the following purposes:</p> </p>
<ul> <ul className="list-disc pl-5 text-white/80">
<li>To process and fulfill your order</li> <li>To process and fulfill your order</li>
<li>To communicate with you regarding your order</li> <li>To communicate with you regarding your order</li>
<li>To improve our services</li> <li>To improve our services</li>
</ul> </ul>
<h2 class="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">4. Data Retention and Deletion</h2>
4. Data Retention and Deletion <p className="text-white/80">
</h2>
<p>
Uploaded training data is only retained for the duration of the Uploaded training data is only retained for the duration of the
model training. Upon training completion, failure, or order model training. Upon training completion, failure, or order
cancellation, the user data is permanently deleted from our cancellation, the user data is permanently deleted from our
storage. storage.
</p> </p>
<p> <p className="text-white/80">
If you partially complete the order flow and do not finalize your If you partially complete the order flow and do not finalize your
order, any details and information associated with your order will order, any details and information associated with your order will
be deleted 1 hour from abandonment. be deleted 1 hour from abandonment.
</p> </p>
<p> <p className="text-white/80">
After you confirm receipt of your resulting model files, you can After you confirm receipt of your resulting model files, you can
request us to delete your model from our storage at any time. request us to delete your model from our storage at any time.
Additionally, we may proactively reach out to you to confirm that Additionally, we may proactively reach out to you to confirm that
@ -90,10 +83,8 @@ export default function PrivacyHandling({ setSettings, setStep }) {
storage. storage.
</p> </p>
<h2 class="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">5. Data Storage and Security</h2>
5. Data Storage and Security <p className="text-white/80">
</h2>
<p>
Our cloud storage provider is AWS. We have implement standard Our cloud storage provider is AWS. We have implement standard
encryption and protection policies to ensure the security of your encryption and protection policies to ensure the security of your
data. The storage solution has no public access, and all requests data. The storage solution has no public access, and all requests
@ -104,43 +95,41 @@ export default function PrivacyHandling({ setSettings, setStep }) {
e-mail you used during checkout. e-mail you used during checkout.
</p> </p>
<h2 class="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">6. Payment Processing</h2>
6. Payment Processing <p className="text-white/80">
</h2>
<p>
We use Stripe as our payment processor. Your email may be shared We use Stripe as our payment processor. Your email may be shared
with Stripe for customer service and payment management purposes. with Stripe for customer service and payment management purposes.
</p> </p>
<h2 class="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">7. Data Sharing</h2>
7. Data Sharing <p className="text-white/80">
</h2>
<p>
We do not sell or share your personal information with third We do not sell or share your personal information with third
parties except as necessary to provide our services, comply with parties except as necessary to provide our services, comply with
legal obligations, or protect our rights. legal obligations, or protect our rights.
</p> </p>
<h2 class="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">8. Your Rights</h2>
8. Your Rights <p className="text-white/80">
</h2>
<p>
You have the right to access, correct, or delete your personal You have the right to access, correct, or delete your personal
information. If you wish to exercise these rights, please contact information. If you wish to exercise these rights, please contact
us at{" "} us at{" "}
<a href="mailto:team@mintplexlabs.com">team@mintplexlabs.com</a>. <a
href="mailto:team@mintplexlabs.com"
className="text-blue-400 hover:underline"
>
team@mintplexlabs.com
</a>
.
</p> </p>
<h2 class="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">9. California Privacy Rights</h2>
9. California Privacy Rights <p className="text-white/80">
</h2>
<p>
Under the California Consumer Privacy Act as amended by the Under the California Consumer Privacy Act as amended by the
California Privacy Rights Act (the CCPA), California residents California Privacy Rights Act (the "CCPA"), California residents
have additional rights beyond what is set out in this privacy have additional rights beyond what is set out in this privacy
notice: notice:
</p> </p>
<ul> <ul className="list-disc pl-5 text-white/80">
<li> <li>
<strong>Right to Know:</strong> You have the right to request <strong>Right to Know:</strong> You have the right to request
information about the categories and specific pieces of personal information about the categories and specific pieces of personal
@ -170,63 +159,70 @@ export default function PrivacyHandling({ setSettings, setStep }) {
your CCPA rights. your CCPA rights.
</li> </li>
</ul> </ul>
<p> <p className="text-white/80">
<strong>Submitting a Request:</strong> <strong>Submitting a Request:</strong>
<br /> <br />
You may submit a request to know, delete, or correct your personal You may submit a request to know, delete, or correct your personal
information by contacting us at{" "} information by contacting us at{" "}
<a href="mailto:team@mintplexlabs.com">team@mintplexlabs.com</a>. <a
We will confirm your identity before processing your request and href="mailto:team@mintplexlabs.com"
className="text-blue-400 hover:underline"
>
team@mintplexlabs.com
</a>
. We will confirm your identity before processing your request and
respond within 45 days. If more time is needed, we will inform you respond within 45 days. If more time is needed, we will inform you
of the reason and extension period in writing. You may make a of the reason and extension period in writing. You may make a
request for your information twice every 12 months. If you are request for your information twice every 12 months. If you are
making an erasure request, please include details of the making an erasure request, please include details of the
information you would like erased. information you would like erased.
</p> </p>
<p> <p className="text-white/80">
Please note that if you request that we remove your information, Please note that if you request that we remove your information,
we may retain some of the information for specific reasons, such we may retain some of the information for specific reasons, such
as to resolve disputes, troubleshoot problems, and as required by as to resolve disputes, troubleshoot problems, and as required by
law. Some information may not be completely removed from our law. Some information may not be completely removed from our
databases due to technical constraints and regular backups. databases due to technical constraints and regular backups.
</p> </p>
<p> <p className="text-white/80">
We will not discriminate against you for exercising any of your We will not discriminate against you for exercising any of your
CCPA rights. CCPA rights.
</p> </p>
<h2 class="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">10. Contact Us</h2>
10. Contact Us <p className="text-white/80">
</h2>
<p>
If you have any questions or concerns about this Privacy Policy, If you have any questions or concerns about this Privacy Policy,
please contact us at{" "} please contact us at{" "}
<a href="mailto:team@mintplexlabs.com">team@mintplexlabs.com</a>. <a
href="mailto:team@mintplexlabs.com"
className="text-blue-400 hover:underline"
>
team@mintplexlabs.com
</a>
.
</p> </p>
<h2 class="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">
11. Changes to This Privacy Policy 11. Changes to This Privacy Policy
</h2> </h2>
<p> <p className="text-white/80">
We may update this Privacy Policy from time to time. We will We may update this Privacy Policy from time to time. We will
notify you of any changes by posting the new Privacy Policy on our notify you of any changes by posting the new Privacy Policy on our
website. You are advised to review this Privacy Policy website. You are advised to review this Privacy Policy
periodically for any changes. periodically for any changes.
</p> </p>
<p> <p className="text-white/80">
By using our services, you agree to the terms of this Privacy By using our services, you agree to the terms of this Privacy
Policy. Policy.
</p> </p>
</div> </div>
</div> <CTAButton
className="text-dark-text w-full mt-[18px] h-[34px] hover:bg-accent"
<button
onClick={handleAccept} onClick={handleAccept}
type="button"
className="mt-8 w-full py-2 text-center text-white hover:bg-primary-button border-none rounded-lg"
> >
Agree and continue &rarr; Agree and continue &rarr;
</button> </CTAButton>
</div>
</div> </div>
</div> </div>
); );

View File

@ -1,3 +1,4 @@
import CTAButton from "@/components/lib/CTAButton";
import FineTuningSteps from ".."; import FineTuningSteps from "..";
export default function TermsAndConditions({ setSettings, setStep }) { export default function TermsAndConditions({ setSettings, setStep }) {
@ -10,24 +11,21 @@ export default function TermsAndConditions({ setSettings, setStep }) {
return ( return (
<div className="flex-[2] flex flex-col gap-y-[18px] mt-10"> <div className="flex-[2] flex flex-col gap-y-[18px] mt-10">
<div className="bg-[#303237] text-white rounded-xl flex-1 p-4"> <div className="bg-[#303237] text-white rounded-xl flex-1 p-6">
<div className="w-full flex flex-col gap-y-4"> <div className="w-full flex flex-col gap-y-3 max-w-[700px]">
<h2 className="text-xl text-white font-semibold"> <h2 className="text-base text-white font-semibold">
Terms and Conditions Terms and Conditions
</h2> </h2>
<p> <p className="text-white/80 text-sm">
Please accept the terms and conditions to continue with creation and Please accept the terms and conditions to continue with creation and
ordering of a fine-tune model. ordering of a fine-tune model.
</p> </p>
<div className="flex flex-col gap-y-2 text-white/75 text-sm border p-2 border-white rounded-lg font-mono h-[60vh] overflow-y-scroll"> <div className="flex flex-col gap-y-2 text-white/80 text-xs font-semibold rounded-lg p-4 h-[60vh] overflow-y-auto bg-dark-text mt-2">
<h1 className="text-white/80 text-lg font-semibold"> <div className="text-xs text-white">
Mintplex Labs Inc. Fine-Tuning Terms of Service <h1>Mintplex Labs Inc. Fine-Tuning Terms of Service</h1>
</h1> <p>Last Updated: July 15, 2024</p>
<p> </div>
<strong>Last Updated:</strong> July 15, 2024 <p className="text-white/80">
</p>
<p>
This Agreement is between Mintplex Labs Inc. ("Company") and the This Agreement is between Mintplex Labs Inc. ("Company") and the
customer ("Customer") accessing or using the services provided by customer ("Customer") accessing or using the services provided by
the Company. By signing up, accessing, or using the services, the Company. By signing up, accessing, or using the services,
@ -35,20 +33,16 @@ export default function TermsAndConditions({ setSettings, setStep }) {
be bound by the terms and conditions outlined below. be bound by the terms and conditions outlined below.
</p> </p>
<h2 className="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">1. Services Provided</h2>
1. Services Provided <p className="text-white/80">
</h2>
<p>
Mintplex Labs Inc. provides model fine-tuning services for Mintplex Labs Inc. provides model fine-tuning services for
customers. The deliverable for these services is a download link customers. The deliverable for these services is a download link
to the output ".GGUF" file that can be used by the Customer for to the output ".GGUF" file that can be used by the Customer for
Large-Language text inferencing. Large-Language text inferencing.
</p> </p>
<h2 className="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">2. Payment Terms</h2>
2. Payment Terms <ul className="list-disc pl-5 text-white/80">
</h2>
<ul>
<li> <li>
<strong>One-Time Payment:</strong> A one-time payment is <strong>One-Time Payment:</strong> A one-time payment is
required before the execution of the training. required before the execution of the training.
@ -64,10 +58,8 @@ export default function TermsAndConditions({ setSettings, setStep }) {
</li> </li>
</ul> </ul>
<h2 className="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">3. Order Form</h2>
3. Order Form <ul className="list-disc pl-5 text-white/80">
</h2>
<ul>
<li> <li>
<strong>Service:</strong> Model fine-tuning <strong>Service:</strong> Model fine-tuning
</li> </li>
@ -79,88 +71,81 @@ export default function TermsAndConditions({ setSettings, setStep }) {
</li> </li>
</ul> </ul>
<h2 className="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">4. Customer Responsibilities</h2>
4. Customer Responsibilities <p className="text-white/80">
</h2>
<p>
The Customer must provide all necessary data and information The Customer must provide all necessary data and information
required for model fine-tuning. required for model fine-tuning.
</p> </p>
<p> <p className="text-white/80">
The Customer must ensure timely payment as per the terms mentioned The Customer must ensure timely payment as per the terms mentioned
above. above.
</p> </p>
<p> <p className="text-white/80">
The Customer understands the data collected for tuning will be The Customer understands the data collected for tuning will be
stored to a private cloud storage location temporarily while stored to a private cloud storage location temporarily while
training is in progress. training is in progress.
</p> </p>
<p> <p className="text-white/80">
The Customer understands the data collected for tuning will be The Customer understands the data collected for tuning will be
fully deleted once the order is completed or canceled by the fully deleted once the order is completed or canceled by the
Company. Company.
</p> </p>
<p> <p className="text-white/80">
The Customer understands and has reviewed the Privacy Policy for The Customer understands and has reviewed the Privacy Policy for
Fine-Tuning by the Company. Fine-Tuning by the Company.
</p> </p>
<h2 className="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">5. Refund Policy</h2>
5. Refund Policy <p className="text-white/80">
</h2>
<p>
Refunds will be processed in the event of training failure or if Refunds will be processed in the event of training failure or if
the complete model file is not delivered to the Customer. Refunds the complete model file is not delivered to the Customer. Refunds
will be issued to the original payment method within 30 days of will be issued to the original payment method within 30 days of
the refund request. the refund request.
</p> </p>
<h2 className="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">6. Governing Law</h2>
6. Governing Law <p className="text-white/80">
</h2>
<p>
This Agreement shall be governed by and construed in accordance This Agreement shall be governed by and construed in accordance
with the laws of the State of California. with the laws of the State of California.
</p> </p>
<h2 className="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">7. Dispute Resolution</h2>
7. Dispute Resolution <p className="text-white/80">
</h2>
<p>
Any disputes arising out of or in connection with this Agreement Any disputes arising out of or in connection with this Agreement
shall be resolved in the state or federal courts located in shall be resolved in the state or federal courts located in
California. California.
</p> </p>
<h2 className="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">8. Notices</h2>
8. Notices <p className="text-white/80">
</h2>
<p>
All notices under this Agreement shall be in writing and shall be All notices under this Agreement shall be in writing and shall be
deemed given when delivered personally, sent by confirmed email, deemed given when delivered personally, sent by confirmed email,
or sent by certified or registered mail, return receipt requested, or sent by certified or registered mail, return receipt requested,
and addressed to the respective parties as follows: and addressed to the respective parties as follows:
</p> </p>
<p> <p className="text-white/80">
For Company:{" "} For Company:{" "}
<a href="mailto:team@mintplexlabs.com">team@mintplexlabs.com</a> <a
href="mailto:team@mintplexlabs.com"
className="text-blue-400 hover:underline"
>
team@mintplexlabs.com
</a>
</p>
<p className="text-white/80">
For Customer: The main email address on Customer's account
</p> </p>
<p>For Customer: The main email address on Customer's account</p>
<h2 className="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">9. Amendments</h2>
9. Amendments <p className="text-white/80">
</h2>
<p>
The Company reserves the right to amend these terms at any time by The Company reserves the right to amend these terms at any time by
providing notice to the Customer. The Customer's continued use of providing notice to the Customer. The Customer's continued use of
the services after such amendments will constitute acceptance of the services after such amendments will constitute acceptance of
the amended terms. the amended terms.
</p> </p>
<h2 className="text-white/80 text-base font-semibold"> <h2 className="text-white mt-4">10. Indemnity</h2>
10. Indemnity <p className="text-white/80">
</h2>
<p>
The Customer agrees to indemnify, defend, and hold harmless The Customer agrees to indemnify, defend, and hold harmless
Mintplex Labs Inc., its affiliates, and their respective officers, Mintplex Labs Inc., its affiliates, and their respective officers,
directors, employees, agents, and representatives from and against directors, employees, agents, and representatives from and against
@ -173,15 +158,13 @@ export default function TermsAndConditions({ setSettings, setStep }) {
person or entity. person or entity.
</p> </p>
</div> </div>
</div> <CTAButton
className="text-dark-text w-full mt-[18px] h-[34px] hover:bg-accent"
<button
onClick={handleAccept} onClick={handleAccept}
type="button"
className="mt-8 w-full py-2 text-center text-white hover:bg-primary-button border-none rounded-lg"
> >
Agree and continue &rarr; Agree and continue &rarr;
</button> </CTAButton>
</div>
</div> </div>
</div> </div>
); );

View File

@ -26,7 +26,7 @@ import OrderPlaced from "./OrderPlaced";
const FineTuningSteps = { const FineTuningSteps = {
intro: { intro: {
name: "Introduction to Fine-Tuning", name: "1. Introduction to Fine-Tuning",
next: () => "privacy", next: () => "privacy",
component: ({ settings, setSettings, setStep }) => ( component: ({ settings, setSettings, setStep }) => (
<Introduction <Introduction
@ -37,7 +37,7 @@ const FineTuningSteps = {
), ),
}, },
privacy: { privacy: {
name: "How your data is handled", name: "2. How your data is handled",
next: () => "tos", next: () => "tos",
component: ({ settings, setSettings, setStep }) => ( component: ({ settings, setSettings, setStep }) => (
<PrivacyPolicy <PrivacyPolicy
@ -48,7 +48,7 @@ const FineTuningSteps = {
), ),
}, },
tos: { tos: {
name: "Terms of service", name: "3. Terms of service",
next: () => "fulfillment", next: () => "fulfillment",
component: ({ settings, setSettings, setStep }) => ( component: ({ settings, setSettings, setStep }) => (
<TermsAndConditions <TermsAndConditions
@ -59,7 +59,7 @@ const FineTuningSteps = {
), ),
}, },
fulfillment: { fulfillment: {
name: "Fulfillment terms", name: "4. Fulfillment terms",
next: () => "order-details", next: () => "order-details",
component: ({ settings, setSettings, setStep }) => ( component: ({ settings, setSettings, setStep }) => (
<Fulfillment <Fulfillment
@ -70,7 +70,7 @@ const FineTuningSteps = {
), ),
}, },
"order-details": { "order-details": {
name: "Model details & information", name: "5. Model details & information",
next: () => "data-selection", next: () => "data-selection",
component: ({ settings, setSettings, setStep }) => ( component: ({ settings, setSettings, setStep }) => (
<OrderDetails <OrderDetails
@ -81,7 +81,7 @@ const FineTuningSteps = {
), ),
}, },
"data-selection": { "data-selection": {
name: "Data selection", name: "6. Data selection",
next: () => "confirmation", next: () => "confirmation",
component: ({ settings, setSettings, setStep }) => ( component: ({ settings, setSettings, setStep }) => (
<DataUpload <DataUpload
@ -92,7 +92,7 @@ const FineTuningSteps = {
), ),
}, },
confirmation: { confirmation: {
name: "Review and Submit", name: "7. Review and Submit",
next: () => "done", next: () => "done",
component: ({ settings, setSettings, setStep }) => ( component: ({ settings, setSettings, setStep }) => (
<Confirmation <Confirmation
@ -103,7 +103,7 @@ const FineTuningSteps = {
), ),
}, },
done: { done: {
name: "Order placed", name: "8. Order placed",
next: () => "done", next: () => "done",
component: ({ settings }) => <OrderPlaced settings={settings} />, component: ({ settings }) => <OrderPlaced settings={settings} />,
}, },
@ -133,7 +133,7 @@ export function FineTuningCreationLayout({ setStep, children }) {
return ( return (
<div <div
id="fine-tune-create-order-container" id="fine-tune-create-order-container"
className="w-screen h-screen overflow-y-auto bg-sidebar flex" className="w-screen h-screen overflow-hidden bg-sidebar flex md:mt-0 mt-6"
> >
<Sidebar /> <Sidebar />
<div <div

View File

@ -1,13 +1,13 @@
import React, { useState } from "react"; import React, { useState } from "react";
import FineTuningSteps, { FineTuningCreationLayout } from "./Steps"; import FineTuningSteps, { FineTuningCreationLayout } from "./Steps";
import { CheckCircle, Circle, Sparkle } from "@phosphor-icons/react"; import { Sparkle } from "@phosphor-icons/react";
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
function SideBarSelection({ setStep, currentStep }) { function SideBarSelection({ setStep, currentStep }) {
const currentIndex = Object.keys(FineTuningSteps).indexOf(currentStep); const currentIndex = Object.keys(FineTuningSteps).indexOf(currentStep);
return ( return (
<div <div
className={`bg-white/5 text-white rounded-xl ${ className={`bg-white/5 text-white rounded-xl py-1 px-4 ${
isMobile ? "w-full" : "min-w-[360px] w-fit" isMobile ? "w-full" : "min-w-[360px] w-fit"
}`} }`}
> >
@ -21,28 +21,32 @@ function SideBarSelection({ setStep, currentStep }) {
<div <div
key={stepKey} key={stepKey}
className={[ className={[
"py-3 px-4 flex items-center justify-between transition-all duration-300", "py-3 flex items-center justify-between transition-all duration-300",
isSelected ? "rounded-t-xl" : "", isSelected ? "rounded-t-xl" : "",
isLast ? "" : "border-b border-white/10", isLast ? "" : "border-b border-white/10",
].join(" ")} ].join(" ")}
> >
{isDone ? ( {isDone || isSelected ? (
<button <button
onClick={() => setStep(stepKey)} onClick={() => setStep(stepKey)}
className="border-none hover:underline text-white/40 text-sm font-light" className="border-none hover:underline text-sm font-medium"
> >
{props.name} {props.name}
</button> </button>
) : ( ) : (
<div className="text-sm font-light">{props.name}</div> <div className="text-sm text-white/40 font-medium">
{props.name}
</div>
)} )}
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
{isDone ? ( {isDone ? (
<CheckCircle className={`text-green-300`} /> <div className="w-[14px] h-[14px] rounded-full border border-[#32D583] flex items-center justify-center">
<div className="w-[5.6px] h-[5.6px] rounded-full bg-[#6CE9A6]"></div>
</div>
) : ( ) : (
<Circle <div
className={`text-white-800 ${ className={`w-[14px] h-[14px] rounded-full border border-white ${
isSelected ? "animate-pulse" : "opacity-10" isSelected ? "animate-pulse" : "opacity-50"
}`} }`}
/> />
)} )}
@ -63,16 +67,20 @@ export default function FineTuningFlow() {
return ( return (
<FineTuningCreationLayout setStep={setStep}> <FineTuningCreationLayout setStep={setStep}>
{(settings, setSettings, setStep) => ( {(settings, setSettings, setStep) => (
<div className="flex-1 flex gap-x-6 p-4 mt-10"> <div className="flex-1 flex h-full">
<div className="flex flex-col gap-y-[18px]"> <div className="flex flex-col gap-y-[18px] p-4 mt-10 w-[360px] flex-shrink-0">
<div className="text-white flex items-center gap-x-2"> <div className="text-white flex items-center gap-x-2">
<Sparkle size={24} /> <Sparkle size={24} />
<p className="text-lg font-medium">Custom Fine-Tuned Model</p> <p className="text-lg font-medium">Custom Fine-Tuned Model</p>
</div> </div>
<SideBarSelection setStep={setStep} currentStep={step} /> <SideBarSelection setStep={setStep} currentStep={step} />
</div> </div>
<div className="flex-1 overflow-y-auto p-4 mt-10 pb-[74px] h-screen">
<div className=" ml-8">
{StepPage.component({ settings, setSettings, setStep })} {StepPage.component({ settings, setSettings, setStep })}
</div> </div>
</div>
</div>
)} )}
</FineTuningCreationLayout> </FineTuningCreationLayout>
); );

View File

@ -7,9 +7,11 @@ import CTAButton from "@/components/lib/CTAButton";
import OpenAiLogo from "@/media/llmprovider/openai.png"; import OpenAiLogo from "@/media/llmprovider/openai.png";
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png"; import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
import ElevenLabsIcon from "@/media/ttsproviders/elevenlabs.png"; import ElevenLabsIcon from "@/media/ttsproviders/elevenlabs.png";
import PiperTTSIcon from "@/media/ttsproviders/piper.png";
import BrowserNative from "@/components/TextToSpeech/BrowserNative"; import BrowserNative from "@/components/TextToSpeech/BrowserNative";
import OpenAiTTSOptions from "@/components/TextToSpeech/OpenAiOptions"; import OpenAiTTSOptions from "@/components/TextToSpeech/OpenAiOptions";
import ElevenLabsTTSOptions from "@/components/TextToSpeech/ElevenLabsOptions"; import ElevenLabsTTSOptions from "@/components/TextToSpeech/ElevenLabsOptions";
import PiperTTSOptions from "@/components/TextToSpeech/PiperTTSOptions";
const PROVIDERS = [ const PROVIDERS = [
{ {
@ -33,6 +35,13 @@ const PROVIDERS = [
options: (settings) => <ElevenLabsTTSOptions settings={settings} />, options: (settings) => <ElevenLabsTTSOptions settings={settings} />,
description: "Use ElevenLabs's text to speech voices and technology.", description: "Use ElevenLabs's text to speech voices and technology.",
}, },
{
name: "PiperTTS",
value: "piper_local",
logo: PiperTTSIcon,
options: (settings) => <PiperTTSOptions settings={settings} />,
description: "Run TTS models locally in your browser privately.",
},
]; ];
export default function TextToSpeechProvider({ settings }) { export default function TextToSpeechProvider({ settings }) {

View File

@ -1,5 +1,6 @@
import { encode as HTMLEncode } from "he"; import { encode as HTMLEncode } from "he";
import markdownIt from "markdown-it"; import markdownIt from "markdown-it";
import markdownItKatex from "markdown-it-katex";
import hljs from "highlight.js"; import hljs from "highlight.js";
import "highlight.js/styles/github-dark-dimmed.min.css"; import "highlight.js/styles/github-dark-dimmed.min.css";
import { v4 } from "uuid"; import { v4 } from "uuid";
@ -43,7 +44,7 @@ const markdown = markdownIt({
"</pre></div>" "</pre></div>"
); );
}, },
}); }).use(markdownItKatex);
export default function renderMarkdown(text = "") { export default function renderMarkdown(text = "") {
return markdown.render(text); return markdown.render(text);

View File

@ -0,0 +1,138 @@
import showToast from "../toast";
export default class PiperTTSClient {
static _instance;
voiceId = "en_US-hfc_female-medium";
worker = null;
constructor({ voiceId } = { voiceId: null }) {
if (PiperTTSClient._instance) {
this.voiceId = voiceId !== null ? voiceId : this.voiceId;
return PiperTTSClient._instance;
}
this.voiceId = voiceId !== null ? voiceId : this.voiceId;
PiperTTSClient._instance = this;
return this;
}
#getWorker() {
if (!this.worker)
this.worker = new Worker(new URL("./worker.js", import.meta.url), {
type: "module",
});
return this.worker;
}
/**
* Get all available voices for a client
* @returns {Promise<import("@mintplex-labs/piper-tts-web/dist/types").Voice[]}>}
*/
static async voices() {
const tmpWorker = new Worker(new URL("./worker.js", import.meta.url), {
type: "module",
});
tmpWorker.postMessage({ type: "voices" });
return new Promise((resolve, reject) => {
let timeout = null;
const handleMessage = (event) => {
if (event.data.type !== "voices") {
console.log("PiperTTSWorker debug event:", event.data);
return;
}
resolve(event.data.voices);
tmpWorker.removeEventListener("message", handleMessage);
timeout && clearTimeout(timeout);
tmpWorker.terminate();
};
timeout = setTimeout(() => {
reject("TTS Worker timed out.");
}, 30_000);
tmpWorker.addEventListener("message", handleMessage);
});
}
static async flush() {
const tmpWorker = new Worker(new URL("./worker.js", import.meta.url), {
type: "module",
});
tmpWorker.postMessage({ type: "flush" });
return new Promise((resolve, reject) => {
let timeout = null;
const handleMessage = (event) => {
if (event.data.type !== "flush") {
console.log("PiperTTSWorker debug event:", event.data);
return;
}
resolve(event.data.flushed);
tmpWorker.removeEventListener("message", handleMessage);
timeout && clearTimeout(timeout);
tmpWorker.terminate();
};
timeout = setTimeout(() => {
reject("TTS Worker timed out.");
}, 30_000);
tmpWorker.addEventListener("message", handleMessage);
});
}
/**
* Runs prediction via webworker so we can get an audio blob back.
* @returns {Promise<{blobURL: string|null, error: string|null}>} objectURL blob: type.
*/
async waitForBlobResponse() {
return new Promise((resolve) => {
let timeout = null;
const handleMessage = (event) => {
if (event.data.type === "error") {
this.worker.removeEventListener("message", handleMessage);
timeout && clearTimeout(timeout);
return resolve({ blobURL: null, error: event.data.message });
}
if (event.data.type !== "result") {
console.log("PiperTTSWorker debug event:", event.data);
return;
}
resolve({
blobURL: URL.createObjectURL(event.data.audio),
error: null,
});
this.worker.removeEventListener("message", handleMessage);
timeout && clearTimeout(timeout);
};
timeout = setTimeout(() => {
resolve({ blobURL: null, error: "PiperTTSWorker Worker timed out." });
}, 30_000);
this.worker.addEventListener("message", handleMessage);
});
}
async getAudioBlobForText(textToSpeak, voiceId = null) {
const primaryWorker = this.#getWorker();
primaryWorker.postMessage({
type: "init",
text: String(textToSpeak),
voiceId: voiceId ?? this.voiceId,
// Don't reference WASM because in the docker image
// the user will be connected to internet (mostly)
// and it bloats the app size on the frontend or app significantly
// and running the docker image fully offline is not an intended use-case unlike the app.
});
const { blobURL, error } = await this.waitForBlobResponse();
if (!!error) {
showToast(
`Could not generate voice prediction. Error: ${error}`,
"error",
{ clear: true }
);
return;
}
return blobURL;
}
}

View File

@ -0,0 +1,94 @@
import * as TTS from "@mintplex-labs/piper-tts-web";
/** @type {import("@mintplexlabs/piper-web-tts").TtsSession | null} */
let PIPER_SESSION = null;
/**
* @typedef PredictionRequest
* @property {('init')} type
* @property {string} text - the text to inference on
* @property {import('@mintplexlabs/piper-web-tts').VoiceId} voiceId - the voiceID key to use.
* @property {string|null} baseUrl - the base URL to fetch WASMs from.
*/
/**
* @typedef PredictionRequestResponse
* @property {('result')} type
* @property {Blob} audio - the text to inference on
*/
/**
* @typedef VoicesRequest
* @property {('voices')} type
* @property {string|null} baseUrl - the base URL to fetch WASMs from.
*/
/**
* @typedef VoicesRequestResponse
* @property {('voices')} type
* @property {[import("@mintplex-labs/piper-tts-web/dist/types")['Voice']]} voices - available voices in array
*/
/**
* @typedef FlushRequest
* @property {('flush')} type
*/
/**
* @typedef FlushRequestResponse
* @property {('flush')} type
* @property {true} flushed
*/
/**
* Web worker for generating client-side PiperTTS predictions
* @param {MessageEvent<PredictionRequest | VoicesRequest | FlushRequest>} event - The event object containing the prediction request
* @returns {Promise<PredictionRequestResponse|VoicesRequestResponse|FlushRequestResponse>}
*/
async function main(event) {
if (event.data.type === "voices") {
const stored = await TTS.stored();
const voices = await TTS.voices();
voices.forEach((voice) => (voice.is_stored = stored.includes(voice.key)));
self.postMessage({ type: "voices", voices });
return;
}
if (event.data.type === "flush") {
await TTS.flush();
self.postMessage({ type: "flush", flushed: true });
return;
}
if (event.data?.type !== "init") return;
if (!PIPER_SESSION) {
PIPER_SESSION = new TTS.TtsSession({
voiceId: event.data.voiceId,
progress: (e) => self.postMessage(JSON.stringify(e)),
logger: (msg) => self.postMessage(msg),
...(!!event.data.baseUrl
? {
wasmPaths: {
onnxWasm: `${event.data.baseUrl}/piper/ort/`,
piperData: `${event.data.baseUrl}/piper/piper_phonemize.data`,
piperWasm: `${event.data.baseUrl}/piper/piper_phonemize.wasm`,
},
}
: {}),
});
}
if (event.data.voiceId && PIPER_SESSION.voiceId !== event.data.voiceId)
PIPER_SESSION.voiceId = event.data.voiceId;
PIPER_SESSION.predict(event.data.text)
.then((res) => {
if (res instanceof Blob) {
self.postMessage({ type: "result", audio: res });
return;
}
})
.catch((error) => {
self.postMessage({ type: "error", message: error.message, error }); // Will be an error.
});
}
self.addEventListener("message", main);

View File

@ -86,7 +86,8 @@ export default {
] ]
}, },
animation: { animation: {
sweep: "sweep 0.5s ease-in-out" sweep: "sweep 0.5s ease-in-out",
"pulse-glow": "pulse-glow 1.5s infinite"
}, },
keyframes: { keyframes: {
sweep: { sweep: {
@ -100,6 +101,26 @@ export default {
fadeOut: { fadeOut: {
"0%": { opacity: 1 }, "0%": { opacity: 1 },
"100%": { opacity: 0 } "100%": { opacity: 0 }
},
"pulse-glow": {
"0%": {
opacity: 1,
transform: "scale(1)",
boxShadow: "0 0 0 rgba(255, 255, 255, 0.0)",
backgroundColor: "rgba(255, 255, 255, 0.0)"
},
"50%": {
opacity: 1,
transform: "scale(1.1)",
boxShadow: "0 0 15px rgba(255, 255, 255, 0.2)",
backgroundColor: "rgba(255, 255, 255, 0.1)"
},
"100%": {
opacity: 1,
transform: "scale(1)",
boxShadow: "0 0 0 rgba(255, 255, 255, 0.0)",
backgroundColor: "rgba(255, 255, 255, 0.0)"
}
} }
} }
} }

View File

@ -9,6 +9,14 @@ dns.setDefaultResultOrder("verbatim")
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
assetsInclude: [
'./public/piper/ort-wasm-simd-threaded.wasm',
'./public/piper/piper_phonemize.wasm',
'./public/piper/piper_phonemize.data',
],
worker: {
format: 'es'
},
server: { server: {
port: 3000, port: 3000,
host: "localhost" host: "localhost"
@ -60,7 +68,7 @@ export default defineConfig({
}, },
external: [ external: [
// Reduces transformation time by 50% and we don't even use this variant, so we can ignore. // 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: { commonjsOptions: {
@ -68,6 +76,7 @@ export default defineConfig({
} }
}, },
optimizeDeps: { optimizeDeps: {
include: ["@mintplex-labs/piper-tts-web"],
esbuildOptions: { esbuildOptions: {
define: { define: {
global: "globalThis" global: "globalThis"

View File

@ -496,6 +496,11 @@
resolved "https://registry.yarnpkg.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz#9ceecc94b49fbaa15666e38ae8587f64acce007d" resolved "https://registry.yarnpkg.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz#9ceecc94b49fbaa15666e38ae8587f64acce007d"
integrity sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA== integrity sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==
"@mintplex-labs/piper-tts-web@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@mintplex-labs/piper-tts-web/-/piper-tts-web-1.0.4.tgz#016b196fa86dc8b616691dd381f3ca1939196444"
integrity sha512-Y24X+CJaGXoY5HFPSstHvJI6408OAtw3Pmq2OIYwpRpcwLLbgadWg8l1ODHNkgpB0Ps5fS9PAAQB60fHA3Bdag==
"@nodelib/fs.scandir@2.1.5": "@nodelib/fs.scandir@2.1.5":
version "2.1.5" version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@ -532,6 +537,59 @@
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31"
integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==
"@protobufjs/base64@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
"@protobufjs/codegen@^2.0.4":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb"
integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==
"@protobufjs/eventemitter@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70"
integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==
"@protobufjs/fetch@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45"
integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==
dependencies:
"@protobufjs/aspromise" "^1.1.1"
"@protobufjs/inquire" "^1.1.0"
"@protobufjs/float@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1"
integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==
"@protobufjs/inquire@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089"
integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==
"@protobufjs/path@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d"
integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==
"@protobufjs/pool@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54"
integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==
"@protobufjs/utf8@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
"@remix-run/router@1.18.0": "@remix-run/router@1.18.0":
version "1.18.0" version "1.18.0"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.18.0.tgz#20b033d1f542a100c1d57cfd18ecf442d1784732" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.18.0.tgz#20b033d1f542a100c1d57cfd18ecf442d1784732"
@ -652,6 +710,13 @@
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64"
integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==
"@types/node@>=13.7.0":
version "22.1.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.1.0.tgz#6d6adc648b5e03f0e83c78dc788c2b037d0ad94b"
integrity sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==
dependencies:
undici-types "~6.13.0"
"@types/prop-types@*": "@types/prop-types@*":
version "15.7.12" version "15.7.12"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6"
@ -1729,6 +1794,11 @@ flat-cache@^3.0.4:
keyv "^4.5.3" keyv "^4.5.3"
rimraf "^3.0.2" rimraf "^3.0.2"
flatbuffers@^1.12.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/flatbuffers/-/flatbuffers-1.12.0.tgz#72e87d1726cb1b216e839ef02658aa87dcef68aa"
integrity sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==
flatted@^3.2.9: flatted@^3.2.9:
version "3.3.1" version "3.3.1"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a"
@ -1898,6 +1968,11 @@ graphemer@^1.4.0:
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
guid-typescript@^1.0.9:
version "1.0.9"
resolved "https://registry.yarnpkg.com/guid-typescript/-/guid-typescript-1.0.9.tgz#e35f77003535b0297ea08548f5ace6adb1480ddc"
integrity sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==
has-bigints@^1.0.1, has-bigints@^1.0.2: has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
@ -2347,6 +2422,13 @@ json5@^2.2.3:
object.assign "^4.1.4" object.assign "^4.1.4"
object.values "^1.1.6" object.values "^1.1.6"
katex@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/katex/-/katex-0.6.0.tgz#12418e09121c05c92041b6b3b9fb6bab213cb6f3"
integrity sha512-rS4mY3SvHYg5LtQV6RBcK0if7ur6plyEukAOV+jGGPqFImuzu8fHL6M752iBmRGoUyF0bhZbAPoezehn7xYksA==
dependencies:
match-at "^0.1.0"
keyv@^4.5.3: keyv@^4.5.3:
version "4.5.4" version "4.5.4"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
@ -2406,6 +2488,11 @@ lodash@^4.17.21:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
long@^5.0.0, long@^5.2.3:
version "5.2.3"
resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1"
integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==
loose-envify@^1.1.0, loose-envify@^1.4.0: loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@ -2425,6 +2512,13 @@ lru-cache@^5.1.1:
dependencies: dependencies:
yallist "^3.0.2" yallist "^3.0.2"
markdown-it-katex@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/markdown-it-katex/-/markdown-it-katex-2.0.3.tgz#d7b86a1aea0b9d6496fab4e7919a18fdef589c39"
integrity sha512-nUkkMtRWeg7OpdflamflE/Ho/pWl64Lk9wNBKOmaj33XkQdumhXAIYhI0WO03GeiycPCsxbmX536V5NEXpC3Ng==
dependencies:
katex "^0.6.0"
markdown-it@^13.0.1: markdown-it@^13.0.1:
version "13.0.2" version "13.0.2"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.2.tgz#1bc22e23379a6952e5d56217fbed881e0c94d536" resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.2.tgz#1bc22e23379a6952e5d56217fbed881e0c94d536"
@ -2436,6 +2530,11 @@ markdown-it@^13.0.1:
mdurl "^1.0.1" mdurl "^1.0.1"
uc.micro "^1.0.5" uc.micro "^1.0.5"
match-at@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/match-at/-/match-at-0.1.1.tgz#25d040d291777704d5e6556bbb79230ec2de0540"
integrity sha512-h4Yd392z9mST+dzc+yjuybOGFNOZjmXIPKWjxBd1Bb23r4SmDOsk2NYCU2BMUBGbSpZqwVsZYNq26QS3xfaT3Q==
mdurl@^1.0.1: mdurl@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
@ -2592,6 +2691,23 @@ once@^1.3.0:
dependencies: dependencies:
wrappy "1" wrappy "1"
onnxruntime-common@1.18.0:
version "1.18.0"
resolved "https://registry.yarnpkg.com/onnxruntime-common/-/onnxruntime-common-1.18.0.tgz#b904dc6ff134e7f21a3eab702fac17538f59e116"
integrity sha512-lufrSzX6QdKrktAELG5x5VkBpapbCeS3dQwrXbN0eD9rHvU0yAWl7Ztju9FvgAKWvwd/teEKJNj3OwM6eTZh3Q==
onnxruntime-web@^1.18.0:
version "1.18.0"
resolved "https://registry.yarnpkg.com/onnxruntime-web/-/onnxruntime-web-1.18.0.tgz#cd46268d9472f89697da0a3282f13129f0acbfa0"
integrity sha512-o1UKj4ABIj1gmG7ae0RKJ3/GT+3yoF0RRpfDfeoe0huzRW4FDRLfbkDETmdFAvnJEXuYDE0YT+hhkia0352StQ==
dependencies:
flatbuffers "^1.12.0"
guid-typescript "^1.0.9"
long "^5.2.3"
onnxruntime-common "1.18.0"
platform "^1.3.6"
protobufjs "^7.2.4"
open@^8.4.0: open@^8.4.0:
version "8.4.2" version "8.4.2"
resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9"
@ -2694,6 +2810,11 @@ pirates@^4.0.1:
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==
platform@^1.3.6:
version "1.3.6"
resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7"
integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==
pluralize@^8.0.0: pluralize@^8.0.0:
version "8.0.0" version "8.0.0"
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
@ -2783,6 +2904,24 @@ prop-types@^15.6.2, prop-types@^15.8.1:
object-assign "^4.1.1" object-assign "^4.1.1"
react-is "^16.13.1" react-is "^16.13.1"
protobufjs@^7.2.4:
version "7.3.2"
resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.3.2.tgz#60f3b7624968868f6f739430cfbc8c9370e26df4"
integrity sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==
dependencies:
"@protobufjs/aspromise" "^1.1.2"
"@protobufjs/base64" "^1.1.2"
"@protobufjs/codegen" "^2.0.4"
"@protobufjs/eventemitter" "^1.1.0"
"@protobufjs/fetch" "^1.1.0"
"@protobufjs/float" "^1.0.2"
"@protobufjs/inquire" "^1.1.0"
"@protobufjs/path" "^1.1.2"
"@protobufjs/pool" "^1.1.0"
"@protobufjs/utf8" "^1.1.0"
"@types/node" ">=13.7.0"
long "^5.0.0"
punycode@^2.1.0: punycode@^2.1.0:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
@ -3593,6 +3732,11 @@ unbox-primitive@^1.0.2:
has-symbols "^1.0.3" has-symbols "^1.0.3"
which-boxed-primitive "^1.0.2" which-boxed-primitive "^1.0.2"
undici-types@~6.13.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.13.0.tgz#e3e79220ab8c81ed1496b5812471afd7cf075ea5"
integrity sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==
update-browserslist-db@^1.1.0: update-browserslist-db@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e"

View File

@ -0,0 +1,230 @@
const { EmbedConfig } = require("../../../models/embedConfig");
const { EmbedChats } = require("../../../models/embedChats");
const { validApiKey } = require("../../../utils/middleware/validApiKey");
function apiEmbedEndpoints(app) {
if (!app) return;
app.get("/v1/embed", [validApiKey], async (request, response) => {
/*
#swagger.tags = ['Embed']
#swagger.description = 'List all active embeds'
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
type: 'object',
example: {
embeds: [
{
"id": 1,
"uuid": "embed-uuid-1",
"enabled": true,
"chat_mode": "query",
"createdAt": "2023-04-01T12:00:00Z",
"workspace": {
"id": 1,
"name": "Workspace 1"
},
"chat_count": 10
},
{
"id": 2,
"uuid": "embed-uuid-2",
"enabled": false,
"chat_mode": "chat",
"createdAt": "2023-04-02T14:30:00Z",
"workspace": {
"id": 1,
"name": "Workspace 1"
},
"chat_count": 10
}
]
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
}
}
*/
try {
const embeds = await EmbedConfig.whereWithWorkspace();
const filteredEmbeds = embeds.map((embed) => ({
id: embed.id,
uuid: embed.uuid,
enabled: embed.enabled,
chat_mode: embed.chat_mode,
createdAt: embed.createdAt,
workspace: {
id: embed.workspace.id,
name: embed.workspace.name,
},
chat_count: embed._count.embed_chats,
}));
response.status(200).json({ embeds: filteredEmbeds });
} catch (e) {
console.error(e.message, e);
response.sendStatus(500).end();
}
});
app.get(
"/v1/embed/:embedUuid/chats",
[validApiKey],
async (request, response) => {
/*
#swagger.tags = ['Embed']
#swagger.description = 'Get all chats for a specific embed'
#swagger.parameters['embedUuid'] = {
in: 'path',
description: 'UUID of the embed',
required: true,
type: 'string'
}
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
type: 'object',
example: {
chats: [
{
"id": 1,
"session_id": "session-uuid-1",
"prompt": "Hello",
"response": "Hi there!",
"createdAt": "2023-04-01T12:00:00Z"
},
{
"id": 2,
"session_id": "session-uuid-2",
"prompt": "How are you?",
"response": "I'm doing well, thank you!",
"createdAt": "2023-04-02T14:30:00Z"
}
]
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
}
}
#swagger.responses[404] = {
description: "Embed not found",
}
*/
try {
const { embedUuid } = request.params;
const embed = await EmbedConfig.get({ uuid: String(embedUuid) });
if (!embed) {
return response.status(404).json({ error: "Embed not found" });
}
const chats = await EmbedChats.where({ embed_id: embed.id });
const formattedChats = chats.map((chat) => ({
id: chat.id,
session_id: chat.session_id,
prompt: chat.prompt,
response: chat.response,
createdAt: chat.createdAt,
}));
response.status(200).json({ chats: formattedChats });
} catch (e) {
console.error(e.message, e);
response.sendStatus(500).end();
}
}
);
app.get(
"/v1/embed/:embedUuid/chats/:sessionUuid",
[validApiKey],
async (request, response) => {
/*
#swagger.tags = ['Embed']
#swagger.description = 'Get chats for a specific embed and session'
#swagger.parameters['embedUuid'] = {
in: 'path',
description: 'UUID of the embed',
required: true,
type: 'string'
}
#swagger.parameters['sessionUuid'] = {
in: 'path',
description: 'UUID of the session',
required: true,
type: 'string'
}
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
type: 'object',
example: {
chats: [
{
"id": 1,
"prompt": "Hello",
"response": "Hi there!",
"createdAt": "2023-04-01T12:00:00Z"
}
]
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
}
}
#swagger.responses[404] = {
description: "Embed or session not found",
}
*/
try {
const { embedUuid, sessionUuid } = request.params;
const embed = await EmbedConfig.get({ uuid: String(embedUuid) });
if (!embed) {
return response.status(404).json({ error: "Embed not found" });
}
const chats = await EmbedChats.where({
embed_id: embed.id,
session_id: String(sessionUuid),
});
if (!chats || chats.length === 0) {
return response
.status(404)
.json({ error: "No chats found for this session" });
}
const formattedChats = chats.map((chat) => ({
id: chat.id,
prompt: chat.prompt,
response: chat.response,
createdAt: chat.createdAt,
}));
response.status(200).json({ chats: formattedChats });
} catch (e) {
console.error(e.message, e);
response.sendStatus(500).end();
}
}
);
}
module.exports = { apiEmbedEndpoints };

View File

@ -7,6 +7,7 @@ const { apiWorkspaceEndpoints } = require("./workspace");
const { apiWorkspaceThreadEndpoints } = require("./workspaceThread"); const { apiWorkspaceThreadEndpoints } = require("./workspaceThread");
const { apiUserManagementEndpoints } = require("./userManagement"); const { apiUserManagementEndpoints } = require("./userManagement");
const { apiOpenAICompatibleEndpoints } = require("./openai"); const { apiOpenAICompatibleEndpoints } = require("./openai");
const { apiEmbedEndpoints } = require("./embed");
// All endpoints must be documented and pass through the validApiKey Middleware. // All endpoints must be documented and pass through the validApiKey Middleware.
// How to JSDoc an endpoint // How to JSDoc an endpoint
@ -22,6 +23,7 @@ function developerEndpoints(app, router) {
apiWorkspaceThreadEndpoints(router); apiWorkspaceThreadEndpoints(router);
apiUserManagementEndpoints(router); apiUserManagementEndpoints(router);
apiOpenAICompatibleEndpoints(router); apiOpenAICompatibleEndpoints(router);
apiEmbedEndpoints(router);
} }
module.exports = { developerEndpoints }; module.exports = { developerEndpoints };

View File

@ -154,6 +154,7 @@ function apiOpenAICompatibleEndpoints(app) {
workspace.chatProvider ?? process.env.LLM_PROVIDER ?? "openai", workspace.chatProvider ?? process.env.LLM_PROVIDER ?? "openai",
Embedder: process.env.EMBEDDING_ENGINE || "inherit", Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "lancedb", VectorDbSelection: process.env.VECTOR_DB || "lancedb",
TTSSelection: process.env.TTS_PROVIDER || "native",
}); });
await EventLogs.logEvent("api_sent_chat", { await EventLogs.logEvent("api_sent_chat", {
workspaceName: workspace?.name, workspaceName: workspace?.name,
@ -180,6 +181,7 @@ function apiOpenAICompatibleEndpoints(app) {
LLMSelection: process.env.LLM_PROVIDER || "openai", LLMSelection: process.env.LLM_PROVIDER || "openai",
Embedder: process.env.EMBEDDING_ENGINE || "inherit", Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "lancedb", VectorDbSelection: process.env.VECTOR_DB || "lancedb",
TTSSelection: process.env.TTS_PROVIDER || "native",
}); });
await EventLogs.logEvent("api_sent_chat", { await EventLogs.logEvent("api_sent_chat", {
workspaceName: workspace?.name, workspaceName: workspace?.name,

View File

@ -73,6 +73,7 @@ function apiWorkspaceEndpoints(app) {
LLMSelection: process.env.LLM_PROVIDER || "openai", LLMSelection: process.env.LLM_PROVIDER || "openai",
Embedder: process.env.EMBEDDING_ENGINE || "inherit", Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "lancedb", VectorDbSelection: process.env.VECTOR_DB || "lancedb",
TTSSelection: process.env.TTS_PROVIDER || "native",
}); });
await EventLogs.logEvent("api_workspace_created", { await EventLogs.logEvent("api_workspace_created", {
workspaceName: workspace?.name || "Unknown Workspace", workspaceName: workspace?.name || "Unknown Workspace",
@ -622,6 +623,7 @@ function apiWorkspaceEndpoints(app) {
LLMSelection: process.env.LLM_PROVIDER || "openai", LLMSelection: process.env.LLM_PROVIDER || "openai",
Embedder: process.env.EMBEDDING_ENGINE || "inherit", Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "lancedb", VectorDbSelection: process.env.VECTOR_DB || "lancedb",
TTSSelection: process.env.TTS_PROVIDER || "native",
}); });
await EventLogs.logEvent("api_sent_chat", { await EventLogs.logEvent("api_sent_chat", {
workspaceName: workspace?.name, workspaceName: workspace?.name,
@ -745,6 +747,7 @@ function apiWorkspaceEndpoints(app) {
LLMSelection: process.env.LLM_PROVIDER || "openai", LLMSelection: process.env.LLM_PROVIDER || "openai",
Embedder: process.env.EMBEDDING_ENGINE || "inherit", Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "lancedb", VectorDbSelection: process.env.VECTOR_DB || "lancedb",
TTSSelection: process.env.TTS_PROVIDER || "native",
}); });
await EventLogs.logEvent("api_sent_chat", { await EventLogs.logEvent("api_sent_chat", {
workspaceName: workspace?.name, workspaceName: workspace?.name,

View File

@ -90,6 +90,7 @@ function apiWorkspaceThreadEndpoints(app) {
LLMSelection: process.env.LLM_PROVIDER || "openai", LLMSelection: process.env.LLM_PROVIDER || "openai",
Embedder: process.env.EMBEDDING_ENGINE || "inherit", Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "lancedb", VectorDbSelection: process.env.VECTOR_DB || "lancedb",
TTSSelection: process.env.TTS_PROVIDER || "native",
}); });
await EventLogs.logEvent("api_workspace_thread_created", { await EventLogs.logEvent("api_workspace_thread_created", {
workspaceName: workspace?.name || "Unknown Workspace", workspaceName: workspace?.name || "Unknown Workspace",
@ -416,6 +417,7 @@ function apiWorkspaceThreadEndpoints(app) {
LLMSelection: process.env.LLM_PROVIDER || "openai", LLMSelection: process.env.LLM_PROVIDER || "openai",
Embedder: process.env.EMBEDDING_ENGINE || "inherit", Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "lancedb", VectorDbSelection: process.env.VECTOR_DB || "lancedb",
TTSSelection: process.env.TTS_PROVIDER || "native",
}); });
await EventLogs.logEvent("api_sent_chat", { await EventLogs.logEvent("api_sent_chat", {
workspaceName: workspace?.name, workspaceName: workspace?.name,
@ -567,6 +569,7 @@ function apiWorkspaceThreadEndpoints(app) {
LLMSelection: process.env.LLM_PROVIDER || "openai", LLMSelection: process.env.LLM_PROVIDER || "openai",
Embedder: process.env.EMBEDDING_ENGINE || "inherit", Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "lancedb", VectorDbSelection: process.env.VECTOR_DB || "lancedb",
TTSSelection: process.env.TTS_PROVIDER || "native",
}); });
await EventLogs.logEvent("api_sent_chat", { await EventLogs.logEvent("api_sent_chat", {
workspaceName: workspace?.name, workspaceName: workspace?.name,

View File

@ -98,6 +98,7 @@ function chatEndpoints(app) {
Embedder: process.env.EMBEDDING_ENGINE || "inherit", Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "lancedb", VectorDbSelection: process.env.VECTOR_DB || "lancedb",
multiModal: Array.isArray(attachments) && attachments?.length !== 0, multiModal: Array.isArray(attachments) && attachments?.length !== 0,
TTSSelection: process.env.TTS_PROVIDER || "native",
}); });
await EventLogs.logEvent( await EventLogs.logEvent(
@ -226,6 +227,7 @@ function chatEndpoints(app) {
Embedder: process.env.EMBEDDING_ENGINE || "inherit", Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "lancedb", VectorDbSelection: process.env.VECTOR_DB || "lancedb",
multiModal: Array.isArray(attachments) && attachments?.length !== 0, multiModal: Array.isArray(attachments) && attachments?.length !== 0,
TTSSelection: process.env.TTS_PROVIDER || "native",
}); });
await EventLogs.logEvent( await EventLogs.logEvent(

View File

@ -40,6 +40,7 @@ function workspaceThreadEndpoints(app) {
LLMSelection: process.env.LLM_PROVIDER || "openai", LLMSelection: process.env.LLM_PROVIDER || "openai",
Embedder: process.env.EMBEDDING_ENGINE || "inherit", Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "lancedb", VectorDbSelection: process.env.VECTOR_DB || "lancedb",
TTSSelection: process.env.TTS_PROVIDER || "native",
}, },
user?.id user?.id
); );

View File

@ -55,6 +55,7 @@ function workspaceEndpoints(app) {
LLMSelection: process.env.LLM_PROVIDER || "openai", LLMSelection: process.env.LLM_PROVIDER || "openai",
Embedder: process.env.EMBEDDING_ENGINE || "inherit", Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "lancedb", VectorDbSelection: process.env.VECTOR_DB || "lancedb",
TTSSelection: process.env.TTS_PROVIDER || "native",
}, },
user?.id user?.id
); );

View File

@ -142,6 +142,7 @@ const Document = {
LLMSelection: process.env.LLM_PROVIDER || "openai", LLMSelection: process.env.LLM_PROVIDER || "openai",
Embedder: process.env.EMBEDDING_ENGINE || "inherit", Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "lancedb", VectorDbSelection: process.env.VECTOR_DB || "lancedb",
TTSSelection: process.env.TTS_PROVIDER || "native",
}); });
await EventLogs.logEvent( await EventLogs.logEvent(
"workspace_documents_added", "workspace_documents_added",
@ -185,6 +186,7 @@ const Document = {
LLMSelection: process.env.LLM_PROVIDER || "openai", LLMSelection: process.env.LLM_PROVIDER || "openai",
Embedder: process.env.EMBEDDING_ENGINE || "inherit", Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "lancedb", VectorDbSelection: process.env.VECTOR_DB || "lancedb",
TTSSelection: process.env.TTS_PROVIDER || "native",
}); });
await EventLogs.logEvent( await EventLogs.logEvent(
"workspace_documents_removed", "workspace_documents_removed",

View File

@ -214,6 +214,9 @@ const SystemSettings = {
// Eleven Labs TTS // Eleven Labs TTS
TTSElevenLabsKey: !!process.env.TTS_ELEVEN_LABS_KEY, TTSElevenLabsKey: !!process.env.TTS_ELEVEN_LABS_KEY,
TTSElevenLabsVoiceModel: process.env.TTS_ELEVEN_LABS_VOICE_MODEL, TTSElevenLabsVoiceModel: process.env.TTS_ELEVEN_LABS_VOICE_MODEL,
// Piper TTS
TTSPiperTTSVoiceModel:
process.env.TTS_PIPER_VOICE_MODEL ?? "en_US-hfc_female-medium",
// -------------------------------------------------------- // --------------------------------------------------------
// Agent Settings & Configs // Agent Settings & Configs

View File

@ -2,6 +2,7 @@ const prisma = require("../utils/prisma");
const { EventLogs } = require("./eventLogs"); const { EventLogs } = require("./eventLogs");
const User = { const User = {
usernameRegex: new RegExp(/^[a-z0-9_-]+$/),
writable: [ writable: [
// Used for generic updates so we can validate keys in request body // Used for generic updates so we can validate keys in request body
"username", "username",
@ -32,7 +33,6 @@ const User = {
return String(role); return String(role);
}, },
}, },
// validations for the above writable fields. // validations for the above writable fields.
castColumnValue: function (key, value) { castColumnValue: function (key, value) {
switch (key) { switch (key) {
@ -55,6 +55,12 @@ const User = {
} }
try { try {
// Do not allow new users to bypass validation
if (!this.usernameRegex.test(username))
throw new Error(
"Username must be only contain lowercase letters, numbers, underscores, and hyphens with no spaces"
);
const bcrypt = require("bcrypt"); const bcrypt = require("bcrypt");
const hashedPassword = bcrypt.hashSync(password, 10); const hashedPassword = bcrypt.hashSync(password, 10);
const user = await prisma.users.create({ const user = await prisma.users.create({
@ -70,7 +76,6 @@ const User = {
return { user: null, error: error.message }; return { user: null, error: error.message };
} }
}, },
// Log the changes to a user object, but omit sensitive fields // Log the changes to a user object, but omit sensitive fields
// that are not meant to be logged. // that are not meant to be logged.
loggedChanges: function (updates, prev = {}) { loggedChanges: function (updates, prev = {}) {
@ -93,7 +98,6 @@ const User = {
where: { id: parseInt(userId) }, where: { id: parseInt(userId) },
}); });
if (!currentUser) return { success: false, error: "User not found" }; if (!currentUser) return { success: false, error: "User not found" };
// Removes non-writable fields for generic updates // Removes non-writable fields for generic updates
// and force-casts to the proper type; // and force-casts to the proper type;
Object.entries(updates).forEach(([key, value]) => { Object.entries(updates).forEach(([key, value]) => {
@ -123,6 +127,17 @@ const User = {
updates.password = bcrypt.hashSync(updates.password, 10); updates.password = bcrypt.hashSync(updates.password, 10);
} }
if (
updates.hasOwnProperty("username") &&
currentUser.username !== updates.username &&
!this.usernameRegex.test(updates.username)
)
return {
success: false,
error:
"Username must be only contain lowercase letters, numbers, underscores, and hyphens with no spaces",
};
const user = await prisma.users.update({ const user = await prisma.users.update({
where: { id: parseInt(userId) }, where: { id: parseInt(userId) },
data: updates, data: updates,
@ -170,7 +185,6 @@ const User = {
return null; return null;
} }
}, },
// Returns user object with all fields // Returns user object with all fields
_get: async function (clause = {}) { _get: async function (clause = {}) {
try { try {

View File

@ -38,6 +38,7 @@ const endpointsFiles = [
"../endpoints/api/workspaceThread/index.js", "../endpoints/api/workspaceThread/index.js",
"../endpoints/api/userManagement/index.js", "../endpoints/api/userManagement/index.js",
"../endpoints/api/openai/index.js", "../endpoints/api/openai/index.js",
"../endpoints/api/embed/index.js",
]; ];
swaggerAutogen(outputFile, endpointsFiles, doc).then(({ data }) => { swaggerAutogen(outputFile, endpointsFiles, doc).then(({ data }) => {

View File

@ -3140,6 +3140,214 @@
} }
} }
} }
},
"/v1/embed": {
"get": {
"tags": [
"Embed"
],
"description": "List all active embeds",
"parameters": [],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"embeds": [
{
"id": 1,
"uuid": "embed-uuid-1",
"enabled": true,
"chat_mode": "query",
"createdAt": "2023-04-01T12:00:00Z",
"workspace": {
"id": 1,
"name": "Workspace 1"
},
"chat_count": 10
},
{
"id": 2,
"uuid": "embed-uuid-2",
"enabled": false,
"chat_mode": "chat",
"createdAt": "2023-04-02T14:30:00Z",
"workspace": {
"id": 1,
"name": "Workspace 1"
},
"chat_count": 10
}
]
}
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidAPIKey"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/InvalidAPIKey"
}
}
}
},
"500": {
"description": "Internal Server Error"
}
}
}
},
"/v1/embed/{embedUuid}/chats": {
"get": {
"tags": [
"Embed"
],
"description": "Get all chats for a specific embed",
"parameters": [
{
"name": "embedUuid",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "UUID of the embed"
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"chats": [
{
"id": 1,
"session_id": "session-uuid-1",
"prompt": "Hello",
"response": "Hi there!",
"createdAt": "2023-04-01T12:00:00Z"
},
{
"id": 2,
"session_id": "session-uuid-2",
"prompt": "How are you?",
"response": "I'm doing well, thank you!",
"createdAt": "2023-04-02T14:30:00Z"
}
]
}
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidAPIKey"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/InvalidAPIKey"
}
}
}
},
"404": {
"description": "Embed not found"
},
"500": {
"description": "Internal Server Error"
}
}
}
},
"/v1/embed/{embedUuid}/chats/{sessionUuid}": {
"get": {
"tags": [
"Embed"
],
"description": "Get chats for a specific embed and session",
"parameters": [
{
"name": "embedUuid",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "UUID of the embed"
},
{
"name": "sessionUuid",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "UUID of the session"
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"chats": [
{
"id": 1,
"prompt": "Hello",
"response": "Hi there!",
"createdAt": "2023-04-01T12:00:00Z"
}
]
}
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidAPIKey"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/InvalidAPIKey"
}
}
}
},
"404": {
"description": "Embed or session not found"
},
"500": {
"description": "Internal Server Error"
}
}
}
} }
}, },
"components": { "components": {

View File

@ -17,10 +17,12 @@ class GeminiLLM {
this.gemini = genAI.getGenerativeModel( this.gemini = genAI.getGenerativeModel(
{ model: this.model }, { model: this.model },
{ {
// Gemini-1.5-pro and Gemini-1.5-flash are only available on the v1beta API. // Gemini-1.5-pro-* and Gemini-1.5-flash are only available on the v1beta API.
apiVersion: apiVersion: [
this.model === "gemini-1.5-pro-latest" || "gemini-1.5-pro-latest",
this.model === "gemini-1.5-flash-latest" "gemini-1.5-flash-latest",
"gemini-1.5-pro-exp-0801",
].includes(this.model)
? "v1beta" ? "v1beta"
: "v1", : "v1",
} }

View File

@ -14,17 +14,6 @@ async function grepCommand(message, user = null) {
const userPresets = await SlashCommandPresets.getUserPresets(user?.id); const userPresets = await SlashCommandPresets.getUserPresets(user?.id);
const availableCommands = Object.keys(VALID_COMMANDS); const availableCommands = Object.keys(VALID_COMMANDS);
// Check if the message starts with any preset command
const foundPreset = userPresets.find((p) => message.startsWith(p.command));
if (!!foundPreset) {
// Replace the preset command with the corresponding prompt
const updatedMessage = message.replace(
foundPreset.command,
foundPreset.prompt
);
return updatedMessage;
}
// Check if the message starts with any built-in command // Check if the message starts with any built-in command
for (let i = 0; i < availableCommands.length; i++) { for (let i = 0; i < availableCommands.length; i++) {
const cmd = availableCommands[i]; const cmd = availableCommands[i];
@ -34,7 +23,15 @@ async function grepCommand(message, user = null) {
} }
} }
return message; // Replace all preset commands with their corresponding prompts
// Allows multiple commands in one message
let updatedMessage = message;
for (const preset of userPresets) {
const regex = new RegExp(preset.command, "g");
updatedMessage = updatedMessage.replace(regex, preset.prompt);
}
return updatedMessage;
} }
async function chatWithWorkspace( async function chatWithWorkspace(

View File

@ -477,6 +477,12 @@ const KEY_MAPPING = {
envKey: "TTS_ELEVEN_LABS_VOICE_MODEL", envKey: "TTS_ELEVEN_LABS_VOICE_MODEL",
checks: [], checks: [],
}, },
// PiperTTS Local
TTSPiperTTSVoiceModel: {
envKey: "TTS_PIPER_VOICE_MODEL",
checks: [],
},
}; };
function isNotEmpty(input = "") { function isNotEmpty(input = "") {
@ -536,7 +542,12 @@ function validOllamaLLMBasePath(input = "") {
} }
function supportedTTSProvider(input = "") { function supportedTTSProvider(input = "") {
const validSelection = ["native", "openai", "elevenlabs"].includes(input); const validSelection = [
"native",
"openai",
"elevenlabs",
"piper_local",
].includes(input);
return validSelection ? null : `${input} is not a valid TTS provider.`; return validSelection ? null : `${input} is not a valid TTS provider.`;
} }