anything-llm/frontend/src/pages/GeneralSettings/EmbedConfigs/NewEmbedModal/index.jsx

329 lines
12 KiB
JavaScript

import React, { useEffect, useState } from "react";
import { X } from "@phosphor-icons/react";
import Workspace from "@/models/workspace";
import { TagsInput } from "react-tag-input-component";
import Embed from "@/models/embed";
export function enforceSubmissionSchema(form) {
const data = {};
for (var [key, value] of form.entries()) {
if (!value || value === null) continue;
data[key] = value;
if (value === "on") data[key] = true;
}
// Always set value on nullable keys since empty or off will not send anything from form element.
if (!data.hasOwnProperty("allowlist_domains")) data.allowlist_domains = null;
if (!data.hasOwnProperty("allow_model_override"))
data.allow_model_override = false;
if (!data.hasOwnProperty("allow_temperature_override"))
data.allow_temperature_override = false;
if (!data.hasOwnProperty("allow_prompt_override"))
data.allow_prompt_override = false;
return data;
}
export default function NewEmbedModal({ closeModal }) {
const [error, setError] = useState(null);
const handleCreate = async (e) => {
setError(null);
e.preventDefault();
const form = new FormData(e.target);
const data = enforceSubmissionSchema(form);
const { embed, error } = await Embed.newEmbed(data);
if (!!embed) window.location.reload();
setError(error);
};
return (
<div className="relative w-full max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<h3 className="text-xl font-semibold text-white">
Create new embed for workspace
</h3>
<button
onClick={closeModal}
type="button"
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
data-modal-hide="staticModal"
>
<X className="text-gray-300 text-lg" />
</button>
</div>
<form onSubmit={handleCreate}>
<div className="p-6 space-y-6 flex h-auto max-h-[80vh] w-full overflow-y-scroll">
<div className="w-full flex flex-col gap-y-6">
<WorkspaceSelection />
<ChatModeSelection />
<PermittedDomains />
<NumberInput
name="max_chats_per_day"
title="Max chats per day"
hint="Limit the amount of chats this embedded chat can process in a 24 hour period. Zero is unlimited."
/>
<NumberInput
name="max_chats_per_session"
title="Max chats per session"
hint="Limit the amount of chats a session user can send with this embed in a 24 hour period. Zero is unlimited."
/>
<BooleanInput
name="allow_model_override"
title="Enable dynamic model use"
hint="Allow setting of the preferred LLM model to override the workspace default."
/>
<BooleanInput
name="allow_temperature_override"
title="Enable dynamic LLM temperature"
hint="Allow setting of the LLM temperature to override the workspace default."
/>
<BooleanInput
name="allow_prompt_override"
title="Enable Prompt Override"
hint="Allow setting of the system prompt to override the workspace default."
/>
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
<p className="text-white text-xs md:text-sm pb-8">
After creating an embed you will be provided a link that you can
publish on your website with a simple
<code className="bg-stone-800 text-white mx-1 px-1 rounded-sm">
&lt;script&gt;
</code>{" "}
tag.
</p>
</div>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
<button
onClick={closeModal}
type="button"
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
>
Cancel
</button>
<button
type="submit"
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
Create embed
</button>
</div>
</form>
</div>
</div>
);
}
export const WorkspaceSelection = ({ defaultValue = null }) => {
const [workspaces, setWorkspaces] = useState([]);
useEffect(() => {
async function fetchWorkspaces() {
const _workspaces = await Workspace.all();
setWorkspaces(_workspaces);
}
fetchWorkspaces();
}, []);
return (
<div>
<div className="flex flex-col mb-2">
<label
htmlFor="workspace_id"
className="block text-sm font-medium text-white"
>
Workspace
</label>
<p className="text-slate-300 text-xs">
This is the workspace your chat window will be based on. All defaults
will be inherited from the workspace unless overridden by this config.
</p>
</div>
<select
name="workspace_id"
required={true}
defaultValue={defaultValue}
className="min-w-[15rem] rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border border-gray-500 focus:ring-blue-500 focus:border-blue-500"
>
{workspaces.map((workspace) => {
return (
<option
selected={defaultValue === workspace.id}
value={workspace.id}
>
{workspace.name}
</option>
);
})}
</select>
</div>
);
};
export const ChatModeSelection = ({ defaultValue = null }) => {
const [chatMode, setChatMode] = useState(defaultValue ?? "query");
return (
<div>
<div className="flex flex-col mb-2">
<label
className="block text-sm font-medium text-white"
htmlFor="chat_mode"
>
Allowed chat method
</label>
<p className="text-slate-300 text-xs">
Set how your chatbot should operate. Query means it will only respond
if a document helps answer the query.
<br />
Chat opens the chat to even general questions and can answer totally
unrelated queries to your workspace.
</p>
</div>
<div className="mt-2 gap-y-3 flex flex-col">
<label
className={`transition-all duration-300 w-full h-11 p-2.5 bg-white/10 rounded-lg flex justify-start items-center gap-2.5 cursor-pointer border border-transparent ${
chatMode === "chat" ? "border-white border-opacity-40" : ""
} hover:border-white/60`}
>
<input
type="radio"
name="chat_mode"
value={"chat"}
checked={chatMode === "chat"}
onChange={(e) => setChatMode(e.target.value)}
className="hidden"
/>
<div
className={`w-4 h-4 rounded-full border-2 border-white mr-2 ${
chatMode === "chat" ? "bg-white" : ""
}`}
></div>
<div className="text-white text-sm font-medium font-['Plus Jakarta Sans'] leading-tight">
Chat: Respond to all questions regardless of context
</div>
</label>
<label
className={`transition-all duration-300 w-full h-11 p-2.5 bg-white/10 rounded-lg flex justify-start items-center gap-2.5 cursor-pointer border border-transparent ${
chatMode === "query" ? "border-white border-opacity-40" : ""
} hover:border-white/60`}
>
<input
type="radio"
name="chat_mode"
value={"query"}
checked={chatMode === "query"}
onChange={(e) => setChatMode(e.target.value)}
className="hidden"
/>
<div
className={`w-4 h-4 rounded-full border-2 border-white mr-2 ${
chatMode === "query" ? "bg-white" : ""
}`}
></div>
<div className="text-white text-sm font-medium font-['Plus Jakarta Sans'] leading-tight">
Query: Only respond to chats related to documents in workspace
</div>
</label>
</div>
</div>
);
};
export const PermittedDomains = ({ defaultValue = [] }) => {
const [domains, setDomains] = useState(defaultValue);
const handleChange = (data) => {
const validDomains = data
.map((input) => {
let url = input;
if (!url.includes("http://") && !url.includes("https://"))
url = `https://${url}`;
try {
new URL(url);
return url;
} catch {
return null;
}
})
.filter((u) => !!u);
setDomains(validDomains);
};
return (
<div>
<div className="flex flex-col mb-2">
<label
htmlFor="allowlist_domains"
className="block text-sm font-medium text-white"
>
Restrict requests from domains
</label>
<p className="text-slate-300 text-xs">
This filter will block any requests that come from a domain other than
the list below.
<br />
Leaving this empty means anyone can use your embed on any site.
</p>
</div>
<input type="hidden" name="allowlist_domains" value={domains.join(",")} />
<TagsInput
value={domains}
onChange={handleChange}
placeholder="https://mysite.com, https://useanything.com"
classNames={{
tag: "bg-blue-300/10 text-zinc-800 m-1",
input:
"flex bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white p-2.5",
}}
/>
</div>
);
};
export const NumberInput = ({ name, title, hint, defaultValue = 0 }) => {
return (
<div>
<div className="flex flex-col mb-2">
<label htmlFor={name} className="block text-sm font-medium text-white">
{title}
</label>
<p className="text-slate-300 text-xs">{hint}</p>
</div>
<input
type="number"
name={name}
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-[15rem] p-2.5"
min={0}
defaultValue={defaultValue}
onScroll={(e) => e.target.blur()}
/>
</div>
);
};
export const BooleanInput = ({ name, title, hint, defaultValue = null }) => {
const [status, setStatus] = useState(defaultValue ?? false);
return (
<div>
<div className="flex flex-col mb-2">
<label htmlFor={name} className="block text-sm font-medium text-white">
{title}
</label>
<p className="text-slate-300 text-xs">{hint}</p>
</div>
<label className="relative inline-flex cursor-pointer items-center">
<input
name={name}
type="checkbox"
onClick={() => setStatus(!status)}
checked={status}
className="peer sr-only pointer-events-none"
/>
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800" />
</label>
</div>
);
};