Uniform modal component (#5741)
* add reusable Modal component, migrate all modals off ModalWrapper, drop duplicate dark overlays * migrate all modals to uniform Modal component with normalized light/dark styles * fix modal light-mode focus, content styles, spacing, scroll, and save-bar overlap * align modal footer right when only one action button * fix embed chat widgets page overflow * fix embed modal toggles not saving and modal scroll spacing * normalize tag inputs and code snippet modal spacing * fix community hub publish success modal views * use static translation keys for community hub publish success view * close nested modals one at a time on escape via shared useModalEscape hook * fix copy chat link modal reopening every click --------- Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
This commit is contained in:
@@ -1,4 +1,11 @@
|
||||
import { Warning, X } from "@phosphor-icons/react";
|
||||
import { Warning } from "@phosphor-icons/react";
|
||||
import {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalSecondaryButton,
|
||||
ModalDangerButton,
|
||||
} from "@/components/lib/Modal";
|
||||
|
||||
export default function ChangeWarningModal({
|
||||
warningText = "",
|
||||
@@ -6,56 +13,37 @@ export default function ChangeWarningModal({
|
||||
onConfirm,
|
||||
}) {
|
||||
return (
|
||||
<div className="w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden z-9999">
|
||||
<div className="relative px-6 py-5 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<Warning className="text-red-500 w-6 h-6" weight="fill" />
|
||||
<h3 className="text-xl font-semibold text-red-500 overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
<div className="flex flex-col gap-y-5">
|
||||
<ModalHeader
|
||||
title={
|
||||
<span className="flex items-center gap-x-2 text-red-500">
|
||||
<Warning className="w-6 h-6 shrink-0" weight="fill" />
|
||||
WARNING - This action is irreversible
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="h-full w-full overflow-y-auto"
|
||||
style={{ maxHeight: "calc(100vh - 200px)" }}
|
||||
>
|
||||
<div className="py-7 px-9 space-y-2 flex-col">
|
||||
<p className="text-white">
|
||||
{warningText.split("\\n").map((line, index) => (
|
||||
<span key={index}>
|
||||
{line}
|
||||
<br />
|
||||
</span>
|
||||
))}
|
||||
<br />
|
||||
<br />
|
||||
Are you sure you want to proceed?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full justify-end items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b">
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="transition-all duration-300 bg-transparent text-white hover:opacity-60 px-4 py-2 rounded-lg text-sm border-none"
|
||||
>
|
||||
</span>
|
||||
}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<ModalBody>
|
||||
<p className="text-zinc-300 light:text-slate-700">
|
||||
{warningText.split("\\n").map((line, index) => (
|
||||
<span key={index}>
|
||||
{line}
|
||||
<br />
|
||||
</span>
|
||||
))}
|
||||
<br />
|
||||
<br />
|
||||
Are you sure you want to proceed?
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter className="justify-end">
|
||||
<ModalSecondaryButton onClick={onClose} type="button">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
type="submit"
|
||||
className="transition-all duration-300 bg-red-500 light:text-white text-white hover:opacity-60 px-4 py-2 rounded-lg text-sm border-none"
|
||||
>
|
||||
</ModalSecondaryButton>
|
||||
<ModalDangerButton onClick={onConfirm} type="submit">
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</ModalDangerButton>
|
||||
</ModalFooter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,19 +2,23 @@ import { useState, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CommunityHub from "@/models/communityHub";
|
||||
import showToast from "@/utils/toast";
|
||||
import paths from "@/utils/paths";
|
||||
import { X, CaretRight } from "@phosphor-icons/react";
|
||||
import { BLOCK_INFO } from "@/pages/Admin/AgentBuilder/BlockList";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
ModalHeader,
|
||||
ModalPrimaryButton,
|
||||
ModalLabel,
|
||||
ModalHint,
|
||||
ModalInput,
|
||||
ModalTextarea,
|
||||
} from "@/components/lib/Modal";
|
||||
|
||||
export default function AgentFlows({ entity }) {
|
||||
export default function AgentFlows({ entity, onSuccess }) {
|
||||
const { t } = useTranslation();
|
||||
const formRef = useRef(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [tags, setTags] = useState([]);
|
||||
const [tagInput, setTagInput] = useState("");
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
const [itemId, setItemId] = useState(null);
|
||||
const [expandedStep, setExpandedStep] = useState(null);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
@@ -39,8 +43,7 @@ export default function AgentFlows({ entity }) {
|
||||
const { success, error, itemId } =
|
||||
await CommunityHub.createAgentFlow(data);
|
||||
if (!success) throw new Error(error);
|
||||
setItemId(itemId);
|
||||
setIsSuccess(true);
|
||||
onSuccess(itemId);
|
||||
} catch (error) {
|
||||
console.error("Failed to publish agent flow:", error);
|
||||
showToast(`Failed to publish agent flow: ${error.message}`, "error", {
|
||||
@@ -67,99 +70,58 @@ export default function AgentFlows({ entity }) {
|
||||
setTags(tags.filter((tag) => tag !== tagToRemove));
|
||||
};
|
||||
|
||||
if (isSuccess) {
|
||||
return (
|
||||
<div className="p-6 -mt-12 w-[400px]">
|
||||
<div className="flex flex-col items-center justify-center gap-y-2">
|
||||
<h3 className="text-lg font-semibold text-theme-text-primary">
|
||||
{t("community_hub.publish.agent_flow.success_title")}
|
||||
</h3>
|
||||
<p className="text-lg text-theme-text-primary text-center max-w-2xl">
|
||||
{t("community_hub.publish.agent_flow.success_description")}
|
||||
</p>
|
||||
<p className="text-theme-text-secondary text-center text-sm">
|
||||
{t("community_hub.publish.agent_flow.success_thank_you")}
|
||||
</p>
|
||||
<Link
|
||||
to={paths.communityHub.viewItem("agent-flow", itemId)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="w-[265px] bg-theme-bg-secondary hover:bg-theme-sidebar-item-hover text-theme-text-primary py-2 px-4 rounded-lg transition-colors mt-4 text-sm font-semibold text-center"
|
||||
>
|
||||
{t("community_hub.publish.agent_flow.view_on_hub")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full flex gap-x-2 items-center mb-3 -mt-8">
|
||||
<h3 className="text-xl font-semibold text-theme-text-primary px-6 py-3">
|
||||
{t("community_hub.publish.agent_flow.modal_title")}
|
||||
</h3>
|
||||
<div className="w-full flex gap-x-2 items-center mb-3 -mt-8 px-6 py-3">
|
||||
<ModalHeader
|
||||
title={t("community_hub.publish.agent_flow.modal_title")}
|
||||
/>
|
||||
</div>
|
||||
<form ref={formRef} className="flex" onSubmit={handleSubmit}>
|
||||
<div className="w-1/2 p-6 pt-0 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-theme-text-primary mb-1">
|
||||
{t("community_hub.publish.agent_flow.name_label")}
|
||||
</label>
|
||||
<div className="text-xs text-theme-text-secondary mb-2">
|
||||
{t("community_hub.publish.agent_flow.name_description")}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={300}
|
||||
defaultValue={entity.name}
|
||||
placeholder={t(
|
||||
"community_hub.publish.agent_flow.name_placeholder"
|
||||
)}
|
||||
className="border-none w-full bg-theme-bg-secondary rounded-lg p-2 text-theme-text-primary text-sm focus:outline-primary-button active:outline-primary-button outline-none placeholder:text-theme-text-placeholder"
|
||||
/>
|
||||
</div>
|
||||
<ModalInput
|
||||
label={t("community_hub.publish.agent_flow.name_label")}
|
||||
hint={t("community_hub.publish.agent_flow.name_description")}
|
||||
type="text"
|
||||
name="name"
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={300}
|
||||
defaultValue={entity.name}
|
||||
placeholder={t("community_hub.publish.agent_flow.name_placeholder")}
|
||||
/>
|
||||
|
||||
<ModalTextarea
|
||||
label={t("community_hub.publish.agent_flow.description_label")}
|
||||
hint={t("community_hub.publish.agent_flow.description_description")}
|
||||
name="description"
|
||||
required
|
||||
minLength={10}
|
||||
maxLength={1000}
|
||||
defaultValue={entity.description}
|
||||
placeholder={t(
|
||||
"community_hub.publish.agent_flow.description_description"
|
||||
)}
|
||||
className="min-h-[80px]"
|
||||
/>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-theme-text-primary mb-1">
|
||||
{t("community_hub.publish.agent_flow.description_label")}
|
||||
</label>
|
||||
<div className="text-xs text-white/60 mb-2">
|
||||
{t("community_hub.publish.agent_flow.description_description")}
|
||||
</div>
|
||||
<textarea
|
||||
name="description"
|
||||
required
|
||||
minLength={10}
|
||||
maxLength={1000}
|
||||
defaultValue={entity.description}
|
||||
placeholder={t(
|
||||
"community_hub.publish.agent_flow.description_description"
|
||||
)}
|
||||
className="border-none w-full bg-theme-bg-secondary rounded-lg p-2 text-white text-sm focus:outline-primary-button active:outline-primary-button outline-none min-h-[80px] placeholder:text-theme-text-placeholder"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-white mb-1">
|
||||
<ModalLabel>
|
||||
{t("community_hub.publish.agent_flow.tags_label")}
|
||||
</label>
|
||||
<div className="text-xs text-white/60 mb-2">
|
||||
</ModalLabel>
|
||||
<ModalHint className="mb-2">
|
||||
{t("community_hub.publish.agent_flow.tags_description")}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 p-2 bg-theme-bg-secondary rounded-lg min-h-[42px]">
|
||||
</ModalHint>
|
||||
<div className="flex flex-wrap gap-2 p-2 bg-zinc-800 light:bg-white border border-zinc-800 light:border-slate-300 rounded-lg min-h-[42px]">
|
||||
{tags.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="flex items-center gap-1 px-2 py-1 text-sm text-theme-text-primary bg-white/10 light:bg-black/10 rounded-md"
|
||||
className="flex items-center gap-1 px-2 py-1 text-sm text-slate-50 light:text-slate-900 bg-white/10 light:bg-black/10 rounded-md"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeTag(tag)}
|
||||
className="border-none text-theme-text-primary hover:text-theme-text-secondary cursor-pointer"
|
||||
className="border-none bg-transparent text-slate-50 light:text-slate-900 hover:text-zinc-400 light:hover:text-slate-600 cursor-pointer"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
@@ -173,27 +135,25 @@ export default function AgentFlows({ entity }) {
|
||||
placeholder={t(
|
||||
"community_hub.publish.agent_flow.tags_placeholder"
|
||||
)}
|
||||
className="flex-1 min-w-[200px] border-none text-sm bg-transparent text-theme-text-primary placeholder:text-theme-text-placeholder p-0 h-[24px] focus:outline-none"
|
||||
className="flex-1 min-w-[200px] border-none text-sm bg-transparent text-slate-50 light:text-slate-900 placeholder:text-zinc-400 light:placeholder:text-slate-400 p-0 h-[24px] focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-white">
|
||||
<ModalLabel>
|
||||
{t("community_hub.publish.agent_flow.visibility_label")}
|
||||
</label>
|
||||
<span className="text-xs text-theme-text-secondary">
|
||||
</ModalLabel>
|
||||
<ModalHint>
|
||||
{t("community_hub.publish.agent_flow.privacy_note")}
|
||||
</span>
|
||||
</ModalHint>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-1/2 p-6 pt-0 flex flex-col gap-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-theme-text-primary mb-1">
|
||||
Flow Steps
|
||||
</label>
|
||||
<div className="text-xs text-white/60">
|
||||
<ModalLabel>Flow Steps</ModalLabel>
|
||||
<ModalHint>
|
||||
The steps the agent will follow when the flow is triggered.
|
||||
</div>
|
||||
</ModalHint>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
{entity.steps && entity.steps.length > 0 ? (
|
||||
@@ -206,33 +166,33 @@ export default function AgentFlows({ entity }) {
|
||||
return (
|
||||
<div key={idx} className="flex flex-col items-center w-full">
|
||||
<div
|
||||
className="flex flex-col bg-theme-bg-secondary rounded-lg px-3 py-2 w-full cursor-pointer group"
|
||||
className="flex flex-col bg-zinc-800 light:bg-white border border-zinc-800 light:border-slate-300 rounded-lg px-3 py-2 w-full cursor-pointer group"
|
||||
onClick={() => setExpandedStep(isExpanded ? null : idx)}
|
||||
>
|
||||
<div className="flex items-center gap-x-3 w-full">
|
||||
<span>{info?.icon}</span>
|
||||
<span className="text-theme-text-primary text-sm font-medium flex-1">
|
||||
<span className="text-slate-50 light:text-slate-900 text-sm font-medium flex-1">
|
||||
{info?.label || step.type}
|
||||
</span>
|
||||
{!isExpanded && (
|
||||
<span className="text-theme-text-secondary text-xs ml-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-[120px] min-w-0">
|
||||
<span className="text-zinc-400 light:text-slate-600 text-xs ml-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-[120px] min-w-0">
|
||||
{summary}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={`ml-2 text-theme-text-secondary transition-transform duration-200 ${isExpanded ? "rotate-90" : ""}`}
|
||||
className={`ml-2 text-zinc-400 light:text-slate-600 transition-transform duration-200 ${isExpanded ? "rotate-90" : ""}`}
|
||||
>
|
||||
<CaretRight size={16} />
|
||||
</span>
|
||||
</div>
|
||||
{isExpanded && summary && (
|
||||
<div className="w-full text-theme-text-secondary text-xs mt-1 whitespace-pre-line break-words">
|
||||
<div className="w-full text-zinc-400 light:text-slate-600 text-xs mt-1 whitespace-pre-line break-words">
|
||||
{summary}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{idx < entity.steps.length - 1 && (
|
||||
<span className="text-theme-text-secondary text-lg my-1">
|
||||
<span className="text-zinc-400 light:text-slate-600 text-lg my-1">
|
||||
↓
|
||||
</span>
|
||||
)}
|
||||
@@ -240,20 +200,20 @@ export default function AgentFlows({ entity }) {
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="text-theme-text-secondary text-xs">
|
||||
<div className="text-zinc-400 light:text-slate-600 text-xs">
|
||||
No steps defined.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
<ModalPrimaryButton
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="border-none mt-4 w-full bg-cta-button hover:opacity-80 text-theme-text-primary font-medium py-2 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="mt-4 w-full"
|
||||
>
|
||||
{isSubmitting
|
||||
? t("community_hub.publish.agent_flow.submitting")
|
||||
: t("community_hub.publish.agent_flow.submit")}
|
||||
</button>
|
||||
</ModalPrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
|
||||
@@ -2,19 +2,23 @@ import { useState, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CommunityHub from "@/models/communityHub";
|
||||
import showToast from "@/utils/toast";
|
||||
import paths from "@/utils/paths";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
ModalHeader,
|
||||
ModalPrimaryButton,
|
||||
ModalLabel,
|
||||
ModalHint,
|
||||
ModalInput,
|
||||
ModalTextarea,
|
||||
} from "@/components/lib/Modal";
|
||||
|
||||
export default function SlashCommands({ entity }) {
|
||||
export default function SlashCommands({ entity, onSuccess }) {
|
||||
const { t } = useTranslation();
|
||||
const formRef = useRef(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [tags, setTags] = useState([]);
|
||||
const [tagInput, setTagInput] = useState("");
|
||||
const [visibility, setVisibility] = useState("public");
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
const [itemId, setItemId] = useState(null);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -34,8 +38,7 @@ export default function SlashCommands({ entity }) {
|
||||
const { success, error, itemId } =
|
||||
await CommunityHub.createSlashCommand(data);
|
||||
if (!success) throw new Error(error);
|
||||
setItemId(itemId);
|
||||
setIsSuccess(true);
|
||||
onSuccess(itemId);
|
||||
} catch (error) {
|
||||
console.error("Failed to publish slash command:", error);
|
||||
showToast(`Failed to publish slash command: ${error.message}`, "error", {
|
||||
@@ -62,102 +65,69 @@ export default function SlashCommands({ entity }) {
|
||||
setTags(tags.filter((tag) => tag !== tagToRemove));
|
||||
};
|
||||
|
||||
if (isSuccess) {
|
||||
return (
|
||||
<div className="p-6 -mt-12 w-[400px]">
|
||||
<div className="flex flex-col items-center justify-center gap-y-2">
|
||||
<h3 className="text-lg font-semibold text-theme-text-primary">
|
||||
{t("community_hub.publish.slash_command.success_title")}
|
||||
</h3>
|
||||
<p className="text-lg text-theme-text-primary text-center max-w-2xl">
|
||||
{t("community_hub.publish.slash_command.success_description")}
|
||||
</p>
|
||||
<p className="text-theme-text-secondary text-center text-sm">
|
||||
{t("community_hub.publish.slash_command.success_thank_you")}
|
||||
</p>
|
||||
<Link
|
||||
to={paths.communityHub.viewItem("slash-command", itemId)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="w-[265px] bg-theme-bg-secondary hover:bg-theme-sidebar-item-hover text-theme-text-primary py-2 px-4 rounded-lg transition-colors mt-4 text-sm font-semibold text-center"
|
||||
>
|
||||
{t("community_hub.publish.slash_command.view_on_hub")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full flex gap-x-2 items-center mb-3 -mt-8">
|
||||
<h3 className="text-xl font-semibold text-theme-text-primary px-6 py-3 flex items-center gap-x-2">
|
||||
{t("community_hub.publish.slash_command.modal_title")}
|
||||
<code className="bg-theme-bg-secondary rounded-lg px-1 text-theme-text-secondary text-lg font-mono">
|
||||
{entity.command}
|
||||
</code>
|
||||
</h3>
|
||||
<div className="w-full flex gap-x-2 items-center mb-3 -mt-8 px-6 py-3">
|
||||
<ModalHeader
|
||||
title={
|
||||
<span className="flex items-center gap-x-2">
|
||||
{t("community_hub.publish.slash_command.modal_title")}
|
||||
<code className="bg-zinc-800 light:bg-slate-100 rounded-lg px-1 text-zinc-400 light:text-slate-600 text-lg font-mono">
|
||||
{entity.command}
|
||||
</code>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<form ref={formRef} className="flex" onSubmit={handleSubmit}>
|
||||
<div className="w-1/2 p-6 pt-0 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-theme-text-primary mb-1">
|
||||
{t("community_hub.publish.slash_command.name_label")}
|
||||
</label>
|
||||
<div className="text-xs text-theme-text-secondary mb-2">
|
||||
{t("community_hub.publish.slash_command.name_description")}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={300}
|
||||
defaultValue={entity.name}
|
||||
placeholder={t(
|
||||
"community_hub.publish.slash_command.name_placeholder"
|
||||
)}
|
||||
className="border-none w-full bg-theme-bg-secondary rounded-lg p-2 text-theme-text-primary text-sm focus:outline-primary-button active:outline-primary-button outline-none placeholder:text-theme-text-placeholder"
|
||||
/>
|
||||
</div>
|
||||
<ModalInput
|
||||
label={t("community_hub.publish.slash_command.name_label")}
|
||||
hint={t("community_hub.publish.slash_command.name_description")}
|
||||
type="text"
|
||||
name="name"
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={300}
|
||||
defaultValue={entity.name}
|
||||
placeholder={t(
|
||||
"community_hub.publish.slash_command.name_placeholder"
|
||||
)}
|
||||
/>
|
||||
|
||||
<ModalTextarea
|
||||
label={t("community_hub.publish.slash_command.description_label")}
|
||||
hint={t(
|
||||
"community_hub.publish.slash_command.description_description"
|
||||
)}
|
||||
name="description"
|
||||
required
|
||||
minLength={10}
|
||||
maxLength={1000}
|
||||
defaultValue={entity.description}
|
||||
placeholder={t(
|
||||
"community_hub.publish.slash_command.description_description"
|
||||
)}
|
||||
className="min-h-[80px]"
|
||||
/>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-theme-text-primary mb-1">
|
||||
{t("community_hub.publish.slash_command.description_label")}
|
||||
</label>
|
||||
<div className="text-xs text-white/60 mb-2">
|
||||
{t("community_hub.publish.slash_command.description_description")}
|
||||
</div>
|
||||
<textarea
|
||||
name="description"
|
||||
required
|
||||
minLength={10}
|
||||
maxLength={1000}
|
||||
defaultValue={entity.description}
|
||||
placeholder={t(
|
||||
"community_hub.publish.slash_command.description_description"
|
||||
)}
|
||||
className="border-none w-full bg-theme-bg-secondary rounded-lg p-2 text-white text-sm focus:outline-primary-button active:outline-primary-button outline-none min-h-[80px] placeholder:text-theme-text-placeholder"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-white mb-1">
|
||||
<ModalLabel>
|
||||
{t("community_hub.publish.slash_command.tags_label")}
|
||||
</label>
|
||||
<div className="text-xs text-white/60 mb-2">
|
||||
</ModalLabel>
|
||||
<ModalHint className="mb-2">
|
||||
{t("community_hub.publish.slash_command.tags_description")}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 p-2 bg-theme-bg-secondary rounded-lg min-h-[42px]">
|
||||
</ModalHint>
|
||||
<div className="flex flex-wrap gap-2 p-2 bg-zinc-800 light:bg-white border border-zinc-800 light:border-slate-300 rounded-lg min-h-[42px]">
|
||||
{tags.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="flex items-center gap-1 px-2 py-1 text-sm text-theme-text-primary bg-white/10 light:bg-black/10 rounded-md"
|
||||
className="flex items-center gap-1 px-2 py-1 text-sm text-slate-50 light:text-slate-900 bg-white/10 light:bg-black/10 rounded-md"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeTag(tag)}
|
||||
className="border-none text-theme-text-primary hover:text-theme-text-secondary cursor-pointer"
|
||||
className="border-none bg-transparent text-slate-50 light:text-slate-900 hover:text-zinc-400 light:hover:text-slate-600 cursor-pointer"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
@@ -171,21 +141,21 @@ export default function SlashCommands({ entity }) {
|
||||
placeholder={t(
|
||||
"community_hub.publish.slash_command.tags_placeholder"
|
||||
)}
|
||||
className="flex-1 min-w-[200px] border-none text-sm bg-transparent text-theme-text-primary placeholder:text-theme-text-placeholder p-0 h-[24px] focus:outline-none"
|
||||
className="flex-1 min-w-[200px] border-none text-sm bg-transparent text-slate-50 light:text-slate-900 placeholder:text-zinc-400 light:placeholder:text-slate-400 p-0 h-[24px] focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-white mb-1">
|
||||
<ModalLabel>
|
||||
{t("community_hub.publish.slash_command.visibility_label")}
|
||||
</label>
|
||||
<div className="text-xs text-white/60 mb-2">
|
||||
</ModalLabel>
|
||||
<ModalHint className="mb-2">
|
||||
{visibility === "public"
|
||||
? t("community_hub.publish.slash_command.public_description")
|
||||
: t("community_hub.publish.slash_command.private_description")}
|
||||
</div>
|
||||
<div className="w-fit h-[42px] bg-theme-bg-secondary rounded-lg p-0.5">
|
||||
</ModalHint>
|
||||
<div className="w-fit h-[42px] bg-zinc-800 light:bg-white border border-zinc-800 light:border-slate-300 rounded-lg p-0.5">
|
||||
<div className="flex items-center" role="group">
|
||||
<input
|
||||
type="radio"
|
||||
@@ -206,13 +176,13 @@ export default function SlashCommands({ entity }) {
|
||||
/>
|
||||
<label
|
||||
htmlFor="public"
|
||||
className="h-[36px] px-4 rounded-lg text-sm font-medium transition-all duration-200 cursor-pointer text-theme-text-primary hover:text-theme-text-secondary peer-checked/public:bg-theme-sidebar-item-hover peer-checked/public:text-theme-primary-button flex items-center justify-center"
|
||||
className="h-[36px] px-4 rounded-lg text-sm font-medium transition-all duration-200 cursor-pointer text-slate-50 light:text-slate-900 hover:text-zinc-400 light:hover:text-slate-600 peer-checked/public:bg-zinc-700 light:peer-checked/public:bg-slate-200 flex items-center justify-center"
|
||||
>
|
||||
Public
|
||||
</label>
|
||||
<label
|
||||
htmlFor="private"
|
||||
className="h-[36px] px-4 rounded-lg text-sm font-medium transition-all duration-200 cursor-pointer text-theme-text-primary hover:text-theme-text-secondary peer-checked/private:bg-theme-sidebar-item-hover peer-checked/private:text-theme-primary-button flex items-center justify-center"
|
||||
className="h-[36px] px-4 rounded-lg text-sm font-medium transition-all duration-200 cursor-pointer text-slate-50 light:text-slate-900 hover:text-zinc-400 light:hover:text-slate-600 peer-checked/private:bg-zinc-700 light:peer-checked/private:bg-slate-200 flex items-center justify-center"
|
||||
>
|
||||
Private
|
||||
</label>
|
||||
@@ -222,34 +192,28 @@ export default function SlashCommands({ entity }) {
|
||||
</div>
|
||||
|
||||
<div className="w-1/2 p-6 pt-0 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-white mb-1">
|
||||
{t("community_hub.publish.slash_command.prompt_label")}
|
||||
</label>
|
||||
<div className="text-xs text-white/60 mb-2">
|
||||
{t("community_hub.publish.slash_command.prompt_description")}
|
||||
</div>
|
||||
<textarea
|
||||
name="prompt"
|
||||
required
|
||||
minLength={10}
|
||||
defaultValue={entity.prompt}
|
||||
placeholder={t(
|
||||
"community_hub.publish.slash_command.prompt_placeholder"
|
||||
)}
|
||||
className="border-none w-full bg-theme-bg-secondary rounded-lg p-2 text-white text-sm focus:outline-primary-button active:outline-primary-button outline-none min-h-[300px] placeholder:text-theme-text-placeholder"
|
||||
/>
|
||||
</div>
|
||||
<ModalTextarea
|
||||
label={t("community_hub.publish.slash_command.prompt_label")}
|
||||
hint={t("community_hub.publish.slash_command.prompt_description")}
|
||||
name="prompt"
|
||||
required
|
||||
minLength={10}
|
||||
defaultValue={entity.prompt}
|
||||
placeholder={t(
|
||||
"community_hub.publish.slash_command.prompt_placeholder"
|
||||
)}
|
||||
className="min-h-[300px]"
|
||||
/>
|
||||
|
||||
<button
|
||||
<ModalPrimaryButton
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="border-none w-full bg-cta-button hover:opacity-80 text-theme-text-primary font-medium py-2 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="w-full"
|
||||
>
|
||||
{isSubmitting
|
||||
? t("community_hub.publish.slash_command.submitting")
|
||||
: t("community_hub.publish.slash_command.publish_button")}
|
||||
</button>
|
||||
</ModalPrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
|
||||
@@ -2,19 +2,23 @@ import { useState, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CommunityHub from "@/models/communityHub";
|
||||
import showToast from "@/utils/toast";
|
||||
import paths from "@/utils/paths";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
ModalHeader,
|
||||
ModalPrimaryButton,
|
||||
ModalLabel,
|
||||
ModalHint,
|
||||
ModalInput,
|
||||
ModalTextarea,
|
||||
} from "@/components/lib/Modal";
|
||||
|
||||
export default function SystemPrompts({ entity }) {
|
||||
export default function SystemPrompts({ entity, onSuccess }) {
|
||||
const { t } = useTranslation();
|
||||
const formRef = useRef(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [tags, setTags] = useState([]);
|
||||
const [tagInput, setTagInput] = useState("");
|
||||
const [visibility, setVisibility] = useState("public");
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
const [itemId, setItemId] = useState(null);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -33,8 +37,7 @@ export default function SystemPrompts({ entity }) {
|
||||
const { success, error, itemId } =
|
||||
await CommunityHub.createSystemPrompt(data);
|
||||
if (!success) throw new Error(error);
|
||||
setItemId(itemId);
|
||||
setIsSuccess(true);
|
||||
onSuccess(itemId);
|
||||
} catch (error) {
|
||||
console.error("Failed to publish prompt:", error);
|
||||
showToast(`Failed to publish prompt: ${error.message}`, "error", {
|
||||
@@ -61,97 +64,60 @@ export default function SystemPrompts({ entity }) {
|
||||
setTags(tags.filter((tag) => tag !== tagToRemove));
|
||||
};
|
||||
|
||||
if (isSuccess) {
|
||||
return (
|
||||
<div className="p-6 -mt-12 w-[400px]">
|
||||
<div className="flex flex-col items-center justify-center gap-y-2">
|
||||
<h3 className="text-lg font-semibold text-theme-text-primary">
|
||||
{t("community_hub.publish.system_prompt.success_title")}
|
||||
</h3>
|
||||
<p className="text-lg text-theme-text-primary text-center max-w-2xl">
|
||||
{t("community_hub.publish.system_prompt.success_description")}
|
||||
</p>
|
||||
<p className="text-theme-text-secondary text-center text-sm">
|
||||
{t("community_hub.publish.system_prompt.success_thank_you")}
|
||||
</p>
|
||||
<Link
|
||||
to={paths.communityHub.viewItem("system-prompt", itemId)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="w-[265px] bg-theme-bg-secondary hover:bg-theme-sidebar-item-hover text-theme-text-primary py-2 px-4 rounded-lg transition-colors mt-4 text-sm font-semibold text-center"
|
||||
>
|
||||
{t("community_hub.publish.system_prompt.view_on_hub")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full flex gap-x-2 items-center mb-3 -mt-8">
|
||||
<h3 className="text-xl font-semibold text-theme-text-primary px-6 py-3">
|
||||
{t(`community_hub.publish.system_prompt.modal_title`)}
|
||||
</h3>
|
||||
<div className="w-full flex gap-x-2 items-center mb-3 -mt-8 px-6 py-3">
|
||||
<ModalHeader
|
||||
title={t(`community_hub.publish.system_prompt.modal_title`)}
|
||||
/>
|
||||
</div>
|
||||
<form ref={formRef} className="flex" onSubmit={handleSubmit}>
|
||||
<div className="w-1/2 p-6 pt-0 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-theme-text-primary mb-1">
|
||||
{t("community_hub.publish.system_prompt.name_label")}
|
||||
</label>
|
||||
<div className="text-xs text-theme-text-secondary mb-2">
|
||||
{t("community_hub.publish.system_prompt.name_description")}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={300}
|
||||
placeholder={t(
|
||||
"community_hub.publish.system_prompt.name_placeholder"
|
||||
)}
|
||||
className="border-none w-full bg-theme-bg-secondary rounded-lg p-2 text-theme-text-primary text-sm focus:outline-primary-button active:outline-primary-button outline-none placeholder:text-theme-text-placeholder"
|
||||
/>
|
||||
</div>
|
||||
<ModalInput
|
||||
label={t("community_hub.publish.system_prompt.name_label")}
|
||||
hint={t("community_hub.publish.system_prompt.name_description")}
|
||||
type="text"
|
||||
name="name"
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={300}
|
||||
placeholder={t(
|
||||
"community_hub.publish.system_prompt.name_placeholder"
|
||||
)}
|
||||
/>
|
||||
|
||||
<ModalTextarea
|
||||
label={t("community_hub.publish.system_prompt.description_label")}
|
||||
hint={t(
|
||||
"community_hub.publish.system_prompt.description_description"
|
||||
)}
|
||||
name="description"
|
||||
required
|
||||
minLength={10}
|
||||
maxLength={1000}
|
||||
placeholder={t(
|
||||
"community_hub.publish.system_prompt.description_description"
|
||||
)}
|
||||
className="min-h-[80px]"
|
||||
/>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-theme-text-primary mb-1">
|
||||
{t("community_hub.publish.system_prompt.description_label")}
|
||||
</label>
|
||||
<div className="text-xs text-white/60 mb-2">
|
||||
{t("community_hub.publish.system_prompt.description_description")}
|
||||
</div>
|
||||
<textarea
|
||||
name="description"
|
||||
required
|
||||
minLength={10}
|
||||
maxLength={1000}
|
||||
placeholder={t(
|
||||
"community_hub.publish.system_prompt.description_description"
|
||||
)}
|
||||
className="border-none w-full bg-theme-bg-secondary rounded-lg p-2 text-white text-sm focus:outline-primary-button active:outline-primary-button outline-none min-h-[80px] placeholder:text-theme-text-placeholder"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-white mb-1">
|
||||
<ModalLabel>
|
||||
{t("community_hub.publish.system_prompt.tags_label")}
|
||||
</label>
|
||||
<div className="text-xs text-white/60 mb-2">
|
||||
</ModalLabel>
|
||||
<ModalHint className="mb-2">
|
||||
{t("community_hub.publish.system_prompt.tags_description")}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 p-2 bg-theme-bg-secondary rounded-lg min-h-[42px]">
|
||||
</ModalHint>
|
||||
<div className="flex flex-wrap gap-2 p-2 bg-zinc-800 light:bg-white border border-zinc-800 light:border-slate-300 rounded-lg min-h-[42px]">
|
||||
{tags.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="flex items-center gap-1 px-2 py-1 text-sm text-theme-text-primary bg-white/10 light:bg-black/10 rounded-md"
|
||||
className="flex items-center gap-1 px-2 py-1 text-sm text-slate-50 light:text-slate-900 bg-white/10 light:bg-black/10 rounded-md"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeTag(tag)}
|
||||
className="border-none text-theme-text-primary hover:text-theme-text-secondary cursor-pointer"
|
||||
className="border-none bg-transparent text-slate-50 light:text-slate-900 hover:text-zinc-400 light:hover:text-slate-600 cursor-pointer"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
@@ -165,21 +131,21 @@ export default function SystemPrompts({ entity }) {
|
||||
placeholder={t(
|
||||
"community_hub.publish.system_prompt.tags_placeholder"
|
||||
)}
|
||||
className="flex-1 min-w-[200px] border-none text-sm bg-transparent text-theme-text-primary placeholder:text-theme-text-placeholder p-0 h-[24px] focus:outline-none"
|
||||
className="flex-1 min-w-[200px] border-none text-sm bg-transparent text-slate-50 light:text-slate-900 placeholder:text-zinc-400 light:placeholder:text-slate-400 p-0 h-[24px] focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-white mb-1">
|
||||
<ModalLabel>
|
||||
{t("community_hub.publish.system_prompt.visibility_label")}
|
||||
</label>
|
||||
<div className="text-xs text-white/60 mb-2">
|
||||
</ModalLabel>
|
||||
<ModalHint className="mb-2">
|
||||
{visibility === "public"
|
||||
? t("community_hub.publish.system_prompt.public_description")
|
||||
: t("community_hub.publish.system_prompt.private_description")}
|
||||
</div>
|
||||
<div className="w-fit h-[42px] bg-theme-bg-secondary rounded-lg p-0.5">
|
||||
</ModalHint>
|
||||
<div className="w-fit h-[42px] bg-zinc-800 light:bg-white border border-zinc-800 light:border-slate-300 rounded-lg p-0.5">
|
||||
<div className="flex items-center" role="group">
|
||||
<input
|
||||
type="radio"
|
||||
@@ -200,13 +166,13 @@ export default function SystemPrompts({ entity }) {
|
||||
/>
|
||||
<label
|
||||
htmlFor="public"
|
||||
className="h-[36px] px-4 rounded-lg text-sm font-medium transition-all duration-200 cursor-pointer text-theme-text-primary hover:text-theme-text-secondary peer-checked/public:bg-theme-sidebar-item-hover peer-checked/public:text-theme-primary-button flex items-center justify-center"
|
||||
className="h-[36px] px-4 rounded-lg text-sm font-medium transition-all duration-200 cursor-pointer text-slate-50 light:text-slate-900 hover:text-zinc-400 light:hover:text-slate-600 peer-checked/public:bg-zinc-700 light:peer-checked/public:bg-slate-200 flex items-center justify-center"
|
||||
>
|
||||
Public
|
||||
</label>
|
||||
<label
|
||||
htmlFor="private"
|
||||
className="h-[36px] px-4 rounded-lg text-sm font-medium transition-all duration-200 cursor-pointer text-theme-text-primary hover:text-theme-text-secondary peer-checked/private:bg-theme-sidebar-item-hover peer-checked/private:text-theme-primary-button flex items-center justify-center"
|
||||
className="h-[36px] px-4 rounded-lg text-sm font-medium transition-all duration-200 cursor-pointer text-slate-50 light:text-slate-900 hover:text-zinc-400 light:hover:text-slate-600 peer-checked/private:bg-zinc-700 light:peer-checked/private:bg-slate-200 flex items-center justify-center"
|
||||
>
|
||||
Private
|
||||
</label>
|
||||
@@ -216,34 +182,28 @@ export default function SystemPrompts({ entity }) {
|
||||
</div>
|
||||
|
||||
<div className="w-1/2 p-6 pt-0 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-white mb-1">
|
||||
{t("community_hub.publish.system_prompt.prompt_label")}
|
||||
</label>
|
||||
<div className="text-xs text-white/60 mb-2">
|
||||
{t("community_hub.publish.system_prompt.prompt_description")}
|
||||
</div>
|
||||
<textarea
|
||||
name="prompt"
|
||||
required
|
||||
minLength={10}
|
||||
defaultValue={entity}
|
||||
placeholder={t(
|
||||
"community_hub.publish.system_prompt.prompt_placeholder"
|
||||
)}
|
||||
className="border-none w-full bg-theme-bg-secondary rounded-lg p-2 text-white text-sm focus:outline-primary-button active:outline-primary-button outline-none min-h-[300px] placeholder:text-theme-text-placeholder"
|
||||
/>
|
||||
</div>
|
||||
<ModalTextarea
|
||||
label={t("community_hub.publish.system_prompt.prompt_label")}
|
||||
hint={t("community_hub.publish.system_prompt.prompt_description")}
|
||||
name="prompt"
|
||||
required
|
||||
minLength={10}
|
||||
defaultValue={entity}
|
||||
placeholder={t(
|
||||
"community_hub.publish.system_prompt.prompt_placeholder"
|
||||
)}
|
||||
className="min-h-[300px]"
|
||||
/>
|
||||
|
||||
<button
|
||||
<ModalPrimaryButton
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="border-none w-full bg-cta-button hover:opacity-80 text-theme-text-primary font-medium py-2 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="w-full"
|
||||
>
|
||||
{isSubmitting
|
||||
? t("community_hub.publish.system_prompt.submitting")
|
||||
: t("community_hub.publish.system_prompt.publish_button")}
|
||||
</button>
|
||||
</ModalPrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { useState } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import paths from "@/utils/paths";
|
||||
import { useCommunityHubAuth } from "@/hooks/useCommunityHubAuth";
|
||||
import UnauthenticatedHubModal from "@/components/CommunityHub/UnauthenticatedHubModal";
|
||||
import SystemPrompts from "./SystemPrompts";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
import AgentFlows from "./AgentFlows";
|
||||
import SlashCommands from "./SlashCommands";
|
||||
|
||||
@@ -13,37 +17,112 @@ export default function PublishEntityModal({
|
||||
entity,
|
||||
}) {
|
||||
const { isAuthenticated, loading } = useCommunityHubAuth();
|
||||
const [successItemId, setSuccessItemId] = useState(null);
|
||||
if (!show || loading) return null;
|
||||
if (!isAuthenticated)
|
||||
return <UnauthenticatedHubModal show={show} onClose={onClose} />;
|
||||
|
||||
const isSuccess = !!successItemId;
|
||||
const handleClose = () => {
|
||||
setSuccessItemId(null);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const renderEntityForm = () => {
|
||||
switch (entityType) {
|
||||
case "system-prompt":
|
||||
return <SystemPrompts entity={entity} />;
|
||||
return <SystemPrompts entity={entity} onSuccess={setSuccessItemId} />;
|
||||
case "agent-flow":
|
||||
return <AgentFlows entity={entity} />;
|
||||
return <AgentFlows entity={entity} onSuccess={setSuccessItemId} />;
|
||||
case "slash-command":
|
||||
return <SlashCommands entity={entity} />;
|
||||
return <SlashCommands entity={entity} onSuccess={setSuccessItemId} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalWrapper isOpen={show}>
|
||||
<div className="relative max-w-[900px] bg-theme-bg-primary rounded-lg shadow border border-theme-modal-border">
|
||||
<Modal isOpen={show} onClose={handleClose} variant="bare">
|
||||
<div
|
||||
className={`relative w-full ${
|
||||
isSuccess ? "max-w-[460px]" : "max-w-[900px]"
|
||||
} mx-4 max-h-[90vh] overflow-y-auto bg-zinc-900 light:bg-white rounded-lg shadow-xs border border-zinc-800 light:border-slate-300`}
|
||||
>
|
||||
<div className="relative p-6">
|
||||
<button
|
||||
onClick={onClose}
|
||||
onClick={handleClose}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
aria-label="Close"
|
||||
className="absolute top-4 right-4 transition-colors duration-200 bg-transparent rounded-lg p-1 inline-flex items-center text-slate-50 light:text-slate-900 hover:bg-zinc-800 light:hover:bg-slate-100 border-none"
|
||||
>
|
||||
<X size={18} weight="bold" className="text-white" />
|
||||
<X size={16} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
{renderEntityForm()}
|
||||
{isSuccess ? (
|
||||
<PublishSuccess entityType={entityType} itemId={successItemId} />
|
||||
) : (
|
||||
renderEntityForm()
|
||||
)}
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function successCopy(entityType, t) {
|
||||
switch (entityType) {
|
||||
case "system-prompt":
|
||||
return {
|
||||
title: t("community_hub.publish.system_prompt.success_title"),
|
||||
description: t(
|
||||
"community_hub.publish.system_prompt.success_description"
|
||||
),
|
||||
thankYou: t("community_hub.publish.system_prompt.success_thank_you"),
|
||||
viewOnHub: t("community_hub.publish.system_prompt.view_on_hub"),
|
||||
};
|
||||
case "agent-flow":
|
||||
return {
|
||||
title: t("community_hub.publish.agent_flow.success_title"),
|
||||
description: t("community_hub.publish.agent_flow.success_description"),
|
||||
thankYou: t("community_hub.publish.agent_flow.success_thank_you"),
|
||||
viewOnHub: t("community_hub.publish.agent_flow.view_on_hub"),
|
||||
};
|
||||
case "slash-command":
|
||||
return {
|
||||
title: t("community_hub.publish.slash_command.success_title"),
|
||||
description: t(
|
||||
"community_hub.publish.slash_command.success_description"
|
||||
),
|
||||
thankYou: t("community_hub.publish.slash_command.success_thank_you"),
|
||||
viewOnHub: t("community_hub.publish.slash_command.view_on_hub"),
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function PublishSuccess({ entityType, itemId }) {
|
||||
const { t } = useTranslation();
|
||||
const copy = successCopy(entityType, t);
|
||||
if (!copy) return null;
|
||||
return (
|
||||
<div className="px-6 pb-8 flex flex-col items-center justify-center text-center gap-y-2">
|
||||
<h3 className="text-lg font-semibold text-slate-50 light:text-slate-900">
|
||||
{copy.title}
|
||||
</h3>
|
||||
<p className="text-lg text-slate-50 light:text-slate-900">
|
||||
{copy.description}
|
||||
</p>
|
||||
<p className="text-zinc-400 light:text-slate-600 text-sm">
|
||||
{copy.thankYou}
|
||||
</p>
|
||||
<Link
|
||||
to={paths.communityHub.viewItem(entityType, itemId)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="w-full bg-zinc-800 light:bg-slate-100 hover:bg-zinc-700 light:hover:bg-slate-200 text-slate-50 light:text-slate-900 py-2 px-4 rounded-lg transition-colors mt-4 text-sm font-semibold text-center"
|
||||
>
|
||||
{copy.viewOnHub}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,40 +1,31 @@
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import paths from "@/utils/paths";
|
||||
import { Link } from "react-router-dom";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal, { ModalHeader, ModalBody } from "@/components/lib/Modal";
|
||||
|
||||
export default function UnauthenticatedHubModal({ show, onClose }) {
|
||||
const { t } = useTranslation();
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
<ModalWrapper isOpen={show}>
|
||||
<div className="relative w-[400px] max-w-full bg-theme-bg-primary rounded-lg shadow border border-theme-modal-border">
|
||||
<div className="p-6">
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
<Modal isOpen={show} onClose={onClose} size="sm">
|
||||
<ModalHeader
|
||||
title={t("community_hub.publish.generic.unauthenticated.title")}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<ModalBody>
|
||||
<div className="flex flex-col items-center justify-center gap-y-4">
|
||||
<p className="text-sm text-zinc-300 light:text-slate-700 text-center max-w-[300px]">
|
||||
{t("community_hub.publish.generic.unauthenticated.description")}
|
||||
</p>
|
||||
<Link
|
||||
to={paths.communityHub.authentication()}
|
||||
className="w-[265px] bg-zinc-50 light:bg-slate-900 text-zinc-950 light:text-white py-2 px-4 rounded-lg hover:opacity-80 transition-all duration-200 mt-4 text-sm font-semibold text-center"
|
||||
>
|
||||
<X size={18} weight="bold" className="text-white" />
|
||||
</button>
|
||||
<div className="flex flex-col items-center justify-center gap-y-4">
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
{t("community_hub.publish.generic.unauthenticated.title")}
|
||||
</h3>
|
||||
<p className="text-lg text-white text-center max-w-[300px]">
|
||||
{t("community_hub.publish.generic.unauthenticated.description")}
|
||||
</p>
|
||||
<Link
|
||||
to={paths.communityHub.authentication()}
|
||||
className="w-[265px] bg-theme-bg-secondary hover:bg-theme-sidebar-item-hover text-theme-text-primary py-2 px-4 rounded-lg transition-colors mt-4 text-sm font-semibold text-center"
|
||||
>
|
||||
{t("community_hub.publish.generic.unauthenticated.button")}
|
||||
</Link>
|
||||
</div>
|
||||
{t("community_hub.publish.generic.unauthenticated.button")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Modal, { ModalHeader, ModalBody } from "@/components/lib/Modal";
|
||||
import {
|
||||
SHORTCUTS,
|
||||
isMac,
|
||||
@@ -22,39 +22,29 @@ export default function KeyboardShortcutsHelp() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!isOpen) return null;
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="relative bg-theme-bg-secondary rounded-lg p-6 max-w-2xl w-full mx-4">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl font-semibold text-white">
|
||||
{t("keyboard-shortcuts.title")}
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="text-white hover:text-gray-300"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Modal isOpen={isOpen} onClose={() => setIsOpen(false)} size="lg">
|
||||
<ModalHeader
|
||||
title={t("keyboard-shortcuts.title")}
|
||||
onClose={() => setIsOpen(false)}
|
||||
/>
|
||||
<ModalBody>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{Object.entries(SHORTCUTS).map(([key, shortcut]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center justify-between p-3 bg-theme-bg-hover rounded-lg"
|
||||
className="flex items-center justify-between p-3 rounded-lg bg-zinc-800 light:bg-slate-100"
|
||||
>
|
||||
<span className="text-white">
|
||||
<span className="text-zinc-100 light:text-slate-900">
|
||||
{t(`keyboard-shortcuts.shortcuts.${shortcut.translationKey}`)}
|
||||
</span>
|
||||
<kbd className="px-2 py-1 bg-theme-bg-secondary text-white rounded border border-gray-600">
|
||||
<kbd className="px-2 py-1 rounded text-zinc-100 light:text-slate-900 bg-zinc-900 light:bg-white border border-zinc-700 light:border-slate-300">
|
||||
{isMac ? key : key.replace("⌘", "Ctrl")}
|
||||
</kbd>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { createPortal } from "react-dom";
|
||||
/**
|
||||
* @typedef {Object} ModalWrapperProps
|
||||
* @property {import("react").ReactComponentElement} children - The DOM/JSX to render
|
||||
* @property {boolean} isOpen - Option that renders the modal
|
||||
* @property {boolean} noPortal - (default: false) Used for creating sub-DOM modals that need to be rendered as a child element instead of a modal placed at the root
|
||||
* Note: This can impact the bg-overlay presentation due to conflicting DOM positions so if using this property you should
|
||||
double check it renders as desired.
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ModalWrapperProps} props - ModalWrapperProps to pass
|
||||
* @returns {import("react").ReactNode}
|
||||
*
|
||||
* @todo Add a closeModal prop to the ModalWrapper component so we can escape dismiss anywhere this is used
|
||||
*/
|
||||
export default function ModalWrapper({ children, isOpen, noPortal = false }) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
if (noPortal) {
|
||||
return (
|
||||
<div className="bg-black/60 backdrop-blur-sm fixed top-0 left-0 outline-none w-screen h-screen flex items-center justify-center z-99">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div className="bg-black/60 backdrop-blur-sm fixed top-0 left-0 outline-none w-screen h-screen flex items-center justify-center z-99">
|
||||
{children}
|
||||
</div>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,12 @@ import showToast from "@/utils/toast";
|
||||
import { DownloadSimple, Key } from "@phosphor-icons/react";
|
||||
import { saveAs } from "file-saver";
|
||||
import { useState } from "react";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
} from "@/components/lib/Modal";
|
||||
|
||||
export default function RecoveryCodeModal({
|
||||
recoveryCodes,
|
||||
@@ -33,59 +38,50 @@ export default function RecoveryCodeModal({
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalWrapper isOpen={true}>
|
||||
<div className="w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<Key size={24} className="text-white" weight="bold" />
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
Recovery Codes
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-5">
|
||||
<ModalHeader
|
||||
title={
|
||||
<span className="flex items-center gap-x-2">
|
||||
<Key size={20} weight="bold" />
|
||||
Recovery Codes
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<ModalBody>
|
||||
<p className="text-sm text-zinc-300 light:text-slate-700 flex flex-col">
|
||||
In order to reset your password in the future, you will need these
|
||||
recovery codes. Download or copy your recovery codes to save them.{" "}
|
||||
<br />
|
||||
<b className="mt-4">These recovery codes are only shown once!</b>
|
||||
</p>
|
||||
<div
|
||||
className="h-full w-full overflow-y-auto"
|
||||
style={{ maxHeight: "calc(100vh - 200px)" }}
|
||||
className="border-none bg-zinc-800 light:bg-white text-zinc-100 light:text-slate-900 hover:opacity-80 flex items-center justify-center rounded-lg cursor-pointer"
|
||||
onClick={handleCopyToClipboard}
|
||||
>
|
||||
<div className="py-7 px-9 space-y-2 flex-col">
|
||||
<p className="text-sm text-white flex flex-col">
|
||||
In order to reset your password in the future, you will need these
|
||||
recovery codes. Download or copy your recovery codes to save them.{" "}
|
||||
<br />
|
||||
<b className="mt-4">These recovery codes are only shown once!</b>
|
||||
</p>
|
||||
<div
|
||||
className="border-none bg-theme-settings-input-bg text-white hover:text-primary-button
|
||||
flex items-center justify-center rounded-md mt-6 cursor-pointer"
|
||||
onClick={handleCopyToClipboard}
|
||||
>
|
||||
<ul className="space-y-2 md:p-6 p-4">
|
||||
{recoveryCodes.map((code, index) => (
|
||||
<li key={index} className="md:text-sm text-xs">
|
||||
{code}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full justify-end items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b">
|
||||
<button
|
||||
type="button"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm flex items-center gap-x-2"
|
||||
onClick={downloadClicked ? handleClose : downloadRecoveryCodes}
|
||||
>
|
||||
{downloadClicked ? (
|
||||
"Close"
|
||||
) : (
|
||||
<>
|
||||
<DownloadSimple weight="bold" size={18} />
|
||||
<p>Download</p>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<ul className="space-y-2 md:p-6 p-4">
|
||||
{recoveryCodes.map((code, index) => (
|
||||
<li key={index} className="md:text-sm text-xs">
|
||||
{code}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</ModalBody>
|
||||
<ModalFooter className="justify-end">
|
||||
<ModalPrimaryButton
|
||||
type="button"
|
||||
onClick={downloadClicked ? handleClose : downloadRecoveryCodes}
|
||||
>
|
||||
{downloadClicked ? (
|
||||
"Close"
|
||||
) : (
|
||||
<span className="flex items-center gap-x-2">
|
||||
<DownloadSimple weight="bold" size={18} />
|
||||
Download
|
||||
</span>
|
||||
)}
|
||||
</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-3
@@ -123,7 +123,7 @@ export default function GithubOptions() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-full py-4 pr-10">
|
||||
<div className="flex flex-col w-full py-4 pr-10 [&_.rti--container]:!bg-theme-settings-input-bg light:[&_.rti--container]:!bg-theme-settings-input-bg [&_.rti--container]:!border-transparent light:[&_.rti--container]:!border-transparent [&_.rti--container]:focus-within:!border-sky-500 light:[&_.rti--container]:focus-within:!border-sky-500">
|
||||
<div className="flex flex-col gap-y-1 mb-4">
|
||||
<label className="text-white text-sm flex gap-x-2 items-center">
|
||||
<p className="text-white text-sm font-bold">
|
||||
@@ -140,9 +140,9 @@ export default function GithubOptions() {
|
||||
name="ignores"
|
||||
placeholder="!*.js, images/*, .DS_Store, bin/*"
|
||||
classNames={{
|
||||
tag: "bg-theme-settings-input-bg light:bg-black/10 bg-blue-300/10 text-zinc-800",
|
||||
tag: "!bg-zinc-700 light:!bg-slate-200 !text-zinc-100 light:!text-slate-800",
|
||||
input:
|
||||
"flex p-1 !bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none",
|
||||
"flex !bg-transparent text-zinc-100 light:text-slate-900 placeholder:text-zinc-400 light:placeholder:text-slate-400 text-sm outline-none",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+3
-3
@@ -149,7 +149,7 @@ export default function GitlabOptions() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-full py-4 pr-10">
|
||||
<div className="flex flex-col w-full py-4 pr-10 [&_.rti--container]:!bg-theme-settings-input-bg light:[&_.rti--container]:!bg-theme-settings-input-bg [&_.rti--container]:!border-transparent light:[&_.rti--container]:!border-transparent [&_.rti--container]:focus-within:!border-sky-500 light:[&_.rti--container]:focus-within:!border-sky-500">
|
||||
<div className="flex flex-col gap-y-1 mb-4">
|
||||
<label className="text-white text-sm flex gap-x-2 items-center">
|
||||
<p className="text-white text-sm font-bold">
|
||||
@@ -166,9 +166,9 @@ export default function GitlabOptions() {
|
||||
name="ignores"
|
||||
placeholder="!*.js, images/*, .DS_Store, bin/*"
|
||||
classNames={{
|
||||
tag: "bg-theme-settings-input-bg light:bg-black/10 bg-blue-300/10 text-zinc-800",
|
||||
tag: "!bg-zinc-700 light:!bg-slate-200 !text-zinc-100 light:!text-slate-800",
|
||||
input:
|
||||
"flex p-1 !bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none",
|
||||
"flex !bg-transparent text-zinc-100 light:text-slate-900 placeholder:text-zinc-400 light:placeholder:text-slate-400 text-sm outline-none",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+37
-58
@@ -1,6 +1,12 @@
|
||||
import React, { useState } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import Document from "@/models/document";
|
||||
import {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
} from "@/components/lib/Modal";
|
||||
|
||||
export default function NewFolderModal({ closeModal, onCreated }) {
|
||||
const [error, setError] = useState(null);
|
||||
@@ -21,64 +27,37 @@ export default function NewFolderModal({ closeModal, onCreated }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
Create New Folder
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
<form onSubmit={handleCreate} className="flex flex-col gap-y-5">
|
||||
<ModalHeader title="Create New Folder" onClose={closeModal} />
|
||||
<ModalBody>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="folderName"
|
||||
className="block mb-1.5 text-sm font-medium text-zinc-50 light:text-slate-700"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
Folder Name
|
||||
</label>
|
||||
<input
|
||||
name="folderName"
|
||||
type="text"
|
||||
className="w-full h-[34px] px-3.5 text-sm rounded-lg outline-none bg-zinc-800 border border-zinc-800 text-zinc-300 placeholder:text-zinc-400 light:bg-white light:border-slate-300 light:text-slate-700 light:placeholder:text-slate-400 focus:border-sky-500 light:focus:border-sky-500"
|
||||
placeholder="Enter folder name"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
value={folderName}
|
||||
onChange={(e) => setFolderName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<form onSubmit={handleCreate}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="folderName"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Folder Name
|
||||
</label>
|
||||
<input
|
||||
name="folderName"
|
||||
type="text"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="Enter folder name"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
value={folderName}
|
||||
onChange={(e) => setFolderName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={creating}
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 disabled:opacity-40 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
{creating ? "Creating..." : "Create Folder"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalSecondaryButton onClick={closeModal} type="button">
|
||||
Cancel
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton type="submit" disabled={creating}>
|
||||
{creating ? "Creating..." : "Create Folder"}
|
||||
</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import showToast from "@/utils/toast";
|
||||
import FolderSelectionPopup from "./FolderSelectionPopup";
|
||||
import MoveToFolderIcon from "./MoveToFolderIcon";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
import NewFolderModal from "./NewFolderModal";
|
||||
import debounce from "lodash.debounce";
|
||||
import ContextMenu from "./ContextMenu";
|
||||
@@ -419,14 +420,12 @@ export default function Directory({
|
||||
onLinkScraped={syncAfterUpload}
|
||||
/>
|
||||
</div>
|
||||
{isFolderModalOpen && (
|
||||
<div className="bg-black/60 backdrop-blur-sm fixed top-0 left-0 outline-none w-screen h-screen flex items-center justify-center z-30">
|
||||
<NewFolderModal
|
||||
closeModal={closeFolderModal}
|
||||
onCreated={handleFolderCreated}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Modal isOpen={isFolderModalOpen} onClose={closeFolderModal} noPortal>
|
||||
<NewFolderModal
|
||||
closeModal={closeFolderModal}
|
||||
onCreated={handleFolderCreated}
|
||||
/>
|
||||
</Modal>
|
||||
<ContextMenu
|
||||
contextMenu={contextMenu}
|
||||
closeContextMenu={closeContextMenu}
|
||||
|
||||
+75
-84
@@ -1,7 +1,12 @@
|
||||
import PreLoader from "@/components/Preloader";
|
||||
import WorkspaceFileRow from "./WorkspaceFileRow";
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal, {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
} from "@/components/lib/Modal";
|
||||
import {
|
||||
Eye,
|
||||
PushPin,
|
||||
@@ -323,48 +328,41 @@ const PinAlert = memo(() => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ModalWrapper isOpen={showAlert} noPortal={true}>
|
||||
<div className="w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="flex items-center gap-2">
|
||||
<PushPin
|
||||
className="text-theme-text-primary text-lg w-6 h-6"
|
||||
weight="regular"
|
||||
<Modal isOpen={showAlert} noPortal={true} onClose={dismissAlert}>
|
||||
<ModalHeader
|
||||
title={
|
||||
<span className="flex items-center gap-x-2">
|
||||
<PushPin className="w-6 h-6" weight="regular" />
|
||||
{t("connectors.pinning.what_pinning")}
|
||||
</span>
|
||||
}
|
||||
onClose={dismissAlert}
|
||||
/>
|
||||
<ModalBody>
|
||||
<div className="w-full text-zinc-300 light:text-slate-700 text-md flex flex-col gap-y-2">
|
||||
<p>
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t("connectors.pinning.pin_explained_block1"),
|
||||
}}
|
||||
/>
|
||||
<h3 className="text-xl font-semibold text-white">
|
||||
{t("connectors.pinning.what_pinning")}
|
||||
</h3>
|
||||
</div>
|
||||
</p>
|
||||
<p>
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t("connectors.pinning.pin_explained_block2"),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<p>{t("connectors.pinning.pin_explained_block3")}</p>
|
||||
</div>
|
||||
<div className="py-7 px-9 space-y-2 flex-col">
|
||||
<div className="w-full text-white text-md flex flex-col gap-y-2">
|
||||
<p>
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t("connectors.pinning.pin_explained_block1"),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t("connectors.pinning.pin_explained_block2"),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<p>{t("connectors.pinning.pin_explained_block3")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full justify-end items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b">
|
||||
<button
|
||||
onClick={dismissAlert}
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
{t("connectors.pinning.accept")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</ModalBody>
|
||||
<ModalFooter className="justify-end">
|
||||
<ModalPrimaryButton onClick={dismissAlert}>
|
||||
{t("connectors.pinning.accept")}
|
||||
</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -388,51 +386,44 @@ const DocumentWatchAlert = memo(() => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ModalWrapper isOpen={showAlert} noPortal={true}>
|
||||
<div className="w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="flex items-center gap-2">
|
||||
<Eye
|
||||
className="text-theme-text-primary text-lg w-6 h-6"
|
||||
weight="regular"
|
||||
<Modal isOpen={showAlert} noPortal={true} onClose={dismissAlert}>
|
||||
<ModalHeader
|
||||
title={
|
||||
<span className="flex items-center gap-x-2">
|
||||
<Eye className="w-6 h-6" weight="regular" />
|
||||
{t("connectors.watching.what_watching")}
|
||||
</span>
|
||||
}
|
||||
onClose={dismissAlert}
|
||||
/>
|
||||
<ModalBody>
|
||||
<div className="w-full text-zinc-300 light:text-slate-700 text-md flex flex-col gap-y-2">
|
||||
<p>
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t("connectors.watching.watch_explained_block1"),
|
||||
}}
|
||||
/>
|
||||
<h3 className="text-xl font-semibold text-white">
|
||||
{t("connectors.watching.what_watching")}
|
||||
</h3>
|
||||
</div>
|
||||
</p>
|
||||
<p>{t("connectors.watching.watch_explained_block2")}</p>
|
||||
<p>
|
||||
{t("connectors.watching.watch_explained_block3_start")}
|
||||
<Link
|
||||
to={paths.experimental.liveDocumentSync.manage()}
|
||||
className="text-blue-600 underline"
|
||||
>
|
||||
{t("connectors.watching.watch_explained_block3_link")}
|
||||
</Link>
|
||||
{t("connectors.watching.watch_explained_block3_end")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="py-7 px-9 space-y-2 flex-col">
|
||||
<div className="w-full text-white text-md flex flex-col gap-y-2">
|
||||
<p>
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t("connectors.watching.watch_explained_block1"),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<p>{t("connectors.watching.watch_explained_block2")}</p>
|
||||
<p>
|
||||
{t("connectors.watching.watch_explained_block3_start")}
|
||||
<Link
|
||||
to={paths.experimental.liveDocumentSync.manage()}
|
||||
className="text-blue-600 underline"
|
||||
>
|
||||
{t("connectors.watching.watch_explained_block3_link")}
|
||||
</Link>
|
||||
{t("connectors.watching.watch_explained_block3_end")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full justify-end items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b">
|
||||
<button
|
||||
onClick={dismissAlert}
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
{t("connectors.watching.accept")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</ModalBody>
|
||||
<ModalFooter className="justify-end">
|
||||
<ModalPrimaryButton onClick={dismissAlert}>
|
||||
{t("connectors.watching.accept")}
|
||||
</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -8,8 +8,14 @@ import { isMobileOnly } from "react-device-detect";
|
||||
import useUser from "../../../hooks/useUser";
|
||||
import DocumentSettings from "./Documents";
|
||||
import DataConnectors from "./DataConnectors";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal, {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
} from "@/components/lib/Modal";
|
||||
import { EmbeddingProgressProvider } from "@/EmbeddingProgressContext";
|
||||
import { useModalEscape } from "@/hooks/useModalEscape";
|
||||
|
||||
const noop = () => {};
|
||||
const ManageWorkspace = ({ hideModal = noop, providedSlug = null }) => {
|
||||
@@ -40,43 +46,22 @@ const ManageWorkspace = ({ hideModal = noop, providedSlug = null }) => {
|
||||
|
||||
if (isMobileOnly) {
|
||||
return (
|
||||
<ModalWrapper isOpen={true}>
|
||||
<div className="w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
{t("connectors.manage.editing")} "{workspace.name}"
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={hideModal}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="h-full w-full overflow-y-auto"
|
||||
style={{ maxHeight: "calc(100vh - 200px)" }}
|
||||
>
|
||||
<div className="py-7 px-9 space-y-2 flex-col">
|
||||
<p className="text-white">
|
||||
{t("connectors.manage.desktop-only")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full justify-end items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b">
|
||||
<button
|
||||
onClick={hideModal}
|
||||
type="button"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
{t("connectors.manage.dismiss")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
<Modal isOpen={true} onClose={hideModal} size="md">
|
||||
<ModalHeader
|
||||
title={`${t("connectors.manage.editing")} "${workspace.name}"`}
|
||||
onClose={hideModal}
|
||||
/>
|
||||
<ModalBody>
|
||||
<p className="text-zinc-300 light:text-slate-700">
|
||||
{t("connectors.manage.desktop-only")}
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter className="justify-end">
|
||||
<ModalPrimaryButton type="button" onClick={hideModal}>
|
||||
{t("connectors.manage.dismiss")}
|
||||
</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,14 +69,14 @@ const ManageWorkspace = ({ hideModal = noop, providedSlug = null }) => {
|
||||
<div className="w-screen h-screen fixed top-0 left-0 flex justify-center items-center z-99">
|
||||
<div className="backdrop h-full w-full absolute top-0 z-10" />
|
||||
<div className="absolute max-h-full w-fit transition duration-300 z-20 md:overflow-y-auto py-10">
|
||||
<div className="relative bg-theme-bg-secondary rounded-[12px] shadow border-2 border-theme-modal-border">
|
||||
<div className="flex items-start justify-between p-2 rounded-t border-theme-modal-border relative">
|
||||
<div className="relative bg-zinc-900 light:bg-white rounded-[12px] shadow border-2 border-zinc-800 light:border-slate-300">
|
||||
<div className="flex items-start justify-between p-2 rounded-t relative">
|
||||
<button
|
||||
onClick={hideModal}
|
||||
type="button"
|
||||
className="z-29 text-white bg-transparent rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
className="z-29 border-none bg-transparent rounded-lg text-sm p-1.5 ml-auto inline-flex items-center text-zinc-50 light:text-slate-900 hover:bg-zinc-800 light:hover:bg-slate-100 transition-colors duration-200"
|
||||
>
|
||||
<X size={20} weight="bold" className="text-white" />
|
||||
<X size={20} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -121,23 +106,23 @@ const ModalTabSwitcher = ({ selectedTab, setSelectedTab }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="w-full flex justify-center z-10 relative">
|
||||
<div className="gap-x-2 flex justify-center -mt-[68px] mb-10 bg-theme-bg-secondary p-1 rounded-xl shadow border-2 border-theme-modal-border w-fit">
|
||||
<div className="gap-x-2 flex justify-center -mt-[68px] mb-10 bg-zinc-900 light:bg-white p-1 rounded-xl shadow border-2 border-zinc-800 light:border-slate-300 w-fit">
|
||||
<button
|
||||
onClick={() => setSelectedTab("documents")}
|
||||
className={`border-none px-4 py-2 rounded-[8px] font-semibold hover:bg-theme-modal-border hover:bg-opacity-60 ${
|
||||
className={`border-none px-4 py-2 rounded-[8px] font-semibold hover:bg-zinc-800 light:hover:bg-sky-100 ${
|
||||
selectedTab === "documents"
|
||||
? "bg-theme-modal-border font-bold text-white light:bg-[#E0F2FE] light:text-[#026AA2]"
|
||||
: "text-white/20 font-medium hover:text-white light:bg-white light:text-[#535862] light:hover:bg-[#E0F2FE]"
|
||||
? "bg-zinc-800 font-bold text-white light:bg-sky-100 light:text-sky-700"
|
||||
: "text-white/20 font-medium hover:text-white light:bg-white light:text-slate-500"
|
||||
}`}
|
||||
>
|
||||
{t("connectors.manage.documents")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedTab("dataConnectors")}
|
||||
className={`border-none px-4 py-2 rounded-[8px] font-semibold hover:bg-theme-modal-border hover:bg-opacity-60 ${
|
||||
className={`border-none px-4 py-2 rounded-[8px] font-semibold hover:bg-zinc-800 light:hover:bg-sky-100 ${
|
||||
selectedTab === "dataConnectors"
|
||||
? "bg-theme-modal-border font-bold text-white light:bg-[#E0F2FE] light:text-[#026AA2]"
|
||||
: "text-white/20 font-medium hover:text-white light:bg-white light:text-[#535862] light:hover:bg-[#E0F2FE]"
|
||||
? "bg-zinc-800 font-bold text-white light:bg-sky-100 light:text-sky-700"
|
||||
: "text-white/20 font-medium hover:text-white light:bg-white light:text-slate-500"
|
||||
}`}
|
||||
>
|
||||
{t("connectors.manage.data-connectors")}
|
||||
@@ -161,17 +146,7 @@ export function useManageWorkspaceModal() {
|
||||
setShowing(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function onEscape(event) {
|
||||
if (!showing || event.key !== "Escape") return;
|
||||
setShowing(false);
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", onEscape);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onEscape);
|
||||
};
|
||||
}, [showing]);
|
||||
useModalEscape(showing, hideModal);
|
||||
|
||||
return { showing, showModal, hideModal };
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import Workspace from "@/models/workspace";
|
||||
import paths from "@/utils/paths";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal, {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalInput,
|
||||
} from "@/components/lib/Modal";
|
||||
|
||||
const noop = () => false;
|
||||
export default function NewWorkspaceModal({ hideModal = noop }) {
|
||||
@@ -24,64 +29,31 @@ export default function NewWorkspaceModal({ hideModal = noop }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalWrapper isOpen={true}>
|
||||
<div className="w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
{t("new-workspace.title")}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={hideModal}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="h-full w-full overflow-y-auto"
|
||||
style={{ maxHeight: "calc(100vh - 200px)" }}
|
||||
>
|
||||
<form ref={formEl} onSubmit={handleCreate}>
|
||||
<div className="py-7 px-9 space-y-2 flex-col">
|
||||
<div className="w-full flex flex-col gap-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
{t("common.workspaces-name")}
|
||||
</label>
|
||||
<input
|
||||
name="name"
|
||||
type="text"
|
||||
id="name"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder={t("new-workspace.placeholder")}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-red-400 text-sm">Error: {error}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full justify-end items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b">
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
<Modal isOpen={true} onClose={hideModal} size="md">
|
||||
<form
|
||||
ref={formEl}
|
||||
onSubmit={handleCreate}
|
||||
className="flex flex-col gap-y-5"
|
||||
>
|
||||
<ModalHeader title={t("new-workspace.title")} onClose={hideModal} />
|
||||
<ModalBody>
|
||||
<ModalInput
|
||||
label={t("common.workspaces-name")}
|
||||
name="name"
|
||||
type="text"
|
||||
id="name"
|
||||
placeholder={t("new-workspace.placeholder")}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
autoFocus={true}
|
||||
/>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
</ModalBody>
|
||||
<ModalFooter className="justify-end">
|
||||
<ModalPrimaryButton type="submit">Save</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import System from "../../../models/system";
|
||||
import { AUTH_TOKEN, AUTH_USER } from "../../../utils/constants";
|
||||
import paths from "../../../utils/paths";
|
||||
import showToast from "@/utils/toast";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import RecoveryCodeModal from "@/components/Modals/DisplayRecoveryCodeModal";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -353,13 +353,17 @@ export default function MultiUserAuth() {
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ModalWrapper isOpen={isRecoveryCodeModalOpen} noPortal={true}>
|
||||
<Modal
|
||||
isOpen={isRecoveryCodeModalOpen}
|
||||
noPortal={true}
|
||||
onClose={closeRecoveryCodeModal}
|
||||
>
|
||||
<RecoveryCodeModal
|
||||
recoveryCodes={recoveryCodes}
|
||||
onDownloadComplete={handleDownloadComplete}
|
||||
onClose={closeRecoveryCodeModal}
|
||||
/>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
|
||||
import System from "../../../models/system";
|
||||
import { AUTH_TOKEN } from "../../../utils/constants";
|
||||
import paths from "../../../utils/paths";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import RecoveryCodeModal from "@/components/Modals/DisplayRecoveryCodeModal";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -115,13 +115,17 @@ export default function SingleUserAuth() {
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ModalWrapper isOpen={isRecoveryCodeModalOpen} noPortal={true}>
|
||||
<Modal
|
||||
isOpen={isRecoveryCodeModalOpen}
|
||||
noPortal={true}
|
||||
onClose={closeRecoveryCodeModal}
|
||||
>
|
||||
<RecoveryCodeModal
|
||||
recoveryCodes={recoveryCodes}
|
||||
onDownloadComplete={handleDownloadComplete}
|
||||
onClose={closeRecoveryCodeModal}
|
||||
/>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,17 @@ import System from "@/models/system";
|
||||
import Appearance from "@/models/appearance";
|
||||
import { AUTH_USER } from "@/utils/constants";
|
||||
import showToast from "@/utils/toast";
|
||||
import { Info, Plus, X } from "@phosphor-icons/react";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import { Info, Plus } from "@phosphor-icons/react";
|
||||
import Modal, {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalInput,
|
||||
ModalTextarea,
|
||||
ModalLabel,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
} from "@/components/lib/Modal";
|
||||
import { useTheme } from "@/hooks/useTheme";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState, useEffect } from "react";
|
||||
@@ -76,152 +85,101 @@ export default function AccountModal({ user, hideModal }) {
|
||||
}
|
||||
};
|
||||
return (
|
||||
<ModalWrapper isOpen={true}>
|
||||
<div className="w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
{t("profile_settings.edit_account")}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={hideModal}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="h-full w-full overflow-y-auto"
|
||||
style={{ maxHeight: "calc(100vh - 200px)" }}
|
||||
>
|
||||
<form onSubmit={handleUpdate} className="space-y-6">
|
||||
<div className="flex flex-col md:flex-row items-center justify-center gap-8">
|
||||
<div className="flex flex-col items-center">
|
||||
<label className="group w-48 h-48 flex flex-col items-center justify-center bg-theme-bg-primary hover:bg-theme-bg-secondary transition-colors duration-300 rounded-full mt-8 border-2 border-dashed border-white light:border-[#686C6F] light:bg-[#E0F2FE] light:hover:bg-transparent cursor-pointer hover:opacity-60">
|
||||
<input
|
||||
id="logo-upload"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleFileUpload}
|
||||
<Modal isOpen={true} onClose={hideModal} size="lg">
|
||||
<form onSubmit={handleUpdate} className="flex flex-col gap-y-5">
|
||||
<ModalHeader
|
||||
title={t("profile_settings.edit_account")}
|
||||
onClose={hideModal}
|
||||
/>
|
||||
<ModalBody>
|
||||
<div className="flex flex-col md:flex-row items-center justify-center gap-8">
|
||||
<div className="flex flex-col items-center">
|
||||
<label className="group w-48 h-48 flex flex-col items-center justify-center bg-zinc-800 hover:bg-zinc-700 light:bg-sky-100 light:hover:bg-transparent transition-colors duration-300 rounded-full border-2 border-dashed border-white light:border-slate-400 cursor-pointer hover:opacity-60">
|
||||
<input
|
||||
id="logo-upload"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
{pfp ? (
|
||||
<img
|
||||
src={pfp}
|
||||
alt="User profile picture"
|
||||
className="w-48 h-48 rounded-full object-cover bg-white"
|
||||
/>
|
||||
{pfp ? (
|
||||
<img
|
||||
src={pfp}
|
||||
alt="User profile picture"
|
||||
className="w-48 h-48 rounded-full object-cover bg-white"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center p-3">
|
||||
<Plus className="w-8 h-8 text-theme-text-secondary m-2" />
|
||||
<span className="text-theme-text-secondary text-opacity-80 text-sm font-semibold">
|
||||
{t("profile_settings.profile_picture")}
|
||||
</span>
|
||||
<span className="text-theme-text-secondary text-opacity-60 text-xs">
|
||||
800 x 800
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
{pfp && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemovePfp}
|
||||
className="mt-3 text-theme-text-secondary text-opacity-60 text-sm font-medium hover:underline"
|
||||
>
|
||||
{t("profile_settings.remove_profile_picture")}
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center p-3">
|
||||
<Plus className="w-8 h-8 text-zinc-400 light:text-slate-500 m-2" />
|
||||
<span className="text-zinc-400 light:text-slate-500 text-sm font-semibold">
|
||||
{t("profile_settings.profile_picture")}
|
||||
</span>
|
||||
<span className="text-zinc-400 light:text-slate-500 text-xs">
|
||||
800 x 800
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-4 px-6">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="username"
|
||||
className="block mb-2 text-sm font-medium text-theme-text-primary"
|
||||
</label>
|
||||
{pfp && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemovePfp}
|
||||
className="border-none bg-transparent mt-3 text-zinc-400 light:text-slate-500 text-sm font-medium hover:underline"
|
||||
>
|
||||
{t("profile_settings.username")}
|
||||
</label>
|
||||
<input
|
||||
name="username"
|
||||
type="text"
|
||||
className="border-none bg-theme-settings-input-bg placeholder:text-theme-settings-input-placeholder border-gray-500 text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="User's username"
|
||||
minLength={USERNAME_MIN_LENGTH}
|
||||
maxLength={USERNAME_MAX_LENGTH}
|
||||
pattern={USERNAME_PATTERN}
|
||||
defaultValue={user.username}
|
||||
required
|
||||
autoComplete="off"
|
||||
/>
|
||||
<p className="mt-2 text-xs text-white/60">
|
||||
{t("common.username_requirements")}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
{t("profile_settings.new_password")}
|
||||
</label>
|
||||
<input
|
||||
name="password"
|
||||
type="text"
|
||||
className="border-none bg-theme-settings-input-bg placeholder:text-theme-settings-input-placeholder border-gray-500 text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder={`${user.username}'s new password`}
|
||||
minLength={8}
|
||||
/>
|
||||
<p className="mt-2 text-xs text-white/60">
|
||||
{t("profile_settings.password_description")}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="bio"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Bio
|
||||
</label>
|
||||
<textarea
|
||||
name="bio"
|
||||
className="border-none bg-theme-settings-input-bg placeholder:text-theme-settings-input-placeholder border-gray-500 text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5 min-h-[100px] resize-y"
|
||||
placeholder="Tell us about yourself..."
|
||||
defaultValue={user.bio}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-x-16">
|
||||
<div className="flex flex-col gap-y-6">
|
||||
<ThemePreference />
|
||||
<LanguagePreference />
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-6">
|
||||
<AutoSubmitPreference />
|
||||
<AutoSpeakPreference />
|
||||
</div>
|
||||
</div>
|
||||
{t("profile_settings.remove_profile_picture")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between items-center border-t border-theme-modal-border pt-4 p-6">
|
||||
<button
|
||||
onClick={hideModal}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
{t("profile_settings.cancel")}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
{t("profile_settings.update_account")}
|
||||
</button>
|
||||
</div>
|
||||
<ModalInput
|
||||
label={t("profile_settings.username")}
|
||||
name="username"
|
||||
type="text"
|
||||
placeholder="User's username"
|
||||
minLength={USERNAME_MIN_LENGTH}
|
||||
maxLength={USERNAME_MAX_LENGTH}
|
||||
pattern={USERNAME_PATTERN}
|
||||
defaultValue={user.username}
|
||||
required
|
||||
autoComplete="off"
|
||||
hint={t("common.username_requirements")}
|
||||
/>
|
||||
<ModalInput
|
||||
label={t("profile_settings.new_password")}
|
||||
name="password"
|
||||
type="text"
|
||||
placeholder={`${user.username}'s new password`}
|
||||
minLength={8}
|
||||
hint={t("profile_settings.password_description")}
|
||||
/>
|
||||
<ModalTextarea
|
||||
label="Bio"
|
||||
name="bio"
|
||||
placeholder="Tell us about yourself..."
|
||||
defaultValue={user.bio}
|
||||
rows={4}
|
||||
/>
|
||||
<div className="flex gap-x-16">
|
||||
<div className="flex flex-col gap-y-6">
|
||||
<ThemePreference />
|
||||
<LanguagePreference />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
<div className="flex flex-col gap-y-6">
|
||||
<AutoSubmitPreference />
|
||||
<AutoSpeakPreference />
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalSecondaryButton type="button" onClick={hideModal}>
|
||||
{t("profile_settings.cancel")}
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton type="submit">
|
||||
{t("profile_settings.update_account")}
|
||||
</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -235,15 +193,12 @@ function LanguagePreference() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="userLang"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
<ModalLabel htmlFor="userLang" className="block mb-2">
|
||||
{t("profile_settings.language")}
|
||||
</label>
|
||||
</ModalLabel>
|
||||
<select
|
||||
name="userLang"
|
||||
className="border-none bg-theme-settings-input-bg w-fit mt-2 px-4 focus:outline-primary-button active:outline-primary-button outline-none text-white text-sm rounded-lg block py-2"
|
||||
className="bg-zinc-800 border border-zinc-800 light:bg-white light:border-slate-300 w-fit mt-2 px-4 outline-none focus:border-sky-500 light:focus:border-sky-500 text-zinc-100 light:text-slate-900 text-sm rounded-lg block py-2"
|
||||
defaultValue={currentLanguage || "en"}
|
||||
onChange={(e) => changeLanguage(e.target.value)}
|
||||
>
|
||||
@@ -264,17 +219,14 @@ function ThemePreference() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="theme"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
<ModalLabel htmlFor="theme" className="block mb-2">
|
||||
{t("profile_settings.theme")}
|
||||
</label>
|
||||
</ModalLabel>
|
||||
<select
|
||||
name="theme"
|
||||
value={theme}
|
||||
onChange={(e) => setTheme(e.target.value)}
|
||||
className="border-none bg-theme-settings-input-bg w-fit px-4 focus:outline-primary-button active:outline-primary-button outline-none text-white text-sm rounded-lg block py-2"
|
||||
className="bg-zinc-800 border border-zinc-800 light:bg-white light:border-slate-300 w-fit px-4 outline-none focus:border-sky-500 light:focus:border-sky-500 text-zinc-100 light:text-slate-900 text-sm rounded-lg block py-2"
|
||||
>
|
||||
{Object.entries(availableThemes).map(([key, value]) => (
|
||||
<option key={key} value={key}>
|
||||
|
||||
+53
-80
@@ -1,13 +1,12 @@
|
||||
import { Fragment, useState, useEffect } from "react";
|
||||
import { decode as HTMLDecode } from "he";
|
||||
import truncate from "truncate";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal, { ModalHeader, ModalBody } from "@/components/lib/Modal";
|
||||
import {
|
||||
FileText,
|
||||
Info,
|
||||
ArrowSquareOut,
|
||||
GithubLogo,
|
||||
X,
|
||||
YoutubeLogo,
|
||||
LinkSimple,
|
||||
GitlabLogo,
|
||||
@@ -202,87 +201,61 @@ export function CitationDetailModal({ source, onClose }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ModalWrapper isOpen={!!source}>
|
||||
<div className="w-full max-w-2xl bg-zinc-900 light:bg-white rounded-lg shadow border-2 border-zinc-700 light:border-slate-300 overflow-hidden">
|
||||
<div className="relative p-6 border-b rounded-t border-zinc-700 light:border-slate-300">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
{isUrl ? (
|
||||
<a
|
||||
href={linkTo}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-xl w-[90%] font-semibold text-white light:text-slate-900 whitespace-nowrap hover:underline hover:text-blue-300 light:hover:text-blue-600 flex items-center gap-x-1"
|
||||
>
|
||||
<div className="flex items-center gap-x-1 max-w-full overflow-hidden">
|
||||
<h3 className="truncate text-ellipsis whitespace-nowrap overflow-hidden w-full">
|
||||
{webpageUrl}
|
||||
</h3>
|
||||
<ArrowSquareOut className="flex-shrink-0" />
|
||||
</div>
|
||||
</a>
|
||||
) : (
|
||||
<h3 className="text-xl font-semibold text-white light:text-slate-900 overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
{truncate(title, 45)}
|
||||
</h3>
|
||||
)}
|
||||
</div>
|
||||
{references > 1 && (
|
||||
<p className="text-xs text-zinc-400 light:text-slate-500 mt-2">
|
||||
Referenced {references} times.
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-zinc-700 light:hover:bg-slate-200 border-transparent border"
|
||||
>
|
||||
<X
|
||||
size={24}
|
||||
weight="bold"
|
||||
className="text-white light:text-slate-900"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="h-full w-full overflow-y-auto"
|
||||
style={{ maxHeight: "calc(100vh - 200px)" }}
|
||||
>
|
||||
<div className="py-7 px-9 space-y-2 flex-col">
|
||||
{chunks.map(({ text, score }, idx) => (
|
||||
<Fragment key={idx}>
|
||||
<div className="pt-6 text-white light:text-slate-900">
|
||||
<div className="flex flex-col w-full justify-start pb-6 gap-y-1">
|
||||
<p className="text-white light:text-slate-900 whitespace-pre-line">
|
||||
{HTMLDecode(omitChunkHeader(text))}
|
||||
</p>
|
||||
<Modal isOpen={!!source} onClose={onClose} size="lg">
|
||||
<ModalHeader
|
||||
title={
|
||||
isUrl ? (
|
||||
<a
|
||||
href={linkTo}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center gap-x-1 max-w-full overflow-hidden hover:underline hover:text-blue-300 light:hover:text-blue-600"
|
||||
>
|
||||
<span className="truncate">{webpageUrl}</span>
|
||||
<ArrowSquareOut className="flex-shrink-0" />
|
||||
</a>
|
||||
) : (
|
||||
truncate(title, 45)
|
||||
)
|
||||
}
|
||||
subtitle={
|
||||
references > 1 ? `Referenced ${references} times.` : undefined
|
||||
}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<ModalBody>
|
||||
{chunks.map(({ text, score }, idx) => (
|
||||
<Fragment key={idx}>
|
||||
<div className="text-zinc-100 light:text-slate-900">
|
||||
<div className="flex flex-col w-full justify-start gap-y-1">
|
||||
<p className="text-zinc-100 light:text-slate-900 whitespace-pre-line">
|
||||
{HTMLDecode(omitChunkHeader(text))}
|
||||
</p>
|
||||
|
||||
{!!score && (
|
||||
<div className="w-full flex items-center text-xs text-white/60 light:text-slate-500 gap-x-2 cursor-default">
|
||||
<div
|
||||
data-tooltip-id="similarity-score"
|
||||
data-tooltip-content={`This is the semantic similarity score of this chunk of text compared to your query calculated by the vector database.`}
|
||||
className="flex items-center gap-x-1"
|
||||
>
|
||||
<Info size={14} />
|
||||
<p>
|
||||
{toPercentString(score)}{" "}
|
||||
{t("chat_window.similarity_match")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!!score && (
|
||||
<div className="w-full flex items-center text-xs text-zinc-400 light:text-slate-500 gap-x-2 cursor-default">
|
||||
<div
|
||||
data-tooltip-id="similarity-score"
|
||||
data-tooltip-content={`This is the semantic similarity score of this chunk of text compared to your query calculated by the vector database.`}
|
||||
className="flex items-center gap-x-1"
|
||||
>
|
||||
<Info size={14} />
|
||||
<p>
|
||||
{toPercentString(score)}{" "}
|
||||
{t("chat_window.similarity_match")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{idx !== chunks.length - 1 && (
|
||||
<hr className="border-zinc-700 light:border-slate-300" />
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
<div className="mb-6"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</div>
|
||||
</div>
|
||||
{idx !== chunks.length - 1 && (
|
||||
<hr className="border-zinc-800 light:border-slate-200" />
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+20
-42
@@ -1,6 +1,5 @@
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal, { ModalHeader, ModalBody } from "@/components/lib/Modal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
@@ -25,12 +24,10 @@ export default function CopyLinkToChatRow() {
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
});
|
||||
|
||||
if (!window.localStorage.getItem(SEEN_COPY_LINK_CHAT_ALERT)) openModal();
|
||||
}
|
||||
|
||||
function handleCloseModal() {
|
||||
closeModal();
|
||||
window.localStorage.setItem(SEEN_COPY_LINK_CHAT_ALERT, "1");
|
||||
if (!window.localStorage.getItem(SEEN_COPY_LINK_CHAT_ALERT)) {
|
||||
window.localStorage.setItem(SEEN_COPY_LINK_CHAT_ALERT, "1");
|
||||
openModal();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -45,7 +42,7 @@ export default function CopyLinkToChatRow() {
|
||||
</div>
|
||||
<CopyLinkModal
|
||||
isOpen={isOpen}
|
||||
closeModal={handleCloseModal}
|
||||
closeModal={closeModal}
|
||||
url={getChatUrl()}
|
||||
/>
|
||||
</>
|
||||
@@ -53,40 +50,21 @@ export default function CopyLinkToChatRow() {
|
||||
}
|
||||
|
||||
function CopyLinkModal({ isOpen, closeModal, url }) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<div
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="w-[500px] bg-theme-bg-sidebar px-6 py-4 rounded-lg flex flex-col items-center justify-between relative shadow-lg border border-white/10"
|
||||
>
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<div className="text-white text-left font-medium text-lg">
|
||||
Chat link copied!
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="text-white opacity-60 hover:text-white hover:opacity-100 border-none outline-none"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
<Modal isOpen={isOpen} onClose={closeModal}>
|
||||
<ModalHeader title="Chat link copied!" onClose={closeModal} />
|
||||
<ModalBody>
|
||||
<p className="text-sm text-zinc-400 light:text-slate-500">
|
||||
The link to this chat has been copied to your clipboard.
|
||||
</p>
|
||||
<p className="text-sm text-zinc-400 light:text-slate-500">
|
||||
This <strong>does not</strong> change permissions on the chat and is
|
||||
simply a way for you to quick link to you own chats.
|
||||
</p>
|
||||
<div className="px-3 py-2 rounded-md bg-zinc-800 light:bg-slate-100 border border-white/10 light:border-slate-300 text-sm text-white light:text-slate-900 break-all select-all">
|
||||
{url}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-full mt-4">
|
||||
<p className="text-sm text-zinc-400 light:text-slate-500">
|
||||
The link to this chat has been copied to your clipboard.
|
||||
</p>
|
||||
<p className="text-sm text-zinc-400 light:text-slate-500 pt-1">
|
||||
This <strong>does not</strong> change permissions on the chat and is
|
||||
simply a way for you to quick link to you own chats.
|
||||
</p>
|
||||
<div className="mt-3 px-3 py-2 rounded-md bg-theme-bg-primary border border-white/10 text-sm text-white break-all select-all">
|
||||
{url}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
+58
-70
@@ -1,5 +1,11 @@
|
||||
import { CircleNotch } from "@phosphor-icons/react";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal, {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
} from "@/components/lib/Modal";
|
||||
import pluralize from "pluralize";
|
||||
import { numberWithCommas } from "@/utils/numbers";
|
||||
import useUser from "@/hooks/useUser";
|
||||
@@ -24,83 +30,65 @@ export default function FileUploadWarningModal({
|
||||
|
||||
if (isEmbedding) {
|
||||
return (
|
||||
<ModalWrapper isOpen={show}>
|
||||
<div className="relative max-w-[600px] bg-theme-bg-primary rounded-lg shadow border border-theme-modal-border">
|
||||
<div className="p-6 flex flex-col items-center justify-center">
|
||||
<p className="text-white text-lg font-semibold mb-4">
|
||||
Embedding {embedProgress + 1} of {fileCount}{" "}
|
||||
{pluralize("file", fileCount)}
|
||||
</p>
|
||||
<CircleNotch size={32} className="animate-spin text-white" />
|
||||
<p className="text-white/60 text-sm mt-2">
|
||||
Please wait while we embed your files...
|
||||
</p>
|
||||
</div>
|
||||
<Modal isOpen={show} onClose={onClose} size="lg">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<p className="text-slate-50 light:text-slate-900 text-lg font-semibold mb-4">
|
||||
Embedding {embedProgress + 1} of {fileCount}{" "}
|
||||
{pluralize("file", fileCount)}
|
||||
</p>
|
||||
<CircleNotch
|
||||
size={32}
|
||||
className="animate-spin text-slate-50 light:text-slate-900"
|
||||
/>
|
||||
<p className="text-zinc-400 light:text-slate-600 text-sm mt-2">
|
||||
Please wait while we embed your files...
|
||||
</p>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalWrapper isOpen={show}>
|
||||
<div className="relative max-w-[600px] bg-theme-bg-primary rounded-lg shadow border border-theme-modal-border">
|
||||
<div className="relative p-6 border-b border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
Context Window Warning
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="py-7 px-9 space-y-4">
|
||||
<p className="text-theme-text-primary text-sm">
|
||||
Your workspace is using {numberWithCommas(tokenCount)} of{" "}
|
||||
{numberWithCommas(maxTokens)} available tokens. We recommend keeping
|
||||
usage below {(Workspace.maxContextWindowLimit * 100).toFixed(0)}% to
|
||||
ensure the best chat experience. Adding {fileCount} more{" "}
|
||||
{pluralize("file", fileCount)} would exceed this limit.{" "}
|
||||
<Link
|
||||
target="_blank"
|
||||
to={Paths.documentation.contextWindows()}
|
||||
className="text-theme-text-secondary text-sm underline"
|
||||
>
|
||||
Learn more about context windows →
|
||||
</Link>
|
||||
</p>
|
||||
<p className="text-theme-text-primary text-sm">
|
||||
Choose how you would like to proceed with these uploads.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b">
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="border-none transition-all duration-300 bg-theme-modal-border text-white hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
<Modal isOpen={show} onClose={onClose} size="lg">
|
||||
<ModalHeader title="Context Window Warning" onClose={onClose} />
|
||||
<ModalBody>
|
||||
<p className="text-zinc-300 light:text-slate-700 text-sm">
|
||||
Your workspace is using {numberWithCommas(tokenCount)} of{" "}
|
||||
{numberWithCommas(maxTokens)} available tokens. We recommend keeping
|
||||
usage below {(Workspace.maxContextWindowLimit * 100).toFixed(0)}% to
|
||||
ensure the best chat experience. Adding {fileCount} more{" "}
|
||||
{pluralize("file", fileCount)} would exceed this limit.{" "}
|
||||
<Link
|
||||
target="_blank"
|
||||
to={Paths.documentation.contextWindows()}
|
||||
className="text-zinc-400 light:text-slate-500 text-sm underline"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<div className="flex w-full justify-end items-center space-x-2">
|
||||
<button
|
||||
onClick={onContinue}
|
||||
Learn more about context windows →
|
||||
</Link>
|
||||
</p>
|
||||
<p className="text-zinc-300 light:text-slate-700 text-sm">
|
||||
Choose how you would like to proceed with these uploads.
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalSecondaryButton onClick={onClose} type="button">
|
||||
Cancel
|
||||
</ModalSecondaryButton>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<ModalSecondaryButton onClick={onContinue} type="button">
|
||||
Continue Anyway
|
||||
</ModalSecondaryButton>
|
||||
{canEmbed && (
|
||||
<ModalPrimaryButton
|
||||
onClick={onEmbed}
|
||||
disabled={isEmbedding || !canEmbed}
|
||||
type="button"
|
||||
className="border-none transition-all duration-300 bg-theme-modal-border text-white hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Continue Anyway
|
||||
</button>
|
||||
{canEmbed && (
|
||||
<button
|
||||
onClick={onEmbed}
|
||||
disabled={isEmbedding || !canEmbed}
|
||||
type="button"
|
||||
className="border-none transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Embed {pluralize("File", fileCount)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
Embed {pluralize("File", fileCount)}
|
||||
</ModalPrimaryButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
+37
-57
@@ -1,7 +1,13 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal, {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
ModalTextarea,
|
||||
} from "@/components/lib/Modal";
|
||||
|
||||
/**
|
||||
* @param {Object} props
|
||||
@@ -48,60 +54,34 @@ export default function MemoryModal({
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<div className="bg-zinc-900 light:bg-white border border-zinc-800 light:border-slate-300 rounded-lg p-6 w-[400px] flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-start justify-between">
|
||||
<p className="font-semibold text-base leading-6 text-zinc-50 light:text-slate-800">
|
||||
{title}
|
||||
</p>
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="text-zinc-400 light:text-slate-400 hover:text-zinc-50 light:hover:text-slate-900 transition-colors border-none bg-transparent cursor-pointer"
|
||||
>
|
||||
<X size={16} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs leading-4 text-zinc-400 light:text-slate-500">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-sm font-medium text-zinc-200 light:text-slate-700">
|
||||
{t("chat_window.memories.modal.label")}
|
||||
</label>
|
||||
<textarea
|
||||
autoFocus
|
||||
onFocus={(e) => {
|
||||
const len = e.currentTarget.value.length;
|
||||
e.currentTarget.setSelectionRange(len, len);
|
||||
}}
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder={t("chat_window.memories.modal.placeholder")}
|
||||
rows={4}
|
||||
className="w-full bg-zinc-800 light:bg-white text-zinc-50 light:border light:border-slate-300 light:text-slate-700 placeholder:text-zinc-500 light:placeholder:text-slate-400 text-sm rounded-lg p-3 resize-none outline-none focus:border-zinc-500 light:focus:border-slate-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-start justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="h-9 px-5 rounded-lg border border-zinc-500 light:border-slate-600 bg-transparent text-zinc-50 light:text-slate-900 text-sm font-medium cursor-pointer hover:bg-zinc-700 light:hover:bg-slate-100 transition-colors"
|
||||
>
|
||||
{t("chat_window.memories.modal.cancel")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
disabled={!content.trim()}
|
||||
className="h-9 px-5 rounded-lg border-none bg-zinc-50 light:bg-slate-900 text-zinc-900 light:text-white text-sm font-medium cursor-pointer hover:bg-white light:hover:bg-slate-800 transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="sm">
|
||||
<ModalHeader title={title} subtitle={description} onClose={onClose} />
|
||||
<ModalBody>
|
||||
<ModalTextarea
|
||||
label={t("chat_window.memories.modal.label")}
|
||||
autoFocus
|
||||
onFocus={(e) => {
|
||||
const len = e.currentTarget.value.length;
|
||||
e.currentTarget.setSelectionRange(len, len);
|
||||
}}
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder={t("chat_window.memories.modal.placeholder")}
|
||||
rows={4}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalSecondaryButton type="button" onClick={onClose}>
|
||||
{t("chat_window.memories.modal.cancel")}
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
disabled={!content.trim()}
|
||||
>
|
||||
{submitLabel}
|
||||
</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
+38
-52
@@ -1,6 +1,11 @@
|
||||
import { createPortal } from "react-dom";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import { X, WarningCircle } from "@phosphor-icons/react";
|
||||
import Modal, {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
} from "@/components/lib/Modal";
|
||||
import { WarningCircle } from "@phosphor-icons/react";
|
||||
import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -34,57 +39,38 @@ export default function SetupProvider({
|
||||
return false;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<div className="fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
{llmProvider.name} Settings
|
||||
</h3>
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={closeModal} size="lg">
|
||||
<form
|
||||
id="provider-form"
|
||||
onSubmit={handleUpdate}
|
||||
className="flex flex-col gap-y-5"
|
||||
>
|
||||
<ModalHeader
|
||||
title={`${llmProvider.name} Settings`}
|
||||
onClose={closeModal}
|
||||
/>
|
||||
<ModalBody>
|
||||
<div className="space-y-6 max-h-[60vh] overflow-y-auto p-1">
|
||||
<p className="text-sm text-zinc-400 light:text-slate-600">
|
||||
To use {llmProvider.name} as this workspace's LLM you need to set
|
||||
it up first.
|
||||
</p>
|
||||
<div>
|
||||
{llmProvider.options(settings, { credentialsOnly: true })}
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<form id="provider-form" onSubmit={handleUpdate}>
|
||||
<div className="px-7 py-6">
|
||||
<div className="space-y-6 max-h-[60vh] overflow-y-auto p-1">
|
||||
<p className="text-sm text-white/60">
|
||||
To use {llmProvider.name} as this workspace's LLM you need to
|
||||
set it up first.
|
||||
</p>
|
||||
<div>
|
||||
{llmProvider.options(settings, { credentialsOnly: true })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border px-7 pb-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
form="provider-form"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Save settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>,
|
||||
document.body
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalSecondaryButton type="button" onClick={closeModal}>
|
||||
Cancel
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton type="submit" form="provider-form">
|
||||
Save settings
|
||||
</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+56
-103
@@ -1,6 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal, {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
ModalInput,
|
||||
ModalTextarea,
|
||||
} from "@/components/lib/Modal";
|
||||
import { CMD_REGEX } from "./constants";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -26,106 +33,52 @@ export default function AddPresetModal({ isOpen, onClose, onSave }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<div className="w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
{t("chat_window.add_new_preset")}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="h-full w-full overflow-y-auto"
|
||||
style={{ maxHeight: "calc(100vh - 200px)" }}
|
||||
>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="py-7 px-9 space-y-2 flex-col">
|
||||
<div className="w-full flex flex-col gap-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="command"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
{t("chat_window.command")}
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<span className="text-white text-sm mr-2 font-bold">/</span>
|
||||
<input
|
||||
name="command"
|
||||
type="text"
|
||||
id="command"
|
||||
placeholder={t("chat_window.your_command")}
|
||||
value={command}
|
||||
onChange={handleCommandChange}
|
||||
maxLength={25}
|
||||
autoComplete="off"
|
||||
required={true}
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="prompt"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Prompt
|
||||
</label>
|
||||
<textarea
|
||||
name="prompt"
|
||||
id="prompt"
|
||||
autoComplete="off"
|
||||
placeholder={t("chat_window.placeholder_prompt")}
|
||||
required={true}
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="description"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
{t("chat_window.description")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="description"
|
||||
id="description"
|
||||
placeholder={t("chat_window.placeholder_description")}
|
||||
maxLength={80}
|
||||
autoComplete="off"
|
||||
required={true}
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full justify-end items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b">
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="transition-all duration-300 bg-transparent text-white hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
{t("chat_window.cancel")}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
{t("chat_window.save")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-y-5">
|
||||
<ModalHeader
|
||||
title={t("chat_window.add_new_preset")}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<ModalBody>
|
||||
<ModalInput
|
||||
label={t("chat_window.command")}
|
||||
name="command"
|
||||
id="command"
|
||||
leading="/"
|
||||
placeholder={t("chat_window.your_command")}
|
||||
value={command}
|
||||
onChange={handleCommandChange}
|
||||
maxLength={25}
|
||||
autoComplete="off"
|
||||
required={true}
|
||||
/>
|
||||
<ModalTextarea
|
||||
label="Prompt"
|
||||
name="prompt"
|
||||
id="prompt"
|
||||
autoComplete="off"
|
||||
placeholder={t("chat_window.placeholder_prompt")}
|
||||
required={true}
|
||||
/>
|
||||
<ModalInput
|
||||
label={t("chat_window.description")}
|
||||
name="description"
|
||||
id="description"
|
||||
placeholder={t("chat_window.placeholder_description")}
|
||||
maxLength={80}
|
||||
autoComplete="off"
|
||||
required={true}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalSecondaryButton onClick={onClose} type="button">
|
||||
{t("chat_window.cancel")}
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton type="submit">
|
||||
{t("chat_window.save")}
|
||||
</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
+56
-105
@@ -1,6 +1,14 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal, {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
ModalDangerButton,
|
||||
ModalInput,
|
||||
ModalTextarea,
|
||||
} from "@/components/lib/Modal";
|
||||
import { CMD_REGEX } from "./constants";
|
||||
|
||||
export default function EditPresetModal({
|
||||
@@ -46,110 +54,53 @@ export default function EditPresetModal({
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<div className="w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
Edit Preset
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-y-5">
|
||||
<ModalHeader title="Edit Preset" onClose={onClose} />
|
||||
<ModalBody>
|
||||
<ModalInput
|
||||
label="Command"
|
||||
name="command"
|
||||
id="command"
|
||||
leading="/"
|
||||
placeholder="your-command"
|
||||
value={command}
|
||||
onChange={handleCommandChange}
|
||||
required={true}
|
||||
/>
|
||||
<ModalTextarea
|
||||
label="Prompt"
|
||||
name="prompt"
|
||||
id="prompt"
|
||||
placeholder="This is a test prompt. Please respond with a poem about LLMs."
|
||||
defaultValue={preset.prompt}
|
||||
required={true}
|
||||
/>
|
||||
<ModalInput
|
||||
label="Description"
|
||||
name="description"
|
||||
id="description"
|
||||
defaultValue={preset.description}
|
||||
placeholder="Responds with a poem about LLMs."
|
||||
required={true}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalDangerButton
|
||||
disabled={deleting}
|
||||
onClick={handleDelete}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="h-full w-full overflow-y-auto"
|
||||
style={{ maxHeight: "calc(100vh - 200px)" }}
|
||||
>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="py-7 px-9 space-y-2 flex-col">
|
||||
<div className="w-full flex flex-col gap-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="command"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Command
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<span className="text-white text-sm mr-2 font-bold">/</span>
|
||||
<input
|
||||
type="text"
|
||||
name="command"
|
||||
placeholder="your-command"
|
||||
value={command}
|
||||
onChange={handleCommandChange}
|
||||
required={true}
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="prompt"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Prompt
|
||||
</label>
|
||||
<textarea
|
||||
name="prompt"
|
||||
placeholder="This is a test prompt. Please respond with a poem about LLMs."
|
||||
defaultValue={preset.prompt}
|
||||
required={true}
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="description"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="description"
|
||||
defaultValue={preset.description}
|
||||
placeholder="Responds with a poem about LLMs."
|
||||
required={true}
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b">
|
||||
<button
|
||||
disabled={deleting}
|
||||
onClick={handleDelete}
|
||||
type="button"
|
||||
className="border-none transition-all duration-300 bg-transparent text-red-500 hover:bg-red-500/25 px-4 py-2 rounded-lg text-sm disabled:opacity-50"
|
||||
>
|
||||
{deleting ? "Deleting..." : "Delete Preset"}
|
||||
</button>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="border-none transition-all duration-300 bg-transparent text-white hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
{deleting ? "Deleting..." : "Delete Preset"}
|
||||
</ModalDangerButton>
|
||||
<div className="flex gap-x-2">
|
||||
<ModalSecondaryButton onClick={onClose} type="button">
|
||||
Cancel
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton type="submit">Save</ModalPrimaryButton>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
import { combineLikeSources } from "../../ChatHistory/Citation";
|
||||
import SourceDetailView from "./SourceDetailView";
|
||||
import SourceItem from "../SourceItem";
|
||||
@@ -16,7 +16,7 @@ export default function MobileCitationModal({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<div className="fixed inset-0" onClick={onClose} />
|
||||
<div className="relative z-10 w-[calc(100%-40px)] max-h-[70vh] rounded-[16px] bg-zinc-800 light:bg-white light:border-2 light:border-slate-300 p-4 flex flex-col gap-4">
|
||||
{selectedSource ? (
|
||||
@@ -51,6 +51,6 @@ export default function MobileCitationModal({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,11 @@ import Workspace from "@/models/workspace";
|
||||
import LoadingChat from "./LoadingChat";
|
||||
import ChatContainer from "./ChatContainer";
|
||||
import paths from "@/utils/paths";
|
||||
import ModalWrapper from "../ModalWrapper";
|
||||
import Modal, {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
} from "@/components/lib/Modal";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
DnDFileUploaderProvider,
|
||||
@@ -81,35 +85,30 @@ export default function WorkspaceChat({ loading, workspace }) {
|
||||
return (
|
||||
<>
|
||||
{loading === false && !workspace && (
|
||||
<ModalWrapper isOpen={true}>
|
||||
<div className="w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<WarningCircle
|
||||
className="text-red-500 w-6 h-6"
|
||||
weight="fill"
|
||||
/>
|
||||
<h3 className="text-xl font-semibold text-red-500 overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
Workspace not found
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-7 px-9 space-y-2 flex-col">
|
||||
<p className="text-white text-sm">
|
||||
The workspace you're looking for is not available. It may have
|
||||
been deleted or you may not have access to it.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex w-full justify-end items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b">
|
||||
<a
|
||||
href={paths.home()}
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Return to homepage
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
<Modal isOpen={true} size="md">
|
||||
<ModalHeader
|
||||
title={
|
||||
<span className="flex items-center gap-x-2 text-red-500">
|
||||
<WarningCircle className="w-5 h-5" weight="fill" />
|
||||
Workspace not found
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<ModalBody>
|
||||
<p className="text-zinc-300 light:text-slate-700 text-sm">
|
||||
The workspace you're looking for is not available. It may have
|
||||
been deleted or you may not have access to it.
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter className="justify-end">
|
||||
<a
|
||||
href={paths.home()}
|
||||
className="flex items-center justify-center h-[34px] px-4 rounded-lg text-sm font-medium border-none bg-zinc-50 light:bg-slate-900 text-zinc-950 light:text-white hover:opacity-80 transition-all duration-200"
|
||||
>
|
||||
Return to homepage
|
||||
</a>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
)}
|
||||
<LoadingChat />
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
import { Children } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import { useModalEscape } from "@/hooks/useModalEscape";
|
||||
|
||||
/** @type {Record<string, string>} max-width per size, matched to the Figma modal frames */
|
||||
const SIZE_CLASSES = {
|
||||
sm: "max-w-[400px]",
|
||||
md: "max-w-[500px]",
|
||||
lg: "max-w-[640px]",
|
||||
xl: "max-w-[868px]",
|
||||
};
|
||||
|
||||
/**
|
||||
* Single, reusable modal shell for the app. Renders a centered, blurred
|
||||
* backdrop and the uniform modal card, then portals to the app root. Compose
|
||||
* the inner layout with the exported `ModalHeader`, `ModalBody`, and
|
||||
* `ModalFooter` pieces - the body stays unopinionated and accepts whatever JSX
|
||||
* you need.
|
||||
*
|
||||
* For modals that are not card-shaped (lightboxes, citations, etc.) pass
|
||||
* `variant="bare"` to skip the card chrome and just center the children.
|
||||
*
|
||||
* @param {Object} props - Component props
|
||||
* @param {import("react").ReactNode} props.children - The DOM/JSX to render inside the modal
|
||||
* @param {boolean} props.isOpen - Renders the modal when true
|
||||
* @param {() => void} [props.onClose] - Called when the modal requests to close (e.g. Escape key)
|
||||
* @param {"sm"|"md"|"lg"|"xl"} [props.size="md"] - Card max-width preset (400/500/640/868px)
|
||||
* @param {"default"|"bare"} [props.variant="default"] - `default` renders the uniform card; `bare` centers raw children
|
||||
* @param {string} [props.className] - Extra classes appended to the backdrop container (e.g. a higher `z-` to clear a page's fixed top bar)
|
||||
* @param {boolean} [props.closeOnEsc=true] - Whether pressing Escape calls `onClose`. Some modals require a forced choice and should opt out.
|
||||
* @param {boolean} [props.noPortal=false] - Render inline instead of portaling to #root. Used for sub-DOM modals that must render as a child element.
|
||||
* Note: this can impact the backdrop presentation due to conflicting DOM positions, so double check it renders as desired.
|
||||
*/
|
||||
export default function Modal({
|
||||
children,
|
||||
isOpen,
|
||||
onClose,
|
||||
size = "md",
|
||||
variant = "default",
|
||||
className = "",
|
||||
closeOnEsc = true,
|
||||
noPortal = false,
|
||||
}) {
|
||||
useModalEscape(isOpen && closeOnEsc, onClose);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const backdrop =
|
||||
`bg-black/60 backdrop-blur-sm fixed top-0 left-0 outline-none w-screen h-screen flex items-center justify-center z-99 ${className}`.trim();
|
||||
|
||||
if (variant === "bare") {
|
||||
const bare = <div className={backdrop}>{children}</div>;
|
||||
if (noPortal) return bare;
|
||||
return createPortal(bare, document.getElementById("root"));
|
||||
}
|
||||
|
||||
const content = (
|
||||
<div className={backdrop}>
|
||||
<div
|
||||
className={`relative w-full ${SIZE_CLASSES[size] || SIZE_CLASSES.md} mx-4 flex flex-col gap-y-5 p-6 rounded-lg shadow-xs max-h-[90vh] overflow-y-auto bg-zinc-900 light:bg-white border border-zinc-800 light:border-slate-300`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (noPortal) return content;
|
||||
return createPortal(content, document.getElementById("root"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal header: title (required), optional muted subtitle, and a close button.
|
||||
*
|
||||
* @param {Object} props - Component props
|
||||
* @param {import("react").ReactNode} props.title - The header title
|
||||
* @param {import("react").ReactNode} [props.subtitle] - Muted helper line under the title
|
||||
* @param {() => void} [props.onClose] - Renders the close button when provided
|
||||
* @param {import("react").ReactNode} [props.children] - Extra header content rendered below the subtitle
|
||||
*/
|
||||
export function ModalHeader({ title, subtitle, onClose, children }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1 w-full">
|
||||
<div className="flex items-start justify-between gap-x-2 w-full">
|
||||
<h3 className="text-base font-semibold text-slate-50 light:text-slate-900 break-words">
|
||||
{title}
|
||||
</h3>
|
||||
{onClose && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
aria-label="Close"
|
||||
className="border-none bg-transparent p-1 -m-1 rounded-lg text-slate-50 light:text-slate-900 hover:bg-zinc-800 light:hover:bg-slate-100 transition-colors duration-200"
|
||||
>
|
||||
<X size={16} weight="bold" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{subtitle && (
|
||||
<p className="text-xs text-zinc-400 light:text-slate-600">{subtitle}</p>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal body: the unopinionated content region. Stacks children with the
|
||||
* standard field spacing; pass `className` to override the layout.
|
||||
*
|
||||
* @param {Object} props - Component props
|
||||
* @param {import("react").ReactNode} props.children - Body content
|
||||
* @param {string} [props.className] - Extra classes appended to the body container
|
||||
*/
|
||||
export function ModalBody({ children, className = "" }) {
|
||||
return (
|
||||
<div className={`flex flex-col gap-y-4 w-full ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal footer: the action row. With multiple actions it defaults to
|
||||
* `justify-between` (Cancel left, primary right) to match the design; with a
|
||||
* single action it right-aligns. Override with `className` when needed.
|
||||
*
|
||||
* @param {Object} props - Component props
|
||||
* @param {import("react").ReactNode} props.children - Footer actions
|
||||
* @param {string} [props.className] - Extra classes appended to the footer container
|
||||
*/
|
||||
export function ModalFooter({ children, className = "" }) {
|
||||
const align =
|
||||
Children.count(children) > 1 ? "justify-between" : "justify-end";
|
||||
return (
|
||||
<div className={`flex items-center ${align} gap-x-2 w-full ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const FIELD_BASE =
|
||||
"w-full text-sm rounded-lg outline-none bg-zinc-800 border border-zinc-800 text-zinc-100 placeholder:text-zinc-400 light:bg-white light:border-slate-300 light:text-slate-900 light:placeholder:text-slate-400 focus:border-sky-500 light:focus:border-sky-500 disabled:opacity-50 disabled:cursor-not-allowed";
|
||||
|
||||
/**
|
||||
* Field label matching the Figma input label (Medium 14).
|
||||
*
|
||||
* @param {Object} props - Component props (forwarded to the label element)
|
||||
* @param {boolean} [props.optional] - Appends a muted "(Optional)" suffix
|
||||
* @param {string} [props.className] - Extra classes appended to the label
|
||||
*/
|
||||
export function ModalLabel({ children, optional, className = "", ...props }) {
|
||||
return (
|
||||
<label
|
||||
{...props}
|
||||
className={`text-sm font-medium text-zinc-50 light:text-slate-700 ${className}`}
|
||||
>
|
||||
{children}
|
||||
{optional && (
|
||||
<span className="font-normal text-zinc-400 light:text-slate-500">
|
||||
{" "}
|
||||
(Optional)
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Muted helper/hint line shown under a label or field (Regular 12).
|
||||
*
|
||||
* @param {Object} props - Component props
|
||||
* @param {string} [props.className] - Extra classes appended to the hint
|
||||
*/
|
||||
export function ModalHint({ children, className = "" }) {
|
||||
return (
|
||||
<p className={`text-xs text-zinc-400 light:text-slate-600 ${className}`}>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Labeled text input - the most-reused modal field. Renders label, input, and
|
||||
* optional hint with the uniform field styling. Extra props pass to `<input>`.
|
||||
*
|
||||
* @param {Object} props - Component props (forwarded to the input element)
|
||||
* @param {import("react").ReactNode} [props.label] - Field label
|
||||
* @param {import("react").ReactNode} [props.hint] - Helper line under the input
|
||||
* @param {boolean} [props.optional] - Marks the label "(Optional)"
|
||||
* @param {import("react").ReactNode} [props.leading] - Inline content rendered before the input (e.g. a "/" prefix)
|
||||
* @param {string} [props.className] - Extra classes appended to the input
|
||||
*/
|
||||
export function ModalInput({
|
||||
label,
|
||||
hint,
|
||||
optional,
|
||||
leading,
|
||||
className = "",
|
||||
id,
|
||||
name,
|
||||
...props
|
||||
}) {
|
||||
const inputId = id || name;
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1.5 w-full">
|
||||
{label && (
|
||||
<ModalLabel htmlFor={inputId} optional={optional}>
|
||||
{label}
|
||||
</ModalLabel>
|
||||
)}
|
||||
<div className="relative flex items-center w-full">
|
||||
{leading && (
|
||||
<span className="absolute left-3.5 text-sm text-zinc-400 light:text-slate-500 pointer-events-none">
|
||||
{leading}
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
id={inputId}
|
||||
name={name}
|
||||
className={`${FIELD_BASE} h-[34px] px-3.5 ${leading ? "pl-7" : ""} ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
{hint && <ModalHint>{hint}</ModalHint>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Labeled textarea matching `ModalInput`. Extra props pass to `<textarea>`.
|
||||
*
|
||||
* @param {Object} props - Component props (forwarded to the textarea element)
|
||||
* @param {import("react").ReactNode} [props.label] - Field label
|
||||
* @param {import("react").ReactNode} [props.hint] - Helper line under the textarea
|
||||
* @param {boolean} [props.optional] - Marks the label "(Optional)"
|
||||
* @param {number} [props.rows=4] - Visible text rows
|
||||
* @param {string} [props.className] - Extra classes appended to the textarea
|
||||
*/
|
||||
export function ModalTextarea({
|
||||
label,
|
||||
hint,
|
||||
optional,
|
||||
rows = 4,
|
||||
className = "",
|
||||
id,
|
||||
name,
|
||||
...props
|
||||
}) {
|
||||
const inputId = id || name;
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1.5 w-full">
|
||||
{label && (
|
||||
<ModalLabel htmlFor={inputId} optional={optional}>
|
||||
{label}
|
||||
</ModalLabel>
|
||||
)}
|
||||
<textarea
|
||||
id={inputId}
|
||||
name={name}
|
||||
rows={rows}
|
||||
className={`${FIELD_BASE} px-3.5 py-2.5 resize-none ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
{hint && <ModalHint>{hint}</ModalHint>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const BUTTON_BASE =
|
||||
"flex items-center justify-center h-[34px] px-4 rounded-lg text-sm font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed";
|
||||
|
||||
/**
|
||||
* Filled primary action button (Save/Create). Dark: light fill on dark text;
|
||||
* Light: dark fill on white text.
|
||||
*
|
||||
* @param {Object} props - Component props (forwarded to the button element)
|
||||
* @param {string} [props.className] - Extra classes appended to the button
|
||||
*/
|
||||
export function ModalPrimaryButton({ children, className = "", ...props }) {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={`${BUTTON_BASE} border-none bg-zinc-50 light:bg-slate-900 text-zinc-950 light:text-white hover:opacity-80 ${className}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Outlined secondary action button (Cancel).
|
||||
*
|
||||
* @param {Object} props - Component props (forwarded to the button element)
|
||||
* @param {string} [props.className] - Extra classes appended to the button
|
||||
*/
|
||||
export function ModalSecondaryButton({ children, className = "", ...props }) {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={`${BUTTON_BASE} bg-transparent border border-zinc-700 light:border-slate-600 text-slate-50 light:text-slate-700 hover:bg-zinc-800 light:hover:bg-slate-100 ${className}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructive action button (Delete). Filled red.
|
||||
*
|
||||
* @param {Object} props - Component props (forwarded to the button element)
|
||||
* @param {string} [props.className] - Extra classes appended to the button
|
||||
*/
|
||||
export function ModalDangerButton({ children, className = "", ...props }) {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={`${BUTTON_BASE} border-none bg-red-500 light:bg-red-600 text-white hover:opacity-80 ${className}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
/**
|
||||
* Shared stack of open modals that close on Escape. Only the top-most (most
|
||||
* recently opened) modal handles Escape, so stacked/nested modals close one at
|
||||
* a time from the top down instead of all at once.
|
||||
* @type {{ onClose: () => void }[]}
|
||||
*/
|
||||
const escapeStack = [];
|
||||
|
||||
function handleEscape(event) {
|
||||
if (event.key !== "Escape") return;
|
||||
const top = escapeStack[escapeStack.length - 1];
|
||||
top?.onClose?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a modal on Escape while respecting nesting. While `active`, the modal
|
||||
* joins a shared stack and only fires `onClose` when it is the top-most open
|
||||
* modal. A single window listener is shared across all modals.
|
||||
*
|
||||
* @param {boolean} active - Whether the modal is open and should close on Escape
|
||||
* @param {() => void} [onClose] - Called when Escape closes this modal
|
||||
*/
|
||||
export function useModalEscape(active, onClose) {
|
||||
// Stable entry so re-renders (e.g. a new inline `onClose`) update the handler
|
||||
// in place rather than reordering the stack.
|
||||
const entryRef = useRef({ onClose });
|
||||
useEffect(() => {
|
||||
entryRef.current.onClose = onClose;
|
||||
}, [onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
const entry = entryRef.current;
|
||||
escapeStack.push(entry);
|
||||
if (escapeStack.length === 1)
|
||||
window.addEventListener("keydown", handleEscape);
|
||||
return () => {
|
||||
const idx = escapeStack.indexOf(entry);
|
||||
if (idx !== -1) escapeStack.splice(idx, 1);
|
||||
if (escapeStack.length === 0)
|
||||
window.removeEventListener("keydown", handleEscape);
|
||||
};
|
||||
}, [active]);
|
||||
}
|
||||
@@ -1085,7 +1085,15 @@ does not extend the close button beyond the viewport. */
|
||||
}
|
||||
|
||||
.rti--container {
|
||||
@apply !bg-theme-settings-input-bg !text-white !placeholder-white !placeholder-opacity-60 !text-sm !rounded-lg !p-2.5;
|
||||
@apply !bg-zinc-800 !border !border-zinc-800 !text-zinc-100 !text-sm !rounded-lg !p-2 !gap-1.5;
|
||||
}
|
||||
|
||||
.light .rti--container {
|
||||
@apply !bg-white !border-slate-300 !text-slate-900;
|
||||
}
|
||||
|
||||
.rti--container:focus-within {
|
||||
@apply !border-sky-500 !shadow-none;
|
||||
}
|
||||
|
||||
@keyframes fadeUpIn {
|
||||
|
||||
@@ -47,21 +47,21 @@ export default function AgentClarifyingQuestions() {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<label className="block text-md font-medium text-white flex items-center gap-x-1">
|
||||
<label className="block text-md font-medium text-zinc-50 light:text-slate-900 flex items-center gap-x-1">
|
||||
{t("agent.settings.clarifying-questions.title")}{" "}
|
||||
<i className="ml-1 text-xs text-white pl-2 bg-blue-500/40 rounded-md px-2 py-0.5">
|
||||
<i className="ml-1 text-xs text-white light:text-sky-700 pl-2 bg-blue-500/40 light:bg-sky-100 rounded-md px-2 py-0.5">
|
||||
{t("agent.settings.clarifying-questions.beta-badge")}
|
||||
</i>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<p className="text-xs text-white/60">
|
||||
<p className="text-xs text-zinc-400 light:text-slate-600">
|
||||
{t("agent.settings.clarifying-questions.description")}
|
||||
</p>
|
||||
{loading ? (
|
||||
<CircleNotch
|
||||
size={16}
|
||||
className="shrink-0 animate-spin text-theme-text-primary"
|
||||
className="shrink-0 animate-spin text-zinc-400 light:text-slate-600"
|
||||
/>
|
||||
) : (
|
||||
<Toggle
|
||||
@@ -76,10 +76,10 @@ export default function AgentClarifyingQuestions() {
|
||||
<>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<div className="flex flex-col gap-y-1 flex-1">
|
||||
<label className="block text-md font-medium text-white">
|
||||
<label className="block text-md font-medium text-zinc-50 light:text-slate-900">
|
||||
{t("agent.settings.clarifying-questions.max-per-turn.title")}
|
||||
</label>
|
||||
<p className="text-xs text-white/60">
|
||||
<p className="text-xs text-zinc-400 light:text-slate-600">
|
||||
{t(
|
||||
"agent.settings.clarifying-questions.max-per-turn.description"
|
||||
)}
|
||||
@@ -96,7 +96,7 @@ export default function AgentClarifyingQuestions() {
|
||||
setMaxPerTurn(parseInt(e.target.value));
|
||||
}}
|
||||
onWheel={(e) => e.target.blur()}
|
||||
className="border border-white/10 bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-[80px] p-2.5 text-center"
|
||||
className="bg-zinc-800 border border-zinc-800 text-zinc-100 placeholder:text-zinc-400 light:bg-white light:border-slate-300 light:text-slate-900 light:placeholder:text-slate-400 text-sm rounded-lg outline-none focus:border-sky-500 light:focus:border-sky-500 block w-[80px] h-[34px] px-3 text-center"
|
||||
placeholder="3"
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
@@ -46,18 +46,18 @@ export default function AgentSkillReranker() {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<label className="block text-md font-medium text-white flex items-center gap-x-1">
|
||||
<label className="block text-md font-medium text-zinc-50 light:text-slate-900 flex items-center gap-x-1">
|
||||
{t("agent.settings.intelligent-skill-selection.title")}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<p className="text-xs text-white/60">
|
||||
<p className="text-xs text-zinc-400 light:text-slate-600">
|
||||
{t("agent.settings.intelligent-skill-selection.description")}
|
||||
</p>
|
||||
{loading ? (
|
||||
<CircleNotch
|
||||
size={16}
|
||||
className="shrink-0 animate-spin text-theme-text-primary"
|
||||
className="shrink-0 animate-spin text-zinc-400 light:text-slate-600"
|
||||
/>
|
||||
) : (
|
||||
<Toggle
|
||||
@@ -71,10 +71,10 @@ export default function AgentSkillReranker() {
|
||||
{enabled && (
|
||||
<div className="flex items-center gap-x-4">
|
||||
<div className="flex flex-col gap-y-1 flex-1">
|
||||
<label className="block text-md font-medium text-white">
|
||||
<label className="block text-md font-medium text-zinc-50 light:text-slate-900">
|
||||
{t("agent.settings.intelligent-skill-selection.max-tools.title")}
|
||||
</label>
|
||||
<p className="text-xs text-white/60">
|
||||
<p className="text-xs text-zinc-400 light:text-slate-600">
|
||||
{t(
|
||||
"agent.settings.intelligent-skill-selection.max-tools.description"
|
||||
)}
|
||||
@@ -91,7 +91,7 @@ export default function AgentSkillReranker() {
|
||||
setMaxTools(parseInt(e.target.value));
|
||||
}}
|
||||
onWheel={(e) => e.target.blur()}
|
||||
className="border border-white/10 bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-[80px] p-2.5 text-center"
|
||||
className="bg-zinc-800 border border-zinc-800 text-zinc-100 placeholder:text-zinc-400 light:bg-white light:border-slate-300 light:text-slate-900 light:placeholder:text-slate-400 text-sm rounded-lg outline-none focus:border-sky-500 light:focus:border-sky-500 block w-[80px] h-[34px] px-3 text-center"
|
||||
placeholder="15"
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
@@ -33,13 +33,13 @@ export default function MaxToolCallStack() {
|
||||
}, [debouncedUpdateMaxCallStack]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2 mt-4">
|
||||
<div className="flex items-center gap-x-4 mt-2">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<div className="flex items-center gap-x-4">
|
||||
<div className="flex flex-col gap-y-1 flex-1">
|
||||
<label className="block text-md font-medium text-white">
|
||||
<label className="block text-md font-medium text-zinc-50 light:text-slate-900">
|
||||
{t("agent.settings.max-tool-calls.title")}
|
||||
</label>
|
||||
<p className="text-xs text-white/60">
|
||||
<p className="text-xs text-zinc-400 light:text-slate-600">
|
||||
{t("agent.settings.max-tool-calls.description")}
|
||||
</p>
|
||||
</div>
|
||||
@@ -55,7 +55,7 @@ export default function MaxToolCallStack() {
|
||||
setMaxCallStack(parseInt(e.target.value));
|
||||
}}
|
||||
onWheel={(e) => e.target.blur()}
|
||||
className="border border-white/10 bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-[80px] p-2.5 text-center"
|
||||
className="bg-zinc-800 border border-zinc-800 text-zinc-100 placeholder:text-zinc-400 light:bg-white light:border-slate-300 light:text-slate-900 light:placeholder:text-slate-400 text-sm rounded-lg outline-none focus:border-sky-500 light:focus:border-sky-500 block w-[80px] h-[34px] px-3 text-center"
|
||||
placeholder="10"
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import { SlidersHorizontal, X } from "@phosphor-icons/react";
|
||||
import Modal, { ModalHeader } from "@/components/lib/Modal";
|
||||
import { SlidersHorizontal } from "@phosphor-icons/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import MaxToolCallStack from "./MaxToolCallStack";
|
||||
import AgentClarifyingQuestions from "./AgentClarifyingQuestions";
|
||||
@@ -27,30 +27,15 @@ function AgentSkillSettingsModal({ isOpen, closeModal }) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<div className="w-[500px] bg-theme-bg-sidebar px-6 py-4 rounded-lg flex flex-col items-center justify-between relative shadow-lg border border-white/10">
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<div className="text-white text-left font-medium text-lg">
|
||||
{t("agent.settings.title")}
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="text-white opacity-60 hover:text-white hover:opacity-100 border-none outline-none"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex flex-col gap-y-5 w-full">
|
||||
<MaxToolCallStack />
|
||||
<div className="border-b border-white/10 h-[1px] w-full" />
|
||||
<AgentSkillReranker />
|
||||
<div className="border-b border-white/10 h-[1px] w-full" />
|
||||
<AgentClarifyingQuestions />
|
||||
</div>
|
||||
</div>
|
||||
<Modal isOpen={isOpen} onClose={closeModal}>
|
||||
<ModalHeader title={t("agent.settings.title")} onClose={closeModal} />
|
||||
<div className="flex flex-col gap-y-5 w-full">
|
||||
<MaxToolCallStack />
|
||||
<div className="border-b border-zinc-800 light:border-slate-200 h-[1px] w-full" />
|
||||
<AgentSkillReranker />
|
||||
<div className="border-b border-zinc-800 light:border-slate-200 h-[1px] w-full" />
|
||||
<AgentClarifyingQuestions />
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import { WarningOctagon, X } from "@phosphor-icons/react";
|
||||
import Modal, {
|
||||
ModalHeader,
|
||||
ModalLabel,
|
||||
ModalInput,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
} from "@/components/lib/Modal";
|
||||
import { WarningOctagon } from "@phosphor-icons/react";
|
||||
import { DB_LOGOS } from "./DBConnection";
|
||||
import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
@@ -268,231 +274,183 @@ export default function SQLConnectionModal({
|
||||
// Cannot do nested forms, it will cause all sorts of issues, so we portal this out
|
||||
// to the parent container form so we don't have nested forms.
|
||||
return createPortal(
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<div className="fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
{isEditMode ? "Edit SQL Connection" : "New SQL Connection"}
|
||||
</h3>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={handleClose}
|
||||
size="lg"
|
||||
noPortal
|
||||
className="!z-[1000]"
|
||||
>
|
||||
<ModalHeader
|
||||
title={isEditMode ? "Edit SQL Connection" : "New SQL Connection"}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
<form
|
||||
id="sql-connection-form"
|
||||
onChange={onFormChange}
|
||||
onSubmit={handleUpdate}
|
||||
className="flex flex-col gap-y-5 w-full"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<p className="text-sm text-zinc-400 light:text-slate-600">
|
||||
{isEditMode
|
||||
? "Update the connection information for your database below."
|
||||
: "Add the connection information for your database below and it will be available for future SQL agent calls."}
|
||||
</p>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="border border-red-800 bg-zinc-800 light:bg-red-200/50 p-4 rounded-lg flex items-center gap-x-2 text-sm text-red-400 light:text-red-500">
|
||||
<WarningOctagon size={28} className="shrink-0" />
|
||||
<p>
|
||||
<b>WARNING:</b> The SQL agent has been <i>instructed</i> to only
|
||||
perform non-modifying queries. This <b>does not</b> prevent a
|
||||
hallucination from still deleting data. Only connect with a user
|
||||
who has <b>READ_ONLY</b> permissions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ModalLabel className="mt-4 mb-2">
|
||||
Select your SQL engine
|
||||
</ModalLabel>
|
||||
<div className="grid md:grid-cols-4 gap-4 grid-cols-2">
|
||||
<DBEngine
|
||||
provider="postgresql"
|
||||
active={engine === "postgresql"}
|
||||
onClick={() => setEngine("postgresql")}
|
||||
/>
|
||||
<DBEngine
|
||||
provider="mysql"
|
||||
active={engine === "mysql"}
|
||||
onClick={() => setEngine("mysql")}
|
||||
/>
|
||||
<DBEngine
|
||||
provider="sql-server"
|
||||
active={engine === "sql-server"}
|
||||
onClick={() => setEngine("sql-server")}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<form
|
||||
id="sql-connection-form"
|
||||
onChange={onFormChange}
|
||||
onSubmit={handleUpdate}
|
||||
>
|
||||
<div className="px-7 py-6">
|
||||
<div className="space-y-6 max-h-[60vh] overflow-y-auto pr-2">
|
||||
<p className="text-sm text-white/60">
|
||||
{isEditMode
|
||||
? "Update the connection information for your database below."
|
||||
: "Add the connection information for your database below and it will be available for future SQL agent calls."}
|
||||
</p>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="border border-red-800 bg-zinc-800 light:bg-red-200/50 p-4 rounded-lg flex items-center gap-x-2 text-sm text-red-400 light:text-red-500">
|
||||
<WarningOctagon size={28} className="shrink-0" />
|
||||
<p>
|
||||
<b>WARNING:</b> The SQL agent has been <i>instructed</i>{" "}
|
||||
to only perform non-modifying queries. This{" "}
|
||||
<b>does not</b> prevent a hallucination from still
|
||||
deleting data. Only connect with a user who has{" "}
|
||||
<b>READ_ONLY</b> permissions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<label className="block mb-2 text-sm font-medium text-white mt-4">
|
||||
Select your SQL engine
|
||||
</label>
|
||||
<div className="grid md:grid-cols-4 gap-4 grid-cols-2">
|
||||
<DBEngine
|
||||
provider="postgresql"
|
||||
active={engine === "postgresql"}
|
||||
onClick={() => setEngine("postgresql")}
|
||||
/>
|
||||
<DBEngine
|
||||
provider="mysql"
|
||||
active={engine === "mysql"}
|
||||
onClick={() => setEngine("mysql")}
|
||||
/>
|
||||
<DBEngine
|
||||
provider="sql-server"
|
||||
active={engine === "sql-server"}
|
||||
onClick={() => setEngine("sql-server")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ModalInput
|
||||
label="Connection name"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="a unique name to identify this SQL connection"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.name || ""}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col w-full">
|
||||
<label className="block mb-2 text-sm font-medium text-white">
|
||||
Connection name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="a unique name to identify this SQL connection"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.name || ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<ModalInput
|
||||
label="Database user"
|
||||
type="text"
|
||||
name="username"
|
||||
placeholder="root"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.username || ""}
|
||||
/>
|
||||
<ModalInput
|
||||
label="Database user password"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="password123"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.password || ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="flex flex-col">
|
||||
<label className="block mb-2 text-sm font-medium text-white">
|
||||
Database user
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="root"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.username || ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<label className="block mb-2 text-sm font-medium text-white">
|
||||
Database user password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="password123"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.password || ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div className="sm:col-span-2">
|
||||
<label className="block mb-2 text-sm font-medium text-white">
|
||||
Server endpoint
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="host"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="the hostname or endpoint for your database"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.host || ""}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-white">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="port"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="3306"
|
||||
required={false}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.port || ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<label className="block mb-2 text-sm font-medium text-white">
|
||||
Database
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="database"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="the database the agent will interact with"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.database || ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{engine === "postgresql" && (
|
||||
<div className="flex flex-col">
|
||||
<label className="block mb-2 text-sm font-medium text-white">
|
||||
Schema (optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="schema"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="public (default schema if not specified)"
|
||||
required={false}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.schema || ""}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{engine === "sql-server" && (
|
||||
<Toggle
|
||||
name="encrypt"
|
||||
value="true"
|
||||
size="md"
|
||||
label="Enable Encryption"
|
||||
enabled={config.encrypt}
|
||||
/>
|
||||
)}
|
||||
|
||||
{engine === "postgresql" && (
|
||||
<Toggle
|
||||
name="ssl"
|
||||
value="true"
|
||||
size="md"
|
||||
label="Use SSL"
|
||||
enabled={config.ssl}
|
||||
/>
|
||||
)}
|
||||
|
||||
<p className="text-theme-text-secondary text-sm">
|
||||
{assembleConnectionString({ engine, ...config })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div className="sm:col-span-2">
|
||||
<ModalInput
|
||||
label="Server endpoint"
|
||||
type="text"
|
||||
name="host"
|
||||
placeholder="the hostname or endpoint for your database"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.host || ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border px-7 pb-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="transition-all duration-300 text-white hover:bg-zinc-700 light:hover:bg-theme-bg-primary px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
form="sql-connection-form"
|
||||
disabled={isValidating}
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm disabled:opacity-50"
|
||||
>
|
||||
{isValidating ? "Validating..." : "Save connection"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<ModalInput
|
||||
label="Port"
|
||||
type="text"
|
||||
name="port"
|
||||
placeholder="3306"
|
||||
required={false}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.port || ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ModalInput
|
||||
label="Database"
|
||||
type="text"
|
||||
name="database"
|
||||
placeholder="the database the agent will interact with"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.database || ""}
|
||||
/>
|
||||
|
||||
{engine === "postgresql" && (
|
||||
<ModalInput
|
||||
label="Schema (optional)"
|
||||
type="text"
|
||||
name="schema"
|
||||
placeholder="public (default schema if not specified)"
|
||||
required={false}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.schema || ""}
|
||||
/>
|
||||
)}
|
||||
|
||||
{engine === "sql-server" && (
|
||||
<Toggle
|
||||
name="encrypt"
|
||||
value="true"
|
||||
size="md"
|
||||
label="Enable Encryption"
|
||||
enabled={config.encrypt}
|
||||
/>
|
||||
)}
|
||||
|
||||
{engine === "postgresql" && (
|
||||
<Toggle
|
||||
name="ssl"
|
||||
value="true"
|
||||
size="md"
|
||||
label="Use SSL"
|
||||
enabled={config.ssl}
|
||||
/>
|
||||
)}
|
||||
|
||||
<p className="text-zinc-400 light:text-slate-500 text-sm">
|
||||
{assembleConnectionString({ engine, ...config })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>,
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<ModalSecondaryButton type="button" onClick={handleClose}>
|
||||
Cancel
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton
|
||||
type="submit"
|
||||
form="sql-connection-form"
|
||||
disabled={isValidating}
|
||||
>
|
||||
{isValidating ? "Validating..." : "Save connection"}
|
||||
</ModalPrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>,
|
||||
document.getElementById("workspace-agent-settings-container")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,12 @@ import Admin from "@/models/admin";
|
||||
import { FullScreenLoader } from "@/components/Preloader";
|
||||
import { CaretRight, Flask } from "@phosphor-icons/react";
|
||||
import { configurableFeatures } from "./features";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal, {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
} from "@/components/lib/Modal";
|
||||
import paths from "@/utils/paths";
|
||||
import showToast from "@/utils/toast";
|
||||
|
||||
@@ -199,100 +204,95 @@ function FeatureVerification({ children }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalWrapper isOpen={true}>
|
||||
<div className="w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="flex items-center gap-2">
|
||||
<Flask size={24} className="text-theme-text-primary" />
|
||||
<h3 className="text-xl font-semibold text-white">
|
||||
<Modal isOpen={true} size="lg" closeOnEsc={false}>
|
||||
<form onSubmit={acceptTos} className="flex flex-col gap-y-5">
|
||||
<ModalHeader
|
||||
title={
|
||||
<span className="flex items-center gap-2">
|
||||
<Flask size={24} />
|
||||
Terms of use for experimental features
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<form onSubmit={acceptTos}>
|
||||
<div className="py-7 px-9 space-y-4 flex-col">
|
||||
<div className="w-full text-white text-md flex flex-col gap-y-4">
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<ModalBody>
|
||||
<div className="w-full text-zinc-300 light:text-slate-700 text-md flex flex-col gap-y-4">
|
||||
<p>
|
||||
Experimental features of AnythingLLM are features that we are
|
||||
piloting and are <b>opt-in</b>. We proactively will condition
|
||||
or warn you on any potential concerns should any exist prior
|
||||
to approval of any feature.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
Experimental features of AnythingLLM are features that we
|
||||
are piloting and are <b>opt-in</b>. We proactively will
|
||||
condition or warn you on any potential concerns should any
|
||||
exist prior to approval of any feature.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
Use of any feature on this page can result in, but not
|
||||
limited to, the following possibilities.
|
||||
</p>
|
||||
<ul className="list-disc ml-6 text-sm font-mono mt-2">
|
||||
<li>Loss of data.</li>
|
||||
<li>Change in quality of results.</li>
|
||||
<li>Increased storage.</li>
|
||||
<li>Increased resource consumption.</li>
|
||||
<li>
|
||||
Increased cost or use of any connected LLM or embedding
|
||||
provider.
|
||||
</li>
|
||||
<li>Potential bugs or issues using AnythingLLM.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
Use of an experimental feature also comes with the
|
||||
following list of non-exhaustive conditions.
|
||||
</p>
|
||||
<ul className="list-disc ml-6 text-sm font-mono mt-2">
|
||||
<li>Feature may not exist in future updates.</li>
|
||||
<li>The feature being used is not currently stable.</li>
|
||||
<li>
|
||||
The feature may not be available in future versions,
|
||||
configurations, or subscriptions of AnythingLLM.
|
||||
</li>
|
||||
<li>
|
||||
Your privacy settings <b>will be honored</b> with use of
|
||||
any beta feature.
|
||||
</li>
|
||||
<li>These conditions may change in future updates.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Access to any features requires approval of this modal. If
|
||||
you would like to read more you can refer to{" "}
|
||||
<a
|
||||
href="https://docs.anythingllm.com/beta-preview/overview"
|
||||
className="underline text-blue-500"
|
||||
>
|
||||
docs.anythingllm.com
|
||||
</a>{" "}
|
||||
or email{" "}
|
||||
<a
|
||||
href="mailto:team@mintplexlabs.com"
|
||||
className="underline text-blue-500"
|
||||
>
|
||||
team@mintplexlabs.com
|
||||
</a>
|
||||
Use of any feature on this page can result in, but not
|
||||
limited to, the following possibilities.
|
||||
</p>
|
||||
<ul className="list-disc ml-6 text-sm font-mono mt-2">
|
||||
<li>Loss of data.</li>
|
||||
<li>Change in quality of results.</li>
|
||||
<li>Increased storage.</li>
|
||||
<li>Increased resource consumption.</li>
|
||||
<li>
|
||||
Increased cost or use of any connected LLM or embedding
|
||||
provider.
|
||||
</li>
|
||||
<li>Potential bugs or issues using AnythingLLM.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
Use of an experimental feature also comes with the following
|
||||
list of non-exhaustive conditions.
|
||||
</p>
|
||||
<ul className="list-disc ml-6 text-sm font-mono mt-2">
|
||||
<li>Feature may not exist in future updates.</li>
|
||||
<li>The feature being used is not currently stable.</li>
|
||||
<li>
|
||||
The feature may not be available in future versions,
|
||||
configurations, or subscriptions of AnythingLLM.
|
||||
</li>
|
||||
<li>
|
||||
Your privacy settings <b>will be honored</b> with use of
|
||||
any beta feature.
|
||||
</li>
|
||||
<li>These conditions may change in future updates.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Access to any features requires approval of this modal. If you
|
||||
would like to read more you can refer to{" "}
|
||||
<a
|
||||
href="https://docs.anythingllm.com/beta-preview/overview"
|
||||
className="underline text-blue-500"
|
||||
>
|
||||
docs.anythingllm.com
|
||||
</a>{" "}
|
||||
or email{" "}
|
||||
<a
|
||||
href="mailto:team@mintplexlabs.com"
|
||||
className="underline text-blue-500"
|
||||
>
|
||||
team@mintplexlabs.com
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b">
|
||||
<a
|
||||
href={paths.home()}
|
||||
className="transition-all duration-300 bg-transparent text-white hover:bg-red-500/50 light:hover:bg-red-300/50 px-4 py-2 rounded-lg text-sm border border-theme-modal-border"
|
||||
>
|
||||
Reject & close
|
||||
</a>
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm border border-theme-modal-border"
|
||||
>
|
||||
I understand
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<a
|
||||
href={paths.home()}
|
||||
className="flex items-center justify-center h-[34px] px-4 rounded-lg text-sm font-medium transition-all duration-200 bg-transparent border border-zinc-700 light:border-slate-600 text-slate-50 light:text-slate-700 hover:bg-red-500/50 light:hover:bg-red-300/50"
|
||||
>
|
||||
Reject & close
|
||||
</a>
|
||||
<ModalPrimaryButton type="submit">
|
||||
I understand
|
||||
</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</Modal>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { X, Copy, Check } from "@phosphor-icons/react";
|
||||
import { Copy, Check } from "@phosphor-icons/react";
|
||||
import Admin from "@/models/admin";
|
||||
import Workspace from "@/models/workspace";
|
||||
import showToast from "@/utils/toast";
|
||||
import {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
ModalLabel,
|
||||
ModalHint,
|
||||
} from "@/components/lib/Modal";
|
||||
|
||||
export default function NewInviteModal({ closeModal, onSuccess }) {
|
||||
const [invite, setInvite] = useState(null);
|
||||
@@ -66,123 +75,84 @@ export default function NewInviteModal({ closeModal, onSuccess }) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
Create new invite
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<form onSubmit={handleCreate}>
|
||||
<div className="space-y-4">
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
{invite && (
|
||||
<div className="relative">
|
||||
<input
|
||||
type="url"
|
||||
defaultValue={`${window.location.origin}/accept-invite/${invite.code}`}
|
||||
disabled={true}
|
||||
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg outline-none block w-full p-2.5 pr-10"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyInviteLink}
|
||||
disabled={copied}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-md hover:bg-theme-modal-border transition-all duration-300"
|
||||
>
|
||||
{copied ? (
|
||||
<Check
|
||||
size={20}
|
||||
className="text-green-400"
|
||||
weight="bold"
|
||||
/>
|
||||
) : (
|
||||
<Copy size={20} className="text-white" weight="bold" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-white text-opacity-60 text-xs md:text-sm">
|
||||
After creation you will be able to copy the invite and send it
|
||||
to a new user where they can create an account as the{" "}
|
||||
<b>default</b> role and automatically be added to workspaces
|
||||
selected.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{workspaces.length > 0 && !invite && (
|
||||
<div className="mt-6">
|
||||
<div className="w-full">
|
||||
<div className="flex flex-col gap-y-1 mb-2">
|
||||
<label
|
||||
htmlFor="workspaces"
|
||||
className="block text-sm font-medium text-white"
|
||||
>
|
||||
Auto-add invitee to workspaces
|
||||
</label>
|
||||
<p className="text-white text-opacity-60 text-xs">
|
||||
You can optionally automatically assign the user to the
|
||||
workspaces below by selecting them. By default, the user
|
||||
will not have any workspaces visible. You can assign
|
||||
workspaces later post-invite acceptance.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-y-2 mt-2">
|
||||
{workspaces.map((workspace) => (
|
||||
<WorkspaceOption
|
||||
key={workspace.id}
|
||||
workspace={workspace}
|
||||
selected={selectedWorkspaceIds.includes(workspace.id)}
|
||||
toggleSelection={handleWorkspaceSelection}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end items-center mt-6 pt-6 border-t border-theme-modal-border">
|
||||
{!invite ? (
|
||||
<>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm mr-2"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Create Invite
|
||||
</button>
|
||||
</>
|
||||
<form onSubmit={handleCreate} className="flex flex-col gap-y-5">
|
||||
<ModalHeader title="Create new invite" onClose={closeModal} />
|
||||
<ModalBody>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
{invite && (
|
||||
<div className="relative">
|
||||
<input
|
||||
type="url"
|
||||
defaultValue={`${window.location.origin}/accept-invite/${invite.code}`}
|
||||
disabled={true}
|
||||
className="w-full h-[34px] px-3.5 pr-10 text-sm rounded-lg outline-none bg-zinc-800 border border-zinc-800 text-zinc-100 placeholder:text-zinc-400 light:bg-white light:border-slate-300 light:text-slate-900 light:placeholder:text-slate-400 focus:border-sky-500 light:focus:border-sky-500"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyInviteLink}
|
||||
disabled={copied}
|
||||
className="border-none absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-md hover:bg-zinc-800 light:hover:bg-slate-100 transition-all duration-300"
|
||||
>
|
||||
{copied ? (
|
||||
<Check size={20} className="text-green-400" weight="bold" />
|
||||
) : (
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<Copy
|
||||
size={20}
|
||||
className="text-slate-50 light:text-slate-900"
|
||||
weight="bold"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-zinc-400 light:text-slate-600 text-xs md:text-sm">
|
||||
After creation you will be able to copy the invite and send it to a
|
||||
new user where they can create an account as the <b>default</b> role
|
||||
and automatically be added to workspaces selected.
|
||||
</p>
|
||||
|
||||
{workspaces.length > 0 && !invite && (
|
||||
<div className="w-full">
|
||||
<div className="flex flex-col gap-y-1 mb-2">
|
||||
<ModalLabel htmlFor="workspaces">
|
||||
Auto-add invitee to workspaces
|
||||
</ModalLabel>
|
||||
<ModalHint>
|
||||
You can optionally automatically assign the user to the
|
||||
workspaces below by selecting them. By default, the user will
|
||||
not have any workspaces visible. You can assign workspaces later
|
||||
post-invite acceptance.
|
||||
</ModalHint>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-y-2 mt-2">
|
||||
{workspaces.map((workspace) => (
|
||||
<WorkspaceOption
|
||||
key={workspace.id}
|
||||
workspace={workspace}
|
||||
selected={selectedWorkspaceIds.includes(workspace.id)}
|
||||
toggleSelection={handleWorkspaceSelection}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter className={invite ? "justify-end" : undefined}>
|
||||
{!invite ? (
|
||||
<>
|
||||
<ModalSecondaryButton onClick={closeModal} type="button">
|
||||
Cancel
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton type="submit">Create Invite</ModalPrimaryButton>
|
||||
</>
|
||||
) : (
|
||||
<ModalSecondaryButton onClick={closeModal} type="button">
|
||||
Close
|
||||
</ModalSecondaryButton>
|
||||
)}
|
||||
</ModalFooter>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import Admin from "@/models/admin";
|
||||
import InviteRow from "./InviteRow";
|
||||
import NewInviteModal from "./NewInviteModal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
import CTAButton from "@/components/lib/CTAButton";
|
||||
|
||||
export default function AdminInvites() {
|
||||
@@ -103,9 +103,9 @@ export default function AdminInvites() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<Modal isOpen={isOpen} onClose={closeModal}>
|
||||
<NewInviteModal closeModal={closeModal} onSuccess={fetchInvites} />
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import React, { useState } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
import {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
ModalInput,
|
||||
} from "@/components/lib/Modal";
|
||||
|
||||
export default function AddVariableModal({ closeModal, onRefresh }) {
|
||||
const [error, setError] = useState(null);
|
||||
@@ -31,99 +38,49 @@ export default function AddVariableModal({ closeModal, onRefresh }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
Add New Variable
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<form onSubmit={handleCreate}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="key"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Key
|
||||
</label>
|
||||
<input
|
||||
name="key"
|
||||
type="text"
|
||||
minLength={3}
|
||||
maxLength={255}
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="e.g., company_name"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
pattern="^[a-zA-Z0-9_]+$"
|
||||
/>
|
||||
<p className="mt-2 text-xs text-white/60">
|
||||
Key must be unique and will be used in prompts as {"{key}"}.
|
||||
Only letters, numbers and underscores are allowed.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="value"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Value
|
||||
</label>
|
||||
<input
|
||||
name="value"
|
||||
type="text"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="e.g., Acme Corp"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="description"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
name="description"
|
||||
type="text"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="Optional description"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Create variable
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form onSubmit={handleCreate} className="flex flex-col gap-y-5">
|
||||
<ModalHeader title="Add New Variable" onClose={closeModal} />
|
||||
<ModalBody>
|
||||
<ModalInput
|
||||
label="Key"
|
||||
name="key"
|
||||
type="text"
|
||||
minLength={3}
|
||||
maxLength={255}
|
||||
placeholder="e.g., company_name"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
pattern="^[a-zA-Z0-9_]+$"
|
||||
hint={
|
||||
<>
|
||||
Key must be unique and will be used in prompts as {"{key}"}. Only
|
||||
letters, numbers and underscores are allowed.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ModalInput
|
||||
label="Value"
|
||||
name="value"
|
||||
type="text"
|
||||
placeholder="e.g., Acme Corp"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<ModalInput
|
||||
label="Description"
|
||||
name="description"
|
||||
type="text"
|
||||
placeholder="Optional description"
|
||||
autoComplete="off"
|
||||
/>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalSecondaryButton onClick={closeModal} type="button">
|
||||
Cancel
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton type="submit">Create variable</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
+55
-98
@@ -1,7 +1,14 @@
|
||||
import React, { useState } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
import {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
ModalInput,
|
||||
} from "@/components/lib/Modal";
|
||||
|
||||
export default function EditVariableModal({ variable, closeModal, onRefresh }) {
|
||||
const [error, setError] = useState(null);
|
||||
@@ -32,102 +39,52 @@ export default function EditVariableModal({ variable, closeModal, onRefresh }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
Edit {variable.key}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<form onSubmit={handleUpdate}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="key"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Key
|
||||
</label>
|
||||
<input
|
||||
name="key"
|
||||
minLength={3}
|
||||
maxLength={255}
|
||||
type="text"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="e.g., company_name"
|
||||
defaultValue={variable.key}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
pattern="^[a-zA-Z0-9_]+$"
|
||||
/>
|
||||
<p className="mt-2 text-xs text-white/60">
|
||||
Key must be unique and will be used in prompts as {"{key}"}.
|
||||
Only letters, numbers and underscores are allowed.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="value"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Value
|
||||
</label>
|
||||
<input
|
||||
name="value"
|
||||
type="text"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="e.g., Acme Corp"
|
||||
defaultValue={variable.value}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="description"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
name="description"
|
||||
type="text"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="Optional description"
|
||||
defaultValue={variable.description}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Update variable
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form onSubmit={handleUpdate} className="flex flex-col gap-y-5">
|
||||
<ModalHeader title={`Edit ${variable.key}`} onClose={closeModal} />
|
||||
<ModalBody>
|
||||
<ModalInput
|
||||
label="Key"
|
||||
name="key"
|
||||
minLength={3}
|
||||
maxLength={255}
|
||||
type="text"
|
||||
placeholder="e.g., company_name"
|
||||
defaultValue={variable.key}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
pattern="^[a-zA-Z0-9_]+$"
|
||||
hint={
|
||||
<>
|
||||
Key must be unique and will be used in prompts as {"{key}"}. Only
|
||||
letters, numbers and underscores are allowed.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ModalInput
|
||||
label="Value"
|
||||
name="value"
|
||||
type="text"
|
||||
placeholder="e.g., Acme Corp"
|
||||
defaultValue={variable.value}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<ModalInput
|
||||
label="Description"
|
||||
name="description"
|
||||
type="text"
|
||||
placeholder="Optional description"
|
||||
defaultValue={variable.description}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalSecondaryButton onClick={closeModal} type="button">
|
||||
Cancel
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton type="submit">Update variable</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useRef } from "react";
|
||||
import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
import EditVariableModal from "./EditVariableModal";
|
||||
import { titleCase } from "text-case";
|
||||
import truncate from "truncate";
|
||||
@@ -108,13 +108,13 @@ export default function VariableRow({ variable, onRefresh }) {
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<Modal isOpen={isOpen} onClose={closeModal}>
|
||||
<EditVariableModal
|
||||
variable={variable}
|
||||
closeModal={closeModal}
|
||||
onRefresh={onRefresh}
|
||||
/>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import Sidebar from "@/components/SettingsSidebar";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import CTAButton from "@/components/lib/CTAButton";
|
||||
import VariableRow from "./VariableRow";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
import AddVariableModal from "./AddVariableModal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import * as Skeleton from "react-loading-skeleton";
|
||||
@@ -112,9 +112,9 @@ export default function SystemPromptVariables() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<Modal isOpen={isOpen} onClose={closeModal}>
|
||||
<AddVariableModal closeModal={closeModal} onRefresh={fetchVariables} />
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import Admin from "@/models/admin";
|
||||
import { userFromStorage } from "@/utils/request";
|
||||
import { MessageLimitInput, RoleHintDisplay } from "..";
|
||||
@@ -9,6 +8,16 @@ import {
|
||||
USERNAME_MAX_LENGTH,
|
||||
USERNAME_PATTERN,
|
||||
} from "@/utils/username";
|
||||
import {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
ModalInput,
|
||||
ModalTextarea,
|
||||
ModalLabel,
|
||||
} from "@/components/lib/Modal";
|
||||
|
||||
export default function NewUserModal({ closeModal }) {
|
||||
const [error, setError] = useState(null);
|
||||
@@ -35,134 +44,73 @@ export default function NewUserModal({ closeModal }) {
|
||||
const user = userFromStorage();
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
Add user to instance
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
<form onSubmit={handleCreate} className="flex flex-col gap-y-5">
|
||||
<ModalHeader title="Add user to instance" onClose={closeModal} />
|
||||
<ModalBody>
|
||||
<ModalInput
|
||||
label="Username"
|
||||
name="username"
|
||||
type="text"
|
||||
placeholder="User's username"
|
||||
minLength={USERNAME_MIN_LENGTH}
|
||||
maxLength={USERNAME_MAX_LENGTH}
|
||||
pattern={USERNAME_PATTERN}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
hint={t("common.username_requirements")}
|
||||
/>
|
||||
<ModalInput
|
||||
label="Password"
|
||||
name="password"
|
||||
type="text"
|
||||
placeholder="User's initial password"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
minLength={8}
|
||||
hint="Password must be at least 8 characters long"
|
||||
/>
|
||||
<ModalTextarea
|
||||
label="Bio"
|
||||
name="bio"
|
||||
placeholder="User's bio"
|
||||
autoComplete="off"
|
||||
rows={3}
|
||||
/>
|
||||
<div className="flex flex-col gap-y-1.5 w-full">
|
||||
<ModalLabel htmlFor="role">Role</ModalLabel>
|
||||
<select
|
||||
name="role"
|
||||
required={true}
|
||||
defaultValue={"default"}
|
||||
onChange={(e) => setRole(e.target.value)}
|
||||
className="w-full h-[34px] px-3.5 text-sm rounded-lg outline-none bg-zinc-800 border border-zinc-800 text-zinc-100 light:bg-white light:border-slate-300 light:text-slate-900 focus:border-sky-500 light:focus:border-sky-500"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
<option value="default">Default</option>
|
||||
<option value="manager">Manager</option>
|
||||
{user?.role === "admin" && (
|
||||
<option value="admin">Administrator</option>
|
||||
)}
|
||||
</select>
|
||||
<RoleHintDisplay role={role} />
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<form onSubmit={handleCreate}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="username"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
name="username"
|
||||
type="text"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="User's username"
|
||||
minLength={USERNAME_MIN_LENGTH}
|
||||
maxLength={USERNAME_MAX_LENGTH}
|
||||
pattern={USERNAME_PATTERN}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<p className="mt-2 text-xs text-white/60">
|
||||
{t("common.username_requirements")}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
name="password"
|
||||
type="text"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="User's initial password"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
minLength={8}
|
||||
/>
|
||||
<p className="mt-2 text-xs text-white/60">
|
||||
Password must be at least 8 characters long
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="bio"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Bio
|
||||
</label>
|
||||
<textarea
|
||||
name="bio"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="User's bio"
|
||||
autoComplete="off"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="role"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Role
|
||||
</label>
|
||||
<select
|
||||
name="role"
|
||||
required={true}
|
||||
defaultValue={"default"}
|
||||
onChange={(e) => setRole(e.target.value)}
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
>
|
||||
<option value="default">Default</option>
|
||||
<option value="manager">Manager</option>
|
||||
{user?.role === "admin" && (
|
||||
<option value="admin">Administrator</option>
|
||||
)}
|
||||
</select>
|
||||
<RoleHintDisplay role={role} />
|
||||
</div>
|
||||
<MessageLimitInput
|
||||
role={role}
|
||||
enabled={messageLimit.enabled}
|
||||
limit={messageLimit.limit}
|
||||
updateState={setMessageLimit}
|
||||
/>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
<p className="text-white text-xs md:text-sm">
|
||||
After creating a user they will need to login with their initial
|
||||
login to get access.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Add user
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MessageLimitInput
|
||||
role={role}
|
||||
enabled={messageLimit.enabled}
|
||||
limit={messageLimit.limit}
|
||||
updateState={setMessageLimit}
|
||||
/>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
<p className="text-zinc-300 light:text-slate-700 text-xs md:text-sm">
|
||||
After creating a user they will need to login with their initial login
|
||||
to get access.
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalSecondaryButton onClick={closeModal} type="button">
|
||||
Cancel
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton type="submit">Add user</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import Admin from "@/models/admin";
|
||||
import { MessageLimitInput, RoleHintDisplay } from "../..";
|
||||
import { AUTH_USER } from "@/utils/constants";
|
||||
@@ -9,6 +8,16 @@ import {
|
||||
USERNAME_MAX_LENGTH,
|
||||
USERNAME_PATTERN,
|
||||
} from "@/utils/username";
|
||||
import {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
ModalInput,
|
||||
ModalTextarea,
|
||||
ModalLabel,
|
||||
} from "@/components/lib/Modal";
|
||||
|
||||
export default function EditUserModal({ currentUser, user, closeModal }) {
|
||||
const [role, setRole] = useState(user.role);
|
||||
@@ -50,131 +59,70 @@ export default function EditUserModal({ currentUser, user, closeModal }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
Edit {user.username}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
<form onSubmit={handleUpdate} className="flex flex-col gap-y-5">
|
||||
<ModalHeader title={`Edit ${user.username}`} onClose={closeModal} />
|
||||
<ModalBody>
|
||||
<ModalInput
|
||||
label="Username"
|
||||
name="username"
|
||||
type="text"
|
||||
placeholder="User's username"
|
||||
defaultValue={user.username}
|
||||
minLength={USERNAME_MIN_LENGTH}
|
||||
maxLength={USERNAME_MAX_LENGTH}
|
||||
pattern={USERNAME_PATTERN}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
hint={t("common.username_requirements")}
|
||||
/>
|
||||
<ModalInput
|
||||
label="New Password"
|
||||
name="password"
|
||||
type="text"
|
||||
placeholder={`${user.username}'s new password`}
|
||||
autoComplete="off"
|
||||
minLength={8}
|
||||
hint="Password must be at least 8 characters long"
|
||||
/>
|
||||
<ModalTextarea
|
||||
label="Bio"
|
||||
name="bio"
|
||||
placeholder="User's bio"
|
||||
defaultValue={user.bio}
|
||||
autoComplete="off"
|
||||
rows={3}
|
||||
/>
|
||||
<div className="flex flex-col gap-y-1.5 w-full">
|
||||
<ModalLabel htmlFor="role">Role</ModalLabel>
|
||||
<select
|
||||
name="role"
|
||||
required={true}
|
||||
defaultValue={user.role}
|
||||
onChange={(e) => setRole(e.target.value)}
|
||||
className="w-full h-[34px] px-3.5 text-sm rounded-lg outline-none bg-zinc-800 border border-zinc-800 text-zinc-100 light:bg-white light:border-slate-300 light:text-slate-900 focus:border-sky-500 light:focus:border-sky-500"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
<option value="default">Default</option>
|
||||
<option value="manager">Manager</option>
|
||||
{currentUser?.role === "admin" && (
|
||||
<option value="admin">Administrator</option>
|
||||
)}
|
||||
</select>
|
||||
<RoleHintDisplay role={role} />
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<form onSubmit={handleUpdate}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="username"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
name="username"
|
||||
type="text"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="User's username"
|
||||
defaultValue={user.username}
|
||||
minLength={USERNAME_MIN_LENGTH}
|
||||
maxLength={USERNAME_MAX_LENGTH}
|
||||
pattern={USERNAME_PATTERN}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<p className="mt-2 text-xs text-white/60">
|
||||
{t("common.username_requirements")}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
New Password
|
||||
</label>
|
||||
<input
|
||||
name="password"
|
||||
type="text"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder={`${user.username}'s new password`}
|
||||
autoComplete="off"
|
||||
minLength={8}
|
||||
/>
|
||||
<p className="mt-2 text-xs text-white/60">
|
||||
Password must be at least 8 characters long
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="bio"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Bio
|
||||
</label>
|
||||
<textarea
|
||||
name="bio"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="User's bio"
|
||||
defaultValue={user.bio}
|
||||
autoComplete="off"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="role"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Role
|
||||
</label>
|
||||
<select
|
||||
name="role"
|
||||
required={true}
|
||||
defaultValue={user.role}
|
||||
onChange={(e) => setRole(e.target.value)}
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
>
|
||||
<option value="default">Default</option>
|
||||
<option value="manager">Manager</option>
|
||||
{currentUser?.role === "admin" && (
|
||||
<option value="admin">Administrator</option>
|
||||
)}
|
||||
</select>
|
||||
<RoleHintDisplay role={role} />
|
||||
</div>
|
||||
<MessageLimitInput
|
||||
role={role}
|
||||
enabled={messageLimit.enabled}
|
||||
limit={messageLimit.limit}
|
||||
updateState={setMessageLimit}
|
||||
/>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Update user
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MessageLimitInput
|
||||
role={role}
|
||||
enabled={messageLimit.enabled}
|
||||
limit={messageLimit.limit}
|
||||
updateState={setMessageLimit}
|
||||
/>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalSecondaryButton onClick={closeModal} type="button">
|
||||
Cancel
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton type="submit">Update user</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import Admin from "@/models/admin";
|
||||
import EditUserModal from "./EditUserModal";
|
||||
import showToast from "@/utils/toast";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
|
||||
const ModMap = {
|
||||
admin: ["admin", "manager", "default"],
|
||||
@@ -91,13 +91,13 @@ export default function UserRow({ currUser, user }) {
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<Modal isOpen={isOpen} onClose={closeModal}>
|
||||
<EditUserModal
|
||||
currentUser={currUser}
|
||||
user={user}
|
||||
closeModal={closeModal}
|
||||
/>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import UserRow from "./UserRow";
|
||||
import useUser from "@/hooks/useUser";
|
||||
import NewUserModal from "./NewUserModal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
import CTAButton from "@/components/lib/CTAButton";
|
||||
import Toggle from "@/components/lib/Toggle";
|
||||
|
||||
@@ -48,9 +48,9 @@ export default function AdminUsers() {
|
||||
<UsersContainer />
|
||||
</div>
|
||||
</div>
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<Modal isOpen={isOpen} onClose={closeModal}>
|
||||
<NewUserModal closeModal={closeModal} />
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -147,7 +147,7 @@ export function RoleHintDisplay({ role }) {
|
||||
export function MessageLimitInput({ enabled, limit, updateState, role }) {
|
||||
if (role === "admin") return null;
|
||||
return (
|
||||
<div className="mt-4 mb-8">
|
||||
<div>
|
||||
<Toggle
|
||||
size="md"
|
||||
variant="horizontal"
|
||||
@@ -163,7 +163,7 @@ export function MessageLimitInput({ enabled, limit, updateState, role }) {
|
||||
/>
|
||||
{enabled && (
|
||||
<div className="mt-4">
|
||||
<label className="text-white text-sm font-semibold block mb-4">
|
||||
<label className="text-zinc-50 light:text-slate-700 text-sm font-semibold block mb-4">
|
||||
Message limit per day
|
||||
</label>
|
||||
<div className="relative mt-2">
|
||||
@@ -178,7 +178,7 @@ export function MessageLimitInput({ enabled, limit, updateState, role }) {
|
||||
}}
|
||||
value={limit}
|
||||
min={1}
|
||||
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
className="w-full h-[34px] px-3.5 text-sm rounded-lg outline-none bg-zinc-800 border border-zinc-800 text-zinc-100 placeholder:text-zinc-400 light:bg-white light:border-slate-300 light:text-slate-900 light:placeholder:text-slate-400 focus:border-sky-500 light:focus:border-sky-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import React, { useState } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import Admin from "@/models/admin";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
ModalInput,
|
||||
} from "@/components/lib/Modal";
|
||||
|
||||
export default function NewWorkspaceModal({ closeModal }) {
|
||||
const [error, setError] = useState(null);
|
||||
@@ -16,66 +23,30 @@ export default function NewWorkspaceModal({ closeModal }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
Create new workspace
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<form onSubmit={handleCreate}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
{t("common.workspaces-name")}
|
||||
</label>
|
||||
<input
|
||||
name="name"
|
||||
type="text"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="My workspace"
|
||||
minLength={4}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
<p className="text-white text-opacity-60 text-xs md:text-sm">
|
||||
After creating this workspace only admins will be able to see
|
||||
it. You can add users after it has been created.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Create workspace
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form onSubmit={handleCreate} className="flex flex-col gap-y-5">
|
||||
<ModalHeader title="Create new workspace" onClose={closeModal} />
|
||||
<ModalBody>
|
||||
<ModalInput
|
||||
label={t("common.workspaces-name")}
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="My workspace"
|
||||
minLength={4}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
<p className="text-zinc-400 light:text-slate-600 text-xs md:text-sm">
|
||||
After creating this workspace only admins will be able to see it. You
|
||||
can add users after it has been created.
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalSecondaryButton onClick={closeModal} type="button">
|
||||
Cancel
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton type="submit">Create workspace</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import System from "@/models/system";
|
||||
import WorkspaceRow from "./WorkspaceRow";
|
||||
import NewWorkspaceModal from "./NewWorkspaceModal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
import CTAButton from "@/components/lib/CTAButton";
|
||||
|
||||
export default function AdminWorkspaces() {
|
||||
@@ -46,9 +46,9 @@ export default function AdminWorkspaces() {
|
||||
<WorkspacesContainer />
|
||||
</div>
|
||||
</div>
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<Modal isOpen={isOpen} onClose={closeModal}>
|
||||
<NewWorkspaceModal closeModal={closeModal} />
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { X, Copy, Check } from "@phosphor-icons/react";
|
||||
import { Copy, Check } from "@phosphor-icons/react";
|
||||
import Admin from "@/models/admin";
|
||||
import paths from "@/utils/paths";
|
||||
import { userFromStorage } from "@/utils/request";
|
||||
import System from "@/models/system";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
ModalInput,
|
||||
} from "@/components/lib/Modal";
|
||||
|
||||
export default function NewApiKeyModal({ closeModal, onSuccess }) {
|
||||
const { t } = useTranslation();
|
||||
@@ -46,115 +54,78 @@ export default function NewApiKeyModal({ closeModal, onSuccess }) {
|
||||
}, [copied]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
{t("api.modal.title")}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-7 py-6">
|
||||
<form onSubmit={handleCreate}>
|
||||
<div className="space-y-6 max-h-[60vh] overflow-y-auto pr-2">
|
||||
{error && (
|
||||
<p className="text-red-400 text-sm">
|
||||
{t("api.messages.error", { error })}
|
||||
</p>
|
||||
)}
|
||||
{!apiKey && (
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-white">
|
||||
{t("api.modal.name.label")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={t("api.modal.name.placeholder")}
|
||||
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg outline-none block w-full p-2.5"
|
||||
/>
|
||||
<p className="text-white text-opacity-60 text-xs md:text-sm mt-2">
|
||||
{t("api.modal.name.helper")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{apiKey && (
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={`${apiKey.secret}`}
|
||||
disabled={true}
|
||||
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg outline-none block w-full p-2.5 pr-10"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyApiKey}
|
||||
disabled={copied}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-md hover:bg-theme-modal-border transition-all duration-300"
|
||||
>
|
||||
{copied ? (
|
||||
<Check
|
||||
size={20}
|
||||
className="text-green-400"
|
||||
weight="bold"
|
||||
/>
|
||||
) : (
|
||||
<Copy size={20} className="text-white" weight="bold" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-white text-opacity-60 text-xs md:text-sm">
|
||||
{t("api.modal.helper")}
|
||||
</p>
|
||||
<a
|
||||
href={paths.apiDocs()}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-400 hover:underline"
|
||||
>
|
||||
Read the API documentation →
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex justify-end items-center mt-6 pt-6 border-t border-theme-modal-border">
|
||||
{!apiKey ? (
|
||||
<>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm mr-2"
|
||||
>
|
||||
{t("api.modal.cancel")}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
{t("api.modal.create")}
|
||||
</button>
|
||||
</>
|
||||
<form onSubmit={handleCreate} className="flex flex-col gap-y-5">
|
||||
<ModalHeader title={t("api.modal.title")} onClose={closeModal} />
|
||||
<ModalBody>
|
||||
{error && (
|
||||
<p className="text-red-400 text-sm">
|
||||
{t("api.messages.error", { error })}
|
||||
</p>
|
||||
)}
|
||||
{!apiKey && (
|
||||
<ModalInput
|
||||
label={t("api.modal.name.label")}
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={t("api.modal.name.placeholder")}
|
||||
hint={t("api.modal.name.helper")}
|
||||
/>
|
||||
)}
|
||||
{apiKey && (
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={`${apiKey.secret}`}
|
||||
disabled={true}
|
||||
className="border-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-400 light:bg-white light:text-slate-900 light:placeholder:text-slate-400 text-sm rounded-lg outline-none block w-full p-2.5 pr-10"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyApiKey}
|
||||
disabled={copied}
|
||||
className="border-none absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-md bg-transparent hover:bg-zinc-800 light:hover:bg-slate-100 transition-all duration-300"
|
||||
>
|
||||
{copied ? (
|
||||
<Check size={20} className="text-green-400" weight="bold" />
|
||||
) : (
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
{t("api.modal.close")}
|
||||
</button>
|
||||
<Copy
|
||||
size={20}
|
||||
className="text-slate-50 light:text-slate-900"
|
||||
weight="bold"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-zinc-400 light:text-slate-600">
|
||||
{t("api.modal.helper")}
|
||||
</p>
|
||||
<a
|
||||
href={paths.apiDocs()}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-400 hover:underline"
|
||||
>
|
||||
Read the API documentation →
|
||||
</a>
|
||||
</ModalBody>
|
||||
<ModalFooter className={apiKey ? "justify-end" : undefined}>
|
||||
{!apiKey ? (
|
||||
<>
|
||||
<ModalSecondaryButton onClick={closeModal} type="button">
|
||||
{t("api.modal.cancel")}
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton type="submit">
|
||||
{t("api.modal.create")}
|
||||
</ModalPrimaryButton>
|
||||
</>
|
||||
) : (
|
||||
<ModalSecondaryButton onClick={closeModal} type="button">
|
||||
{t("api.modal.close")}
|
||||
</ModalSecondaryButton>
|
||||
)}
|
||||
</ModalFooter>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import NewApiKeyModal from "./NewApiKeyModal";
|
||||
import paths from "@/utils/paths";
|
||||
import { userFromStorage } from "@/utils/request";
|
||||
import System from "@/models/system";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import CTAButton from "@/components/lib/CTAButton";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -125,12 +125,12 @@ export default function AdminApiKeys() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<Modal isOpen={isOpen} onClose={closeModal}>
|
||||
<NewApiKeyModal
|
||||
closeModal={closeModal}
|
||||
onSuccess={fetchExistingKeys}
|
||||
/>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+56
-76
@@ -1,7 +1,13 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import BrowserExtensionApiKey from "@/models/browserExtensionApiKey";
|
||||
import { fullApiUrl, POPUP_BROWSER_EXTENSION_EVENT } from "@/utils/constants";
|
||||
import {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
} from "@/components/lib/Modal";
|
||||
|
||||
export default function NewBrowserExtensionApiKeyModal({
|
||||
closeModal,
|
||||
@@ -48,82 +54,56 @@ export default function NewBrowserExtensionApiKeyModal({
|
||||
}, [copied]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
New Browser Extension API Key
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
<form onSubmit={handleCreate} className="flex flex-col gap-y-5">
|
||||
<ModalHeader title="New Browser Extension API Key" onClose={closeModal} />
|
||||
<ModalBody>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
{apiKey && (
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={apiKey}
|
||||
disabled={true}
|
||||
className="border-none bg-zinc-800 w-full text-zinc-100 placeholder:text-zinc-400 light:bg-white light:text-slate-900 light:placeholder:text-slate-400 text-sm rounded-lg block p-2.5"
|
||||
/>
|
||||
)}
|
||||
{isMultiUser && (
|
||||
<p className="text-yellow-300 light:text-orange-500 text-xs md:text-sm font-semibold">
|
||||
Warning: You are in multi-user mode, this API key will allow access
|
||||
to all workspaces associated with your account. Please share it
|
||||
cautiously.
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-zinc-400 light:text-slate-600">
|
||||
After clicking "Create API Key", AnythingLLM will attempt to connect
|
||||
to your browser extension automatically.
|
||||
</p>
|
||||
<p className="text-xs text-zinc-400 light:text-slate-600">
|
||||
If you see "Connected to AnythingLLM" in the extension, the connection
|
||||
was successful. If not, please copy the connection string and paste it
|
||||
into the extension manually.
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{!apiKey ? (
|
||||
<>
|
||||
<ModalSecondaryButton onClick={closeModal} type="button">
|
||||
Cancel
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton type="submit">
|
||||
Create API Key
|
||||
</ModalPrimaryButton>
|
||||
</>
|
||||
) : (
|
||||
<ModalPrimaryButton
|
||||
onClick={copyApiKey}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
disabled={copied}
|
||||
className="w-full"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-7 py-6">
|
||||
<form onSubmit={handleCreate}>
|
||||
<div className="space-y-6 max-h-[60vh] overflow-y-auto pr-2">
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
{apiKey && (
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={apiKey}
|
||||
disabled={true}
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg block w-full p-2.5"
|
||||
/>
|
||||
)}
|
||||
{isMultiUser && (
|
||||
<p className="text-yellow-300 light:text-orange-500 text-xs md:text-sm font-semibold">
|
||||
Warning: You are in multi-user mode, this API key will allow
|
||||
access to all workspaces associated with your account. Please
|
||||
share it cautiously.
|
||||
</p>
|
||||
)}
|
||||
<p className="text-white text-opacity-60 text-xs md:text-sm">
|
||||
After clicking "Create API Key", AnythingLLM will attempt to
|
||||
connect to your browser extension automatically.
|
||||
</p>
|
||||
<p className="text-white text-opacity-60 text-xs md:text-sm">
|
||||
If you see "Connected to AnythingLLM" in the extension, the
|
||||
connection was successful. If not, please copy the connection
|
||||
string and paste it into the extension manually.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border">
|
||||
{!apiKey ? (
|
||||
<>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Create API Key
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={copyApiKey}
|
||||
type="button"
|
||||
disabled={copied}
|
||||
className="w-full transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm cursor-pointer"
|
||||
>
|
||||
{copied ? "API Key Copied!" : "Copy API Key"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{copied ? "API Key Copied!" : "Copy API Key"}
|
||||
</ModalPrimaryButton>
|
||||
)}
|
||||
</ModalFooter>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import BrowserExtensionApiKey from "@/models/browserExtensionApiKey";
|
||||
import BrowserExtensionApiKeyRow from "./BrowserExtensionApiKeyRow";
|
||||
import CTAButton from "@/components/lib/CTAButton";
|
||||
import NewBrowserExtensionApiKeyModal from "./NewBrowserExtensionApiKeyModal";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import { fullApiUrl } from "@/utils/constants";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
@@ -127,13 +127,13 @@ export default function BrowserExtensionApiKeys() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<Modal isOpen={isOpen} onClose={closeModal}>
|
||||
<NewBrowserExtensionApiKeyModal
|
||||
closeModal={closeModal}
|
||||
onSuccess={fetchExistingKeys}
|
||||
isMultiUser={isMultiUser}
|
||||
/>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
<Tooltip
|
||||
id="auto-connection"
|
||||
place="bottom"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import truncate from "truncate";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal, { ModalHeader, ModalBody } from "@/components/lib/Modal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import paths from "@/utils/paths";
|
||||
import Embed from "@/models/embed";
|
||||
@@ -80,10 +79,10 @@ export default function ChatRow({ chat, onDelete }) {
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<ModalWrapper isOpen={isPromptOpen}>
|
||||
<Modal isOpen={isPromptOpen} onClose={closePromptModal}>
|
||||
<TextPreview text={chat.prompt} closeModal={closePromptModal} />
|
||||
</ModalWrapper>
|
||||
<ModalWrapper isOpen={isResponseOpen}>
|
||||
</Modal>
|
||||
<Modal isOpen={isResponseOpen} onClose={closeResponseModal}>
|
||||
<TextPreview
|
||||
text={
|
||||
<MarkdownRenderer
|
||||
@@ -92,8 +91,11 @@ export default function ChatRow({ chat, onDelete }) {
|
||||
}
|
||||
closeModal={closeResponseModal}
|
||||
/>
|
||||
</ModalWrapper>
|
||||
<ModalWrapper isOpen={isConnectionDetailsModalOpen}>
|
||||
</Modal>
|
||||
<Modal
|
||||
isOpen={isConnectionDetailsModalOpen}
|
||||
onClose={closeConnectionDetailsModal}
|
||||
>
|
||||
<TextPreview
|
||||
text={
|
||||
<ConnectionDetails
|
||||
@@ -104,32 +106,21 @@ export default function ChatRow({ chat, onDelete }) {
|
||||
}
|
||||
closeModal={closeConnectionDetailsModal}
|
||||
/>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const TextPreview = ({ text, closeModal }) => {
|
||||
return (
|
||||
<div className="relative w-full md:max-w-2xl max-h-full">
|
||||
<div className="w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden">
|
||||
<div className="flex items-center justify-between p-6 border-b rounded-t border-theme-modal-border">
|
||||
<h3 className="text-xl font-semibold text-white">Viewing Text</h3>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="bg-transparent rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X className="text-white text-lg" />
|
||||
</button>
|
||||
<form className="flex flex-col gap-y-5">
|
||||
<ModalHeader title="Viewing Text" onClose={closeModal} />
|
||||
<ModalBody>
|
||||
<div className="w-full h-[60vh] py-2 px-4 whitespace-pre-line overflow-auto rounded-lg bg-zinc-800 light:bg-white border border-zinc-800 light:border-slate-300 text-zinc-100 light:text-slate-900 text-sm">
|
||||
{text}
|
||||
</div>
|
||||
<div className="w-full p-6">
|
||||
<div className="w-full h-[60vh] py-2 px-4 whitespace-pre-line overflow-auto rounded-lg bg-zinc-900 light:bg-theme-bg-secondary border border-gray-500 text-white text-sm">
|
||||
{text}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -144,21 +135,21 @@ const ConnectionDetails = ({
|
||||
if (verbose) {
|
||||
return (
|
||||
<>
|
||||
<p className="text-xs text-theme-text-secondary">
|
||||
<p className="text-xs text-zinc-400 light:text-slate-500">
|
||||
sessionID: {sessionId}
|
||||
</p>
|
||||
{details.username && (
|
||||
<p className="text-xs text-theme-text-secondary">
|
||||
<p className="text-xs text-zinc-400 light:text-slate-500">
|
||||
username: {details.username}
|
||||
</p>
|
||||
)}
|
||||
{details.ip && (
|
||||
<p className="text-xs text-theme-text-secondary">
|
||||
<p className="text-xs text-zinc-400 light:text-slate-500">
|
||||
client ip address: {details.ip}
|
||||
</p>
|
||||
)}
|
||||
{details.host && (
|
||||
<p className="text-xs text-theme-text-secondary">
|
||||
<p className="text-xs text-zinc-400 light:text-slate-500">
|
||||
client host URL: {details.host}
|
||||
</p>
|
||||
)}
|
||||
@@ -169,13 +160,19 @@ const ConnectionDetails = ({
|
||||
return (
|
||||
<>
|
||||
{details.username && (
|
||||
<p className="text-xs text-theme-text-secondary">{details.username}</p>
|
||||
<p className="text-xs text-zinc-400 light:text-slate-500">
|
||||
{details.username}
|
||||
</p>
|
||||
)}
|
||||
{details.ip && (
|
||||
<p className="text-xs text-theme-text-secondary">{details.ip}</p>
|
||||
<p className="text-xs text-zinc-400 light:text-slate-500">
|
||||
{details.ip}
|
||||
</p>
|
||||
)}
|
||||
{details.host && (
|
||||
<p className="text-xs text-theme-text-secondary">{details.host}</p>
|
||||
<p className="text-xs text-zinc-400 light:text-slate-500">
|
||||
{details.host}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
+25
-41
@@ -1,44 +1,30 @@
|
||||
import React, { useState } from "react";
|
||||
import { CheckCircle, CopySimple, X } from "@phosphor-icons/react";
|
||||
import { CheckCircle, CopySimple } from "@phosphor-icons/react";
|
||||
import showToast from "@/utils/toast";
|
||||
import hljs from "highlight.js";
|
||||
import "@/utils/chat/themes/github-dark.css";
|
||||
import "@/utils/chat/themes/github.css";
|
||||
import {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalSecondaryButton,
|
||||
ModalLabel,
|
||||
ModalHint,
|
||||
} from "@/components/lib/Modal";
|
||||
|
||||
export default function CodeSnippetModal({ embed, closeModal }) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
Copy your embed code
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-7 py-6">
|
||||
<div className="space-y-6 max-h-[60vh] overflow-y-auto pr-2">
|
||||
<ScriptTag embed={embed} />
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<div hidden={true} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-5">
|
||||
<ModalHeader title="Copy your embed code" onClose={closeModal} />
|
||||
<ModalBody className="max-h-[60vh] overflow-y-auto">
|
||||
<ScriptTag embed={embed} />
|
||||
</ModalBody>
|
||||
<ModalFooter className="justify-end">
|
||||
<ModalSecondaryButton onClick={closeModal} type="button">
|
||||
Close
|
||||
</ModalSecondaryButton>
|
||||
</ModalFooter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -81,14 +67,12 @@ const ScriptTag = ({ embed }) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col mb-2">
|
||||
<label className="block text-sm font-medium text-white">
|
||||
HTML Script Tag Embed Code
|
||||
</label>
|
||||
<p className="text-theme-text-secondary text-xs">
|
||||
<div className="flex flex-col gap-y-1 mb-3">
|
||||
<ModalLabel>HTML Script Tag Embed Code</ModalLabel>
|
||||
<ModalHint>
|
||||
Have your workspace chat embed function like a help desk chat bottom
|
||||
in the corner of your website.
|
||||
</p>
|
||||
</ModalHint>
|
||||
<a
|
||||
href="https://github.com/Mintplex-Labs/anythingllm-embed/blob/main/README.md"
|
||||
target="_blank"
|
||||
@@ -101,10 +85,10 @@ const ScriptTag = ({ embed }) => {
|
||||
<button
|
||||
disabled={copied}
|
||||
onClick={handleClick}
|
||||
className={`disabled:border disabled:border-green-300 disabled:light:border-green-600 border border-transparent relative w-full font-mono flex hljs ${theme} light:border light:border-gray-700 text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5 m-1`}
|
||||
className={`border-none disabled:border disabled:border-green-300 disabled:light:border-green-600 border border-transparent relative w-full font-mono flex hljs ${theme} light:border light:border-gray-700 text-white placeholder:text-zinc-400 text-sm rounded-lg focus:border-sky-500 light:focus:border-sky-500 outline-none p-3`}
|
||||
>
|
||||
<div
|
||||
className="flex w-full text-left flex-col gap-y-1 pr-6 pl-4 whitespace-pre-line"
|
||||
className="flex w-full text-left flex-col gap-y-1 pr-6 whitespace-pre-line"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: hljs.highlight(snippet, {
|
||||
language: "html",
|
||||
|
||||
+68
-92
@@ -1,5 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import {
|
||||
BooleanInput,
|
||||
ChatModeSelection,
|
||||
@@ -11,6 +10,13 @@ import {
|
||||
import Embed from "@/models/embed";
|
||||
import showToast from "@/utils/toast";
|
||||
import { safeJsonParse } from "@/utils/request";
|
||||
import {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
} from "@/components/lib/Modal";
|
||||
|
||||
export default function EditEmbedModal({ embed, closeModal }) {
|
||||
const [error, setError] = useState(null);
|
||||
@@ -31,97 +37,67 @@ export default function EditEmbedModal({ embed, closeModal }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
Update embed #{embed.id}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-7 py-6">
|
||||
<form onSubmit={handleUpdate}>
|
||||
<div className="space-y-6 max-h-[60vh] overflow-y-auto pr-2">
|
||||
<WorkspaceSelection defaultValue={embed.workspace.id} />
|
||||
<ChatModeSelection defaultValue={embed.chat_mode} />
|
||||
<PermittedDomains
|
||||
defaultValue={
|
||||
safeJsonParse(embed.allowlist_domains, null) || []
|
||||
}
|
||||
/>
|
||||
<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."
|
||||
defaultValue={embed.max_chats_per_day}
|
||||
/>
|
||||
<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."
|
||||
defaultValue={embed.max_chats_per_session}
|
||||
/>
|
||||
<NumberInput
|
||||
name="message_limit"
|
||||
title="Message History Limit"
|
||||
hint="The number of previous messages to include in the chat context. Default is 20."
|
||||
defaultValue={embed.message_limit}
|
||||
/>
|
||||
<BooleanInput
|
||||
name="allow_model_override"
|
||||
title="Enable dynamic model use"
|
||||
hint="Allow setting of the preferred LLM model to override the workspace default."
|
||||
defaultValue={embed.allow_model_override}
|
||||
/>
|
||||
<BooleanInput
|
||||
name="allow_temperature_override"
|
||||
title="Enable dynamic LLM temperature"
|
||||
hint="Allow setting of the LLM temperature to override the workspace default."
|
||||
defaultValue={embed.allow_temperature_override}
|
||||
/>
|
||||
<BooleanInput
|
||||
name="allow_prompt_override"
|
||||
title="Enable Prompt Override"
|
||||
hint="Allow setting of the system prompt to override the workspace default."
|
||||
defaultValue={embed.allow_prompt_override}
|
||||
/>
|
||||
<form onSubmit={handleUpdate} className="flex flex-col gap-y-5">
|
||||
<ModalHeader title={`Update embed #${embed.id}`} onClose={closeModal} />
|
||||
<ModalBody>
|
||||
<WorkspaceSelection defaultValue={embed.workspace.id} />
|
||||
<ChatModeSelection defaultValue={embed.chat_mode} />
|
||||
<PermittedDomains
|
||||
defaultValue={safeJsonParse(embed.allowlist_domains, null) || []}
|
||||
/>
|
||||
<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."
|
||||
defaultValue={embed.max_chats_per_day}
|
||||
/>
|
||||
<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."
|
||||
defaultValue={embed.max_chats_per_session}
|
||||
/>
|
||||
<NumberInput
|
||||
name="message_limit"
|
||||
title="Message History Limit"
|
||||
hint="The number of previous messages to include in the chat context. Default is 20."
|
||||
defaultValue={embed.message_limit}
|
||||
/>
|
||||
<BooleanInput
|
||||
name="allow_model_override"
|
||||
title="Enable dynamic model use"
|
||||
hint="Allow setting of the preferred LLM model to override the workspace default."
|
||||
defaultValue={embed.allow_model_override}
|
||||
/>
|
||||
<BooleanInput
|
||||
name="allow_temperature_override"
|
||||
title="Enable dynamic LLM temperature"
|
||||
hint="Allow setting of the LLM temperature to override the workspace default."
|
||||
defaultValue={embed.allow_temperature_override}
|
||||
/>
|
||||
<BooleanInput
|
||||
name="allow_prompt_override"
|
||||
title="Enable Prompt Override"
|
||||
hint="Allow setting of the system prompt to override the workspace default."
|
||||
defaultValue={embed.allow_prompt_override}
|
||||
/>
|
||||
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
<p className="text-white text-opacity-60 text-xs md:text-sm">
|
||||
After creating an embed you will be provided a link that you can
|
||||
publish on your website with a simple
|
||||
<code className="border-none bg-theme-settings-input-bg text-white mx-1 px-1 rounded-sm">
|
||||
<script>
|
||||
</code>{" "}
|
||||
tag.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Update embed
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
<p className="text-xs text-zinc-400 light:text-slate-600">
|
||||
After creating an embed you will be provided a link that you can
|
||||
publish on your website with a simple
|
||||
<code className="border-none bg-zinc-800 light:bg-stone-300 text-white mx-1 px-1 rounded-sm">
|
||||
<script>
|
||||
</code>{" "}
|
||||
tag.
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalSecondaryButton onClick={closeModal} type="button">
|
||||
Cancel
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton type="submit">Update embed</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useRef, useState } from "react";
|
||||
import { DotsThreeOutline } from "@phosphor-icons/react";
|
||||
import showToast from "@/utils/toast";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
import Embed from "@/models/embed";
|
||||
import paths from "@/utils/paths";
|
||||
import { nFormatter } from "@/utils/numbers";
|
||||
@@ -130,12 +130,12 @@ export default function EmbedRow({ embed }) {
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<ModalWrapper isOpen={isSettingsOpen}>
|
||||
<Modal isOpen={isSettingsOpen} onClose={closeSettingsModal}>
|
||||
<EditEmbedModal embed={embed} closeModal={closeSettingsModal} />
|
||||
</ModalWrapper>
|
||||
<ModalWrapper isOpen={isSnippetOpen}>
|
||||
</Modal>
|
||||
<Modal isOpen={isSnippetOpen} onClose={closeSnippetModal}>
|
||||
<CodeSnippetModal embed={embed} closeModal={closeSnippetModal} />
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+93
-128
@@ -1,9 +1,17 @@
|
||||
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";
|
||||
import Toggle from "@/components/lib/Toggle";
|
||||
import {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
ModalLabel,
|
||||
ModalHint,
|
||||
} from "@/components/lib/Modal";
|
||||
|
||||
export function enforceSubmissionSchema(form) {
|
||||
const data = {};
|
||||
@@ -39,89 +47,64 @@ export default function NewEmbedModal({ closeModal }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
Create new embed for workspace
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-7 py-6">
|
||||
<form onSubmit={handleCreate}>
|
||||
<div className="space-y-6 max-h-[60vh] overflow-y-auto pr-2">
|
||||
<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."
|
||||
/>
|
||||
<NumberInput
|
||||
name="message_limit"
|
||||
title="Message History Limit"
|
||||
hint="The number of previous messages to include in the chat context. Default is 20."
|
||||
defaultValue={20}
|
||||
/>
|
||||
<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."
|
||||
/>
|
||||
<form onSubmit={handleCreate} className="flex flex-col gap-y-5">
|
||||
<ModalHeader
|
||||
title="Create new embed for workspace"
|
||||
onClose={closeModal}
|
||||
/>
|
||||
<ModalBody>
|
||||
<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."
|
||||
/>
|
||||
<NumberInput
|
||||
name="message_limit"
|
||||
title="Message History Limit"
|
||||
hint="The number of previous messages to include in the chat context. Default is 20."
|
||||
defaultValue={20}
|
||||
/>
|
||||
<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-opacity-60 text-xs md:text-sm">
|
||||
After creating an embed you will be provided a link that you can
|
||||
publish on your website with a simple
|
||||
<code className="light:bg-stone-300 bg-stone-900 text-white mx-1 px-1 rounded-sm">
|
||||
<script>
|
||||
</code>{" "}
|
||||
tag.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Create embed
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
<p className="text-xs text-zinc-400 light:text-slate-600">
|
||||
After creating an embed you will be provided a link that you can
|
||||
publish on your website with a simple
|
||||
<code className="light:bg-stone-300 bg-stone-900 text-white mx-1 px-1 rounded-sm">
|
||||
<script>
|
||||
</code>{" "}
|
||||
tag.
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalSecondaryButton onClick={closeModal} type="button">
|
||||
Cancel
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton type="submit">Create embed</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -138,22 +121,17 @@ export const WorkspaceSelection = ({ defaultValue = null }) => {
|
||||
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-theme-text-secondary text-xs">
|
||||
<ModalLabel htmlFor="workspace_id">Workspace</ModalLabel>
|
||||
<ModalHint>
|
||||
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>
|
||||
</ModalHint>
|
||||
</div>
|
||||
<select
|
||||
name="workspace_id"
|
||||
required={true}
|
||||
defaultValue={defaultValue}
|
||||
className="min-w-[15rem] rounded-lg bg-theme-settings-input-bg px-4 py-2 text-sm text-white focus:ring-blue-500 focus:border-blue-500"
|
||||
className="min-w-[15rem] rounded-lg bg-zinc-800 light:bg-white border border-zinc-800 light:border-slate-300 px-4 py-2 text-sm text-zinc-100 light:text-slate-900 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
{workspaces.map((workspace) => {
|
||||
return (
|
||||
@@ -177,26 +155,21 @@ export const ChatModeSelection = ({ defaultValue = null }) => {
|
||||
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-theme-text-secondary text-xs">
|
||||
<ModalLabel htmlFor="chat_mode">Allowed chat method</ModalLabel>
|
||||
<ModalHint>
|
||||
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>
|
||||
</ModalHint>
|
||||
</div>
|
||||
<div className="mt-2 gap-y-3 flex flex-col">
|
||||
<label
|
||||
className={`transition-all duration-300 w-full h-11 p-2.5 rounded-lg flex justify-start items-center gap-2.5 cursor-pointer border ${
|
||||
chatMode === "chat"
|
||||
? "border-theme-sidebar-item-workspace-active bg-theme-bg-secondary"
|
||||
: "border-theme-sidebar-border hover:border-theme-sidebar-border hover:bg-theme-bg-secondary"
|
||||
? "border-sky-500 bg-zinc-800 light:bg-slate-100"
|
||||
: "border-zinc-800 light:border-slate-300 hover:bg-zinc-800 light:hover:bg-slate-100"
|
||||
} `}
|
||||
>
|
||||
<input
|
||||
@@ -208,21 +181,19 @@ export const ChatModeSelection = ({ defaultValue = null }) => {
|
||||
className="hidden"
|
||||
/>
|
||||
<div
|
||||
className={`w-4 h-4 rounded-full border-2 border-theme-sidebar-border mr-2 ${
|
||||
chatMode === "chat"
|
||||
? "bg-[var(--theme-sidebar-item-workspace-active)]"
|
||||
: ""
|
||||
className={`w-4 h-4 rounded-full border-2 border-zinc-700 light:border-slate-400 mr-2 ${
|
||||
chatMode === "chat" ? "bg-sky-500" : ""
|
||||
}`}
|
||||
></div>
|
||||
<div className="text-theme-text-primary text-sm font-medium font-['Plus Jakarta Sans'] leading-tight">
|
||||
<div className="text-zinc-100 light:text-slate-900 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 rounded-lg flex justify-start items-center gap-2.5 cursor-pointer border ${
|
||||
chatMode === "query"
|
||||
? "border-theme-sidebar-item-workspace-active bg-theme-bg-secondary"
|
||||
: "border-theme-sidebar-border hover:border-theme-sidebar-border hover:bg-theme-bg-secondary"
|
||||
? "border-sky-500 bg-zinc-800 light:bg-slate-100"
|
||||
: "border-zinc-800 light:border-slate-300 hover:bg-zinc-800 light:hover:bg-slate-100"
|
||||
} `}
|
||||
>
|
||||
<input
|
||||
@@ -234,13 +205,11 @@ export const ChatModeSelection = ({ defaultValue = null }) => {
|
||||
className="hidden"
|
||||
/>
|
||||
<div
|
||||
className={`w-4 h-4 rounded-full border-2 border-theme-sidebar-border mr-2 ${
|
||||
chatMode === "query"
|
||||
? "bg-[var(--theme-sidebar-item-workspace-active)]"
|
||||
: ""
|
||||
className={`w-4 h-4 rounded-full border-2 border-zinc-700 light:border-slate-400 mr-2 ${
|
||||
chatMode === "query" ? "bg-sky-500" : ""
|
||||
}`}
|
||||
></div>
|
||||
<div className="text-theme-text-primary text-sm font-medium font-['Plus Jakarta Sans'] leading-tight">
|
||||
<div className="text-zinc-100 light:text-slate-900 text-sm font-medium font-['Plus Jakarta Sans'] leading-tight">
|
||||
Query: Only respond to chats related to documents in workspace
|
||||
</div>
|
||||
</label>
|
||||
@@ -290,18 +259,15 @@ export const PermittedDomains = ({ defaultValue = [] }) => {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col mb-2">
|
||||
<label
|
||||
htmlFor="allowlist_domains"
|
||||
className="block text-sm font-medium text-white"
|
||||
>
|
||||
<ModalLabel htmlFor="allowlist_domains">
|
||||
Restrict requests from domains
|
||||
</label>
|
||||
<p className="text-theme-text-secondary text-xs">
|
||||
</ModalLabel>
|
||||
<ModalHint>
|
||||
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>
|
||||
</ModalHint>
|
||||
</div>
|
||||
<input type="hidden" name="allowlist_domains" value={domains.join(",")} />
|
||||
<TagsInput
|
||||
@@ -310,9 +276,9 @@ export const PermittedDomains = ({ defaultValue = [] }) => {
|
||||
onBlur={handleBlur}
|
||||
placeholder="https://mysite.com, https://anythingllm.com"
|
||||
classNames={{
|
||||
tag: "bg-theme-settings-input-bg light:bg-black/10 bg-blue-300/10 text-zinc-800",
|
||||
tag: "!bg-zinc-700 light:!bg-slate-200 !text-zinc-100 light:!text-slate-800",
|
||||
input:
|
||||
"flex p-1 !bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none",
|
||||
"flex !bg-transparent text-zinc-100 light:text-slate-900 placeholder:text-zinc-400 light:placeholder:text-slate-400 text-sm outline-none",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -323,15 +289,13 @@ 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-theme-text-secondary text-xs">{hint}</p>
|
||||
<ModalLabel htmlFor={name}>{title}</ModalLabel>
|
||||
<ModalHint>{hint}</ModalHint>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
name={name}
|
||||
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-[15rem] p-2.5"
|
||||
className="border border-zinc-800 light:border-slate-300 bg-zinc-800 light:bg-white text-zinc-100 light:text-slate-900 placeholder:text-zinc-400 light:placeholder:text-slate-400 text-sm rounded-lg focus:border-sky-500 light:focus:border-sky-500 outline-none block w-[15rem] p-2.5"
|
||||
min={0}
|
||||
defaultValue={defaultValue}
|
||||
onScroll={(e) => e.target.blur()}
|
||||
@@ -350,6 +314,7 @@ export const BooleanInput = ({ name, title, hint, defaultValue = null }) => {
|
||||
variant="horizontal"
|
||||
label={title}
|
||||
description={hint}
|
||||
value="on"
|
||||
enabled={status}
|
||||
onChange={(checked) => setStatus(checked)}
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CodeBlock } from "@phosphor-icons/react";
|
||||
import EmbedRow from "./EmbedRow";
|
||||
import NewEmbedModal from "./NewEmbedModal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
import Embed from "@/models/embed";
|
||||
import CTAButton from "@/components/lib/CTAButton";
|
||||
|
||||
@@ -89,9 +89,9 @@ export default function EmbedConfigsView() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<Modal isOpen={isOpen} onClose={closeModal}>
|
||||
<NewEmbedModal closeModal={closeModal} />
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export default function ChatEmbedWidgets() {
|
||||
|
||||
return (
|
||||
<WidgetLayout>
|
||||
<div className="flex-1 flex gap-x-6 p-4 mt-10">
|
||||
<div className="flex-1 flex gap-x-6 p-4 mt-10 min-w-0">
|
||||
<div className="flex flex-col min-w-[360px] h-[calc(100vh-90px)]">
|
||||
<div className="flex-none mb-4">
|
||||
<div className="text-theme-text-primary flex items-center gap-x-2">
|
||||
@@ -82,7 +82,7 @@ export default function ChatEmbedWidgets() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-[2] flex flex-col gap-y-[18px] mt-10">
|
||||
<div className="flex-[2] flex flex-col gap-y-[18px] mt-10 min-w-0">
|
||||
<div className="bg-theme-bg-secondary text-white rounded-xl flex-1 p-4 overflow-y-scroll no-scroll">
|
||||
{selectedView === "configs" ? (
|
||||
<EmbedConfigsView />
|
||||
@@ -105,7 +105,7 @@ function WidgetLayout({ children }) {
|
||||
<Sidebar />
|
||||
<div
|
||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] w-full h-full flex"
|
||||
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] w-full h-full flex min-w-0"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import truncate from "truncate";
|
||||
import { X, Trash } from "@phosphor-icons/react";
|
||||
import { Trash } from "@phosphor-icons/react";
|
||||
import System from "@/models/system";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal, { ModalHeader, ModalBody } from "@/components/lib/Modal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import MarkdownRenderer from "../MarkdownRenderer";
|
||||
import { safeJsonParse } from "@/utils/request";
|
||||
@@ -61,10 +61,10 @@ export default function ChatRow({ chat, onDelete }) {
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<ModalWrapper isOpen={isPromptOpen}>
|
||||
<Modal isOpen={isPromptOpen} onClose={closePromptModal}>
|
||||
<TextPreview text={chat.prompt} closeModal={closePromptModal} />
|
||||
</ModalWrapper>
|
||||
<ModalWrapper isOpen={isResponseOpen}>
|
||||
</Modal>
|
||||
<Modal isOpen={isResponseOpen} onClose={closeResponseModal}>
|
||||
<TextPreview
|
||||
text={
|
||||
<MarkdownRenderer
|
||||
@@ -73,30 +73,19 @@ export default function ChatRow({ chat, onDelete }) {
|
||||
}
|
||||
closeModal={closeResponseModal}
|
||||
/>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
const TextPreview = ({ text, closeModal }) => {
|
||||
return (
|
||||
<div className="relative w-full md:max-w-2xl max-h-full">
|
||||
<div className="w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden">
|
||||
<div className="flex items-center justify-between p-6 border-b rounded-t border-theme-modal-border">
|
||||
<h3 className="text-xl font-semibold text-white">Viewing Text</h3>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="bg-transparent rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X className="text-white text-lg" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="w-full p-6">
|
||||
<pre className="w-full h-[200px] py-2 px-4 whitespace-pre-line overflow-auto rounded-lg bg-zinc-900 light:bg-theme-bg-secondary border border-gray-500 text-white text-sm">
|
||||
{text}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form className="flex flex-col gap-y-5">
|
||||
<ModalHeader title="Viewing Text" onClose={closeModal} />
|
||||
<ModalBody>
|
||||
<pre className="w-full h-[200px] py-2 px-4 whitespace-pre-line overflow-auto rounded-lg bg-zinc-800 light:bg-white border border-zinc-800 light:border-slate-300 text-zinc-100 light:text-slate-900 text-sm">
|
||||
{text}
|
||||
</pre>
|
||||
</ModalBody>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ import LemonadeOptions from "@/components/EmbeddingSelection/LemonadeOptions";
|
||||
import EmbedderItem from "@/components/EmbeddingSelection/EmbedderItem";
|
||||
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
import CTAButton from "@/components/lib/CTAButton";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -387,13 +387,13 @@ export default function GeneralEmbeddingPreference() {
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<Modal isOpen={isOpen} onClose={closeModal} size="lg">
|
||||
<ChangeWarningModal
|
||||
warningText="Switching the embedding model will reset all previously embedded documents in all workspaces.\n\nConfirming will clear all embeddings from your vector database and remove all documents from your workspaces. Your uploaded documents will not be deleted, they will be available for re-embedding."
|
||||
onClose={closeModal}
|
||||
onConfirm={handleSaveSettings}
|
||||
/>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import showToast from "@/utils/toast";
|
||||
import { numberWithCommas } from "@/utils/numbers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
import ChangeWarningModal from "@/components/ChangeWarning";
|
||||
|
||||
function isNullOrNaN(value) {
|
||||
@@ -193,13 +193,13 @@ export default function EmbeddingTextSplitterPreference() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<Modal isOpen={isOpen} onClose={closeModal} size="lg">
|
||||
<ChangeWarningModal
|
||||
warningText="Changing text splitter settings will clear any previously cached documents.\n\nThese new settings will be applied to all documents when embedding them into a workspace."
|
||||
onClose={closeModal}
|
||||
onConfirm={handleSaveSettings}
|
||||
/>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
import BG from "./bg.png";
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -12,7 +12,7 @@ import GetOnGooglePlay from "./gplay-badge.svg";
|
||||
|
||||
export default function MobileConnectModal({ isOpen, onClose }) {
|
||||
return (
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<Modal isOpen={isOpen} onClose={onClose} variant="bare">
|
||||
<div
|
||||
className="relative w-full rounded-lg shadow"
|
||||
style={{
|
||||
@@ -26,9 +26,9 @@ export default function MobileConnectModal({ isOpen, onClose }) {
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
className="border-none absolute top-4 right-4 transition-all duration-200 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-zinc-800 light:hover:bg-slate-100"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-[#FFF]" />
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
|
||||
<div className="flex w-full h-full justify-between p-[35px]">
|
||||
@@ -76,7 +76,7 @@ export default function MobileConnectModal({ isOpen, onClose }) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,14 @@ import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AVAILABLE_LLM_PROVIDERS } from "@/pages/GeneralSettings/LLMPreference";
|
||||
import System from "@/models/system";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal, {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
} from "@/components/lib/Modal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import showToast from "@/utils/toast";
|
||||
|
||||
// Providers that can't be routing targets
|
||||
@@ -225,58 +230,46 @@ function ProviderSetupModal({ isOpen, provider, settings, onSave, onClose }) {
|
||||
if (!isOpen || !provider) return null;
|
||||
|
||||
return (
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<div className="w-full max-w-2xl bg-zinc-900 light:bg-white rounded-[8px] shadow-lg border border-zinc-700 light:border-slate-300">
|
||||
<div className="flex items-center justify-between p-6 border-b border-zinc-700 light:border-slate-300">
|
||||
<div className="flex items-center gap-x-3">
|
||||
{provider.logo && (
|
||||
<img
|
||||
src={provider.logo}
|
||||
alt={`${provider.name} logo`}
|
||||
className="w-8 h-8 rounded-md"
|
||||
/>
|
||||
)}
|
||||
<h3 className="text-base font-semibold leading-6 text-white light:text-slate-950">
|
||||
{t("model-router.provider-picker.configure-provider", {
|
||||
name: provider.name,
|
||||
})}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="border-none p-1 rounded-lg text-zinc-400 light:text-slate-500 hover:text-white light:hover:text-slate-900 hover:bg-zinc-800 light:hover:bg-slate-100 transition-colors"
|
||||
>
|
||||
<X size={16} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
<form id="provider-setup-form" onSubmit={onSave}>
|
||||
<div className="px-6 py-5">
|
||||
<p className="text-xs leading-4 text-zinc-400 light:text-slate-600 mb-4">
|
||||
{t("model-router.provider-picker.setup-credentials", {
|
||||
name: provider.name,
|
||||
})}
|
||||
</p>
|
||||
<div className="space-y-4">{provider.options(settings ?? {})}</div>
|
||||
</div>
|
||||
<div className="flex justify-between gap-x-3 px-6 py-4 border-t border-zinc-700 light:border-slate-300">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="border border-zinc-600 light:border-slate-600 text-white light:text-slate-900 text-sm font-medium leading-5 rounded-[8px] h-[34px] px-3.5 hover:opacity-90 transition-opacity"
|
||||
>
|
||||
{t("model-router.provider-picker.cancel")}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
form="provider-setup-form"
|
||||
className="border-none text-sm font-medium leading-5 bg-zinc-50 light:bg-slate-900 text-zinc-900 light:text-white rounded-[8px] h-[34px] px-3.5 hover:opacity-90 transition-opacity"
|
||||
>
|
||||
{t("model-router.provider-picker.save-settings")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="lg">
|
||||
<form
|
||||
id="provider-setup-form"
|
||||
onSubmit={onSave}
|
||||
className="flex flex-col gap-y-5"
|
||||
>
|
||||
<ModalHeader
|
||||
title={
|
||||
<div className="flex items-center gap-x-3">
|
||||
{provider.logo && (
|
||||
<img
|
||||
src={provider.logo}
|
||||
alt={`${provider.name} logo`}
|
||||
className="w-8 h-8 rounded-md"
|
||||
/>
|
||||
)}
|
||||
<span>
|
||||
{t("model-router.provider-picker.configure-provider", {
|
||||
name: provider.name,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
subtitle={t("model-router.provider-picker.setup-credentials", {
|
||||
name: provider.name,
|
||||
})}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<ModalBody>
|
||||
<div className="space-y-4">{provider.options(settings ?? {})}</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalSecondaryButton type="button" onClick={onClose}>
|
||||
{t("model-router.provider-picker.cancel")}
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton type="submit" form="provider-setup-form">
|
||||
{t("model-router.provider-picker.save-settings")}
|
||||
</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CircleNotch, X } from "@phosphor-icons/react";
|
||||
import { CircleNotch } from "@phosphor-icons/react";
|
||||
import ModelRouter from "@/models/modelRouter";
|
||||
import System from "@/models/system";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal, {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
ModalInput,
|
||||
} from "@/components/lib/Modal";
|
||||
import LLMProviderModelPicker from "../LLMProviderModelPicker";
|
||||
|
||||
export default function NewRouterModal({
|
||||
@@ -67,57 +74,34 @@ export default function NewRouterModal({
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<div className="relative w-full max-w-2xl bg-zinc-900 light:bg-white rounded-[8px] shadow border border-zinc-700 light:border-slate-300">
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-y-5 p-6">
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<div className="flex items-start justify-between">
|
||||
<h3 className="text-base font-semibold leading-6 text-white light:text-slate-950">
|
||||
{isEdit
|
||||
? t("model-router.edit-router.title", { name: router.name })
|
||||
: t("model-router.new-router.title")}
|
||||
</h3>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="border-none text-zinc-400 light:text-slate-500 hover:text-white light:hover:text-slate-900 transition-colors"
|
||||
>
|
||||
<X size={16} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
{isEdit && router?.description && (
|
||||
<p className="text-xs leading-4 text-zinc-400 light:text-slate-600 truncate">
|
||||
{router.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Modal isOpen={isOpen} onClose={closeModal} size="md">
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-y-5">
|
||||
<ModalHeader
|
||||
title={
|
||||
isEdit
|
||||
? t("model-router.edit-router.title", { name: router.name })
|
||||
: t("model-router.new-router.title")
|
||||
}
|
||||
subtitle={isEdit && router?.description ? router.description : null}
|
||||
onClose={closeModal}
|
||||
/>
|
||||
<ModalBody>
|
||||
<ModalInput
|
||||
label={t("model-router.new-router.name")}
|
||||
type="text"
|
||||
name="name"
|
||||
defaultValue={router?.name || ""}
|
||||
placeholder={t("model-router.new-router.name-placeholder")}
|
||||
required
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-y-1.5">
|
||||
<label className="text-sm font-medium leading-5 text-white light:text-slate-950">
|
||||
{t("model-router.new-router.name")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
defaultValue={router?.name || ""}
|
||||
placeholder={t("model-router.new-router.name-placeholder")}
|
||||
className="bg-zinc-800 light:bg-white light:border light:border-slate-300 text-white light:text-slate-700 placeholder:text-zinc-400 light:placeholder:text-slate-400 text-sm rounded-[8px] outline-none block w-full h-8 px-3.5"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-y-1.5">
|
||||
<label className="text-sm font-medium leading-5 text-white light:text-slate-950">
|
||||
{t("model-router.new-router.description")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="description"
|
||||
defaultValue={router?.description || ""}
|
||||
placeholder={t("model-router.new-router.description-placeholder")}
|
||||
className="bg-zinc-800 light:bg-white light:border light:border-slate-300 text-white light:text-slate-700 placeholder:text-zinc-400 light:placeholder:text-slate-400 text-sm rounded-[8px] outline-none block w-full h-8 px-3.5"
|
||||
/>
|
||||
</div>
|
||||
<ModalInput
|
||||
label={t("model-router.new-router.description")}
|
||||
type="text"
|
||||
name="description"
|
||||
defaultValue={router?.description || ""}
|
||||
placeholder={t("model-router.new-router.description-placeholder")}
|
||||
/>
|
||||
|
||||
<LLMProviderModelPicker
|
||||
providerFieldName="fallback_provider"
|
||||
@@ -130,55 +114,39 @@ export default function NewRouterModal({
|
||||
defaultModel={router?.fallback_model ?? systemSettings?.LLMModel}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-y-1.5">
|
||||
<label className="text-sm font-medium leading-5 text-white light:text-slate-950">
|
||||
{t("model-router.new-router.cooldown-label")}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="cooldown_seconds"
|
||||
defaultValue={router?.cooldown_seconds ?? 300}
|
||||
min={0}
|
||||
className="bg-zinc-800 light:bg-white light:border light:border-slate-300 text-white light:text-slate-700 placeholder:text-zinc-400 light:placeholder:text-slate-400 text-sm rounded-[8px] outline-none block w-full h-8 px-3.5"
|
||||
/>
|
||||
<p className="text-xs leading-4 text-zinc-400 light:text-slate-600">
|
||||
{t("model-router.new-router.cooldown-help")}
|
||||
</p>
|
||||
</div>
|
||||
<ModalInput
|
||||
label={t("model-router.new-router.cooldown-label")}
|
||||
hint={t("model-router.new-router.cooldown-help")}
|
||||
type="number"
|
||||
name="cooldown_seconds"
|
||||
defaultValue={router?.cooldown_seconds ?? 300}
|
||||
min={0}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<p className="text-xs leading-4 text-red-400 light:text-red-600">
|
||||
Error: {error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="border border-zinc-600 light:border-slate-600 text-white light:text-slate-900 text-sm font-medium leading-5 rounded-[8px] h-[34px] px-3.5 hover:opacity-90 transition-opacity"
|
||||
>
|
||||
{t("model-router.new-router.cancel")}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="border-none flex items-center gap-x-1.5 text-sm font-medium leading-5 bg-zinc-50 light:bg-slate-900 text-zinc-900 light:text-white rounded-[8px] h-[34px] px-3.5 hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<CircleNotch className="h-4 w-4 animate-spin" />
|
||||
{t("common.saving")}
|
||||
</>
|
||||
) : isEdit ? (
|
||||
t("model-router.edit-router.save")
|
||||
) : (
|
||||
t("model-router.new-router.create")
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalSecondaryButton onClick={closeModal} type="button">
|
||||
{t("model-router.new-router.cancel")}
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton type="submit" disabled={loading}>
|
||||
{loading ? (
|
||||
<>
|
||||
<CircleNotch className="h-4 w-4 animate-spin" />
|
||||
{t("common.saving")}
|
||||
</>
|
||||
) : isEdit ? (
|
||||
t("model-router.edit-router.save")
|
||||
) : (
|
||||
t("model-router.new-router.create")
|
||||
)}
|
||||
</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CircleNotch, X } from "@phosphor-icons/react";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import { CircleNotch } from "@phosphor-icons/react";
|
||||
import Modal, {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
ModalInput,
|
||||
ModalLabel,
|
||||
ModalHint,
|
||||
} from "@/components/lib/Modal";
|
||||
import ModelRouterAPI from "@/models/modelRouter";
|
||||
import showToast from "@/utils/toast";
|
||||
import LLMProviderModelPicker from "../../LLMProviderModelPicker";
|
||||
@@ -121,111 +130,83 @@ export default function RuleForm({
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<div className="relative w-full max-w-3xl bg-zinc-900 light:bg-white rounded-[8px] shadow border border-zinc-700 light:border-slate-300">
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-y-5 p-6">
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<div className="flex items-start justify-between">
|
||||
<h3 className="text-base font-semibold leading-6 text-white light:text-slate-950">
|
||||
{t("model-router.rules.title")}
|
||||
</h3>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="border-none text-zinc-400 light:text-slate-500 hover:text-white light:hover:text-slate-900 transition-colors"
|
||||
>
|
||||
<X size={16} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs leading-4 text-zinc-400 light:text-slate-600">
|
||||
{t("model-router.rules.description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-y-5 max-h-[60vh] overflow-y-auto">
|
||||
<div className="flex gap-x-5 items-start">
|
||||
<div className="flex flex-col gap-y-1.5 w-[500px]">
|
||||
<label className="text-sm font-medium leading-5 text-white light:text-slate-950">
|
||||
{t("model-router.rule-form.title-label")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
defaultValue={existingRule?.title || ""}
|
||||
placeholder="e.g. route_code_to_claude"
|
||||
className="bg-zinc-800 light:bg-white light:border light:border-slate-300 text-white light:text-slate-700 placeholder:text-zinc-400 light:placeholder:text-slate-400 text-sm rounded-[8px] outline-none block w-full h-8 px-3.5 font-mono"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-1.5 w-[300px]">
|
||||
<label className="text-sm font-medium leading-5 text-white light:text-slate-950">
|
||||
{t("model-router.rule-form.rule-type")}
|
||||
</label>
|
||||
<select
|
||||
value={ruleType}
|
||||
onChange={(e) => setRuleType(e.target.value)}
|
||||
className="bg-zinc-800 light:bg-white light:border light:border-slate-300 text-white light:text-slate-700 text-sm rounded-[8px] outline-none block w-full h-8 px-3.5"
|
||||
>
|
||||
{ruleTypes.map((rt) => (
|
||||
<option key={rt.value} value={rt.value}>
|
||||
{rt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs leading-4 text-zinc-400 light:text-slate-600">
|
||||
{ruleTypes.find((rt) => rt.value === ruleType)?.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ruleType === "calculated" ? (
|
||||
<CalculatedFields
|
||||
conditionLogic={conditionLogic}
|
||||
setConditionLogic={setConditionLogic}
|
||||
conditions={conditions}
|
||||
setConditions={setConditions}
|
||||
<Modal isOpen={isOpen} onClose={closeModal} size="lg">
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-y-5">
|
||||
<ModalHeader
|
||||
title={t("model-router.rules.title")}
|
||||
subtitle={t("model-router.rules.description")}
|
||||
onClose={closeModal}
|
||||
/>
|
||||
<ModalBody className="max-h-[60vh] overflow-y-auto">
|
||||
<div className="flex gap-x-5 items-start">
|
||||
<div className="w-[500px]">
|
||||
<ModalInput
|
||||
label={t("model-router.rule-form.title-label")}
|
||||
type="text"
|
||||
name="title"
|
||||
defaultValue={existingRule?.title || ""}
|
||||
placeholder="e.g. route_code_to_claude"
|
||||
className="font-mono"
|
||||
required
|
||||
/>
|
||||
) : (
|
||||
<LLMDescriptionField existingRule={existingRule} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-1.5 w-[300px]">
|
||||
<ModalLabel>{t("model-router.rule-form.rule-type")}</ModalLabel>
|
||||
<select
|
||||
value={ruleType}
|
||||
onChange={(e) => setRuleType(e.target.value)}
|
||||
className="bg-zinc-800 light:bg-white border border-zinc-800 light:border-slate-300 text-zinc-100 light:text-slate-900 text-sm rounded-lg outline-none block w-full h-[34px] px-3.5 focus:border-sky-500 light:focus:border-sky-500"
|
||||
>
|
||||
{ruleTypes.map((rt) => (
|
||||
<option key={rt.value} value={rt.value}>
|
||||
{rt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ModalHint>
|
||||
{ruleTypes.find((rt) => rt.value === ruleType)?.description}
|
||||
</ModalHint>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LLMProviderModelPicker
|
||||
providerFieldName="route_provider"
|
||||
modelFieldName="route_model"
|
||||
label={t("model-router.rule-form.route-to-label")}
|
||||
description={t("model-router.rule-form.route-to-description")}
|
||||
defaultProvider={existingRule?.route_provider || ""}
|
||||
defaultModel={existingRule?.route_model || ""}
|
||||
{ruleType === "calculated" ? (
|
||||
<CalculatedFields
|
||||
conditionLogic={conditionLogic}
|
||||
setConditionLogic={setConditionLogic}
|
||||
conditions={conditions}
|
||||
setConditions={setConditions}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<LLMDescriptionField existingRule={existingRule} />
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
className="border border-zinc-600 light:border-slate-600 text-white light:text-slate-900 text-sm font-medium leading-5 rounded-[8px] h-[34px] px-3.5 hover:opacity-90 transition-opacity"
|
||||
>
|
||||
{t("model-router.rule-form.cancel")}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="border-none flex items-center gap-x-1.5 text-sm font-medium leading-5 bg-zinc-50 light:bg-slate-900 text-zinc-900 light:text-white rounded-[8px] h-[34px] px-3.5 hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<CircleNotch className="h-4 w-4 animate-spin" />
|
||||
{t("model-router.rule-form.saving")}
|
||||
</>
|
||||
) : isEditing ? (
|
||||
t("model-router.rule-form.update-rule")
|
||||
) : (
|
||||
t("model-router.rule-form.create-rule")
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
<LLMProviderModelPicker
|
||||
providerFieldName="route_provider"
|
||||
modelFieldName="route_model"
|
||||
label={t("model-router.rule-form.route-to-label")}
|
||||
description={t("model-router.rule-form.route-to-description")}
|
||||
defaultProvider={existingRule?.route_provider || ""}
|
||||
defaultModel={existingRule?.route_model || ""}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalSecondaryButton type="button" onClick={closeModal}>
|
||||
{t("model-router.rule-form.cancel")}
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton type="submit" disabled={loading}>
|
||||
{loading ? (
|
||||
<>
|
||||
<CircleNotch className="h-4 w-4 animate-spin" />
|
||||
{t("model-router.rule-form.saving")}
|
||||
</>
|
||||
) : isEditing ? (
|
||||
t("model-router.rule-form.update-rule")
|
||||
) : (
|
||||
t("model-router.rule-form.create-rule")
|
||||
)}
|
||||
</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { X, WarningCircle } from "@phosphor-icons/react";
|
||||
import { WarningCircle } from "@phosphor-icons/react";
|
||||
import ScheduledJobs from "@/models/scheduledJobs";
|
||||
import showToast from "@/utils/toast";
|
||||
import { safeJsonParse } from "@/utils/request";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ModalHeader, ModalBody } from "@/components/lib/Modal";
|
||||
import JobDescription from "./JobDescription";
|
||||
import JobSchedule from "./JobSchedule";
|
||||
import ToolsSelector from "./ToolsSelector";
|
||||
@@ -102,62 +103,48 @@ export default function JobFormModal({ job = null, onClose, onSaved }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full max-w-2xl max-h-full">
|
||||
<div className="relative bg-theme-bg-secondary rounded-lg shadow border border-theme-modal-border">
|
||||
<div className="flex flex-col gap-1 p-4 border-b rounded-t border-theme-modal-border">
|
||||
<div className="flex items-start justify-between">
|
||||
<h3 className="text-xl font-semibold text-theme-text-primary">
|
||||
{isEditing
|
||||
? t("scheduledJobs.modal.titleEdit")
|
||||
: t("scheduledJobs.modal.titleNew")}
|
||||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="border-none 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"
|
||||
>
|
||||
<X className="text-gray-300 text-lg" />
|
||||
</button>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-y-5">
|
||||
<ModalHeader
|
||||
title={
|
||||
isEditing
|
||||
? t("scheduledJobs.modal.titleEdit")
|
||||
: t("scheduledJobs.modal.titleNew")
|
||||
}
|
||||
onClose={onClose}
|
||||
>
|
||||
{hasErrors() && (
|
||||
<div className="flex gap-1 items-center mt-1">
|
||||
<WarningCircle size={16} className="text-red-400 shrink-0" />
|
||||
<p className="text-sm text-red-400">
|
||||
{t(
|
||||
"scheduledJobs.modal.requiredFieldsBanner",
|
||||
"Please fill out all required fields in order to create job."
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{hasErrors() && (
|
||||
<div className="flex gap-1 items-center">
|
||||
<WarningCircle size={16} className="text-red-400 shrink-0" />
|
||||
<p className="text-sm text-red-400">
|
||||
{t(
|
||||
"scheduledJobs.modal.requiredFieldsBanner",
|
||||
"Please fill out all required fields in order to create job."
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<JobDescription form={form} errors={errors} onChange={handleChange} />
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
||||
<JobDescription form={form} errors={errors} onChange={handleChange} />
|
||||
<JobSchedule
|
||||
schedule={form.schedule}
|
||||
scheduleMode={form.scheduleMode}
|
||||
error={errors.schedule}
|
||||
onScheduleChange={handleScheduleChange}
|
||||
onModeChange={handleModeChange}
|
||||
/>
|
||||
|
||||
<JobSchedule
|
||||
schedule={form.schedule}
|
||||
scheduleMode={form.scheduleMode}
|
||||
error={errors.schedule}
|
||||
onScheduleChange={handleScheduleChange}
|
||||
onModeChange={handleModeChange}
|
||||
{availableTools.length > 0 && (
|
||||
<ToolsSelector
|
||||
availableTools={availableTools}
|
||||
selectedTools={form.selectedTools}
|
||||
onChange={setSelectedTools}
|
||||
/>
|
||||
)}
|
||||
|
||||
{availableTools.length > 0 && (
|
||||
<ToolsSelector
|
||||
availableTools={availableTools}
|
||||
selectedTools={form.selectedTools}
|
||||
onChange={setSelectedTools}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormActions
|
||||
isEditing={isEditing}
|
||||
saving={saving}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<FormActions isEditing={isEditing} saving={saving} onClose={onClose} />
|
||||
</ModalBody>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { subscribeToPushNotifications } from "@/hooks/useWebPushNotifications";
|
||||
import useWebPushNotifications from "@/hooks/useWebPushNotifications";
|
||||
import usePolling from "@/hooks/usePolling";
|
||||
import JobFormModal from "./JobFormModal";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import showToast from "@/utils/toast";
|
||||
import JobRow from "./components/JobRow";
|
||||
@@ -141,7 +141,7 @@ export default function ScheduledJobsPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<Modal isOpen={isOpen} onClose={closeModal} size="md">
|
||||
<JobFormModal
|
||||
job={editingJob}
|
||||
onClose={closeModal}
|
||||
@@ -150,7 +150,7 @@ export default function ScheduledJobsPage() {
|
||||
fetchJobs();
|
||||
}}
|
||||
/>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PreLoader from "@/components/Preloader";
|
||||
import ChangeWarningModal from "@/components/ChangeWarning";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
import VectorDBItem from "@/components/VectorDBSelection/VectorDBItem";
|
||||
|
||||
import LanceDbLogo from "@/media/vectordbs/lancedb.png";
|
||||
@@ -329,13 +329,13 @@ export default function GeneralVectorDatabase() {
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<Modal isOpen={isOpen} onClose={closeModal} size="lg">
|
||||
<ChangeWarningModal
|
||||
warningText="Switching the vector database will reset all previously embedded documents in all workspaces.\n\nConfirming will clear all embeddings from your vector database and remove all documents from your workspaces. Your uploaded documents will not be deleted, they will be available for re-embedding."
|
||||
onClose={closeModal}
|
||||
onConfirm={handleSaveSettings}
|
||||
/>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,13 @@ import {
|
||||
USERNAME_MAX_LENGTH,
|
||||
USERNAME_PATTERN,
|
||||
} from "@/utils/username";
|
||||
import {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalInput,
|
||||
} from "@/components/lib/Modal";
|
||||
|
||||
export default function NewUserModal() {
|
||||
const { code } = useParams();
|
||||
@@ -38,72 +45,41 @@ export default function NewUserModal() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full max-w-2xl max-h-full">
|
||||
<div className="relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border">
|
||||
<div className="flex items-start justify-between p-4 border-b rounded-t border-theme-modal-border">
|
||||
<h3 className="text-xl font-semibold text-theme-text-primary">
|
||||
Create a new account
|
||||
</h3>
|
||||
</div>
|
||||
<form onSubmit={handleCreate}>
|
||||
<div className="p-6 space-y-6 flex h-full w-full">
|
||||
<div className="w-full flex flex-col gap-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="username"
|
||||
className="block mb-2 text-sm font-medium text-theme-text-primary"
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
name="username"
|
||||
type="text"
|
||||
className="border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="My username"
|
||||
minLength={USERNAME_MIN_LENGTH}
|
||||
maxLength={USERNAME_MAX_LENGTH}
|
||||
pattern={USERNAME_PATTERN}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<p className="mt-2 text-xs text-theme-text-secondary">
|
||||
{t("common.username_requirements")}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block mb-2 text-sm font-medium text-theme-text-primary"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
name="password"
|
||||
type="password"
|
||||
className="border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="Your password"
|
||||
required={true}
|
||||
minLength={8}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
<p className="text-theme-text-secondary text-xs md:text-sm">
|
||||
After creating your account you will be able to login with these
|
||||
credentials and start using workspaces.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-theme-modal-border">
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full transition-all duration-300 border border-theme-text-primary px-4 py-2 rounded-lg text-theme-text-primary text-sm items-center flex gap-x-2 hover:bg-theme-text-primary hover:text-theme-bg-primary focus:ring-gray-800 text-center justify-center"
|
||||
>
|
||||
Accept Invitation
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<form onSubmit={handleCreate} className="flex flex-col gap-y-5">
|
||||
<ModalHeader title="Create a new account" />
|
||||
<ModalBody>
|
||||
<ModalInput
|
||||
label="Username"
|
||||
name="username"
|
||||
type="text"
|
||||
placeholder="My username"
|
||||
minLength={USERNAME_MIN_LENGTH}
|
||||
maxLength={USERNAME_MAX_LENGTH}
|
||||
pattern={USERNAME_PATTERN}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
hint={t("common.username_requirements")}
|
||||
/>
|
||||
<ModalInput
|
||||
label="Password"
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="Your password"
|
||||
required={true}
|
||||
minLength={8}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
<p className="text-zinc-300 light:text-slate-700 text-xs md:text-sm">
|
||||
After creating your account you will be able to login with these
|
||||
credentials and start using workspaces.
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalPrimaryButton type="submit" className="w-full">
|
||||
Accept Invitation
|
||||
</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useParams } from "react-router-dom";
|
||||
import { FullScreenLoader } from "@/components/Preloader";
|
||||
import Invite from "@/models/invite";
|
||||
import NewUserModal from "./NewUserModal";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
|
||||
export default function InvitePage() {
|
||||
const { code } = useParams();
|
||||
@@ -48,9 +48,9 @@ export default function InvitePage() {
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen overflow-hidden bg-theme-bg-container flex items-center justify-center">
|
||||
<ModalWrapper isOpen={true}>
|
||||
<Modal isOpen={true}>
|
||||
<NewUserModal />
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+35
-51
@@ -2,9 +2,15 @@
|
||||
// "ready for use" and if not - will then highjack the click handler to show a modal
|
||||
// of the provider options that must be saved to continue.
|
||||
import { createPortal } from "react-dom";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal, {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
} from "@/components/lib/Modal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import { X, Gear } from "@phosphor-icons/react";
|
||||
import { Gear } from "@phosphor-icons/react";
|
||||
import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -134,55 +140,33 @@ function SetupProvider({
|
||||
// Cannot do nested forms, it will cause all sorts of issues, so we portal this out
|
||||
// to the parent container form so we don't have nested forms.
|
||||
return createPortal(
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<div className="fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
{LLMOption.name} Settings
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<form id="provider-form" onSubmit={handleUpdate}>
|
||||
<div className="px-7 py-6">
|
||||
<div className="space-y-6 max-h-[60vh] overflow-y-auto p-1">
|
||||
<p className="text-sm text-white/60">
|
||||
To use {LLMOption.name} as this workspace's agent LLM you need
|
||||
to set it up first.
|
||||
</p>
|
||||
<div>
|
||||
{LLMOption.options(settings, { credentialsOnly: true })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border px-7 pb-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
form="provider-form"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Save {LLMOption.name} settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>,
|
||||
<Modal isOpen={isOpen} onClose={closeModal} size="lg" noPortal>
|
||||
<form
|
||||
id="provider-form"
|
||||
onSubmit={handleUpdate}
|
||||
className="flex flex-col gap-y-5"
|
||||
>
|
||||
<ModalHeader
|
||||
title={`${LLMOption.name} Settings`}
|
||||
onClose={closeModal}
|
||||
/>
|
||||
<ModalBody className="max-h-[60vh] overflow-y-auto p-1">
|
||||
<p className="text-sm text-zinc-400 light:text-slate-600">
|
||||
To use {LLMOption.name} as this workspace's agent LLM you need to
|
||||
set it up first.
|
||||
</p>
|
||||
<div>{LLMOption.options(settings, { credentialsOnly: true })}</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalSecondaryButton type="button" onClick={closeModal}>
|
||||
Cancel
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton type="submit" form="provider-form">
|
||||
Save {LLMOption.name} settings
|
||||
</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</Modal>,
|
||||
document.getElementById("workspace-agent-settings-container")
|
||||
);
|
||||
}
|
||||
|
||||
+36
-52
@@ -2,9 +2,15 @@
|
||||
// "ready for use" and if not - will then highjack the click handler to show a modal
|
||||
// of the provider options that must be saved to continue.
|
||||
import { createPortal } from "react-dom";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal, {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
ModalSecondaryButton,
|
||||
} from "@/components/lib/Modal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import { X, Gear } from "@phosphor-icons/react";
|
||||
import { Gear } from "@phosphor-icons/react";
|
||||
import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -80,7 +86,7 @@ export default function WorkspaceLLM({
|
||||
e.preventDefault();
|
||||
openModal();
|
||||
}}
|
||||
className="p-2 text-white/60 hover:text-white hover:bg-theme-bg-hover rounded-md transition-all duration-300"
|
||||
className="border-none p-2 text-white/60 hover:text-white hover:bg-theme-bg-hover rounded-md transition-all duration-300"
|
||||
title="Edit Settings"
|
||||
>
|
||||
<Gear size={20} weight="bold" />
|
||||
@@ -132,55 +138,33 @@ function SetupProvider({
|
||||
// Cannot do nested forms, it will cause all sorts of issues, so we portal this out
|
||||
// to the parent container form so we don't have nested forms.
|
||||
return createPortal(
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<div className="fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
{LLMOption.name} Settings
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<form id="provider-form" onSubmit={handleUpdate}>
|
||||
<div className="px-7 py-6">
|
||||
<div className="space-y-6 max-h-[60vh] overflow-y-auto p-1">
|
||||
<p className="text-sm text-white/60">
|
||||
To use {LLMOption.name} as this workspace's LLM you need to
|
||||
set it up first.
|
||||
</p>
|
||||
<div>
|
||||
{LLMOption.options(settings, { credentialsOnly: true })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border px-7 pb-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
form="provider-form"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Save settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>,
|
||||
<Modal isOpen={isOpen} onClose={closeModal} size="lg" noPortal>
|
||||
<form
|
||||
id="provider-form"
|
||||
onSubmit={handleUpdate}
|
||||
className="flex flex-col gap-y-5"
|
||||
>
|
||||
<ModalHeader
|
||||
title={`${LLMOption.name} Settings`}
|
||||
onClose={closeModal}
|
||||
/>
|
||||
<ModalBody className="max-h-[60vh] overflow-y-auto p-1">
|
||||
<p className="text-sm text-zinc-400 light:text-slate-600">
|
||||
To use {LLMOption.name} as this workspace's LLM you need to set it
|
||||
up first.
|
||||
</p>
|
||||
<div>{LLMOption.options(settings, { credentialsOnly: true })}</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalSecondaryButton type="button" onClick={closeModal}>
|
||||
Cancel
|
||||
</ModalSecondaryButton>
|
||||
<ModalPrimaryButton type="submit" form="provider-form">
|
||||
Save settings
|
||||
</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</Modal>,
|
||||
document.getElementById("workspace-chat-settings-container")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import React, { useState } from "react";
|
||||
import { MagnifyingGlass, X } from "@phosphor-icons/react";
|
||||
import { MagnifyingGlass } from "@phosphor-icons/react";
|
||||
import Admin from "@/models/admin";
|
||||
import showToast from "@/utils/toast";
|
||||
import {
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalPrimaryButton,
|
||||
} from "@/components/lib/Modal";
|
||||
|
||||
export default function AddMemberModal({ closeModal, workspace, users }) {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
@@ -60,104 +66,87 @@ export default function AddMemberModal({ closeModal, workspace, users }) {
|
||||
.filter((user) => user.role !== "manager");
|
||||
|
||||
return (
|
||||
<div className="relative w-full max-w-[550px] max-h-full">
|
||||
<div className="w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden">
|
||||
<div className="flex items-center justify-between p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="flex items-center gap-x-4">
|
||||
<h3 className="text-base font-semibold text-white">Users</h3>
|
||||
<div className="relative">
|
||||
<input
|
||||
onChange={handleSearch}
|
||||
className="w-[400px] h-[34px] bg-theme-bg-primary rounded-[100px] text-white placeholder:text-theme-text-secondary text-sm px-10 pl-10"
|
||||
placeholder="Search for a user"
|
||||
/>
|
||||
<MagnifyingGlass
|
||||
size={16}
|
||||
weight="bold"
|
||||
className="text-white text-lg absolute left-3 top-1/2 transform -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="border-none bg-transparent rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X className="text-white text-lg" />
|
||||
</button>
|
||||
<form onSubmit={handleUpdate} className="flex flex-col gap-y-5">
|
||||
<ModalHeader title="Users" onClose={closeModal}>
|
||||
<div className="relative mt-2 w-full">
|
||||
<input
|
||||
onChange={handleSearch}
|
||||
className="w-full h-[34px] bg-zinc-800 light:bg-white border border-zinc-800 light:border-slate-300 rounded-[100px] text-zinc-100 light:text-slate-900 placeholder:text-zinc-400 light:placeholder:text-slate-400 text-sm px-10 pl-10 outline-none focus:border-sky-500 light:focus:border-sky-500"
|
||||
placeholder="Search for a user"
|
||||
/>
|
||||
<MagnifyingGlass
|
||||
size={16}
|
||||
weight="bold"
|
||||
className="text-zinc-400 light:text-slate-500 absolute left-3 top-1/2 transform -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<form onSubmit={handleUpdate}>
|
||||
<div className="py-[17px] px-[20px]">
|
||||
<table className="gap-y-[8px] flex flex-col max-h-[385px] overflow-y-auto no-scroll">
|
||||
{filteredUsers.length > 0 ? (
|
||||
filteredUsers.map((user) => (
|
||||
<tr
|
||||
key={user.id}
|
||||
className="flex items-center gap-x-2 cursor-pointer"
|
||||
onClick={() => handleUserSelect(user.id)}
|
||||
>
|
||||
<div
|
||||
className="shrink-0 w-3 h-3 rounded border-[1px] border-solid border-white light:border-black flex justify-center items-center"
|
||||
role="checkbox"
|
||||
aria-checked={isUserSelected(user.id)}
|
||||
tabIndex={0}
|
||||
>
|
||||
{isUserSelected(user.id) && (
|
||||
<div className="w-2 h-2 bg-white light:bg-black rounded-[2px]" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-theme-text-primary text-sm font-medium">
|
||||
{user.username}
|
||||
</p>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<p className="text-theme-text-secondary text-sm font-medium ">
|
||||
No users found
|
||||
</p>
|
||||
)}
|
||||
</table>
|
||||
</div>
|
||||
<div className="flex w-full justify-between items-center p-3 space-x-2 border-t rounded-b border-gray-500/50">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSelectAll}
|
||||
className="flex items-center gap-x-2 ml-2"
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<table className="gap-y-[8px] flex flex-col max-h-[385px] overflow-y-auto no-scroll">
|
||||
{filteredUsers.length > 0 ? (
|
||||
filteredUsers.map((user) => (
|
||||
<tr
|
||||
key={user.id}
|
||||
className="flex items-center gap-x-2 cursor-pointer"
|
||||
onClick={() => handleUserSelect(user.id)}
|
||||
>
|
||||
<div
|
||||
className="shrink-0 w-3 h-3 rounded border-[1px] border-white flex justify-center items-center cursor-pointer"
|
||||
className="shrink-0 w-3 h-3 rounded border-[1px] border-solid border-white light:border-black flex justify-center items-center"
|
||||
role="checkbox"
|
||||
aria-checked={selectedUsers.length === filteredUsers.length}
|
||||
aria-checked={isUserSelected(user.id)}
|
||||
tabIndex={0}
|
||||
>
|
||||
{selectedUsers.length === filteredUsers.length && (
|
||||
<div className="w-2 h-2 bg-white rounded-[2px]" />
|
||||
{isUserSelected(user.id) && (
|
||||
<div className="w-2 h-2 bg-white light:bg-black rounded-[2px]" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-white text-sm font-medium">Select All</p>
|
||||
</button>
|
||||
{selectedUsers.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleUnselect}
|
||||
className="flex items-center gap-x-2 ml-2"
|
||||
>
|
||||
<p className="text-theme-text-secondary text-sm font-medium hover:text-theme-text-primary">
|
||||
Unselect
|
||||
</p>
|
||||
</button>
|
||||
<p className="text-zinc-100 light:text-slate-900 text-sm font-medium">
|
||||
{user.username}
|
||||
</p>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<p className="text-zinc-400 light:text-slate-500 text-sm font-medium ">
|
||||
No users found
|
||||
</p>
|
||||
)}
|
||||
</table>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSelectAll}
|
||||
className="border-none bg-transparent flex items-center gap-x-2"
|
||||
>
|
||||
<div
|
||||
className="shrink-0 w-3 h-3 rounded border-[1px] border-white light:border-black flex justify-center items-center cursor-pointer"
|
||||
role="checkbox"
|
||||
aria-checked={selectedUsers.length === filteredUsers.length}
|
||||
tabIndex={0}
|
||||
>
|
||||
{selectedUsers.length === filteredUsers.length && (
|
||||
<div className="w-2 h-2 bg-white light:bg-black rounded-[2px]" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-zinc-100 light:text-slate-900 text-sm font-medium">
|
||||
Select All
|
||||
</p>
|
||||
</button>
|
||||
{selectedUsers.length > 0 && (
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 text-xs px-2 py-1 font-semibold rounded-lg bg-primary-button hover:bg-secondary border-2 border-transparent hover:border-primary-button hover:text-white h-[32px] w-[68px] -mr-8 whitespace-nowrap shadow-[0_4px_14px_rgba(0,0,0,0.25)]"
|
||||
type="button"
|
||||
onClick={handleUnselect}
|
||||
className="border-none bg-transparent flex items-center gap-x-2"
|
||||
>
|
||||
Save
|
||||
<p className="text-zinc-400 light:text-slate-500 text-sm font-medium hover:text-zinc-100 light:hover:text-slate-900">
|
||||
Unselect
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ModalPrimaryButton type="submit">Save</ModalPrimaryButton>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import Modal from "@/components/lib/Modal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import Admin from "@/models/admin";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -79,13 +79,13 @@ export default function Members({ workspace }) {
|
||||
</tbody>
|
||||
</table>
|
||||
<CTAButton onClick={openModal}>Manage Users</CTAButton>
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<Modal isOpen={isOpen} onClose={closeModal} size="lg">
|
||||
<AddMemberModal
|
||||
closeModal={closeModal}
|
||||
users={users}
|
||||
workspace={adminWorkspace}
|
||||
/>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user