mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-04-02 08:58:11 +02:00
Cleaner + cleaner assistants creation flow etc. (#2232)
* rework assistants creation flow + components * remove unnecessary padding + validate each page * remove additional spacing * rebase + form
This commit is contained in:
parent
97ba71e1b3
commit
b36cd4937f
File diff suppressed because it is too large
Load Diff
@ -40,7 +40,6 @@ const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
|
||||
onClick={toggleCollapse}
|
||||
>
|
||||
{" "}
|
||||
Great and also a
|
||||
{isCollapsed ? (
|
||||
<span className="collapse-toggle text-lg absolute left-0 top-0 text-sm flex items-center gap-x-3 cursor-pointer">
|
||||
<FiSettings className="pointer-events-none my-auto" size={16} />
|
||||
|
@ -20,6 +20,7 @@ export default async function Page() {
|
||||
<Card>
|
||||
<AssistantEditor
|
||||
{...values}
|
||||
admin
|
||||
defaultPublic={true}
|
||||
redirectType={SuccessfulPersonaUpdateRedirectType.ADMIN}
|
||||
/>
|
||||
|
@ -219,8 +219,6 @@ export function CustomLLMProviderUpdateForm({
|
||||
placeholder="Display Name"
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<TextFormField
|
||||
name="provider"
|
||||
label="Provider Name"
|
||||
@ -426,7 +424,7 @@ export function CustomLLMProviderUpdateForm({
|
||||
<>
|
||||
<BooleanFormField
|
||||
small
|
||||
noPadding
|
||||
removeIndent
|
||||
alignTop
|
||||
name="is_public"
|
||||
label="Is Public?"
|
||||
|
@ -218,7 +218,7 @@ export function LLMProviderUpdateForm({
|
||||
}}
|
||||
>
|
||||
{({ values, setFieldValue }) => (
|
||||
<Form>
|
||||
<Form className="w-full items-stretch">
|
||||
<TextFormField
|
||||
name="name"
|
||||
label="Display Name"
|
||||
@ -227,8 +227,6 @@ export function LLMProviderUpdateForm({
|
||||
disabled={existingLlmProvider ? true : false}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
{llmProviderDescriptor.api_key_required && (
|
||||
<TextFormField
|
||||
name="api_key"
|
||||
@ -348,7 +346,7 @@ export function LLMProviderUpdateForm({
|
||||
<>
|
||||
<BooleanFormField
|
||||
small
|
||||
noPadding
|
||||
removeIndent
|
||||
alignTop
|
||||
name="is_public"
|
||||
label="Is Public?"
|
||||
|
@ -128,7 +128,7 @@ function Main({ ccPairId }: { ccPairId: number }) {
|
||||
{popup}
|
||||
<BackButton />
|
||||
<div className="pb-1 flex mt-1">
|
||||
<div className="mr-2 my-auto ">
|
||||
<div className="mr-2 my-auto">
|
||||
<SourceIcon iconSize={24} sourceType={ccPair.connector.source} />
|
||||
</div>
|
||||
|
||||
|
@ -1,11 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import * as Yup from "yup";
|
||||
import { TrashIcon } from "@/components/icons/icons";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||
|
||||
import { Card, Title } from "@tremor/react";
|
||||
import { Card, Divider, Title } from "@tremor/react";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { buildSimilarCredentialInfoURL } from "@/app/admin/connector/[ccPairId]/lib";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
@ -18,7 +19,7 @@ import { deleteCredential, linkCredential } from "@/lib/credential";
|
||||
import { submitFiles } from "./pages/utils/files";
|
||||
import { submitGoogleSite } from "./pages/utils/google_site";
|
||||
import AdvancedFormPage from "./pages/Advanced";
|
||||
import DynamicConnectionForm from "./pages/Create";
|
||||
import DynamicConnectionForm from "./pages/DynamicConnectorCreationForm";
|
||||
import CreateCredential from "@/components/credentials/actions/CreateCredential";
|
||||
import ModifyCredential from "@/components/credentials/actions/ModifyCredential";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
@ -37,10 +38,15 @@ import {
|
||||
useGmailCredentials,
|
||||
useGoogleDriveCredentials,
|
||||
} from "./pages/utils/hooks";
|
||||
import { FormikProps } from "formik";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
import { Formik, FormikProps } from "formik";
|
||||
import {
|
||||
IsPublicGroupSelector,
|
||||
IsPublicGroupSelectorFormType,
|
||||
} from "@/components/IsPublicGroupSelector";
|
||||
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
|
||||
import { AdminBooleanFormField } from "@/components/credentials/CredentialFields";
|
||||
|
||||
export type AdvancedConfig = {
|
||||
export type AdvancedConfigFinal = {
|
||||
pruneFreq: number | null;
|
||||
refreshFreq: number | null;
|
||||
indexingStart: Date | null;
|
||||
@ -51,7 +57,6 @@ export default function AddConnector({
|
||||
}: {
|
||||
connector: ValidSources;
|
||||
}) {
|
||||
const [name, setName] = useState("");
|
||||
const [currentCredential, setCurrentCredential] =
|
||||
useState<Credential<any> | null>(null);
|
||||
|
||||
@ -60,6 +65,7 @@ export default function AddConnector({
|
||||
errorHandlingFetcher,
|
||||
{ refreshInterval: 5000 }
|
||||
);
|
||||
|
||||
const { data: editableCredentials } = useSWR<Credential<any>[]>(
|
||||
buildSimilarCredentialInfoURL(connector, true),
|
||||
errorHandlingFetcher,
|
||||
@ -69,50 +75,54 @@ export default function AddConnector({
|
||||
|
||||
const credentialTemplate = credentialTemplates[connector];
|
||||
|
||||
const { setFormStep, setAlowCreate, formStep, nextFormStep, prevFormStep } =
|
||||
useFormContext();
|
||||
const {
|
||||
setFormStep,
|
||||
setAllowAdvanced,
|
||||
setAlowCreate,
|
||||
formStep,
|
||||
nextFormStep,
|
||||
prevFormStep,
|
||||
} = useFormContext();
|
||||
|
||||
const { popup, setPopup } = usePopup();
|
||||
|
||||
const configuration: ConnectionConfiguration = connectorConfigs[connector];
|
||||
const [formValues, setFormValues] = useState<
|
||||
Record<string, any> & IsPublicGroupSelectorFormType
|
||||
>({
|
||||
name: "",
|
||||
groups: [],
|
||||
is_public: false,
|
||||
...configuration.values.reduce(
|
||||
(acc, field) => {
|
||||
if (field.type === "list") {
|
||||
acc[field.name] = field.default || [];
|
||||
} else if (field.type === "checkbox") {
|
||||
acc[field.name] = field.default || false;
|
||||
} else if (field.default !== undefined) {
|
||||
acc[field.name] = field.default;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as { [record: string]: any }
|
||||
),
|
||||
});
|
||||
|
||||
const initialValues = configuration.values.reduce(
|
||||
(acc, field) => {
|
||||
if (field.type === "list") {
|
||||
acc[field.name] = field.default || [];
|
||||
} else if (field.default !== undefined) {
|
||||
acc[field.name] = field.default;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as { [record: string]: any }
|
||||
);
|
||||
|
||||
const [values, setValues] = useState<{ [record: string]: any } | null>(
|
||||
Object.keys(initialValues).length > 0 ? initialValues : null
|
||||
);
|
||||
const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled();
|
||||
|
||||
// Default to 10 minutes unless otherwise specified
|
||||
const defaultRefresh = configuration.overrideDefaultFreq || 10;
|
||||
// Default is 30 days
|
||||
const defaultPrune = 30;
|
||||
const defaultAdvancedSettings = {
|
||||
refreshFreq: formValues.overrideDefaultFreq || 10,
|
||||
pruneFreq: 30,
|
||||
indexingStart: null as string | null,
|
||||
};
|
||||
|
||||
const [refreshFreq, setRefreshFreq] = useState<number>(defaultRefresh || 0);
|
||||
const [pruneFreq, setPruneFreq] = useState<number>(defaultPrune);
|
||||
const [indexingStart, setIndexingStart] = useState<Date | null>(null);
|
||||
const { isAdmin, isLoadingUser } = useUser();
|
||||
const [advancedSettings, setAdvancedSettings] = useState(
|
||||
defaultAdvancedSettings
|
||||
);
|
||||
|
||||
const [isPublic, setIsPublic] = useState(isAdmin);
|
||||
useEffect(() => {
|
||||
if (!isLoadingUser) {
|
||||
setIsPublic(isAdmin);
|
||||
}
|
||||
}, [isLoadingUser, isAdmin]);
|
||||
|
||||
const [groups, setGroups] = useState<number[]>([]);
|
||||
const [createConnectorToggle, setCreateConnectorToggle] = useState(false);
|
||||
const formRef = useRef<FormikProps<any>>(null);
|
||||
const [advancedFormPageState, setAdvancedFormPageState] = useState(true);
|
||||
|
||||
const [isFormValid, setIsFormValid] = useState(false);
|
||||
|
||||
@ -130,6 +140,7 @@ export default function AddConnector({
|
||||
currentCredential;
|
||||
|
||||
const noCredentials = credentialTemplate == null;
|
||||
|
||||
if (noCredentials && 1 != formStep) {
|
||||
setFormStep(Math.max(1, formStep));
|
||||
}
|
||||
@ -138,39 +149,35 @@ export default function AddConnector({
|
||||
setFormStep(Math.min(formStep, 0));
|
||||
}
|
||||
|
||||
if (isLoadingUser) {
|
||||
return <></>;
|
||||
}
|
||||
const resetAdvancedConfigs = (formikProps: FormikProps<any>) => {
|
||||
formikProps.resetForm({ values: defaultAdvancedSettings });
|
||||
setAdvancedSettings(defaultAdvancedSettings);
|
||||
};
|
||||
|
||||
const resetAdvancedConfigs = () => {
|
||||
const resetRefreshFreq = defaultRefresh || 0;
|
||||
const resetPruneFreq = defaultPrune;
|
||||
const resetIndexingStart = null;
|
||||
|
||||
setRefreshFreq(resetRefreshFreq);
|
||||
setPruneFreq(resetPruneFreq);
|
||||
setIndexingStart(resetIndexingStart);
|
||||
setAdvancedFormPageState((advancedFormPageState) => !advancedFormPageState);
|
||||
// Update the form values
|
||||
if (formRef.current) {
|
||||
formRef.current.setFieldValue("refreshFreq", resetRefreshFreq);
|
||||
formRef.current.setFieldValue("pruneFreq", resetPruneFreq);
|
||||
formRef.current.setFieldValue("indexingStart", resetIndexingStart);
|
||||
}
|
||||
const convertStringToDateTime = (indexingStart: string | null) => {
|
||||
return indexingStart ? new Date(indexingStart) : null;
|
||||
};
|
||||
|
||||
const createConnector = async () => {
|
||||
const AdvancedConfig: AdvancedConfig = {
|
||||
pruneFreq: pruneFreq * 60 * 60 * 24,
|
||||
indexingStart,
|
||||
refreshFreq: refreshFreq * 60,
|
||||
const {
|
||||
name,
|
||||
groups,
|
||||
is_public: isPublic,
|
||||
...connector_specific_config
|
||||
} = formValues;
|
||||
const { pruneFreq, indexingStart, refreshFreq } = advancedSettings;
|
||||
|
||||
const AdvancedConfig: AdvancedConfigFinal = {
|
||||
pruneFreq: advancedSettings.pruneFreq * 60 * 60 * 24,
|
||||
indexingStart: convertStringToDateTime(indexingStart),
|
||||
refreshFreq: advancedSettings.refreshFreq * 60,
|
||||
};
|
||||
|
||||
// google sites-specific handling
|
||||
if (connector == "google_site") {
|
||||
const response = await submitGoogleSite(
|
||||
selectedFiles,
|
||||
values?.base_url,
|
||||
formValues?.base_url,
|
||||
setPopup,
|
||||
AdvancedConfig,
|
||||
name
|
||||
@ -204,13 +211,13 @@ export default function AddConnector({
|
||||
|
||||
const { message, isSuccess, response } = await submitConnector<any>(
|
||||
{
|
||||
connector_specific_config: values,
|
||||
connector_specific_config: connector_specific_config,
|
||||
input_type: connector == "web" ? "load_state" : "poll", // single case
|
||||
name: name,
|
||||
source: connector,
|
||||
refresh_freq: refreshFreq * 60 || null,
|
||||
prune_freq: pruneFreq * 60 * 60 * 24 || null,
|
||||
indexing_start: indexingStart,
|
||||
indexing_start: convertStringToDateTime(indexingStart),
|
||||
is_public: isPublic,
|
||||
groups: groups,
|
||||
},
|
||||
@ -218,7 +225,6 @@ export default function AddConnector({
|
||||
credentialActivated ? false : true,
|
||||
isPublic
|
||||
);
|
||||
|
||||
// If no credential
|
||||
if (!credentialActivated) {
|
||||
if (isSuccess) {
|
||||
@ -305,17 +311,41 @@ export default function AddConnector({
|
||||
refresh();
|
||||
};
|
||||
|
||||
const updateValues = (field: string, value: any) => {
|
||||
if (field == "name") {
|
||||
return;
|
||||
}
|
||||
setValues((values) => {
|
||||
if (!values) {
|
||||
return { [field]: value };
|
||||
} else {
|
||||
return { ...values, [field]: value };
|
||||
}
|
||||
});
|
||||
const validationSchema = Yup.object().shape({
|
||||
name: Yup.string().required("Connector Name is required"),
|
||||
...configuration.values.reduce(
|
||||
(acc, field) => {
|
||||
let schema: any =
|
||||
field.type === "list"
|
||||
? Yup.array().of(Yup.string())
|
||||
: field.type === "checkbox"
|
||||
? Yup.boolean()
|
||||
: Yup.string();
|
||||
|
||||
if (!field.optional) {
|
||||
schema = schema.required(`${field.label} is required`);
|
||||
}
|
||||
acc[field.name] = schema;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>
|
||||
),
|
||||
});
|
||||
|
||||
const advancedValidationSchema = Yup.object().shape({
|
||||
indexingStart: Yup.string().nullable(),
|
||||
pruneFreq: Yup.number().min(0, "Prune frequency must be non-negative"),
|
||||
refreshFreq: Yup.number().min(0, "Refresh frequency must be non-negative"),
|
||||
});
|
||||
|
||||
const isFormSubmittable = (values: any) => {
|
||||
return (
|
||||
values.name.trim() !== "" &&
|
||||
Object.keys(values).every((key) => {
|
||||
const field = configuration.values.find((f) => f.name === key);
|
||||
return field?.optional || values[key] !== "";
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -429,20 +459,60 @@ export default function AddConnector({
|
||||
{formStep == 1 && (
|
||||
<>
|
||||
<Card>
|
||||
<DynamicConnectionForm
|
||||
setSelectedFiles={setSelectedFiles}
|
||||
selectedFiles={selectedFiles}
|
||||
setIsPublic={setIsPublic}
|
||||
updateValues={updateValues}
|
||||
setName={setName}
|
||||
config={configuration}
|
||||
isPublic={isPublic}
|
||||
groups={groups}
|
||||
setGroups={setGroups}
|
||||
defaultValues={values}
|
||||
initialName={name}
|
||||
onFormStatusChange={handleFormStatusChange}
|
||||
/>
|
||||
<Formik
|
||||
initialValues={formValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={() => {
|
||||
// Can be utilized for logging purposes
|
||||
}}
|
||||
>
|
||||
{(formikProps) => {
|
||||
setFormValues(formikProps.values);
|
||||
console.log(formikProps.values);
|
||||
handleFormStatusChange(
|
||||
formikProps.isValid && isFormSubmittable(formikProps.values)
|
||||
);
|
||||
setAllowAdvanced(
|
||||
formikProps.isValid && isFormSubmittable(formikProps.values)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full py-4 flex gap-y-6 flex-col max-w-2xl mx-auto">
|
||||
<DynamicConnectionForm
|
||||
values={formikProps.values}
|
||||
config={configuration}
|
||||
setSelectedFiles={setSelectedFiles}
|
||||
selectedFiles={selectedFiles}
|
||||
/>
|
||||
{isPaidEnterpriseFeaturesEnabled && (
|
||||
<>
|
||||
<Divider className="my-0 py-0" />
|
||||
{formikProps.values.groups.length > 0 ? (
|
||||
<IsPublicGroupSelector
|
||||
formikProps={formikProps}
|
||||
objectName="Connector"
|
||||
/>
|
||||
) : (
|
||||
<AdminBooleanFormField
|
||||
subtext={`If set, then documents indexed by this connector will be visible to all users. If turned off, then only users who explicitly have been given access to the documents (e.g. through a User Group) will have access`}
|
||||
checked={formikProps.values.is_public}
|
||||
onChange={(e) => {
|
||||
const value = e.target.checked;
|
||||
formikProps.setFieldValue("is_public", value);
|
||||
if (value) {
|
||||
formikProps.setFieldValue("groups", []);
|
||||
}
|
||||
}}
|
||||
label={"Documents are Public?"}
|
||||
name={"is_public"}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Formik>
|
||||
</Card>
|
||||
<div className={`mt-4 w-full grid grid-cols-3`}>
|
||||
{!noCredentials ? (
|
||||
@ -491,26 +561,32 @@ export default function AddConnector({
|
||||
{formStep === 2 && (
|
||||
<>
|
||||
<Card>
|
||||
<AdvancedFormPage
|
||||
key={advancedFormPageState ? 0 : 1}
|
||||
setIndexingStart={setIndexingStart}
|
||||
indexingStart={indexingStart}
|
||||
setPruneFreq={setPruneFreq}
|
||||
currentPruneFreq={pruneFreq}
|
||||
setRefreshFreq={setRefreshFreq}
|
||||
currentRefreshFreq={refreshFreq}
|
||||
ref={formRef}
|
||||
/>
|
||||
<Formik
|
||||
initialValues={advancedSettings}
|
||||
validationSchema={advancedValidationSchema}
|
||||
onSubmit={() => {}}
|
||||
>
|
||||
{(formikProps) => {
|
||||
setAdvancedSettings(formikProps.values);
|
||||
|
||||
<div className="mt-4 flex w-full mx-auto max-w-2xl justify-start">
|
||||
<button
|
||||
className="flex gap-x-1 bg-red-500 hover:bg-red-500/80 items-center text-white py-2.5 px-3.5 text-sm font-regular rounded "
|
||||
onClick={() => resetAdvancedConfigs()}
|
||||
>
|
||||
<TrashIcon size={20} className="text-white" />
|
||||
<div className="w-full items-center gap-x-2 flex">Reset</div>
|
||||
</button>
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
<AdvancedFormPage formikProps={formikProps} ref={formRef} />
|
||||
<div className="mt-4 flex w-full mx-auto max-w-2xl justify-start">
|
||||
<button
|
||||
className="flex gap-x-1 bg-red-500 hover:bg-red-500/80 items-center text-white py-2.5 px-3.5 text-sm font-regular rounded "
|
||||
onClick={() => resetAdvancedConfigs(formikProps)}
|
||||
>
|
||||
<TrashIcon size={20} className="text-white" />
|
||||
<div className="w-full items-center gap-x-2 flex">
|
||||
Reset
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Formik>
|
||||
</Card>
|
||||
<div className={`mt-4 grid grid-cols-3 w-full `}>
|
||||
<button
|
||||
|
@ -25,7 +25,7 @@ export default function Sidebar() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex bg-background text-default ">
|
||||
<div className="flex bg-background text-default">
|
||||
<div
|
||||
className={`flex-none
|
||||
bg-background-100
|
||||
|
@ -1,108 +1,64 @@
|
||||
import React, { Dispatch, forwardRef, SetStateAction } from "react";
|
||||
import { Formik, Form, FormikProps } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import { EditingValue } from "@/components/credentials/EditingValue";
|
||||
import NumberInput from "./ConnectorInput/NumberInput";
|
||||
import { TextFormField } from "@/components/admin/connectors/Field";
|
||||
|
||||
interface AdvancedFormPageProps {
|
||||
setRefreshFreq: Dispatch<SetStateAction<number>>;
|
||||
setPruneFreq: Dispatch<SetStateAction<number>>;
|
||||
currentPruneFreq: number;
|
||||
currentRefreshFreq: number;
|
||||
indexingStart: Date | null;
|
||||
setIndexingStart: Dispatch<SetStateAction<Date | null>>;
|
||||
formikProps: FormikProps<{
|
||||
indexingStart: string | null;
|
||||
pruneFreq: number;
|
||||
refreshFreq: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
const AdvancedFormPage = forwardRef<FormikProps<any>, AdvancedFormPageProps>(
|
||||
(
|
||||
{
|
||||
setIndexingStart,
|
||||
indexingStart,
|
||||
setRefreshFreq,
|
||||
currentRefreshFreq,
|
||||
setPruneFreq,
|
||||
currentPruneFreq,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
({ formikProps }, ref) => {
|
||||
const { indexingStart, refreshFreq, pruneFreq } = formikProps.values;
|
||||
|
||||
return (
|
||||
<div className="py-4 rounded-lg max-w-2xl mx-auto">
|
||||
<h2 className="text-2xl font-bold mb-4 text-text-800">
|
||||
Advanced Configuration
|
||||
</h2>
|
||||
<Formik
|
||||
innerRef={ref}
|
||||
initialValues={{
|
||||
pruneFreq: currentPruneFreq,
|
||||
refreshFreq: currentRefreshFreq,
|
||||
indexingStart: indexingStart,
|
||||
}}
|
||||
validationSchema={Yup.object().shape({
|
||||
pruneFreq: Yup.number().min(0, "Must be a positive number"),
|
||||
refreshFreq: Yup.number().min(0, "Must be a positive number"),
|
||||
indexingStart: Yup.date().nullable(),
|
||||
})}
|
||||
onSubmit={async (_, { setSubmitting }) => {
|
||||
setSubmitting(false);
|
||||
}}
|
||||
enableReinitialize={true}
|
||||
>
|
||||
{({ values, setFieldValue }) => (
|
||||
<Form className="space-y-6">
|
||||
<div key="prune_freq">
|
||||
<EditingValue
|
||||
showNever
|
||||
description={`
|
||||
Checks all documents against the source to delete those that no longer exist.
|
||||
Note: This process checks every document, so be cautious when increasing frequency.
|
||||
Default is 30 days.
|
||||
Enter 0 to disable pruning for this connector.
|
||||
`}
|
||||
currentValue={values.pruneFreq}
|
||||
onChangeNumber={(value: number) => {
|
||||
setPruneFreq(value);
|
||||
setFieldValue("pruneFreq", value);
|
||||
}}
|
||||
setFieldValue={setFieldValue}
|
||||
type="number"
|
||||
label="Prune Frequency (days)"
|
||||
name="pruneFreq"
|
||||
/>
|
||||
</div>
|
||||
<div key="refresh_freq">
|
||||
<EditingValue
|
||||
showNever
|
||||
description="This is how frequently we pull new documents from the source (in minutes). If you input 0, we will never pull new documents for this connector."
|
||||
currentValue={values.refreshFreq}
|
||||
onChangeNumber={(value: number) => {
|
||||
setRefreshFreq(value);
|
||||
setFieldValue("refreshFreq", value);
|
||||
}}
|
||||
setFieldValue={setFieldValue}
|
||||
type="number"
|
||||
label="Refresh Frequency (minutes)"
|
||||
name="refreshFreq"
|
||||
/>
|
||||
</div>
|
||||
<div key="indexing_start">
|
||||
<EditingValue
|
||||
description="Documents prior to this date will not be pulled in"
|
||||
optional
|
||||
currentValue={
|
||||
values.indexingStart ? values.indexingStart : undefined
|
||||
}
|
||||
onChangeDate={(value: Date | null) => {
|
||||
setIndexingStart(value);
|
||||
setFieldValue("indexingStart", value);
|
||||
}}
|
||||
setFieldValue={setFieldValue}
|
||||
type="date"
|
||||
label="Indexing Start Date"
|
||||
name="indexingStart"
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
|
||||
<Form>
|
||||
<div key="prune_freq">
|
||||
<NumberInput
|
||||
showNeverIfZero
|
||||
description={`
|
||||
Checks all documents against the source to delete those that no longer exist.
|
||||
Note: This process checks every document, so be cautious when increasing frequency.
|
||||
Default is 30 days.
|
||||
Enter 0 to disable pruning for this connector.
|
||||
`}
|
||||
value={pruneFreq}
|
||||
label="Prune Frequency (days)"
|
||||
name="pruneFreq"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div key="refresh_freq">
|
||||
<NumberInput
|
||||
description="This is how frequently we pull new documents from the source (in minutes). If you input 0, we will never pull new documents for this connector."
|
||||
value={refreshFreq}
|
||||
showNeverIfZero
|
||||
label="Refresh Frequency (minutes)"
|
||||
name="refreshFreq"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div key="indexing_start">
|
||||
<TextFormField
|
||||
type="date"
|
||||
subtext="Documents prior to this date will not be pulled in"
|
||||
optional
|
||||
label="Indexing Start Date"
|
||||
name="indexingStart"
|
||||
value={indexingStart!}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,37 @@
|
||||
import { FileUpload } from "@/components/admin/connectors/FileUpload";
|
||||
import CredentialSubText from "@/components/credentials/CredentialFields";
|
||||
|
||||
interface FileInputProps {
|
||||
name: string;
|
||||
label: string;
|
||||
optional?: boolean;
|
||||
description?: string;
|
||||
selectedFiles: File[];
|
||||
setSelectedFiles: (files: File[]) => void;
|
||||
}
|
||||
|
||||
export default function FileInput({
|
||||
name,
|
||||
label,
|
||||
optional = false,
|
||||
description,
|
||||
selectedFiles,
|
||||
setSelectedFiles,
|
||||
}: FileInputProps) {
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
htmlFor={name}
|
||||
className="block text-sm font-medium text-text-700 mb-1"
|
||||
>
|
||||
{label}
|
||||
{optional && <span className="text-text-500 ml-1">(optional)</span>}
|
||||
</label>
|
||||
{description && <CredentialSubText>{description}</CredentialSubText>}
|
||||
<FileUpload
|
||||
selectedFiles={selectedFiles}
|
||||
setSelectedFiles={setSelectedFiles}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
import CredentialSubText from "@/components/credentials/CredentialFields";
|
||||
import { TrashIcon } from "@/components/icons/icons";
|
||||
import { ListOption } from "@/lib/connectors/connectors";
|
||||
import { Field, FieldArray, useField } from "formik";
|
||||
import { FaPlus } from "react-icons/fa";
|
||||
|
||||
export default function ListInput({
|
||||
field,
|
||||
onUpdate,
|
||||
}: {
|
||||
field: ListOption;
|
||||
onUpdate?: (values: string[]) => void;
|
||||
}) {
|
||||
const [fieldProps, , helpers] = useField(field.name);
|
||||
|
||||
return (
|
||||
<FieldArray name={field.name}>
|
||||
{({ push, remove }) => (
|
||||
<div>
|
||||
<label
|
||||
htmlFor={field.name}
|
||||
className="block text-sm font-medium text-text-700 mb-1"
|
||||
>
|
||||
{field.label}
|
||||
{field.optional && (
|
||||
<span className="text-text-500 ml-1">(optional)</span>
|
||||
)}
|
||||
</label>
|
||||
{field.description && (
|
||||
<CredentialSubText>{field.description}</CredentialSubText>
|
||||
)}
|
||||
|
||||
{fieldProps.value.map((value: string, index: number) => (
|
||||
<div key={index} className="w-full flex mb-4">
|
||||
<Field
|
||||
name={`${field.name}.${index}`}
|
||||
className="w-full bg-input text-sm p-2 border border-border-medium rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 mr-2"
|
||||
/>
|
||||
<button
|
||||
className="p-2 my-auto bg-input flex-none rounded-md bg-red-500 text-white hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
remove(index);
|
||||
if (onUpdate) {
|
||||
const newValue = fieldProps.value.filter(
|
||||
(_: any, i: number) => i !== index
|
||||
);
|
||||
onUpdate(newValue);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="text-white my-auto" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
push("");
|
||||
if (onUpdate) {
|
||||
onUpdate([...fieldProps.value, ""]);
|
||||
}
|
||||
}}
|
||||
className="mt-2 p-2 bg-rose-500 text-xs text-white rounded-md hover:bg-rose-600 focus:outline-none focus:ring-2 focus:ring-rose-500 focus:ring-opacity-50 flex items-center"
|
||||
>
|
||||
<FaPlus className="mr-2" />
|
||||
Add {field.label}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</FieldArray>
|
||||
);
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import { SubLabel } from "@/components/admin/connectors/Field";
|
||||
import { Field } from "formik";
|
||||
|
||||
export default function NumberInput({
|
||||
label,
|
||||
value,
|
||||
optional,
|
||||
description,
|
||||
name,
|
||||
showNeverIfZero,
|
||||
}: {
|
||||
value?: number;
|
||||
label: string;
|
||||
name: string;
|
||||
optional?: boolean;
|
||||
description?: string;
|
||||
showNeverIfZero?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="w-full flex flex-col">
|
||||
<label className="block text-base font-medium text-text-700 mb-1">
|
||||
{label}
|
||||
{optional && <span className="text-text-500 ml-1">(optional)</span>}
|
||||
</label>
|
||||
{description && <SubLabel>{description}</SubLabel>}
|
||||
|
||||
<Field
|
||||
type="number"
|
||||
name={name}
|
||||
min="-1"
|
||||
value={value === 0 && showNeverIfZero ? "Never" : value}
|
||||
className={`mt-2 block w-full px-3 py-2
|
||||
bg-white border border-gray-300 rounded-md
|
||||
text-sm shadow-sm placeholder-gray-400
|
||||
focus:outline-none focus:border-sky-500 focus:ring-1 focus:ring-sky-500
|
||||
disabled:bg-gray-50 disabled:text-gray-500 disabled:border-gray-200 disabled:shadow-none
|
||||
invalid:border-pink-500 invalid:text-pink-600
|
||||
focus:invalid:border-pink-500 focus:invalid:ring-pink-500`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import CredentialSubText from "@/components/credentials/CredentialFields";
|
||||
import { ListOption, SelectOption } from "@/lib/connectors/connectors";
|
||||
import { Field } from "formik";
|
||||
|
||||
export default function SelectInput({
|
||||
field,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
field: SelectOption;
|
||||
value: any;
|
||||
onChange?: (e: Event) => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
htmlFor={field.name}
|
||||
className="block text-sm font-medium text-text-700 mb-1"
|
||||
>
|
||||
{field.label}
|
||||
{field.optional && (
|
||||
<span className="text-text-500 ml-1">(optional)</span>
|
||||
)}
|
||||
</label>
|
||||
{field.description && (
|
||||
<CredentialSubText>{field.description}</CredentialSubText>
|
||||
)}
|
||||
|
||||
<Field
|
||||
onChange={onChange}
|
||||
as="select"
|
||||
value={value}
|
||||
name={field.name}
|
||||
className="w-full p-2 border bg-input border-border-medium rounded-md bg-black focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
||||
>
|
||||
<option value="">Select an option</option>
|
||||
{field.options?.map((option: any) => (
|
||||
<option key={option.name} value={option.name}>
|
||||
{option.name}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,440 +0,0 @@
|
||||
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
import { Formik, Form, Field, FieldArray } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import { FaPlus } from "react-icons/fa";
|
||||
import { useUserGroups } from "@/lib/hooks";
|
||||
import { UserGroup, User, UserRole } from "@/lib/types";
|
||||
import { EditingValue } from "@/components/credentials/EditingValue";
|
||||
import { Divider } from "@tremor/react";
|
||||
import CredentialSubText from "@/components/credentials/CredentialFields";
|
||||
import { TrashIcon } from "@/components/icons/icons";
|
||||
import { FileUpload } from "@/components/admin/connectors/FileUpload";
|
||||
import { ConnectionConfiguration } from "@/lib/connectors/connectors";
|
||||
import { useFormContext } from "@/components/context/FormContext";
|
||||
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
|
||||
import { Text } from "@tremor/react";
|
||||
|
||||
import { FiUsers } from "react-icons/fi";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
|
||||
export interface DynamicConnectionFormProps {
|
||||
config: ConnectionConfiguration;
|
||||
selectedFiles: File[];
|
||||
initialName?: string;
|
||||
setSelectedFiles: Dispatch<SetStateAction<File[]>>;
|
||||
setIsPublic: Dispatch<SetStateAction<boolean>>;
|
||||
defaultValues: any;
|
||||
setName: Dispatch<SetStateAction<string>>;
|
||||
updateValues: (field: string, value: any) => void;
|
||||
isPublic: boolean;
|
||||
groups: number[];
|
||||
setGroups: Dispatch<SetStateAction<number[]>>;
|
||||
onFormStatusChange: (isValid: boolean) => void; // New prop
|
||||
}
|
||||
|
||||
const DynamicConnectionForm: React.FC<DynamicConnectionFormProps> = ({
|
||||
config,
|
||||
setName,
|
||||
updateValues,
|
||||
defaultValues,
|
||||
selectedFiles,
|
||||
setSelectedFiles,
|
||||
isPublic,
|
||||
setIsPublic,
|
||||
groups,
|
||||
setGroups,
|
||||
initialName,
|
||||
onFormStatusChange,
|
||||
}) => {
|
||||
const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled();
|
||||
const { setAllowAdvanced } = useFormContext();
|
||||
const { data: userGroups, isLoading: userGroupsIsLoading } = useUserGroups();
|
||||
|
||||
const { isLoadingUser, isAdmin, user } = useUser();
|
||||
|
||||
if (isLoadingUser) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const initialValues = {
|
||||
name: initialName || "",
|
||||
groups: [], // Initialize groups as an empty array
|
||||
...(defaultValues ||
|
||||
config.values.reduce(
|
||||
(acc, field, ind) => {
|
||||
acc[field.name] = defaultValues
|
||||
? defaultValues[field.name]
|
||||
: config.values[ind].hidden
|
||||
? config.values[ind].default
|
||||
: field.type === "list"
|
||||
? [""]
|
||||
: field.type === "checkbox"
|
||||
? false
|
||||
: "";
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>
|
||||
)),
|
||||
};
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
name: Yup.string().required("Connector Name is required"),
|
||||
...config.values.reduce(
|
||||
(acc, field) => {
|
||||
let schema: any =
|
||||
field.type === "list"
|
||||
? Yup.array().of(Yup.string())
|
||||
: field.type === "checkbox"
|
||||
? Yup.boolean()
|
||||
: Yup.string();
|
||||
|
||||
if (!field.optional) {
|
||||
schema = schema.required(`${field.label} is required`);
|
||||
}
|
||||
acc[field.name] = schema;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>
|
||||
),
|
||||
});
|
||||
|
||||
const updateValue =
|
||||
(setFieldValue: Function) => (field: string, value: any) => {
|
||||
setFieldValue(field, value);
|
||||
updateValues(field, value);
|
||||
};
|
||||
|
||||
const isFormSubmittable = (values: any) => {
|
||||
return (
|
||||
values.name.trim() !== "" &&
|
||||
Object.keys(values).every((key) => {
|
||||
const field = config.values.find((f) => f.name === key);
|
||||
return field?.optional || values[key] !== "";
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="py-4 rounded-lg max-w-2xl mx-auto">
|
||||
<h2 className="text-2xl font-bold mb-4 text-text-800">
|
||||
{config.description}
|
||||
</h2>
|
||||
{config.subtext && (
|
||||
<CredentialSubText>{config.subtext}</CredentialSubText>
|
||||
)}
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={() => {
|
||||
// Can be used for logging
|
||||
}}
|
||||
>
|
||||
{({ setFieldValue, values, isValid }) => {
|
||||
onFormStatusChange(isValid && isFormSubmittable(values));
|
||||
setAllowAdvanced(isValid && isFormSubmittable(values));
|
||||
return (
|
||||
<Form className="space-y-6">
|
||||
<EditingValue
|
||||
description="A descriptive name for the connector. This will be used to identify the connector in the Admin UI."
|
||||
setFieldValue={updateValue(setFieldValue)}
|
||||
type={"text"}
|
||||
label={"Connector Name"}
|
||||
name={"name"}
|
||||
currentValue=""
|
||||
onChange={(value: string) => setName(value)}
|
||||
/>
|
||||
{config.values.map((field) => {
|
||||
if (!field.hidden) {
|
||||
return (
|
||||
<div key={field.name}>
|
||||
{field.type == "file" ? (
|
||||
<FileUpload
|
||||
selectedFiles={selectedFiles}
|
||||
setSelectedFiles={setSelectedFiles}
|
||||
/>
|
||||
) : field.type == "zip" ? (
|
||||
<>
|
||||
<label
|
||||
htmlFor={field.name}
|
||||
className="block text-sm font-medium text-text-700 mb-1"
|
||||
>
|
||||
{field.label}
|
||||
{field.optional && (
|
||||
<span className="text-text-500 ml-1">
|
||||
(optional)
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
{field.description && (
|
||||
<CredentialSubText>
|
||||
{field.description}
|
||||
</CredentialSubText>
|
||||
)}
|
||||
<FileUpload
|
||||
selectedFiles={selectedFiles}
|
||||
setSelectedFiles={setSelectedFiles}
|
||||
/>
|
||||
</>
|
||||
) : field.type === "list" ? (
|
||||
<FieldArray name={field.name}>
|
||||
{({ push, remove }) => (
|
||||
<div>
|
||||
<label
|
||||
htmlFor={field.name}
|
||||
className="block text-sm font-medium text-text-700 mb-1"
|
||||
>
|
||||
{field.label}
|
||||
{field.optional && (
|
||||
<span className="text-text-500 ml-1">
|
||||
(optional)
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
{field.description && (
|
||||
<CredentialSubText>
|
||||
{field.description}
|
||||
</CredentialSubText>
|
||||
)}
|
||||
|
||||
{values[field.name].map(
|
||||
(_: any, index: number) => (
|
||||
<div key={index} className="w-full flex mb-4">
|
||||
<Field
|
||||
name={`${field.name}.${index}`}
|
||||
className="w-full bg-input text-sm p-2 border border-border-medium rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 mr-2"
|
||||
onChange={(
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const newValue = [
|
||||
...values[field.name],
|
||||
];
|
||||
newValue[index] = e.target.value;
|
||||
updateValue(setFieldValue)(
|
||||
field.name,
|
||||
newValue
|
||||
);
|
||||
}}
|
||||
value={values[field.name][index]}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
remove(index);
|
||||
const newValue = values[
|
||||
field.name
|
||||
].filter(
|
||||
(_: any, i: number) => i !== index
|
||||
);
|
||||
updateValue(setFieldValue)(
|
||||
field.name,
|
||||
newValue
|
||||
);
|
||||
}}
|
||||
className="p-2 my-auto bg-input flex-none rounded-md bg-red-500 text-white hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50"
|
||||
>
|
||||
<TrashIcon className="text-white my-auto" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => push("")}
|
||||
className="mt-2 p-2 bg-rose-500 text-xs text-white rounded-md hover:bg-rose-600 focus:outline-none focus:ring-2 focus:ring-rose-500 focus:ring-opacity-50 flex items-center"
|
||||
>
|
||||
<FaPlus className="mr-2" />
|
||||
Add {field.label}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</FieldArray>
|
||||
) : field.type === "select" ? (
|
||||
<>
|
||||
<label
|
||||
htmlFor={field.name}
|
||||
className="block text-sm font-medium text-text-700 mb-1"
|
||||
>
|
||||
{field.label}
|
||||
{field.optional && (
|
||||
<span className="text-text-500 ml-1">
|
||||
(optional)
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
{field.description && (
|
||||
<CredentialSubText>
|
||||
{field.description}
|
||||
</CredentialSubText>
|
||||
)}
|
||||
|
||||
<Field
|
||||
onChange={(
|
||||
e: React.ChangeEvent<HTMLSelectElement>
|
||||
) =>
|
||||
updateValue(setFieldValue)(
|
||||
field.name,
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
as="select"
|
||||
value={values[field.name]}
|
||||
name={field.name}
|
||||
className="w-full p-2 border bg-input border-border-medium rounded-md bg-black
|
||||
focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
||||
>
|
||||
<option value="">Select an option</option>
|
||||
{field.options?.map((option) => (
|
||||
<option key={option.name} value={option.name}>
|
||||
{option.name}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
</>
|
||||
) : (
|
||||
<EditingValue
|
||||
description={field.description}
|
||||
optional={field.optional}
|
||||
setFieldValue={updateValue(setFieldValue)}
|
||||
type={field.type}
|
||||
label={field.label}
|
||||
name={field.name}
|
||||
currentValue={values[field.name]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
{isPaidEnterpriseFeaturesEnabled && (
|
||||
<>
|
||||
<Divider />
|
||||
{isAdmin && (
|
||||
<EditingValue
|
||||
description={`If set, then documents indexed by this connector will be visible to all users. If turned off, then only users who explicitly have been given access to the documents (e.g. through a User Group) will have access`}
|
||||
optional
|
||||
setFieldValue={(field: string, value: boolean) => {
|
||||
setIsPublic(value);
|
||||
if (value) {
|
||||
setGroups([]); // Clear groups when setting to public
|
||||
}
|
||||
}}
|
||||
type={"checkbox"}
|
||||
label={"Documents are Public?"}
|
||||
name={"public"}
|
||||
currentValue={isPublic}
|
||||
/>
|
||||
)}
|
||||
|
||||
{userGroups ? (
|
||||
<>
|
||||
{!isPublic &&
|
||||
((isAdmin && userGroups.length > 0) ||
|
||||
(!isAdmin && userGroups.length > 1)) ? (
|
||||
<div>
|
||||
<div className="flex gap-x-2 items-center">
|
||||
<div className="block font-medium text-base">
|
||||
Assign group access for this Connector
|
||||
</div>
|
||||
</div>
|
||||
<Text className="mb-3">
|
||||
{isAdmin ? (
|
||||
<>
|
||||
This Connector will be visible/accessible by the
|
||||
groups selected below
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Curators must select one or more groups to give
|
||||
access to this Connector
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
<FieldArray
|
||||
name="groups"
|
||||
render={() => (
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{!userGroupsIsLoading &&
|
||||
userGroups.map((userGroup: UserGroup) => {
|
||||
const isSelected =
|
||||
groups?.includes(userGroup.id) ||
|
||||
(!isAdmin && userGroups.length === 1);
|
||||
|
||||
// Auto-select the only group for non-admin users
|
||||
if (
|
||||
!isAdmin &&
|
||||
userGroups.length === 1 &&
|
||||
groups.length === 0
|
||||
) {
|
||||
setGroups([userGroup.id]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={userGroup.id}
|
||||
className={`
|
||||
px-3
|
||||
py-1
|
||||
rounded-lg
|
||||
border
|
||||
border-border
|
||||
w-fit
|
||||
flex
|
||||
cursor-pointer
|
||||
${isSelected ? "bg-background-strong" : "hover:bg-hover"}
|
||||
`}
|
||||
onClick={() => {
|
||||
if (setGroups) {
|
||||
if (
|
||||
isSelected &&
|
||||
(isAdmin || userGroups.length > 1)
|
||||
) {
|
||||
setGroups(
|
||||
groups?.filter(
|
||||
(id) => id !== userGroup.id
|
||||
) || []
|
||||
);
|
||||
} else if (!isSelected) {
|
||||
setGroups([
|
||||
...(groups || []),
|
||||
userGroup.id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="my-auto flex">
|
||||
<FiUsers className="my-auto mr-2" />{" "}
|
||||
{userGroup.name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
) : userGroups && userGroups.length > 0 ? (
|
||||
<Text className="mb-3">
|
||||
These documents will be assigned to group:{" "}
|
||||
<b>{userGroups[0].name}</b>.
|
||||
</Text>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
}}
|
||||
</Formik>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DynamicConnectionForm;
|
@ -0,0 +1,115 @@
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
Dispatch,
|
||||
FC,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Formik, Form, Field, FieldArray, FormikProps } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import { FaPlus } from "react-icons/fa";
|
||||
import { useUserGroups } from "@/lib/hooks";
|
||||
import { UserGroup, User, UserRole } from "@/lib/types";
|
||||
import { Divider } from "@tremor/react";
|
||||
import CredentialSubText, {
|
||||
AdminBooleanFormField,
|
||||
} from "@/components/credentials/CredentialFields";
|
||||
import { TrashIcon } from "@/components/icons/icons";
|
||||
import { FileUpload } from "@/components/admin/connectors/FileUpload";
|
||||
import { ConnectionConfiguration } from "@/lib/connectors/connectors";
|
||||
import { useFormContext } from "@/components/context/FormContext";
|
||||
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
|
||||
import { Text } from "@tremor/react";
|
||||
import { getCurrentUser } from "@/lib/user";
|
||||
import { FiUsers } from "react-icons/fi";
|
||||
import SelectInput from "./ConnectorInput/SelectInput";
|
||||
import NumberInput from "./ConnectorInput/NumberInput";
|
||||
import { TextFormField } from "@/components/admin/connectors/Field";
|
||||
import ListInput from "./ConnectorInput/ListInput";
|
||||
import FileInput from "./ConnectorInput/FileInput";
|
||||
|
||||
export interface DynamicConnectionFormProps {
|
||||
config: ConnectionConfiguration;
|
||||
selectedFiles: File[];
|
||||
setSelectedFiles: Dispatch<SetStateAction<File[]>>;
|
||||
values: any;
|
||||
}
|
||||
|
||||
const DynamicConnectionForm: FC<DynamicConnectionFormProps> = ({
|
||||
config,
|
||||
selectedFiles,
|
||||
setSelectedFiles,
|
||||
values,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<h2 className="text-2xl font-bold text-text-800">{config.description}</h2>
|
||||
|
||||
{config.subtext && (
|
||||
<CredentialSubText>{config.subtext}</CredentialSubText>
|
||||
)}
|
||||
|
||||
<TextFormField
|
||||
subtext="A descriptive name for the connector. This will be used to identify the connector in the Admin UI."
|
||||
type={"text"}
|
||||
label={"Connector Name"}
|
||||
name={"name"}
|
||||
/>
|
||||
|
||||
{config.values.map((field) => {
|
||||
if (!field.hidden) {
|
||||
return (
|
||||
<div key={field.name}>
|
||||
{field.type == "file" ? (
|
||||
<FileUpload
|
||||
selectedFiles={selectedFiles}
|
||||
setSelectedFiles={setSelectedFiles}
|
||||
/>
|
||||
) : field.type == "zip" ? (
|
||||
<FileInput
|
||||
name={field.name}
|
||||
label={field.label}
|
||||
optional={field.optional}
|
||||
description={field.description}
|
||||
selectedFiles={selectedFiles}
|
||||
setSelectedFiles={setSelectedFiles}
|
||||
/>
|
||||
) : field.type === "list" ? (
|
||||
<ListInput field={field} />
|
||||
) : field.type === "select" ? (
|
||||
<SelectInput field={field} value={values[field.name]} />
|
||||
) : field.type === "number" ? (
|
||||
<NumberInput
|
||||
label={field.label}
|
||||
value={values[field.name]}
|
||||
optional={field.optional}
|
||||
description={field.description}
|
||||
name={field.name}
|
||||
showNeverIfZero
|
||||
/>
|
||||
) : field.type === "checkbox" ? (
|
||||
<AdminBooleanFormField
|
||||
checked={values[field.name]}
|
||||
subtext={field.description}
|
||||
name={field.name}
|
||||
label={field.label}
|
||||
/>
|
||||
) : (
|
||||
<TextFormField
|
||||
subtext={field.description}
|
||||
optional={field.optional}
|
||||
type={field.type}
|
||||
label={field.label}
|
||||
name={field.name}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DynamicConnectionForm;
|
@ -0,0 +1,42 @@
|
||||
import { SubLabel } from "@/components/admin/connectors/Field";
|
||||
import { Field } from "formik";
|
||||
|
||||
export default function NumberInput({
|
||||
label,
|
||||
value,
|
||||
optional,
|
||||
description,
|
||||
name,
|
||||
showNeverIfZero,
|
||||
}: {
|
||||
value?: number;
|
||||
label: string;
|
||||
name: string;
|
||||
optional?: boolean;
|
||||
description?: string;
|
||||
showNeverIfZero?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="w-full flex flex-col">
|
||||
<label className="block text-base font-medium text-text-700 mb-1">
|
||||
{label}
|
||||
{optional && <span className="text-text-500 ml-1">(optional)</span>}
|
||||
</label>
|
||||
{description && <SubLabel>{description}</SubLabel>}
|
||||
|
||||
<Field
|
||||
type="number"
|
||||
name={name}
|
||||
min="-1"
|
||||
value={value === 0 && showNeverIfZero ? "Never" : value}
|
||||
className={`mt-2 block w-full px-3 py-2
|
||||
bg-white border border-gray-300 rounded-md
|
||||
text-sm shadow-sm placeholder-gray-400
|
||||
focus:outline-none focus:border-sky-500 focus:ring-1 focus:ring-sky-500
|
||||
disabled:bg-gray-50 disabled:text-gray-500 disabled:border-gray-200 disabled:shadow-none
|
||||
invalid:border-pink-500 invalid:text-pink-600
|
||||
focus:invalid:border-pink-500 focus:invalid:ring-pink-500`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -2,14 +2,14 @@ import { PopupSpec } from "@/components/admin/connectors/Popup";
|
||||
import { createConnector, runConnector } from "@/lib/connector";
|
||||
import { createCredential, linkCredential } from "@/lib/credential";
|
||||
import { FileConfig } from "@/lib/connectors/connectors";
|
||||
import { AdvancedConfig } from "../../AddConnectorPage";
|
||||
import { AdvancedConfigFinal } from "../../AddConnectorPage";
|
||||
|
||||
export const submitFiles = async (
|
||||
selectedFiles: File[],
|
||||
setPopup: (popup: PopupSpec) => void,
|
||||
setSelectedFiles: (files: File[]) => void,
|
||||
name: string,
|
||||
advancedConfig: AdvancedConfig,
|
||||
advancedConfig: AdvancedConfigFinal,
|
||||
isPublic: boolean,
|
||||
groups?: number[]
|
||||
) => {
|
||||
|
@ -2,13 +2,13 @@ import { PopupSpec } from "@/components/admin/connectors/Popup";
|
||||
import { createConnector, runConnector } from "@/lib/connector";
|
||||
import { linkCredential } from "@/lib/credential";
|
||||
import { GoogleSitesConfig } from "@/lib/connectors/connectors";
|
||||
import { AdvancedConfig } from "../../AddConnectorPage";
|
||||
import { AdvancedConfigFinal } from "../../AddConnectorPage";
|
||||
|
||||
export const submitGoogleSite = async (
|
||||
selectedFiles: File[],
|
||||
base_url: any,
|
||||
setPopup: (popup: PopupSpec) => void,
|
||||
advancedConfig: AdvancedConfig,
|
||||
advancedConfig: AdvancedConfigFinal,
|
||||
name?: string
|
||||
) => {
|
||||
const uploadCreateAndTriggerConnector = async () => {
|
||||
|
@ -128,51 +128,51 @@ export const DocumentSetCreationForm = ({
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<h2 className="mb-1 font-medium text-base">
|
||||
These are the connectors available to{" "}
|
||||
{userGroups && userGroups.length > 1
|
||||
? "the selected group"
|
||||
: "the group you curate"}
|
||||
:
|
||||
</h2>
|
||||
<p className="mb-text-sm">
|
||||
All documents indexed by these selected connectors will be a
|
||||
part of this document set.
|
||||
</p>
|
||||
<FieldArray
|
||||
name="cc_pair_ids"
|
||||
render={(arrayHelpers: ArrayHelpers) => {
|
||||
// Filter visible cc pairs
|
||||
const visibleCcPairs = localCcPairs.filter(
|
||||
(ccPair) =>
|
||||
ccPair.public_doc ||
|
||||
(ccPair.groups.length > 0 &&
|
||||
props.values.groups.every((group) =>
|
||||
ccPair.groups.includes(group)
|
||||
))
|
||||
);
|
||||
|
||||
<h2 className="mb-1 font-medium text-base">
|
||||
These are the connectors available to{" "}
|
||||
{userGroups && userGroups.length > 1
|
||||
? "the selected group"
|
||||
: "the group you curate"}
|
||||
:
|
||||
</h2>
|
||||
<p className="mb-3 text-sm">
|
||||
All documents indexed by these selected connectors will be a
|
||||
part of this document set.
|
||||
</p>
|
||||
<FieldArray
|
||||
name="cc_pair_ids"
|
||||
render={(arrayHelpers: ArrayHelpers) => {
|
||||
// Filter visible cc pairs
|
||||
const visibleCcPairs = localCcPairs.filter(
|
||||
(ccPair) =>
|
||||
ccPair.public_doc ||
|
||||
(ccPair.groups.length > 0 &&
|
||||
props.values.groups.every((group) =>
|
||||
ccPair.groups.includes(group)
|
||||
))
|
||||
);
|
||||
// Deselect filtered out cc pairs
|
||||
const visibleCcPairIds = visibleCcPairs.map(
|
||||
(ccPair) => ccPair.cc_pair_id
|
||||
);
|
||||
props.values.cc_pair_ids = props.values.cc_pair_ids.filter(
|
||||
(id) => visibleCcPairIds.includes(id)
|
||||
);
|
||||
|
||||
// Deselect filtered out cc pairs
|
||||
const visibleCcPairIds = visibleCcPairs.map(
|
||||
(ccPair) => ccPair.cc_pair_id
|
||||
);
|
||||
props.values.cc_pair_ids = props.values.cc_pair_ids.filter(
|
||||
(id) => visibleCcPairIds.includes(id)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mb-3 flex gap-2 flex-wrap">
|
||||
{visibleCcPairs.map((ccPair) => {
|
||||
const ind = props.values.cc_pair_ids.indexOf(
|
||||
ccPair.cc_pair_id
|
||||
);
|
||||
let isSelected = ind !== -1;
|
||||
return (
|
||||
<div
|
||||
key={`${ccPair.connector.id}-${ccPair.credential.id}`}
|
||||
className={
|
||||
`
|
||||
return (
|
||||
<div className="mb-3 flex gap-2 flex-wrap">
|
||||
{visibleCcPairs.map((ccPair) => {
|
||||
const ind = props.values.cc_pair_ids.indexOf(
|
||||
ccPair.cc_pair_id
|
||||
);
|
||||
let isSelected = ind !== -1;
|
||||
return (
|
||||
<div
|
||||
key={`${ccPair.connector.id}-${ccPair.credential.id}`}
|
||||
className={
|
||||
`
|
||||
px-3
|
||||
py-1
|
||||
rounded-lg
|
||||
@ -181,85 +181,87 @@ export const DocumentSetCreationForm = ({
|
||||
w-fit
|
||||
flex
|
||||
cursor-pointer ` +
|
||||
(isSelected
|
||||
? " bg-background-strong"
|
||||
: " hover:bg-hover")
|
||||
}
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
arrayHelpers.remove(ind);
|
||||
} else {
|
||||
arrayHelpers.push(ccPair.cc_pair_id);
|
||||
(isSelected
|
||||
? " bg-background-strong"
|
||||
: " hover:bg-hover")
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="my-auto">
|
||||
<ConnectorTitle
|
||||
connector={ccPair.connector}
|
||||
ccPairId={ccPair.cc_pair_id}
|
||||
ccPairName={ccPair.name}
|
||||
isLink={false}
|
||||
showMetadata={false}
|
||||
/>
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
arrayHelpers.remove(ind);
|
||||
} else {
|
||||
arrayHelpers.push(ccPair.cc_pair_id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="my-auto">
|
||||
<ConnectorTitle
|
||||
connector={ccPair.connector}
|
||||
ccPairId={ccPair.cc_pair_id}
|
||||
ccPairName={ccPair.name}
|
||||
isLink={false}
|
||||
showMetadata={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FieldArray
|
||||
name="cc_pair_ids"
|
||||
render={() => {
|
||||
// Filter non-visible cc pairs
|
||||
const nonVisibleCcPairs = localCcPairs.filter(
|
||||
(ccPair) =>
|
||||
!ccPair.public_doc &&
|
||||
(ccPair.groups.length === 0 ||
|
||||
!props.values.groups.every((group) =>
|
||||
ccPair.groups.includes(group)
|
||||
))
|
||||
);
|
||||
|
||||
return nonVisibleCcPairs.length > 0 ? (
|
||||
<>
|
||||
<Divider />
|
||||
<h2 className="mb-1 font-medium text-base">
|
||||
These connectors are not available to the{" "}
|
||||
{userGroups && userGroups.length > 1
|
||||
? `group${props.values.groups.length > 1 ? "s" : ""} you have selected`
|
||||
: "group you curate"}
|
||||
:
|
||||
</h2>
|
||||
<p className="mb-3 text-sm">
|
||||
Only connectors that are directly assigned to the group
|
||||
you are trying to add the document set to will be
|
||||
available.
|
||||
</p>
|
||||
<div className="mb-3 flex gap-2 flex-wrap">
|
||||
{nonVisibleCcPairs.map((ccPair) => (
|
||||
<div
|
||||
key={`${ccPair.connector.id}-${ccPair.credential.id}`}
|
||||
className="px-3 py-1 rounded-lg border border-non-selectable-border w-fit flex cursor-not-allowed"
|
||||
>
|
||||
<div className="my-auto">
|
||||
<ConnectorTitle
|
||||
connector={ccPair.connector}
|
||||
ccPairId={ccPair.cc_pair_id}
|
||||
ccPairName={ccPair.name}
|
||||
isLink={false}
|
||||
showMetadata={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
) : null;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FieldArray
|
||||
name="cc_pair_ids"
|
||||
render={() => {
|
||||
// Filter non-visible cc pairs
|
||||
const nonVisibleCcPairs = localCcPairs.filter(
|
||||
(ccPair) =>
|
||||
!ccPair.public_doc &&
|
||||
(ccPair.groups.length === 0 ||
|
||||
!props.values.groups.every((group) =>
|
||||
ccPair.groups.includes(group)
|
||||
))
|
||||
);
|
||||
|
||||
return nonVisibleCcPairs.length > 0 ? (
|
||||
<>
|
||||
<Divider />
|
||||
<h2 className="mb-1 font-medium text-base">
|
||||
These connectors are not available to the{" "}
|
||||
{userGroups && userGroups.length > 1
|
||||
? `group${props.values.groups.length > 1 ? "s" : ""} you have selected`
|
||||
: "group you curate"}
|
||||
:
|
||||
</h2>
|
||||
<p className="mb-3 text-sm">
|
||||
Only connectors that are directly assigned to the
|
||||
group you are trying to add the document set to will
|
||||
be available.
|
||||
</p>
|
||||
<div className="mb-3 flex gap-2 flex-wrap">
|
||||
{nonVisibleCcPairs.map((ccPair) => (
|
||||
<div
|
||||
key={`${ccPair.connector.id}-${ccPair.credential.id}`}
|
||||
className="px-3 py-1 rounded-lg border border-non-selectable-border w-fit flex cursor-not-allowed"
|
||||
>
|
||||
<div className="my-auto">
|
||||
<ConnectorTitle
|
||||
connector={ccPair.connector}
|
||||
ccPairId={ccPair.cc_pair_id}
|
||||
ccPairName={ccPair.name}
|
||||
isLink={false}
|
||||
showMetadata={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : null;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex mt-6">
|
||||
<Button
|
||||
|
@ -227,7 +227,7 @@ export function EmbeddingModelSelection({
|
||||
/>
|
||||
)}
|
||||
|
||||
<p className=" t mb-4">
|
||||
<p className="t mb-4">
|
||||
Select from cloud, self-hosted models, or continue with your current
|
||||
embedding model.
|
||||
</p>
|
||||
@ -242,7 +242,7 @@ export function EmbeddingModelSelection({
|
||||
>
|
||||
Current
|
||||
</button>
|
||||
<div className="px-2 ">
|
||||
<div className="px-2">
|
||||
<button
|
||||
onClick={() => setModelTab("cloud")}
|
||||
className={`mx-2 p-2 font-bold ${
|
||||
@ -254,7 +254,7 @@ export function EmbeddingModelSelection({
|
||||
Cloud-based
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-2 ">
|
||||
<div className="px-2">
|
||||
<button
|
||||
onClick={() => setModelTab("open")}
|
||||
className={` mx-2 p-2 font-bold ${
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { Dispatch, forwardRef, SetStateAction, useState } from "react";
|
||||
import { Formik, Form, FormikProps } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import { EditingValue } from "@/components/credentials/EditingValue";
|
||||
import {
|
||||
RerankerProvider,
|
||||
RerankingDetails,
|
||||
@ -11,6 +10,7 @@ import { FiExternalLink } from "react-icons/fi";
|
||||
import { CohereIcon, MixedBreadIcon } from "@/components/icons/icons";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import { Button } from "@tremor/react";
|
||||
import { TextFormField } from "@/components/admin/connectors/Field";
|
||||
|
||||
interface RerankingDetailsFormProps {
|
||||
setRerankingDetails: Dispatch<SetStateAction<RerankingDetails>>;
|
||||
@ -69,7 +69,7 @@ const RerankingDetailsForm = forwardRef<
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="px-2 ">
|
||||
<div className="px-2">
|
||||
<button
|
||||
onClick={() => setModelTab("open")}
|
||||
className={` mx-2 p-2 font-bold ${
|
||||
@ -101,7 +101,7 @@ const RerankingDetailsForm = forwardRef<
|
||||
enableReinitialize={true}
|
||||
>
|
||||
{({ values, setFieldValue }) => (
|
||||
<Form className="space-y-6">
|
||||
<Form>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{(modelTab
|
||||
? rerankingModels.filter(
|
||||
@ -191,14 +191,13 @@ const RerankingDetailsForm = forwardRef<
|
||||
title="API Key Configuration"
|
||||
>
|
||||
<div className="w-full px-4">
|
||||
<EditingValue
|
||||
optional={false}
|
||||
currentValue={values.api_key}
|
||||
onChange={(value: string | null) => {
|
||||
<TextFormField
|
||||
placeholder={values.api_key || undefined}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setRerankingDetails({ ...values, api_key: value });
|
||||
setFieldValue("api_key", value);
|
||||
}}
|
||||
setFieldValue={setFieldValue}
|
||||
type="password"
|
||||
label="Cohere API Key"
|
||||
name="api_key"
|
||||
|
@ -1,11 +1,12 @@
|
||||
import React, { Dispatch, forwardRef, SetStateAction } from "react";
|
||||
import { Formik, Form, FormikProps, FieldArray, Field } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import { EditingValue } from "@/components/credentials/EditingValue";
|
||||
import CredentialSubText from "@/components/credentials/CredentialFields";
|
||||
import { TrashIcon } from "@/components/icons/icons";
|
||||
import { FaPlus } from "react-icons/fa";
|
||||
import { AdvancedSearchConfiguration, RerankingDetails } from "../interfaces";
|
||||
import { BooleanFormField } from "@/components/admin/connectors/Field";
|
||||
import NumberInput from "../../connectors/[connector]/pages/ConnectorInput/NumberInput";
|
||||
|
||||
interface AdvancedEmbeddingFormPageProps {
|
||||
updateAdvancedEmbeddingDetails: (
|
||||
@ -13,7 +14,6 @@ interface AdvancedEmbeddingFormPageProps {
|
||||
value: any
|
||||
) => void;
|
||||
advancedEmbeddingDetails: AdvancedSearchConfiguration;
|
||||
setRerankingDetails: Dispatch<SetStateAction<RerankingDetails>>;
|
||||
numRerank: number;
|
||||
}
|
||||
|
||||
@ -22,12 +22,7 @@ const AdvancedEmbeddingFormPage = forwardRef<
|
||||
AdvancedEmbeddingFormPageProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
updateAdvancedEmbeddingDetails,
|
||||
advancedEmbeddingDetails,
|
||||
setRerankingDetails,
|
||||
numRerank,
|
||||
},
|
||||
{ updateAdvancedEmbeddingDetails, advancedEmbeddingDetails, numRerank },
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
@ -56,7 +51,7 @@ const AdvancedEmbeddingFormPage = forwardRef<
|
||||
enableReinitialize={true}
|
||||
>
|
||||
{({ values, setFieldValue }) => (
|
||||
<Form className="space-y-6">
|
||||
<Form>
|
||||
<FieldArray name="multilingual_expansion">
|
||||
{({ push, remove }) => (
|
||||
<div>
|
||||
@ -131,54 +126,40 @@ const AdvancedEmbeddingFormPage = forwardRef<
|
||||
)}
|
||||
</FieldArray>
|
||||
|
||||
<div key="multipass_indexing">
|
||||
<EditingValue
|
||||
description="Enable multipass indexing for both mini and large chunks."
|
||||
optional
|
||||
currentValue={values.multipass_indexing}
|
||||
onChangeBool={(value: boolean) => {
|
||||
updateAdvancedEmbeddingDetails("multipass_indexing", value);
|
||||
setFieldValue("multipass_indexing", value);
|
||||
}}
|
||||
setFieldValue={setFieldValue}
|
||||
type="checkbox"
|
||||
label="Multipass Indexing"
|
||||
name="multipassIndexing"
|
||||
/>
|
||||
</div>
|
||||
<div key="disable_rerank_for_streaming">
|
||||
<EditingValue
|
||||
description="Disable reranking for streaming to improve response time."
|
||||
optional
|
||||
currentValue={values.disable_rerank_for_streaming}
|
||||
onChangeBool={(value: boolean) => {
|
||||
updateAdvancedEmbeddingDetails(
|
||||
"disable_rerank_for_streaming",
|
||||
value
|
||||
);
|
||||
setFieldValue("disable_rerank_for_streaming", value);
|
||||
}}
|
||||
setFieldValue={setFieldValue}
|
||||
type="checkbox"
|
||||
label="Disable Rerank for Streaming"
|
||||
name="disableRerankForStreaming"
|
||||
/>
|
||||
</div>
|
||||
<div key="num_rerank">
|
||||
<EditingValue
|
||||
description="Number of results to rerank"
|
||||
optional={false}
|
||||
currentValue={values.num_rerank}
|
||||
onChangeNumber={(value: number) => {
|
||||
setRerankingDetails({ ...values, num_rerank: value });
|
||||
setFieldValue("num_rerank", value);
|
||||
}}
|
||||
setFieldValue={setFieldValue}
|
||||
type="number"
|
||||
label="Number of Results to Rerank"
|
||||
name="num_rerank"
|
||||
/>
|
||||
</div>
|
||||
<BooleanFormField
|
||||
subtext="Enable multipass indexing for both mini and large chunks."
|
||||
optional
|
||||
checked={values.multipass_indexing}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const checked = e.target.checked;
|
||||
updateAdvancedEmbeddingDetails("multipass_indexing", checked);
|
||||
setFieldValue("multipass_indexing", checked);
|
||||
}}
|
||||
label="Multipass Indexing"
|
||||
name="multipassIndexing"
|
||||
/>
|
||||
<BooleanFormField
|
||||
subtext="Disable reranking for streaming to improve response time."
|
||||
optional
|
||||
checked={values.disable_rerank_for_streaming}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const checked = e.target.checked;
|
||||
updateAdvancedEmbeddingDetails(
|
||||
"disable_rerank_for_streaming",
|
||||
checked
|
||||
);
|
||||
setFieldValue("disable_rerank_for_streaming", checked);
|
||||
}}
|
||||
label="Disable Rerank for Streaming"
|
||||
name="disableRerankForStreaming"
|
||||
/>
|
||||
<NumberInput
|
||||
description="Number of results to rerank"
|
||||
optional={false}
|
||||
value={values.num_rerank}
|
||||
label="Number of Results to Rerank"
|
||||
name="num_rerank"
|
||||
/>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
|
@ -416,7 +416,6 @@ export default function EmbeddingForm() {
|
||||
<Card>
|
||||
<AdvancedEmbeddingFormPage
|
||||
numRerank={rerankingDetails.num_rerank}
|
||||
setRerankingDetails={setRerankingDetails}
|
||||
advancedEmbeddingDetails={advancedEmbeddingDetails}
|
||||
updateAdvancedEmbeddingDetails={updateAdvancedEmbeddingDetails}
|
||||
/>
|
||||
|
@ -58,30 +58,22 @@ const AddPromptModal = ({ onClose, onSubmit }: AddPromptModalProps) => {
|
||||
Add prompt
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<TextFormField
|
||||
label="Title"
|
||||
name="title"
|
||||
placeholder="Title (e.g. 'Reword')"
|
||||
/>
|
||||
<TextFormField
|
||||
label="Title"
|
||||
name="title"
|
||||
placeholder="Title (e.g. 'Reword')"
|
||||
/>
|
||||
|
||||
<TextFormField
|
||||
isTextArea
|
||||
label="Prompt"
|
||||
name="prompt"
|
||||
placeholder="Enter a prompt (e.g. 'help me rewrite the following politely and concisely for professional communication')"
|
||||
/>
|
||||
<TextFormField
|
||||
isTextArea
|
||||
label="Prompt"
|
||||
name="prompt"
|
||||
placeholder="Enter a prompt (e.g. 'help me rewrite the following politely and concisely for professional communication')"
|
||||
/>
|
||||
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Add prompt
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
||||
Add prompt
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
|
@ -93,11 +93,13 @@ export const StandardAnswerCreationForm = ({
|
||||
placeholder="e.g. Wifi Password"
|
||||
autoCompleteDisabled={true}
|
||||
/>
|
||||
<MarkdownFormField
|
||||
name="answer"
|
||||
label="Answer"
|
||||
placeholder="The answer in markdown"
|
||||
/>
|
||||
<div className="w-full">
|
||||
<MarkdownFormField
|
||||
name="answer"
|
||||
label="Answer"
|
||||
placeholder="The answer in markdown"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-4/12">
|
||||
<MultiSelectDropdown
|
||||
name="categories"
|
||||
|
@ -156,17 +156,14 @@ export const CreateRateLimitModal = ({
|
||||
type="number"
|
||||
placeholder=""
|
||||
/>
|
||||
<div className="flex">
|
||||
<Button
|
||||
type="submit"
|
||||
size="xs"
|
||||
color="green"
|
||||
disabled={isSubmitting}
|
||||
className="mx-auto w-64"
|
||||
>
|
||||
Create!
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
size="xs"
|
||||
color="green"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Create!
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
|
@ -194,6 +194,7 @@ const AddUserButton = ({
|
||||
Invite Users
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
{modal && (
|
||||
<Modal title="Bulk Add Users" onOutsideClick={() => setModal(false)}>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
|
@ -22,6 +22,7 @@ export function AssistantsGallery({
|
||||
user,
|
||||
}: {
|
||||
assistants: Persona[];
|
||||
|
||||
user: User | null;
|
||||
}) {
|
||||
function filterAssistants(assistants: Persona[], query: string): Persona[] {
|
||||
@ -150,9 +151,9 @@ export function AssistantsGallery({
|
||||
}
|
||||
}}
|
||||
size="xs"
|
||||
color="red"
|
||||
color="blue"
|
||||
>
|
||||
Remove
|
||||
Deselect
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
|
@ -1,13 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
import { MinimalUserSnapshot, User } from "@/lib/types";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { Divider, Text } from "@tremor/react";
|
||||
import { FiEdit2, FiPlus, FiSearch, FiShare2 } from "react-icons/fi";
|
||||
import {
|
||||
FiEdit2,
|
||||
FiMenu,
|
||||
FiMoreHorizontal,
|
||||
FiPlus,
|
||||
FiSearch,
|
||||
FiShare2,
|
||||
FiTrash,
|
||||
FiX,
|
||||
} from "react-icons/fi";
|
||||
import Link from "next/link";
|
||||
import { orderAssistantsForUser } from "@/lib/assistants/orderAssistants";
|
||||
import { updateUserAssistantList } from "@/lib/assistants/updateAssistantPreferences";
|
||||
import {
|
||||
addAssistantToList,
|
||||
removeAssistantFromList,
|
||||
updateUserAssistantList,
|
||||
} from "@/lib/assistants/updateAssistantPreferences";
|
||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||
import { DefaultPopover } from "@/components/popover/DefaultPopover";
|
||||
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
|
||||
@ -40,6 +53,8 @@ import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
import { DragHandle } from "@/components/table/DragHandle";
|
||||
import { deletePersona } from "@/app/admin/assistants/lib";
|
||||
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
|
||||
|
||||
function DraggableAssistantListItem(props: any) {
|
||||
const {
|
||||
@ -66,7 +81,7 @@ function DraggableAssistantListItem(props: any) {
|
||||
<DragHandle />
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<AssistantListItem {...props} />
|
||||
<AssistantListItem del {...props} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -77,24 +92,25 @@ function AssistantListItem({
|
||||
user,
|
||||
allAssistantIds,
|
||||
allUsers,
|
||||
isFirst,
|
||||
isLast,
|
||||
isVisible,
|
||||
setPopup,
|
||||
deleteAssistant,
|
||||
}: {
|
||||
assistant: Persona;
|
||||
user: User | null;
|
||||
allUsers: MinimalUserSnapshot[];
|
||||
allAssistantIds: string[];
|
||||
isFirst: boolean;
|
||||
isLast: boolean;
|
||||
isVisible: boolean;
|
||||
deleteAssistant: Dispatch<SetStateAction<Persona | null>>;
|
||||
|
||||
setPopup: (popupSpec: PopupSpec | null) => void;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [showSharingModal, setShowSharingModal] = useState(false);
|
||||
|
||||
const isOwnedByUser = checkUserOwnsAssistant(user, assistant);
|
||||
const currentChosenAssistants = user?.preferences
|
||||
?.chosen_assistants as number[];
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -158,6 +174,92 @@ function AssistantListItem({
|
||||
>
|
||||
<FiEdit2 size={16} />
|
||||
</Link>
|
||||
|
||||
<DefaultPopover
|
||||
content={
|
||||
<div className="hover:bg-hover rounded p-2 cursor-pointer">
|
||||
<FiMoreHorizontal size={16} />
|
||||
</div>
|
||||
}
|
||||
side="bottom"
|
||||
align="start"
|
||||
sideOffset={5}
|
||||
>
|
||||
{[
|
||||
isVisible ? (
|
||||
<div
|
||||
key="remove"
|
||||
className="flex items-center gap-x-2"
|
||||
onClick={async () => {
|
||||
if (
|
||||
currentChosenAssistants &&
|
||||
currentChosenAssistants.length === 1
|
||||
) {
|
||||
setPopup({
|
||||
message: `Cannot remove "${assistant.name}" - you must have at least one assistant.`,
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const success = await removeAssistantFromList(
|
||||
assistant.id,
|
||||
currentChosenAssistants || allAssistantIds
|
||||
);
|
||||
if (success) {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" has been removed from your list.`,
|
||||
type: "success",
|
||||
});
|
||||
router.refresh();
|
||||
} else {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" could not be removed from your list.`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FiX /> {isOwnedByUser ? "Hide" : "Remove"}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
key="add"
|
||||
className="flex items-center gap-x-2"
|
||||
onClick={async () => {
|
||||
const success = await addAssistantToList(
|
||||
assistant.id,
|
||||
currentChosenAssistants || allAssistantIds
|
||||
);
|
||||
if (success) {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" has been added to your list.`,
|
||||
type: "success",
|
||||
});
|
||||
router.refresh();
|
||||
} else {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" could not be added to your list.`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FiPlus /> Add
|
||||
</div>
|
||||
),
|
||||
isOwnedByUser ? (
|
||||
<div
|
||||
key="delete"
|
||||
className="flex items-center gap-x-2"
|
||||
onClick={() => deleteAssistant(assistant)}
|
||||
>
|
||||
<FiTrash /> Delete
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
]}
|
||||
</DefaultPopover>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -172,9 +274,11 @@ export function AssistantsList({
|
||||
user: User | null;
|
||||
assistants: Persona[];
|
||||
}) {
|
||||
const [filteredAssistants, setFilteredAssistants] = useState(
|
||||
orderAssistantsForUser(assistants, user)
|
||||
);
|
||||
const [filteredAssistants, setFilteredAssistants] = useState<Persona[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setFilteredAssistants(orderAssistantsForUser(assistants, user));
|
||||
}, [user, assistants, orderAssistantsForUser]);
|
||||
|
||||
const ownedButHiddenAssistants = assistants.filter(
|
||||
(assistant) =>
|
||||
@ -185,9 +289,10 @@ export function AssistantsList({
|
||||
const allAssistantIds = assistants.map((assistant) =>
|
||||
assistant.id.toString()
|
||||
);
|
||||
const [deletingPersona, setDeletingPersona] = useState<Persona | null>(null);
|
||||
|
||||
const { popup, setPopup } = usePopup();
|
||||
|
||||
const router = useRouter();
|
||||
const { data: users } = useSWR<MinimalUserSnapshot[]>(
|
||||
"/api/users",
|
||||
errorHandlingFetcher
|
||||
@ -222,6 +327,30 @@ export function AssistantsList({
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
{deletingPersona && (
|
||||
<DeleteEntityModal
|
||||
entityType="Assistant"
|
||||
entityName={deletingPersona.name}
|
||||
onClose={() => setDeletingPersona(null)}
|
||||
onSubmit={async () => {
|
||||
const success = await deletePersona(deletingPersona.id);
|
||||
if (success) {
|
||||
setPopup({
|
||||
message: `"${deletingPersona.name}" has been deleted.`,
|
||||
type: "success",
|
||||
});
|
||||
router.refresh();
|
||||
} else {
|
||||
setPopup({
|
||||
message: `"${deletingPersona.name}" could not be deleted.`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
setDeletingPersona(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="mx-auto mobile:w-[90%] desktop:w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
|
||||
<AssistantsPageTitle>My Assistants</AssistantsPageTitle>
|
||||
|
||||
@ -273,13 +402,12 @@ export function AssistantsList({
|
||||
<div className="w-full p-4 mt-3">
|
||||
{filteredAssistants.map((assistant, index) => (
|
||||
<DraggableAssistantListItem
|
||||
deleteAssistant={setDeletingPersona}
|
||||
key={assistant.id}
|
||||
assistant={assistant}
|
||||
user={user}
|
||||
allAssistantIds={allAssistantIds}
|
||||
allUsers={users || []}
|
||||
isFirst={false}
|
||||
isLast={index === filteredAssistants.length - 1}
|
||||
isVisible
|
||||
setPopup={setPopup}
|
||||
/>
|
||||
@ -302,13 +430,12 @@ export function AssistantsList({
|
||||
<div className="w-full p-4">
|
||||
{ownedButHiddenAssistants.map((assistant, index) => (
|
||||
<AssistantListItem
|
||||
deleteAssistant={setDeletingPersona}
|
||||
key={assistant.id}
|
||||
assistant={assistant}
|
||||
user={user}
|
||||
allAssistantIds={allAssistantIds}
|
||||
allUsers={users || []}
|
||||
isFirst={index === 0}
|
||||
isLast={index === ownedButHiddenAssistants.length - 1}
|
||||
isVisible={false}
|
||||
setPopup={setPopup}
|
||||
/>
|
||||
|
@ -94,7 +94,7 @@ export function ChatBanner() {
|
||||
content={settings.enterpriseSettings.custom_header_content}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-0 right-0 ">
|
||||
<div className="absolute bottom-0 right-0">
|
||||
{isOverflowing && (
|
||||
<Popover
|
||||
open={isPopoverOpen}
|
||||
|
@ -88,7 +88,7 @@ import { SIDEBAR_TOGGLED_COOKIE_NAME } from "@/components/resizable/constants";
|
||||
import FixedLogo from "./shared_chat_search/FixedLogo";
|
||||
import { getSecondsUntilExpiration } from "@/lib/time";
|
||||
import { SetDefaultModelModal } from "./modal/SetDefaultModelModal";
|
||||
import { DeleteChatModal } from "./modal/DeleteChatModal";
|
||||
import { DeleteEntityModal } from "../../components/modals/DeleteEntityModal";
|
||||
import { MinimalMarkdown } from "@/components/chat_search/MinimalMarkdown";
|
||||
import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal";
|
||||
|
||||
@ -1571,7 +1571,9 @@ export function ChatPage({
|
||||
)}
|
||||
|
||||
{deletingChatSession && (
|
||||
<DeleteChatModal
|
||||
<DeleteEntityModal
|
||||
entityType="chat"
|
||||
entityName={deletingChatSession.name.slice(0, 30)}
|
||||
onClose={() => setDeletingChatSession(null)}
|
||||
onSubmit={async () => {
|
||||
const response = await deleteChatSession(deletingChatSession.id);
|
||||
@ -1583,7 +1585,6 @@ export function ChatPage({
|
||||
alert("Failed to delete chat session");
|
||||
}
|
||||
}}
|
||||
chatSessionName={deletingChatSession.name}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -1706,7 +1707,7 @@ export function ChatPage({
|
||||
>
|
||||
{/* <input {...getInputProps()} /> */}
|
||||
<div
|
||||
className={`w-full h-full flex flex-col overflow-y-auto include-scrollbar overflow-x-hidden relative`}
|
||||
className={`w-full h-full flex flex-col overflow-y-auto include-scrollbar overflow-x-hidden relative`}
|
||||
ref={scrollableDivRef}
|
||||
>
|
||||
{/* ChatBanner is a custom banner that displays a admin-specified message at
|
||||
@ -1722,7 +1723,7 @@ export function ChatPage({
|
||||
)}
|
||||
<div
|
||||
className={
|
||||
"mt-4 -ml-4 w-full mx-auto " +
|
||||
"mt-4 -ml-4 w-full mx-auto " +
|
||||
"absolute mobile:top-0 desktop:top-12 left-0" +
|
||||
(hasPerformedInitialScroll ? "" : "invisible")
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ export function ChatDocumentDisplay({
|
||||
) : (
|
||||
<SourceIcon sourceType={document.source_type} iconSize={18} />
|
||||
)}
|
||||
<p className="overflow-hidden text-left text-ellipsis mx-2 my-auto text-sm ">
|
||||
<p className="overflow-hidden text-left text-ellipsis mx-2 my-auto text-sm">
|
||||
{document.semantic_identifier || document.document_id}
|
||||
</p>
|
||||
</a>
|
||||
|
@ -69,7 +69,7 @@ export const DocumentSidebar = forwardRef<HTMLDivElement, DocumentSidebarProps>(
|
||||
width: initialWidth,
|
||||
}}
|
||||
>
|
||||
<div className="pb-6 flex-initial overflow-y-hidden flex flex-col h-screen ">
|
||||
<div className="pb-6 flex-initial overflow-y-hidden flex flex-col h-screen">
|
||||
{popup}
|
||||
<div className="pl-3 mx-2 pr-6 mt-3 flex text-text-800 flex-col text-2xl text-emphasis flex font-semibold">
|
||||
{dedupedDocuments.length} Documents
|
||||
|
@ -273,12 +273,10 @@ export function ChatInputBar({
|
||||
|
||||
return (
|
||||
<div id="danswer-chat-input">
|
||||
<div className="flex justify-center max-w-screen-lg mx-auto">
|
||||
<div className="flex justify-center mx-auto">
|
||||
<div
|
||||
className="
|
||||
w-[90%]
|
||||
max-w-searchbar-max
|
||||
shrink
|
||||
w-[800px]
|
||||
relative
|
||||
desktop:px-4
|
||||
mx-auto
|
||||
@ -340,7 +338,7 @@ export function ChatInputBar({
|
||||
updateInputPrompt(currentPrompt);
|
||||
}}
|
||||
>
|
||||
<p className="font-bold ">{currentPrompt.prompt}</p>
|
||||
<p className="font-bold">{currentPrompt.prompt}</p>
|
||||
<p className="line-clamp-1">
|
||||
{currentPrompt.id == selectedAssistant.id && "(default) "}
|
||||
{currentPrompt.content}
|
||||
@ -485,8 +483,7 @@ export function ChatInputBar({
|
||||
outline-none
|
||||
placeholder-subtle
|
||||
resize-none
|
||||
pl-4
|
||||
pr-12
|
||||
px-5
|
||||
py-4
|
||||
h-14
|
||||
`}
|
||||
@ -514,7 +511,7 @@ export function ChatInputBar({
|
||||
}}
|
||||
suppressContentEditableWarning={true}
|
||||
/>
|
||||
<div className="flex items-center space-x-3 mr-12 px-4 pb-2 ">
|
||||
<div className="flex items-center space-x-3 mr-12 px-4 pb-2">
|
||||
<Popup
|
||||
removePadding
|
||||
content={(close) => (
|
||||
@ -558,7 +555,6 @@ export function ChatInputBar({
|
||||
ref={ref}
|
||||
llmOverrideManager={llmOverrideManager}
|
||||
chatSessionId={chatSessionId}
|
||||
currentAssistant={selectedAssistant}
|
||||
/>
|
||||
)}
|
||||
position="top"
|
||||
|
@ -1,5 +1,9 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { ChevronRightIcon, IconProps } from "@/components/icons/icons";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
IconProps,
|
||||
} from "@/components/icons/icons";
|
||||
|
||||
interface ChatInputOptionProps {
|
||||
name?: string;
|
||||
@ -75,7 +79,9 @@ export const ChatInputOption: React.FC<ChatInputOptionProps> = ({
|
||||
<Icon size={size} className="flex-none" />
|
||||
<div className="flex items-center gap-x-.5">
|
||||
{name && <span className="text-sm break-all line-clamp-1">{name}</span>}
|
||||
{toggle && <ChevronRightIcon className="flex-none" size={size} />}
|
||||
{toggle && (
|
||||
<ChevronDownIcon className="flex-none ml-1" size={size - 4} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isTooltipVisible && tooltipContent && (
|
||||
|
@ -133,9 +133,7 @@ export function CodeBlock({
|
||||
)}
|
||||
</div>
|
||||
<pre {...props} className="overflow-x-scroll" style={{ padding: "1rem" }}>
|
||||
<code className={`text-sm overflow-x-auto ${className}`}>
|
||||
{children}
|
||||
</code>
|
||||
<code className={`text-xs overflow-x-auto `}>{children}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
|
@ -272,12 +272,12 @@ export const AIMessage = ({
|
||||
<div
|
||||
id="danswer-ai-message"
|
||||
ref={trackedElementRef}
|
||||
className={"py-5 px-2 lg:px-5 relative flex "}
|
||||
className={"py-5 ml-4 px-5 relative flex "}
|
||||
>
|
||||
<div
|
||||
className={`mx-auto ${shared ? "w-full" : "w-[90%]"} max-w-message-max`}
|
||||
className={`mx-auto ${shared ? "w-full" : "w-[90%]"} max-w-message-max`}
|
||||
>
|
||||
<div className={`${!shared && "mobile:ml-4 xl:ml-8"}`}>
|
||||
<div className={`desktop:mr-12 ${!shared && "mobile:ml-0 md:ml-8"}`}>
|
||||
<div className="flex">
|
||||
<AssistantIcon
|
||||
size="small"
|
||||
@ -358,10 +358,10 @@ export const AIMessage = ({
|
||||
<FileDisplay files={files || []} />
|
||||
|
||||
{typeof content === "string" ? (
|
||||
<div className="overflow-x-visible w-full pr-2 max-w-[675px]">
|
||||
<div className="overflow-x-visible w-full pr-2">
|
||||
<ReactMarkdown
|
||||
key={messageId}
|
||||
className="prose max-w-full"
|
||||
className="prose max-w-full text-base"
|
||||
components={{
|
||||
a: (props) => {
|
||||
const { node, ...rest } = props;
|
||||
@ -446,7 +446,7 @@ export const AIMessage = ({
|
||||
className="text-sm flex w-full pt-1 gap-x-1.5 overflow-hidden justify-between font-semibold text-text-700"
|
||||
>
|
||||
<Citation link={doc.link} index={ind + 1} />
|
||||
<p className="shrink truncate ellipsis break-all ">
|
||||
<p className="shrink truncate ellipsis break-all">
|
||||
{doc.semantic_identifier ||
|
||||
doc.document_id}
|
||||
</p>
|
||||
@ -727,14 +727,12 @@ export const HumanMessage = ({
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<div
|
||||
className={`mx-auto ${shared ? "w-full" : "w-[90%]"} max-w-searchbar-max`}
|
||||
>
|
||||
<div className={`mx-auto ${shared ? "w-full" : "w-[90%]"} max-w-[790px]`}>
|
||||
<div className="xl:ml-8">
|
||||
<div className="flex flex-col mr-4">
|
||||
<FileDisplay alignBubble files={files || []} />
|
||||
<div className="flex justify-end">
|
||||
<div className="w-full ml-8 flex w-full max-w-message-max break-words">
|
||||
<div className="w-full ml-8 flex w-full w-[800px] break-words">
|
||||
{isEditing ? (
|
||||
<div className="w-full">
|
||||
<div
|
||||
|
@ -1,19 +1,16 @@
|
||||
import { useChatContext } from "@/components/context/ChatContext";
|
||||
import { getDisplayNameForModel, LlmOverrideManager } from "@/lib/hooks";
|
||||
import React, { forwardRef, useCallback, useRef, useState } from "react";
|
||||
import React, { forwardRef, useCallback, useState } from "react";
|
||||
import { debounce } from "lodash";
|
||||
import { DefaultDropdown } from "@/components/Dropdown";
|
||||
import { Text } from "@tremor/react";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { destructureValue, getFinalLLM, structureValue } from "@/lib/llm/utils";
|
||||
import { destructureValue, structureValue } from "@/lib/llm/utils";
|
||||
import { updateModelOverrideForChatSession } from "../../lib";
|
||||
import { Tooltip } from "@/components/tooltip/Tooltip";
|
||||
import { GearIcon, InfoIcon } from "@/components/icons/icons";
|
||||
import { CustomTooltip } from "@/components/tooltip/CustomTooltip";
|
||||
import { GearIcon } from "@/components/icons/icons";
|
||||
import { LlmList } from "@/components/llm/LLMList";
|
||||
|
||||
interface LlmTabProps {
|
||||
llmOverrideManager: LlmOverrideManager;
|
||||
currentAssistant: Persona;
|
||||
currentLlm: string;
|
||||
openModelSettings: () => void;
|
||||
chatSessionId?: number;
|
||||
@ -22,14 +19,7 @@ interface LlmTabProps {
|
||||
|
||||
export const LlmTab = forwardRef<HTMLDivElement, LlmTabProps>(
|
||||
(
|
||||
{
|
||||
llmOverrideManager,
|
||||
currentAssistant,
|
||||
chatSessionId,
|
||||
currentLlm,
|
||||
close,
|
||||
openModelSettings,
|
||||
},
|
||||
{ llmOverrideManager, chatSessionId, currentLlm, close, openModelSettings },
|
||||
ref
|
||||
) => {
|
||||
const { llmProviders } = useChatContext();
|
||||
@ -51,40 +41,10 @@ export const LlmTab = forwardRef<HTMLDivElement, LlmTabProps>(
|
||||
debouncedSetTemperature(value);
|
||||
};
|
||||
|
||||
const llmOptionsByProvider: {
|
||||
[provider: string]: { name: string; value: string }[];
|
||||
} = {};
|
||||
const uniqueModelNames = new Set<string>();
|
||||
|
||||
llmProviders.forEach((llmProvider) => {
|
||||
if (!llmOptionsByProvider[llmProvider.provider]) {
|
||||
llmOptionsByProvider[llmProvider.provider] = [];
|
||||
}
|
||||
|
||||
(llmProvider.display_model_names || llmProvider.model_names).forEach(
|
||||
(modelName) => {
|
||||
if (!uniqueModelNames.has(modelName)) {
|
||||
uniqueModelNames.add(modelName);
|
||||
llmOptionsByProvider[llmProvider.provider].push({
|
||||
name: modelName,
|
||||
value: structureValue(
|
||||
llmProvider.name,
|
||||
llmProvider.provider,
|
||||
modelName
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const llmOptions = Object.entries(llmOptionsByProvider).flatMap(
|
||||
([provider, options]) => [...options]
|
||||
);
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex w-full justify-between content-center mb-2 gap-x-2">
|
||||
<label className="block text-sm font-medium ">Choose Model</label>
|
||||
<label className="block text-sm font-medium">Choose Model</label>
|
||||
<button
|
||||
onClick={() => {
|
||||
close();
|
||||
@ -94,28 +54,21 @@ export const LlmTab = forwardRef<HTMLDivElement, LlmTabProps>(
|
||||
<GearIcon />
|
||||
</button>
|
||||
</div>
|
||||
<div className="max-h-[300px] flex flex-col gap-y-1 overflow-y-scroll">
|
||||
{llmOptions.map(({ name, value }, index) => {
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
className={`w-full py-1.5 px-2 text-sm ${currentLlm == name ? "bg-background-200" : "bg-background-100/50 hover:bg-background-100"} text-left rounded`}
|
||||
onClick={() => {
|
||||
setLlmOverride(destructureValue(value));
|
||||
if (chatSessionId) {
|
||||
updateModelOverrideForChatSession(
|
||||
chatSessionId,
|
||||
value as string
|
||||
);
|
||||
}
|
||||
close();
|
||||
}}
|
||||
>
|
||||
{getDisplayNameForModel(name)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<LlmList
|
||||
llmProviders={llmProviders}
|
||||
currentLlm={currentLlm}
|
||||
onSelect={(value: string | null) => {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
setLlmOverride(destructureValue(value));
|
||||
if (chatSessionId) {
|
||||
updateModelOverrideForChatSession(chatSessionId, value as string);
|
||||
}
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="mt-4">
|
||||
<button
|
||||
className="flex items-center text-sm font-medium transition-colors duration-200"
|
||||
|
@ -68,7 +68,7 @@ export function SharedChatDisplay({
|
||||
<div className="w-full h-[100dvh] overflow-hidden">
|
||||
<div className="flex max-h-full overflow-hidden pb-[72px]">
|
||||
<div className="flex w-full overflow-hidden overflow-y-scroll">
|
||||
<div className="w-full h-full flex-col flex max-w-message-max mx-auto">
|
||||
<div className="w-full h-full flex-col flex max-w-message-max mx-auto">
|
||||
<div className="px-5 pt-8">
|
||||
<h1 className="text-3xl text-strong font-bold">
|
||||
{chatSession.description ||
|
||||
|
@ -108,24 +108,21 @@ export const DanswerApiKeyForm = ({
|
||||
|
||||
<BooleanFormField
|
||||
small
|
||||
noPadding
|
||||
removeIndent
|
||||
alignTop
|
||||
name="is_admin"
|
||||
label="Is Admin?"
|
||||
subtext="If set, this API key will have access to admin level server API's."
|
||||
/>
|
||||
|
||||
<div className="flex">
|
||||
<Button
|
||||
type="submit"
|
||||
size="xs"
|
||||
color="green"
|
||||
disabled={isSubmitting}
|
||||
className="mx-auto w-64"
|
||||
>
|
||||
{isUpdate ? "Update!" : "Create!"}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
size="xs"
|
||||
color="green"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isUpdate ? "Update!" : "Create!"}
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
|
@ -94,7 +94,7 @@ const UserRoleDropdown = ({
|
||||
|
||||
if (isEditable) {
|
||||
return (
|
||||
<div className="w-40 ">
|
||||
<div className="w-40">
|
||||
<Select
|
||||
value={localRole}
|
||||
onValueChange={handleChange}
|
||||
|
@ -131,7 +131,7 @@ export function WhitelabelingForm() {
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
|
||||
<Label>Custom Logo</Label>
|
||||
<Label className="mt-4">Custom Logo</Label>
|
||||
|
||||
{values.use_custom_logo ? (
|
||||
<div className="mt-3">
|
||||
@ -183,10 +183,8 @@ export function WhitelabelingForm() {
|
||||
setShowAdvancedOptions={setShowAdvancedOptions}
|
||||
/>
|
||||
|
||||
<br />
|
||||
|
||||
{showAdvancedOptions && (
|
||||
<>
|
||||
<div className="w-full flex flex-col gap-y-4">
|
||||
<Text>
|
||||
Read{" "}
|
||||
<Link
|
||||
@ -197,101 +195,99 @@ export function WhitelabelingForm() {
|
||||
</Link>{" "}
|
||||
to see whitelabelling examples in action.
|
||||
</Text>
|
||||
<div className="mt-4">
|
||||
<TextFormField
|
||||
label="Chat Header Content"
|
||||
name="custom_header_content"
|
||||
subtext={`Custom Markdown content that will be displayed as a banner at the top of the Chat page.`}
|
||||
placeholder="Your header content..."
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TextFormField
|
||||
label="Chat Header Content"
|
||||
name="custom_header_content"
|
||||
subtext={`Custom Markdown content that will be displayed as a banner at the top of the Chat page.`}
|
||||
placeholder="Your header content..."
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div className="mt-4">
|
||||
<TextFormField
|
||||
label="Popup Header"
|
||||
name="custom_popup_header"
|
||||
subtext={`The title for the popup that will be displayed for each user on their initial visit
|
||||
to the application. If left blank AND Custom Popup Content is specified, will use "Welcome to ${
|
||||
values.application_name || "Danswer"
|
||||
}!".`}
|
||||
placeholder="Initial Popup Header"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<TextFormField
|
||||
label="Popup Content"
|
||||
name="custom_popup_content"
|
||||
subtext={`Custom Markdown content that will be displayed as a popup on initial visit to the application.`}
|
||||
placeholder="Your popup content..."
|
||||
isTextArea
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<TextFormField
|
||||
label="Chat Footer Text"
|
||||
name="custom_lower_disclaimer_content"
|
||||
subtext={`Custom Markdown content that will be displayed at the bottom of the Chat page.`}
|
||||
placeholder="Your disclaimer content..."
|
||||
isTextArea
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<Label>Chat Footer Logotype</Label>
|
||||
|
||||
{values.use_custom_logotype ? (
|
||||
<div className="mt-3">
|
||||
<SubLabel>Current Custom Logotype: </SubLabel>
|
||||
<img
|
||||
src={"/api/enterprise-settings/logotype?u=" + Date.now()}
|
||||
alt="logotype"
|
||||
style={{ objectFit: "contain" }}
|
||||
className="w-32 h-32 mb-10 mt-4"
|
||||
/>
|
||||
|
||||
<Button
|
||||
color="red"
|
||||
size="xs"
|
||||
type="button"
|
||||
className="mb-8"
|
||||
onClick={async () => {
|
||||
const valuesWithoutLogotype = {
|
||||
...values,
|
||||
use_custom_logotype: false,
|
||||
};
|
||||
await updateEnterpriseSettings(valuesWithoutLogotype);
|
||||
setValues(valuesWithoutLogotype);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
<SubLabel>
|
||||
Override your uploaded custom logotype by uploading a new
|
||||
image below and clicking the Update button. This logotype
|
||||
is the text-based logo that will be rendered at the bottom
|
||||
right of the chat screen.
|
||||
</SubLabel>
|
||||
</div>
|
||||
) : (
|
||||
<SubLabel>
|
||||
Add a custom logotype by uploading a new image below and
|
||||
clicking the Update button. This logotype is the text-based
|
||||
logo that will be rendered at the bottom right of the chat
|
||||
screen.
|
||||
</SubLabel>
|
||||
)}
|
||||
<ImageUpload
|
||||
selectedFile={selectedLogotype}
|
||||
setSelectedFile={setSelectedLogotype}
|
||||
<TextFormField
|
||||
label="Popup Header"
|
||||
name="custom_popup_header"
|
||||
subtext={`The title for the popup that will be displayed for each user on their initial visit
|
||||
to the application. If left blank AND Custom Popup Content is specified, will use "Welcome to ${
|
||||
values.application_name || "Danswer"
|
||||
}!".`}
|
||||
placeholder="Initial Popup Header"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</>
|
||||
|
||||
<TextFormField
|
||||
label="Popup Content"
|
||||
name="custom_popup_content"
|
||||
subtext={`Custom Markdown content that will be displayed as a popup on initial visit to the application.`}
|
||||
placeholder="Your popup content..."
|
||||
isTextArea
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
|
||||
<TextFormField
|
||||
label="Chat Footer Text"
|
||||
name="custom_lower_disclaimer_content"
|
||||
subtext={`Custom Markdown content that will be displayed at the bottom of the Chat page.`}
|
||||
placeholder="Your disclaimer content..."
|
||||
isTextArea
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Label>Chat Footer Logotype</Label>
|
||||
|
||||
{values.use_custom_logotype ? (
|
||||
<div className="mt-3">
|
||||
<SubLabel>Current Custom Logotype: </SubLabel>
|
||||
<img
|
||||
src={
|
||||
"/api/enterprise-settings/logotype?u=" + Date.now()
|
||||
}
|
||||
alt="logotype"
|
||||
style={{ objectFit: "contain" }}
|
||||
className="w-32 h-32 mb-10 mt-4"
|
||||
/>
|
||||
|
||||
<Button
|
||||
color="red"
|
||||
size="xs"
|
||||
type="button"
|
||||
className="mb-8"
|
||||
onClick={async () => {
|
||||
const valuesWithoutLogotype = {
|
||||
...values,
|
||||
use_custom_logotype: false,
|
||||
};
|
||||
await updateEnterpriseSettings(valuesWithoutLogotype);
|
||||
setValues(valuesWithoutLogotype);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
<SubLabel>
|
||||
Override your uploaded custom logotype by uploading a
|
||||
new image below and clicking the Update button. This
|
||||
logotype is the text-based logo that will be rendered at
|
||||
the bottom right of the chat screen.
|
||||
</SubLabel>
|
||||
</div>
|
||||
) : (
|
||||
<SubLabel>
|
||||
Add a custom logotype by uploading a new image below and
|
||||
clicking the Update button. This logotype is the
|
||||
text-based logo that will be rendered at the bottom right
|
||||
of the chat screen.
|
||||
</SubLabel>
|
||||
)}
|
||||
<ImageUpload
|
||||
selectedFile={selectedLogotype}
|
||||
setSelectedFile={setSelectedLogotype}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="mt-4">
|
||||
|
@ -223,3 +223,10 @@ code[class*="language-"] {
|
||||
.code-line .token.attr-name {
|
||||
color: theme("colors.token-attr-name");
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
@ -12,15 +12,17 @@ export function AdvancedOptionsToggle({
|
||||
setShowAdvancedOptions,
|
||||
}: AdvancedOptionsToggleProps) {
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="light"
|
||||
size="xs"
|
||||
icon={showAdvancedOptions ? FiChevronDown : FiChevronRight}
|
||||
onClick={() => setShowAdvancedOptions(!showAdvancedOptions)}
|
||||
className="mb-4 text-xs text-text-500 hover:text-text-400"
|
||||
>
|
||||
Advanced Options
|
||||
</Button>
|
||||
<div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="light"
|
||||
size="xs"
|
||||
icon={showAdvancedOptions ? FiChevronDown : FiChevronRight}
|
||||
onClick={() => setShowAdvancedOptions(!showAdvancedOptions)}
|
||||
className="text-xs text-text-950 hover:text-text-500"
|
||||
>
|
||||
Advanced Options
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ export const Hoverable: React.FC<{
|
||||
className={`group relative flex items-center overflow-hidden p-1.5 h-fit rounded-md cursor-pointer transition-all duration-300 ease-in-out hover:bg-hover`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex items-center ">
|
||||
<div className="flex items-center">
|
||||
<Icon size={size} className="text-gray-600 shrink-0" />
|
||||
{hoverText && (
|
||||
<div className="max-w-0 leading-none whitespace-nowrap overflow-hidden transition-all duration-300 ease-in-out group-hover:max-w-xs group-hover:ml-2">
|
||||
|
@ -37,7 +37,7 @@ export function AdminSidebar({ collections }: { collections: Collection[] }) {
|
||||
|
||||
return (
|
||||
<div className="pl-0">
|
||||
<nav className="space-y-2 ">
|
||||
<nav className="space-y-2">
|
||||
<div className="w-full justify-center mb-4 flex">
|
||||
<div className="w-52">
|
||||
<Link
|
||||
|
@ -21,6 +21,7 @@ import ReactMarkdown from "react-markdown";
|
||||
import { FaMarkdown } from "react-icons/fa";
|
||||
import { useState } from "react";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { EditIcon } from "@/components/icons/icons";
|
||||
|
||||
export function SectionHeader({
|
||||
children,
|
||||
@ -33,13 +34,15 @@ export function SectionHeader({
|
||||
export function Label({
|
||||
children,
|
||||
small,
|
||||
className,
|
||||
}: {
|
||||
children: string | JSX.Element;
|
||||
small?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`block font-medium base ${small ? "text-sm" : "text-base"}`}
|
||||
className={`block font-medium base ${className} ${small ? "text-sm" : "text-base"}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
@ -63,7 +66,7 @@ export function ExplanationText({
|
||||
}) {
|
||||
return link ? (
|
||||
<a
|
||||
className="underline cursor-pointer text-sm font-medium"
|
||||
className="underline text-text-500 cursor-pointer text-sm font-medium"
|
||||
target="_blank"
|
||||
href={link}
|
||||
>
|
||||
@ -100,8 +103,11 @@ export function TextFormField({
|
||||
label,
|
||||
subtext,
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
type = "text",
|
||||
optional,
|
||||
includeRevert,
|
||||
isTextArea = false,
|
||||
disabled = false,
|
||||
autoCompleteDisabled = true,
|
||||
@ -114,19 +120,20 @@ export function TextFormField({
|
||||
explanationText,
|
||||
explanationLink,
|
||||
small,
|
||||
noPadding,
|
||||
removeLabel,
|
||||
}: {
|
||||
value?: string;
|
||||
name: string;
|
||||
removeLabel?: boolean;
|
||||
label: string;
|
||||
subtext?: string | JSX.Element;
|
||||
placeholder?: string;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
includeRevert?: boolean;
|
||||
optional?: boolean;
|
||||
type?: string;
|
||||
isTextArea?: boolean;
|
||||
disabled?: boolean;
|
||||
noPadding?: boolean;
|
||||
autoCompleteDisabled?: boolean;
|
||||
error?: string;
|
||||
defaultHeight?: string;
|
||||
@ -144,9 +151,14 @@ export function TextFormField({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${!noPadding && "mb-6"}`}>
|
||||
<div className="w-full">
|
||||
<div className="flex gap-x-2 items-center">
|
||||
{!removeLabel && <Label small={small}>{label}</Label>}
|
||||
{!removeLabel && (
|
||||
<Label className="text-text-950" small={small}>
|
||||
{label}
|
||||
</Label>
|
||||
)}
|
||||
{optional ? <span>(optional) </span> : ""}
|
||||
{tooltip && <ToolTipDetails>{tooltip}</ToolTipDetails>}
|
||||
{error ? (
|
||||
<ManualErrorMessage>{error}</ManualErrorMessage>
|
||||
@ -160,33 +172,55 @@ export function TextFormField({
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{subtext && <SubLabel>{subtext}</SubLabel>}
|
||||
|
||||
<Field
|
||||
as={isTextArea ? "textarea" : "input"}
|
||||
type={type}
|
||||
name={name}
|
||||
id={name}
|
||||
className={`
|
||||
<div className={`w-full flex ${includeRevert && "gap-x-2"}`}>
|
||||
<Field
|
||||
as={isTextArea ? "textarea" : "input"}
|
||||
type={type}
|
||||
defaultValue={value}
|
||||
name={name}
|
||||
id={name}
|
||||
className={`
|
||||
${small && "text-sm"}
|
||||
border
|
||||
border-border
|
||||
rounded
|
||||
rounded-lg
|
||||
w-full
|
||||
py-2
|
||||
px-3
|
||||
mt-1
|
||||
placeholder:font-description
|
||||
placeholder:text-base
|
||||
placeholder:text-text-400
|
||||
${heightString}
|
||||
${fontSize}
|
||||
${disabled ? " bg-background-strong" : " bg-background-emphasis"}
|
||||
${disabled ? " bg-background-strong" : " bg-white"}
|
||||
${isCode ? " font-mono" : ""}
|
||||
`}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
autoComplete={autoCompleteDisabled ? "off" : undefined}
|
||||
{...(onChange ? { onChange } : {})}
|
||||
/>
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
autoComplete={autoCompleteDisabled ? "off" : undefined}
|
||||
// onChange={onChange}
|
||||
/>
|
||||
{includeRevert && (
|
||||
<div className="flex-none mt-auto">
|
||||
<button
|
||||
className="text-xs h-[35px] my-auto p-1.5 rounded bg-background-900 border-border-dark text-text-300 flex gap-x-1"
|
||||
onClick={(e) => {
|
||||
if (onChange) {
|
||||
onChange({
|
||||
target: { value: "" },
|
||||
} as React.ChangeEvent<HTMLInputElement>);
|
||||
}
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<EditIcon className="text-netural-300 my-auto" />
|
||||
<p className="my-auto">Revert</p>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{explanationText && (
|
||||
<ExplanationText link={explanationLink} text={explanationText} />
|
||||
@ -340,11 +374,13 @@ interface BooleanFormFieldProps {
|
||||
label: string;
|
||||
subtext?: string | JSX.Element;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
noPadding?: boolean;
|
||||
removeIndent?: boolean;
|
||||
small?: boolean;
|
||||
alignTop?: boolean;
|
||||
noLabel?: boolean;
|
||||
disabled?: boolean;
|
||||
checked?: boolean;
|
||||
optional?: boolean;
|
||||
}
|
||||
|
||||
export const BooleanFormField = ({
|
||||
@ -352,27 +388,31 @@ export const BooleanFormField = ({
|
||||
label,
|
||||
subtext,
|
||||
onChange,
|
||||
noPadding,
|
||||
removeIndent,
|
||||
noLabel,
|
||||
optional,
|
||||
small,
|
||||
disabled,
|
||||
alignTop,
|
||||
checked,
|
||||
}: BooleanFormFieldProps) => {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div>
|
||||
<label className="flex text-sm">
|
||||
<Field
|
||||
disabled={disabled}
|
||||
name={name}
|
||||
checked={checked}
|
||||
type="checkbox"
|
||||
className={`${noPadding ? "mr-3" : "mx-3"} px-5 w-3.5 h-3.5 ${
|
||||
alignTop ? "mt-1" : "my-auto"
|
||||
}`}
|
||||
className={`${removeIndent ? "mr-2" : "mx-3"}
|
||||
px-5 w-3.5 h-3.5 ${alignTop ? "mt-1" : "my-auto"}`}
|
||||
{...(onChange ? { onChange } : {})}
|
||||
/>
|
||||
{!noLabel && (
|
||||
<div>
|
||||
<Label small={small}>{label}</Label>
|
||||
<Label
|
||||
small={small}
|
||||
>{`${label}${optional ? " (Optional)" : ""}`}</Label>
|
||||
{subtext && <SubLabel>{subtext}</SubLabel>}
|
||||
</div>
|
||||
)}
|
||||
|
@ -31,22 +31,20 @@ const AddUserFormRenderer = ({
|
||||
errors,
|
||||
isSubmitting,
|
||||
}: FormikProps<FormValues>) => (
|
||||
<Form>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Field id="emails" name="emails" as="textarea" className="p-4" />
|
||||
{touched.emails && errors.emails && (
|
||||
<div className="text-error text-sm">{errors.emails}</div>
|
||||
)}
|
||||
<Button
|
||||
className="mx-auto"
|
||||
color="green"
|
||||
size="md"
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Add!
|
||||
</Button>
|
||||
</div>
|
||||
<Form className="w-full">
|
||||
<Field id="emails" name="emails" as="textarea" className="w-full p-4" />
|
||||
{touched.emails && errors.emails && (
|
||||
<div className="text-error text-sm">{errors.emails}</div>
|
||||
)}
|
||||
<Button
|
||||
className="mx-auto"
|
||||
color="green"
|
||||
size="md"
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Add!
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
|
||||
|
@ -49,7 +49,7 @@ export function AssistantIcon({
|
||||
{createSVG(
|
||||
{ encodedGrid: assistant.icon_shape, filledSquares: 0 },
|
||||
assistant.icon_color,
|
||||
size == "large" ? 48 : 36
|
||||
size == "large" ? 36 : 24
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
|
@ -1,144 +0,0 @@
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { buildImgUrl } from "@/app/chat/files/images/utils";
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
import Dropzone from "react-dropzone";
|
||||
import { usePopup } from "../admin/connectors/Popup";
|
||||
|
||||
export const IconImageSelection = ({
|
||||
setFieldValue,
|
||||
existingPersonaImageId,
|
||||
setExistingPersonaImageId,
|
||||
setRemovePersonaImage,
|
||||
}: {
|
||||
setExistingPersonaImageId: Dispatch<SetStateAction<string | null>>;
|
||||
existingPersonaImageId: string | null;
|
||||
setFieldValue: (
|
||||
field: string,
|
||||
value: any,
|
||||
shouldValidate?: boolean
|
||||
) => Promise<any>;
|
||||
setRemovePersonaImage: Dispatch<SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const [uploadedImage, setUploadedImage] = useState<File | null>(null);
|
||||
|
||||
const updateFile = (image: File | null) => {
|
||||
setUploadedImage(image);
|
||||
setFieldValue("uploaded_image", image);
|
||||
};
|
||||
|
||||
const resetPreviousAssistantImage = () => {
|
||||
setRemovePersonaImage(true);
|
||||
setExistingPersonaImageId(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-2 gap-y-2 flex flex-col">
|
||||
<p className="font-bold text-sm text-gray-800">Or Upload Image</p>
|
||||
{existingPersonaImageId && (
|
||||
<div className="flex gap-x-2">
|
||||
Current image:
|
||||
<img
|
||||
className="h-12 w-12"
|
||||
src={buildImgUrl(existingPersonaImageId)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-x-2">
|
||||
<IconImageUpload selectedFile={uploadedImage} updateFile={updateFile} />
|
||||
{existingPersonaImageId && (
|
||||
<button
|
||||
onClick={resetPreviousAssistantImage}
|
||||
className={
|
||||
"text-sm text-text-800 max-w-[200px] p-2 rounded " +
|
||||
"shadow-lg border border-border cursor-pointer"
|
||||
}
|
||||
>
|
||||
Remove current image
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">
|
||||
Uploading an image will override the generated icon.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function IconImageUpload({
|
||||
selectedFile,
|
||||
updateFile,
|
||||
}: {
|
||||
selectedFile: File | null;
|
||||
updateFile: (image: File | null) => void;
|
||||
}) {
|
||||
const [tmpImageUrl, setTmpImageUrl] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedFile) {
|
||||
setTmpImageUrl(URL.createObjectURL(selectedFile));
|
||||
} else {
|
||||
setTmpImageUrl("");
|
||||
}
|
||||
}, [selectedFile]);
|
||||
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
const { popup, setPopup } = usePopup();
|
||||
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
|
||||
<Dropzone
|
||||
onDrop={(acceptedFiles) => {
|
||||
if (acceptedFiles.length !== 1) {
|
||||
setPopup({
|
||||
type: "error",
|
||||
message: "Only one file can be uploaded at a time",
|
||||
});
|
||||
}
|
||||
setTmpImageUrl(URL.createObjectURL(acceptedFiles[0]));
|
||||
updateFile(acceptedFiles[0]);
|
||||
setDragActive(false);
|
||||
}}
|
||||
onDragLeave={() => setDragActive(false)}
|
||||
onDragEnter={() => setDragActive(true)}
|
||||
>
|
||||
{({ getRootProps, getInputProps }) => (
|
||||
<section>
|
||||
{!selectedFile && (
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={
|
||||
"flex flex-col items-center max-w-[200px] p-2 rounded " +
|
||||
"shadow-lg border border-border cursor-pointer" +
|
||||
(dragActive ? " border-accent" : "")
|
||||
}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<p className="font-base text-sm text-text-800">
|
||||
Upload a .png or .jpg file
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{tmpImageUrl && (
|
||||
<div className="flex mt-2 gap-x-2">
|
||||
Uploaded Image:
|
||||
<img src={tmpImageUrl} className="h-12 w-12"></img>
|
||||
</div>
|
||||
)}
|
||||
{selectedFile && (
|
||||
<button
|
||||
onClick={() => {
|
||||
updateFile(null);
|
||||
setTmpImageUrl("");
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
</Dropzone>
|
||||
</>
|
||||
);
|
||||
}
|
@ -125,27 +125,27 @@ export function AdminTextField({
|
||||
interface BooleanFormFieldProps {
|
||||
name: string;
|
||||
label: string;
|
||||
checked: boolean;
|
||||
subtext?: string | JSX.Element;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
small?: boolean;
|
||||
alignTop?: boolean;
|
||||
noLabel?: boolean;
|
||||
checked: boolean;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
export const AdminBooleanFormField = ({
|
||||
name,
|
||||
label,
|
||||
subtext,
|
||||
onChange,
|
||||
noLabel,
|
||||
small,
|
||||
checked,
|
||||
alignTop,
|
||||
onChange,
|
||||
}: BooleanFormFieldProps) => {
|
||||
return (
|
||||
<div>
|
||||
<label className="flex text-sm">
|
||||
<label className={`flex text-sm`}>
|
||||
<Field
|
||||
name={name}
|
||||
checked={checked}
|
||||
@ -153,7 +153,6 @@ export const AdminBooleanFormField = ({
|
||||
className={`mr-3 bg-white px-5 w-3.5 h-3.5 ${
|
||||
alignTop ? "mt-1" : "my-auto"
|
||||
}`}
|
||||
{...(onChange ? { onChange } : {})}
|
||||
/>
|
||||
{!noLabel && (
|
||||
<div>
|
||||
|
@ -1,221 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { SubLabel } from "../admin/connectors/Field";
|
||||
import { EditIcon } from "../icons/icons";
|
||||
import { AdminBooleanFormField, AdminTextField } from "./CredentialFields";
|
||||
|
||||
// Our own input component, to be used across forms
|
||||
export const EditingValue: React.FC<{
|
||||
name: string;
|
||||
currentValue?: any;
|
||||
label: string;
|
||||
type?: string;
|
||||
includRevert?: boolean;
|
||||
className?: string;
|
||||
optional?: boolean;
|
||||
description?: string;
|
||||
setFieldValue: (field: string, value: any) => void;
|
||||
showNever?: boolean;
|
||||
|
||||
// These are escape hatches from the overall
|
||||
// value editing component (when need to modify)
|
||||
options?: { value: string; label: string }[];
|
||||
onChange?: (value: string) => void;
|
||||
onChangeBool?: (value: boolean) => void;
|
||||
onChangeNumber?: (value: number) => void;
|
||||
onChangeDate?: (value: Date | null) => void;
|
||||
}> = ({
|
||||
name,
|
||||
currentValue,
|
||||
label,
|
||||
options,
|
||||
type,
|
||||
includRevert,
|
||||
className,
|
||||
description,
|
||||
optional,
|
||||
setFieldValue,
|
||||
showNever,
|
||||
onChange,
|
||||
onChangeBool,
|
||||
onChangeNumber,
|
||||
onChangeDate,
|
||||
}) => {
|
||||
const [value, setValue] = useState<boolean | string | number | Date>(
|
||||
currentValue
|
||||
);
|
||||
|
||||
const updateValue = (newValue: string | boolean | number | Date) => {
|
||||
setValue(newValue);
|
||||
setFieldValue(name, newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex text-text-800 flex-col">
|
||||
<div className={`w-full flex gap-x-2 justify-between ${className}`}>
|
||||
<div className="text-sm w-full">
|
||||
{type === "checkbox" ? (
|
||||
<div className="flex items-center">
|
||||
<AdminBooleanFormField
|
||||
checked={currentValue as boolean}
|
||||
subtext={description}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.checked;
|
||||
updateValue(newValue);
|
||||
if (onChangeBool) {
|
||||
onChangeBool(newValue);
|
||||
}
|
||||
}}
|
||||
name={name}
|
||||
label={label}
|
||||
/>
|
||||
</div>
|
||||
) : type === "date" ? (
|
||||
// Date handling
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-700 mb-1">
|
||||
{label}
|
||||
{optional && (
|
||||
<span className="text-text-500 ml-1">(optional)</span>
|
||||
)}
|
||||
</label>
|
||||
{description && <SubLabel>{description}</SubLabel>}
|
||||
|
||||
<input
|
||||
type="date"
|
||||
name={name}
|
||||
value={
|
||||
currentValue instanceof Date
|
||||
? currentValue.toISOString().split("T")[0]
|
||||
: ""
|
||||
}
|
||||
placeholder={currentValue}
|
||||
onChange={(e) => {
|
||||
const dateValue = e.target.value
|
||||
? new Date(e.target.value)
|
||||
: null;
|
||||
if (dateValue) {
|
||||
updateValue(dateValue);
|
||||
}
|
||||
if (onChangeDate) {
|
||||
onChangeDate(dateValue);
|
||||
}
|
||||
}}
|
||||
className="mt-2 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md text-sm shadow-sm placeholder-gray-400
|
||||
focus:outline-none focus:border-sky-500 focus:ring-1 focus:ring-sky-500
|
||||
disabled:bg-gray-50 disabled:text-gray-500 disabled:border-gray-200 disabled:shadow-none
|
||||
invalid:border-pink-500 invalid:text-pink-600
|
||||
focus:invalid:border-pink-500 focus:invalid:ring-pink-500"
|
||||
/>
|
||||
</div>
|
||||
) : type === "number" ? (
|
||||
<>
|
||||
<label className="block text-sm font-medium text-text-700 mb-1">
|
||||
{label}
|
||||
{optional && (
|
||||
<span className="text-text-500 ml-1">(optional)</span>
|
||||
)}
|
||||
</label>
|
||||
{description && <SubLabel>{description}</SubLabel>}
|
||||
|
||||
<input
|
||||
type="number"
|
||||
name={name}
|
||||
value={value as number}
|
||||
placeholder={
|
||||
currentValue === 0 && showNever
|
||||
? "Never"
|
||||
: currentValue?.toString()
|
||||
}
|
||||
onChange={(e) => {
|
||||
const inputValue = e.target.value;
|
||||
if (inputValue === "") {
|
||||
updateValue("");
|
||||
if (onChangeNumber) {
|
||||
onChangeNumber(0);
|
||||
}
|
||||
} else {
|
||||
let value = Math.max(0, parseInt(inputValue));
|
||||
if (!isNaN(value)) {
|
||||
updateValue(value);
|
||||
if (onChangeNumber) {
|
||||
onChangeNumber(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
className={`mt-2 block w-full px-3 py-2
|
||||
bg-white border border-gray-300 rounded-md
|
||||
text-sm shadow-sm placeholder-gray-400
|
||||
focus:outline-none focus:border-sky-500 focus:ring-1 focus:ring-sky-500
|
||||
disabled:bg-gray-50 disabled:text-gray-500 disabled:border-gray-200 disabled:shadow-none
|
||||
invalid:border-pink-500 invalid:text-pink-600
|
||||
focus:invalid:border-pink-500 focus:invalid:ring-pink-500`}
|
||||
/>
|
||||
</>
|
||||
) : type === "select" ? (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-700 mb-1">
|
||||
{label}
|
||||
{optional && (
|
||||
<span className="text-text-500 ml-1">(optional)</span>
|
||||
)}
|
||||
</label>
|
||||
{description && <SubLabel>{description}</SubLabel>}
|
||||
|
||||
<select
|
||||
name={name}
|
||||
value={value as string}
|
||||
onChange={(e) => {
|
||||
updateValue(e.target.value);
|
||||
if (onChange) {
|
||||
onChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
className="mt-2 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md text-sm shadow-sm
|
||||
focus:outline-none focus:border-sky-500 focus:ring-1 focus:ring-sky-500"
|
||||
>
|
||||
<option value="">Select an option</option>
|
||||
{options?.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
) : (
|
||||
// Default
|
||||
<AdminTextField
|
||||
optional={optional}
|
||||
noPadding
|
||||
description={description}
|
||||
onChange={(e) => {
|
||||
updateValue(e.target.value);
|
||||
if (onChange) {
|
||||
onChange(e.target.value!);
|
||||
}
|
||||
}}
|
||||
type={type}
|
||||
name={name}
|
||||
placeholder={currentValue}
|
||||
label={label}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{includRevert && (
|
||||
<div className="flex-none mt-auto">
|
||||
<button
|
||||
className="text-xs h-[35px] my-auto p-1.5 rounded bg-background-900 border-border-dark text-text-300 flex gap-x-1"
|
||||
onClick={(e) => {
|
||||
updateValue("");
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<EditIcon className="text-netural-300 my-auto" />
|
||||
<p className="my-auto">Revert</p>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -201,7 +201,7 @@ export default function CreateCredential({
|
||||
for information on setting up this connector.
|
||||
</p>
|
||||
)}
|
||||
<Card className="!border-0 mt-4">
|
||||
<Card className="!border-0 mt-4 flex flex-col gap-y-6">
|
||||
<TextFormField
|
||||
name="name"
|
||||
placeholder="(Optional) credential name.."
|
||||
|
@ -5,7 +5,6 @@ import { FaNewspaper } from "react-icons/fa";
|
||||
import { TextFormField } from "@/components/admin/connectors/Field";
|
||||
import { Form, Formik, FormikHelpers } from "formik";
|
||||
import { PopupSpec } from "@/components/admin/connectors/Popup";
|
||||
import { EditingValue } from "../EditingValue";
|
||||
import {
|
||||
Credential,
|
||||
getDisplayNameForCredentialKey,
|
||||
@ -63,8 +62,8 @@ const EditCredential = ({
|
||||
<Form>
|
||||
<Card className="mt-4 flex flex-col gap-y-4">
|
||||
<TextFormField
|
||||
includeRevert
|
||||
onChange={(e) => setFieldValue("name", e.target.value)}
|
||||
noPadding
|
||||
name="name"
|
||||
placeholder={credential.name || ""}
|
||||
label="Name (optional):"
|
||||
@ -72,12 +71,12 @@ const EditCredential = ({
|
||||
|
||||
{Object.entries(credential.credential_json).map(
|
||||
([key, value]) => (
|
||||
<EditingValue
|
||||
includRevert
|
||||
<TextFormField
|
||||
includeRevert
|
||||
key={key}
|
||||
setFieldValue={setFieldValue}
|
||||
onChange={(e) => setFieldValue(key, e.target.value)}
|
||||
name={key}
|
||||
currentValue={value}
|
||||
placeholder={value}
|
||||
label={getDisplayNameForCredentialKey(key)}
|
||||
type={
|
||||
key.toLowerCase().includes("token") ||
|
||||
|
@ -211,7 +211,7 @@ export default function ModifyCredential({
|
||||
)}
|
||||
|
||||
<div className="mb-0">
|
||||
<Text className="mb-4 ">
|
||||
<Text className="mb-4">
|
||||
Select a credential as needed! Ensure that you have selected a
|
||||
credential with the proper permissions for this connector!
|
||||
</Text>
|
||||
|
@ -19,7 +19,7 @@ export default function EmbeddingSidebar() {
|
||||
const settingSteps = ["Embedding Model", "Reranking Model", "Advanced"];
|
||||
|
||||
return (
|
||||
<div className="flex bg-background text-default ">
|
||||
<div className="flex bg-background text-default">
|
||||
<div
|
||||
className={`flex-none
|
||||
bg-background-100
|
||||
|
@ -2728,3 +2728,29 @@ export const MinusIcon = ({
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const CameraIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
}: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
style={{ width: `${size}px`, height: `${size}px` }}
|
||||
className={`w-[${size}px] h-[${size}px] ` + className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="200"
|
||||
height="200"
|
||||
viewBox="0 0 14 14"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M13.5 5a1 1 0 0 0-1-1h-2L9 2H5L3.5 4h-2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1z" />
|
||||
<path d="M7 9.75a2.25 2.25 0 1 0 0-4.5a2.25 2.25 0 0 0 0 4.5" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
84
web/src/components/llm/LLMList.tsx
Normal file
84
web/src/components/llm/LLMList.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import React from "react";
|
||||
import { getDisplayNameForModel } from "@/lib/hooks";
|
||||
import { structureValue } from "@/lib/llm/utils";
|
||||
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
|
||||
|
||||
interface LlmListProps {
|
||||
llmProviders: LLMProviderDescriptor[];
|
||||
currentLlm: string;
|
||||
onSelect: (value: string | null) => void;
|
||||
userDefault?: string | null;
|
||||
scrollable?: boolean;
|
||||
}
|
||||
|
||||
export const LlmList: React.FC<LlmListProps> = ({
|
||||
llmProviders,
|
||||
currentLlm,
|
||||
onSelect,
|
||||
userDefault,
|
||||
scrollable,
|
||||
}) => {
|
||||
const llmOptionsByProvider: {
|
||||
[provider: string]: { name: string; value: string }[];
|
||||
} = {};
|
||||
const uniqueModelNames = new Set<string>();
|
||||
|
||||
llmProviders.forEach((llmProvider) => {
|
||||
if (!llmOptionsByProvider[llmProvider.provider]) {
|
||||
llmOptionsByProvider[llmProvider.provider] = [];
|
||||
}
|
||||
|
||||
(llmProvider.display_model_names || llmProvider.model_names).forEach(
|
||||
(modelName) => {
|
||||
if (!uniqueModelNames.has(modelName)) {
|
||||
uniqueModelNames.add(modelName);
|
||||
llmOptionsByProvider[llmProvider.provider].push({
|
||||
name: modelName,
|
||||
value: structureValue(
|
||||
llmProvider.name,
|
||||
llmProvider.provider,
|
||||
modelName
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const llmOptions = Object.entries(llmOptionsByProvider).flatMap(
|
||||
([provider, options]) => [...options]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${scrollable ? "max-h-[200px] include-scrollbar" : "max-h-[300px]"} bg-background-175 flex flex-col gap-y-1 overflow-y-scroll`}
|
||||
>
|
||||
{userDefault && (
|
||||
<button
|
||||
key={-1}
|
||||
className={`w-full py-1.5 px-2 text-sm ${
|
||||
currentLlm == null
|
||||
? "bg-background-200"
|
||||
: "bg-background hover:bg-background-100"
|
||||
} text-left rounded`}
|
||||
onClick={() => onSelect(null)}
|
||||
>
|
||||
User Default (currently {getDisplayNameForModel(userDefault)})
|
||||
</button>
|
||||
)}
|
||||
{llmOptions.map(({ name, value }, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={`w-full py-1.5 px-2 text-sm ${
|
||||
currentLlm == name
|
||||
? "bg-background-200"
|
||||
: "bg-background hover:bg-background-100"
|
||||
} text-left rounded`}
|
||||
onClick={() => onSelect(value)}
|
||||
>
|
||||
{getDisplayNameForModel(name)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -2,12 +2,14 @@ import { FiTrash, FiX } from "react-icons/fi";
|
||||
import { ModalWrapper } from "@/components/modals/ModalWrapper";
|
||||
import { BasicClickable } from "@/components/BasicClickable";
|
||||
|
||||
export const DeleteChatModal = ({
|
||||
chatSessionName,
|
||||
export const DeleteEntityModal = ({
|
||||
onClose,
|
||||
onSubmit,
|
||||
entityType,
|
||||
entityName,
|
||||
}: {
|
||||
chatSessionName: string;
|
||||
entityType: string;
|
||||
entityName: string;
|
||||
onClose: () => void;
|
||||
onSubmit: () => void;
|
||||
}) => {
|
||||
@ -15,11 +17,11 @@ export const DeleteChatModal = ({
|
||||
<ModalWrapper onClose={onClose}>
|
||||
<>
|
||||
<div className="flex mb-4">
|
||||
<h2 className="my-auto text-2xl font-bold">Delete chat?</h2>
|
||||
<h2 className="my-auto text-2xl font-bold">Delete {entityType}?</h2>
|
||||
</div>
|
||||
<p className="mb-4">
|
||||
Click below to confirm that you want to delete{" "}
|
||||
<b>"{chatSessionName.slice(0, 30)}"</b>
|
||||
<b>"{entityName}"</b>
|
||||
</p>
|
||||
<div className="flex">
|
||||
<div className="mx-auto">
|
@ -74,7 +74,7 @@ export default function SearchAnswer({
|
||||
>
|
||||
<div>
|
||||
<div className="flex gap-x-2">
|
||||
<h2 className="text-emphasis font-bold my-auto mb-1 ">AI Answer</h2>
|
||||
<h2 className="text-emphasis font-bold my-auto mb-1">AI Answer</h2>
|
||||
|
||||
{searchState == "generating" && (
|
||||
<div key={"generating"} className="relative inline-block">
|
||||
|
@ -101,7 +101,7 @@ export function SourceSelector({
|
||||
showDocSidebar ? "4xl:block" : "!block"
|
||||
} duration-1000 flex ease-out transition-all transform origin-top-right`}
|
||||
>
|
||||
<div className=" mb-4 pb-2 flex border-b border-border text-emphasis">
|
||||
<div className="mb-4 pb-2 flex border-b border-border text-emphasis">
|
||||
<h2 className="font-bold my-auto">Filters</h2>
|
||||
<FiFilter className="my-auto ml-2" size="16" />
|
||||
</div>
|
||||
@ -114,7 +114,7 @@ export function SourceSelector({
|
||||
{existingSources.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<div className="flex w-full gap-x-2 items-center">
|
||||
<div className="font-bold text-xs mt-2 flex items-center gap-x-2">
|
||||
<div className="font-bold text-xs mt-2 flex items-center gap-x-2">
|
||||
<p>Sources</p>
|
||||
<input
|
||||
type="checkbox"
|
||||
|
@ -82,10 +82,12 @@ function decodeGrid(encoded: number): boolean[][] {
|
||||
export function createSVG(
|
||||
shape: GridShape,
|
||||
color: string = "#FF6FBF",
|
||||
size: number = 48
|
||||
size: number = 48,
|
||||
padding?: boolean
|
||||
) {
|
||||
const cellSize = size / 6;
|
||||
const cellSize = size / 4;
|
||||
const grid = decodeGrid(shape.encodedGrid);
|
||||
|
||||
let path = "";
|
||||
for (let row = 0; row < 4; row++) {
|
||||
for (let col = 0; col < 4; col++) {
|
||||
@ -99,6 +101,7 @@ export function createSVG(
|
||||
|
||||
return (
|
||||
<svg
|
||||
className={`${padding && "p-1.5"} m-auto`}
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
|
@ -24,5 +24,6 @@ export function orderAssistantsForUser(
|
||||
return orderA - orderB;
|
||||
});
|
||||
}
|
||||
|
||||
return assistants;
|
||||
}
|
||||
|
@ -222,9 +222,7 @@ export const connectorConfigs: Record<ValidSources, ConnectionConfiguration> = {
|
||||
|
||||
For example, entering https://danswer.atlassian.net/wiki/spaces/Engineering/overview and clicking the Index button will index the whole Engineering Confluence space, but entering https://danswer.atlassian.net/wiki/spaces/Engineering/pages/164331/example+page will index that page (and optionally the page's children).
|
||||
|
||||
Selecting the "Index Recursively" checkbox will index the single page's children in addition to itself.
|
||||
|
||||
We pull the latest pages and comments from each space every 10 minutes`,
|
||||
Selecting the "Index Recursively" checkbox will index the single page's children in addition to itself.`,
|
||||
values: [
|
||||
{
|
||||
type: "text",
|
||||
|
@ -60,11 +60,12 @@ module.exports = {
|
||||
searchbar: "850px",
|
||||
"document-sidebar": "800px",
|
||||
"document-sidebar-large": "1000px",
|
||||
"searchbar-max": "60px",
|
||||
},
|
||||
maxWidth: {
|
||||
"document-sidebar": "1000px",
|
||||
"message-max": "725px",
|
||||
"searchbar-max": "750px",
|
||||
"message-max": "850px",
|
||||
"searchbar-max": "800px",
|
||||
},
|
||||
colors: {
|
||||
// code styling
|
||||
@ -83,7 +84,8 @@ module.exports = {
|
||||
|
||||
// background
|
||||
"background-search": "#ffffff", // white
|
||||
input: "#f5f5f5",
|
||||
|
||||
input: "#ffffff",
|
||||
|
||||
background: "#fafafa", // 50
|
||||
"background-100": "#f5f5f5", // neutral-100
|
||||
@ -113,7 +115,10 @@ module.exports = {
|
||||
"text-600": "#525252", // dark, neutral-600
|
||||
"text-700": "#404040", // solid, neutral-700
|
||||
"text-800": "#262626", // solidDark, neutral-800
|
||||
"text-900": "#111827", // neutral-900
|
||||
"text-950": "#0a0a0a", // solidDark, neutral-800
|
||||
|
||||
description: "#a3a3a3",
|
||||
subtle: "#6b7280", // gray-500
|
||||
default: "#4b5563", // gray-600
|
||||
emphasis: "#374151", // gray-700
|
||||
@ -130,7 +135,7 @@ module.exports = {
|
||||
accent: "#6366F1", // indigo-500
|
||||
|
||||
// borders
|
||||
border: "#e5e7eb", // gray-200
|
||||
border: "#d1d5db", // gray-200
|
||||
"border-light": "#f3f4f6", // gray-100
|
||||
"border-medium": "#d1d5db", // gray-300
|
||||
"border-strong": "#9ca3af", // gray-400
|
||||
@ -257,6 +262,7 @@ module.exports = {
|
||||
"tremor-metric": ["1.875rem", { lineHeight: "2.25rem" }],
|
||||
},
|
||||
fontWeight: {
|
||||
description: "375",
|
||||
"token-bold": "bold",
|
||||
},
|
||||
fontStyle: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user