Improve Dev Experience (#2347)

* clean interfaces + improve dex experience

* update formatting

* update ports

* ports

* remove some number of unnecessary lines

* remove unnecssary isPublicGroupSelector checks in all spots

* add comment

* update building
This commit is contained in:
pablodanswer
2024-09-10 13:49:04 -07:00
committed by GitHub
parent 3a2a6abed4
commit 9f6e8bd124
24 changed files with 685 additions and 1004 deletions

View File

@@ -1184,20 +1184,16 @@ export function AssistantEditor({
/>
</div>
{isPaidEnterpriseFeaturesEnabled &&
userGroups &&
userGroups.length > 0 && (
<IsPublicGroupSelector
formikProps={{
values,
isSubmitting,
setFieldValue,
...formikProps,
}}
objectName="assistant"
enforceGroupSelection={false}
/>
)}
<IsPublicGroupSelector
formikProps={{
values,
isSubmitting,
setFieldValue,
...formikProps,
}}
objectName="assistant"
enforceGroupSelection={false}
/>
</>
)}

View File

@@ -28,6 +28,7 @@ import { PopupSpec } from "@/components/admin/connectors/Popup";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
import * as Yup from "yup";
import isEqual from "lodash/isEqual";
import { IsPublicGroupSelector } from "@/components/IsPublicGroupSelector";
function customConfigProcessing(customConfigsList: [string, string][]) {
const customConfig: { [key: string]: string } = {};
@@ -209,7 +210,7 @@ export function CustomLLMProviderUpdateForm({
setSubmitting(false);
}}
>
{({ values, setFieldValue }) => {
{(formikProps) => {
return (
<Form className="gap-y-6 mt-8">
<TextFormField
@@ -285,7 +286,7 @@ export function CustomLLMProviderUpdateForm({
name="custom_config_list"
render={(arrayHelpers: ArrayHelpers<any[]>) => (
<div>
{values.custom_config_list.map((_, index) => {
{formikProps.values.custom_config_list.map((_, index) => {
return (
<div
key={index}
@@ -371,7 +372,7 @@ export function CustomLLMProviderUpdateForm({
<TextArrayField
name="model_names"
label="Model Names"
values={values}
values={formikProps.values}
subtext={
<>
List the individual models that you want to make available as
@@ -419,64 +420,12 @@ export function CustomLLMProviderUpdateForm({
/>
{showAdvancedOptions && (
<>
{isPaidEnterpriseFeaturesEnabled && userGroups && (
<>
<BooleanFormField
small
removeIndent
alignTop
name="is_public"
label="Is Public?"
subtext="If set, this LLM Provider will be available to all users. If not, only the specified User Groups will be able to use it."
/>
{userGroups &&
userGroups.length > 0 &&
!values.is_public && (
<div>
<Text>
Select which User Groups should have access to this
LLM Provider.
</Text>
<div className="flex flex-wrap gap-2 mt-2">
{userGroups.map((userGroup) => {
const isSelected = values.groups.includes(
userGroup.id
);
return (
<Bubble
key={userGroup.id}
isSelected={isSelected}
onClick={() => {
if (isSelected) {
setFieldValue(
"groups",
values.groups.filter(
(id) => id !== userGroup.id
)
);
} else {
setFieldValue("groups", [
...values.groups,
userGroup.id,
]);
}
}}
>
<div className="flex">
<GroupsIcon />
<div className="ml-1">{userGroup.name}</div>
</div>
</Bubble>
);
})}
</div>
</div>
)}
</>
)}
</>
<IsPublicGroupSelector
formikProps={formikProps}
objectName="LLM Provider"
publicToWhom="all users"
enforceGroupSelection={true}
/>
)}
<div>

View File

@@ -24,6 +24,7 @@ import { PopupSpec } from "@/components/admin/connectors/Popup";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
import * as Yup from "yup";
import isEqual from "lodash/isEqual";
import { IsPublicGroupSelector } from "@/components/IsPublicGroupSelector";
export function LLMProviderUpdateForm({
llmProviderDescriptor,
@@ -219,7 +220,7 @@ export function LLMProviderUpdateForm({
setSubmitting(false);
}}
>
{({ values, setFieldValue }) => (
{(formikProps) => (
<Form className="gap-y-4 items-stretch mt-6">
{!hideAdvanced && (
<TextFormField
@@ -337,7 +338,9 @@ export function LLMProviderUpdateForm({
{llmProviderDescriptor.llm_names.length > 0 && (
<div className="w-full">
<MultiSelectField
selectedInitially={values.display_model_names}
selectedInitially={
formikProps.values.display_model_names
}
name="display_model_names"
label="Display Models"
subtext="Select the models to make available to users. Unselected models will not be available."
@@ -348,70 +351,21 @@ export function LLMProviderUpdateForm({
})
)}
onChange={(selected) =>
setFieldValue("display_model_names", selected)
formikProps.setFieldValue(
"display_model_names",
selected
)
}
/>
</div>
)}
{isPaidEnterpriseFeaturesEnabled && userGroups && (
<>
<BooleanFormField
small
removeIndent
alignTop
name="is_public"
label="Is Public?"
subtext="If set, this LLM Provider will be available to all users. If not, only the specified User Groups will be able to use it."
/>
{userGroups &&
userGroups.length > 0 &&
!values.is_public && (
<div>
<Text>
Select which User Groups should have access to
this LLM Provider.
</Text>
<div className="flex flex-wrap gap-2 mt-2">
{userGroups.map((userGroup) => {
const isSelected = values.groups.includes(
userGroup.id
);
return (
<Bubble
key={userGroup.id}
isSelected={isSelected}
onClick={() => {
if (isSelected) {
setFieldValue(
"groups",
values.groups.filter(
(id) => id !== userGroup.id
)
);
} else {
setFieldValue("groups", [
...values.groups,
userGroup.id,
]);
}
}}
>
<div className="flex">
<GroupsIcon />
<div className="ml-1">
{userGroup.name}
</div>
</div>
</Bubble>
);
})}
</div>
</div>
)}
</>
)}
<IsPublicGroupSelector
formikProps={formikProps}
objectName="LLM Provider"
publicToWhom="all users"
enforceGroupSelection={true}
/>
</>
)}
</>

View File

@@ -1,19 +1,17 @@
"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, Divider, Title } from "@tremor/react";
import { Card, 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";
import { useFormContext } from "@/components/context/FormContext";
import { getSourceDisplayName } from "@/lib/sources";
import { SourceIcon } from "@/components/SourceIcon";
import { useRef, useState, useEffect } from "react";
import { useState } from "react";
import { submitConnector } from "@/components/admin/connectors/ConnectorForm";
import { deleteCredential, linkCredential } from "@/lib/credential";
import { submitFiles } from "./pages/utils/files";
@@ -27,38 +25,38 @@ import { Credential, credentialTemplates } from "@/lib/connectors/credentials";
import {
ConnectionConfiguration,
connectorConfigs,
createConnectorInitialValues,
createConnectorValidationSchema,
} from "@/lib/connectors/connectors";
import { Modal } from "@/components/Modal";
import { ArrowRight } from "@phosphor-icons/react";
import { ArrowLeft } from "@phosphor-icons/react/dist/ssr";
import { FiPlus } from "react-icons/fi";
import GDriveMain from "./pages/gdrive/GoogleDrivePage";
import { GmailMain } from "./pages/gmail/GmailPage";
import {
useGmailCredentials,
useGoogleDriveCredentials,
} from "./pages/utils/hooks";
import { Formik, FormikProps } from "formik";
import {
IsPublicGroupSelector,
IsPublicGroupSelectorFormType,
} from "@/components/IsPublicGroupSelector";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
import { Formik } from "formik";
import { IsPublicGroupSelector } from "@/components/IsPublicGroupSelector";
import NavigationRow from "./NavigationRow";
export type AdvancedConfigFinal = {
pruneFreq: number | null;
refreshFreq: number | null;
indexingStart: Date | null;
};
export interface AdvancedConfig {
refreshFreq: number;
pruneFreq: number;
indexingStart: string;
}
export default function AddConnector({
connector,
}: {
connector: ConfigurableSources;
}) {
// State for managing credentials and files
const [currentCredential, setCurrentCredential] =
useState<Credential<any> | null>(null);
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const [createConnectorToggle, setCreateConnectorToggle] = useState(false);
// Fetch credentials data
const { data: credentials } = useSWR<Credential<any>[]>(
buildSimilarCredentialInfoURL(connector),
errorHandlingFetcher,
@@ -70,76 +68,27 @@ export default function AddConnector({
errorHandlingFetcher,
{ refreshInterval: 5000 }
);
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
// Get credential template and configuration
const credentialTemplate = credentialTemplates[connector];
const configuration: ConnectionConfiguration = connectorConfigs[connector];
const {
setFormStep,
setAllowAdvanced,
setAlowCreate,
formStep,
nextFormStep,
prevFormStep,
} = useFormContext();
// Form context and popup management
const { setFormStep, 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: true,
...configuration.values.reduce(
(acc, field) => {
if (field.type === "select") {
acc[field.name] = null;
} else 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 isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled();
// Default to 10 minutes unless otherwise specified
const defaultAdvancedSettings = {
refreshFreq: formValues.overrideDefaultFreq || 10,
pruneFreq: 30,
indexingStart: null as string | null,
};
const [advancedSettings, setAdvancedSettings] = useState(
defaultAdvancedSettings
);
const [createConnectorToggle, setCreateConnectorToggle] = useState(false);
const formRef = useRef<FormikProps<any>>(null);
const [isFormValid, setIsFormValid] = useState(false);
const handleFormStatusChange = (isValid: boolean) => {
setIsFormValid(isValid || connector == "file");
};
// Hooks for Google Drive and Gmail credentials
const { liveGDriveCredential } = useGoogleDriveCredentials();
const { liveGmailCredential } = useGmailCredentials();
// Check if credential is activated
const credentialActivated =
(connector === "google_drive" && liveGDriveCredential) ||
(connector === "gmail" && liveGmailCredential) ||
currentCredential;
// Check if there are no credentials
const noCredentials = credentialTemplate == null;
if (noCredentials && 1 != formStep) {
@@ -150,164 +99,20 @@ export default function AddConnector({
setFormStep(Math.min(formStep, 0));
}
const resetAdvancedConfigs = (formikProps: FormikProps<any>) => {
formikProps.resetForm({ values: defaultAdvancedSettings });
setAdvancedSettings(defaultAdvancedSettings);
};
const convertStringToDateTime = (indexingStart: string | null) => {
return indexingStart ? new Date(indexingStart) : null;
};
const createConnector = async () => {
const {
name,
groups,
is_public: isPublic,
...connector_specific_config
} = formValues;
const { pruneFreq, indexingStart, refreshFreq } = advancedSettings;
// Apply transforms from connectors.ts configuration
const transformedConnectorSpecificConfig = Object.entries(
connector_specific_config
).reduce(
(acc, [key, value]) => {
const matchingConfigValue = configuration.values.find(
(configValue) => configValue.name === key
);
if (
matchingConfigValue &&
"transform" in matchingConfigValue &&
matchingConfigValue.transform
) {
acc[key] = matchingConfigValue.transform(value as string[]);
} else {
acc[key] = value;
}
return acc;
},
{} as Record<string, any>
);
const AdvancedConfig: AdvancedConfigFinal = {
pruneFreq: advancedSettings.pruneFreq * 60 * 60 * 24,
indexingStart: convertStringToDateTime(indexingStart),
refreshFreq: advancedSettings.refreshFreq * 60,
};
// google sites-specific handling
if (connector == "google_sites") {
const response = await submitGoogleSite(
selectedFiles,
formValues?.base_url,
setPopup,
AdvancedConfig,
name
);
if (response) {
setTimeout(() => {
window.open("/admin/indexing/status", "_self");
}, 1000);
}
return;
}
// file-specific handling
if (connector == "file" && selectedFiles.length > 0) {
const response = await submitFiles(
selectedFiles,
setPopup,
setSelectedFiles,
name,
AdvancedConfig,
isPublic,
groups
);
if (response) {
setTimeout(() => {
window.open("/admin/indexing/status", "_self");
}, 1000);
}
return;
}
const { message, isSuccess, response } = await submitConnector<any>(
{
connector_specific_config: transformedConnectorSpecificConfig,
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: convertStringToDateTime(indexingStart),
is_public: isPublic,
groups: groups,
},
undefined,
credentialActivated ? false : true,
isPublic
);
// If no credential
if (!credentialActivated) {
if (isSuccess) {
setPopup({
message: "Connector created! Redirecting to connector home page",
type: "success",
});
setTimeout(() => {
window.open("/admin/indexing/status", "_self");
}, 1000);
} else {
setPopup({ message: message, type: "error" });
}
}
// Without credential
if (credentialActivated && isSuccess && response) {
const credential =
currentCredential || liveGDriveCredential || liveGmailCredential;
const linkCredentialResponse = await linkCredential(
response.id,
credential?.id!,
name,
isPublic,
groups
);
if (linkCredentialResponse.ok) {
setPopup({
message: "Connector created! Redirecting to connector home page",
type: "success",
});
setTimeout(() => {
window.open("/admin/indexing/status", "_self");
}, 1000);
} else {
const errorData = await linkCredentialResponse.json();
setPopup({
message: errorData.message,
type: "error",
});
}
} else if (isSuccess) {
setPopup({
message:
"Credential created succsfully! Redirecting to connector home page",
type: "success",
});
} else {
setPopup({ message: message, type: "error" });
}
};
const displayName = getSourceDisplayName(connector) || connector;
if (!credentials || !editableCredentials) {
return <></>;
}
// Credential handler functions
const refresh = () => {
mutate(buildSimilarCredentialInfoURL(connector));
};
const onDeleteCredential = async (credential: Credential<any | null>) => {
const response = await deleteCredential(credential.id, true);
if (response.ok) {
@@ -334,291 +139,256 @@ export default function AddConnector({
refresh();
};
const validationSchema = Yup.object().shape({
name: Yup.string().required("Connector Name is required"),
...configuration.values.reduce(
(acc, field) => {
let schema: any =
field.type === "select"
? Yup.string()
: 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] !== "";
})
);
const onSuccess = () => {
setPopup({
message: "Connector created! Redirecting to connector home page",
type: "success",
});
setTimeout(() => {
window.open("/admin/indexing/status", "_self");
}, 1000);
};
return (
<div className="mx-auto mb-8 w-full">
{popup}
<div className="mb-4">
<HealthCheckBanner />
</div>
<Formik
initialValues={createConnectorInitialValues(connector)}
validationSchema={createConnectorValidationSchema(connector)}
onSubmit={async (values) => {
console.log(" Iam submiing the connector");
const {
name,
groups,
is_public: isPublic,
pruneFreq,
indexingStart,
refreshFreq,
...connector_specific_config
} = values;
<AdminPageTitle
includeDivider={false}
icon={<SourceIcon iconSize={32} sourceType={connector} />}
title={displayName}
/>
// Apply transforms from connectors.ts configuration
const transformedConnectorSpecificConfig = Object.entries(
connector_specific_config
).reduce(
(acc, [key, value]) => {
const matchingConfigValue = configuration.values.find(
(configValue) => configValue.name === key
);
if (
matchingConfigValue &&
"transform" in matchingConfigValue &&
matchingConfigValue.transform
) {
acc[key] = matchingConfigValue.transform(value as string[]);
} else {
acc[key] = value;
}
return acc;
},
{} as Record<string, any>
);
{formStep == 0 &&
(connector == "google_drive" ? (
<>
<Card>
<Title className="mb-2 text-lg">Select a credential</Title>
<GDriveMain />
</Card>
<div className="mt-4 flex w-full justify-end">
<button
className="enabled:cursor-pointer disabled:bg-blue-200 bg-blue-400 flex gap-x-1 items-center text-white py-2.5 px-3.5 text-sm font-regular rounded-sm"
disabled={!credentialActivated}
onClick={() => nextFormStep()}
>
Continue
<ArrowRight />
</button>
// Apply advanced configuration-specific transforms.
const advancedConfiguration: any = {
pruneFreq: pruneFreq * 60 * 60 * 24,
indexingStart: convertStringToDateTime(indexingStart),
refreshFreq: refreshFreq * 60,
};
// Google sites-specific handling
if (connector == "google_sites") {
const response = await submitGoogleSite(
selectedFiles,
values?.base_url,
setPopup,
advancedConfiguration.refreshFreq,
advancedConfiguration.pruneFreq,
advancedConfiguration.indexingStart,
name
);
if (response) {
onSuccess();
}
return;
}
// File-specific handling
if (connector == "file" && selectedFiles.length > 0) {
const response = await submitFiles(
selectedFiles,
setPopup,
setSelectedFiles,
name,
isPublic,
groups
);
if (response) {
onSuccess();
}
return;
}
const { message, isSuccess, response } = await submitConnector<any>(
{
connector_specific_config: transformedConnectorSpecificConfig,
input_type: connector == "web" ? "load_state" : "poll", // single case
name: name,
source: connector,
refresh_freq: advancedConfiguration.refreshFreq || null,
prune_freq: advancedConfiguration.pruneFreq || null,
indexing_start: advancedConfiguration.indexingStart || null,
is_public: isPublic,
groups: groups,
},
undefined,
credentialActivated ? false : true,
isPublic
);
// If no credential
if (!credentialActivated) {
if (isSuccess) {
onSuccess();
} else {
setPopup({ message: message, type: "error" });
}
}
// Without credential
if (credentialActivated && isSuccess && response) {
const credential =
currentCredential || liveGDriveCredential || liveGmailCredential;
const linkCredentialResponse = await linkCredential(
response.id,
credential?.id!,
name,
isPublic,
groups
);
if (linkCredentialResponse.ok) {
onSuccess();
} else {
const errorData = await linkCredentialResponse.json();
setPopup({
message: errorData.message,
type: "error",
});
}
} else if (isSuccess) {
onSuccess();
} else {
setPopup({ message: message, type: "error" });
}
return;
}}
>
{(formikProps) => {
return (
<div className="mx-auto mb-8 w-full">
{popup}
<div className="mb-4">
<HealthCheckBanner />
</div>
</>
) : connector == "gmail" ? (
<>
<Card>
<Title className="mb-2 text-lg">Select a credential</Title>
<GmailMain />
</Card>
<div className="mt-4 flex w-full justify-end">
<button
className="enabled:cursor-pointer disabled:bg-blue-200 bg-blue-400 flex gap-x-1 items-center text-white py-2.5 px-3.5 text-sm font-regular rounded-sm"
disabled={!credentialActivated}
onClick={() => nextFormStep()}
>
Continue
<ArrowRight />
</button>
</div>
</>
) : (
<>
<Card>
<Title className="mb-2 text-lg">Select a credential</Title>
<ModifyCredential
showIfEmpty
source={connector}
defaultedCredential={currentCredential!}
credentials={credentials}
editableCredentials={editableCredentials}
onDeleteCredential={onDeleteCredential}
onSwitch={onSwap}
/>
{!createConnectorToggle && (
<button
className="mt-6 text-sm bg-background-900 px-2 py-1.5 flex text-text-200 flex-none rounded"
onClick={() =>
setCreateConnectorToggle(
(createConnectorToggle) => !createConnectorToggle
)
}
>
Create New
</button>
)}
{/* NOTE: connector will never be google_drive, since the ternary above will
prevent that, but still keeping this here for safety in case the above changes. */}
{(connector as ValidSources) !== "google_drive" &&
createConnectorToggle && (
<Modal
className="max-w-3xl rounded-lg"
onOutsideClick={() => setCreateConnectorToggle(false)}
>
<>
<Title className="mb-2 text-lg">
Create a {getSourceDisplayName(connector)} credential
</Title>
<CreateCredential
close
refresh={refresh}
sourceType={connector}
setPopup={setPopup}
onSwitch={onSwap}
onClose={() => setCreateConnectorToggle(false)}
/>
</>
</Modal>
)}
</Card>
<div className="mt-4 flex w-full justify-end">
<button
className="enabled:cursor-pointer disabled:cursor-not-allowed disabled:bg-blue-200 bg-blue-400 flex gap-x-1 items-center text-white py-2.5 px-3.5 text-sm font-regular rounded-sm"
disabled={currentCredential == null}
onClick={() => nextFormStep()}
>
Continue
<ArrowRight />
</button>
</div>
</>
))}
<AdminPageTitle
includeDivider={false}
icon={<SourceIcon iconSize={32} sourceType={connector} />}
title={displayName}
/>
{formStep == 1 && (
<>
<Card>
<Formik
initialValues={formValues}
validationSchema={validationSchema}
onSubmit={() => {
// Can be utilized for logging purposes
}}
>
{(formikProps) => {
setFormValues(formikProps.values);
handleFormStatusChange(
formikProps.isValid && isFormSubmittable(formikProps.values)
);
setAllowAdvanced(
formikProps.isValid && isFormSubmittable(formikProps.values)
);
{formStep == 0 && (
<Card>
<Title className="mb-2 text-lg">Select a credential</Title>
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 && (
<>
<IsPublicGroupSelector
removeIndent
formikProps={formikProps}
objectName="Connector"
/>
</>
)}
</div>
);
}}
</Formik>
</Card>
<div className={`mt-4 w-full grid grid-cols-3`}>
{!noCredentials ? (
<button
className="border-border-dark mr-auto border flex gap-x-1 items-center text-text p-2.5 text-sm font-regular rounded-sm "
onClick={() => prevFormStep()}
>
<ArrowLeft />
Previous
</button>
) : (
<div />
)}
<button
className="enabled:cursor-pointer ml-auto disabled:bg-accent/50 disabled:cursor-not-allowed bg-accent flex mx-auto gap-x-1 items-center text-white py-2.5 px-3.5 text-sm font-regular rounded-sm"
disabled={
!isFormValid ||
(connector == "file" && selectedFiles.length == 0)
}
onClick={async () => {
await createConnector();
}}
>
Create Connector
<FiPlus className="text-white h-4 w-4" />
</button>
{!(connector == "file") && (
<div className="flex w-full justify-end">
<button
className={`enabled:cursor-pointer enabled:hover:underline disabled:cursor-not-allowed mt-auto enabled:text-text-600 disabled:text-text-400 ml-auto flex gap-x-1 items-center py-2.5 px-3.5 text-sm font-regular rounded-sm`}
disabled={!isFormValid}
onClick={() => {
nextFormStep();
}}
>
Advanced
<ArrowRight />
</button>
</div>
)}
</div>
</>
)}
{formStep === 2 && (
<>
<Card>
<Formik
initialValues={advancedSettings}
validationSchema={advancedValidationSchema}
onSubmit={() => {}}
>
{(formikProps) => {
setAdvancedSettings(formikProps.values);
return (
{connector == "google_drive" ? (
<GDriveMain />
) : connector == "gmail" ? (
<GmailMain />
) : (
<>
<AdvancedFormPage formikProps={formikProps} ref={formRef} />
<div className="mt-4 flex w-full mx-auto max-w-2xl justify-start">
<ModifyCredential
showIfEmpty
source={connector}
defaultedCredential={currentCredential!}
credentials={credentials}
editableCredentials={editableCredentials}
onDeleteCredential={onDeleteCredential}
onSwitch={onSwap}
/>
{!createConnectorToggle && (
<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)}
className="mt-6 text-sm bg-background-900 px-2 py-1.5 flex text-text-200 flex-none rounded"
onClick={() =>
setCreateConnectorToggle(
(createConnectorToggle) => !createConnectorToggle
)
}
>
<TrashIcon size={20} className="text-white" />
<div className="w-full items-center gap-x-2 flex">
Reset
</div>
Create New
</button>
</div>
)}
{/* NOTE: connector will never be google_drive, since the ternary above will
prevent that, but still keeping this here for safety in case the above changes. */}
{(connector as ValidSources) !== "google_drive" &&
createConnectorToggle && (
<Modal
className="max-w-3xl rounded-lg"
onOutsideClick={() => setCreateConnectorToggle(false)}
>
<>
<Title className="mb-2 text-lg">
Create a {getSourceDisplayName(connector)}{" "}
credential
</Title>
<CreateCredential
close
refresh={refresh}
sourceType={connector}
setPopup={setPopup}
onSwitch={onSwap}
onClose={() => setCreateConnectorToggle(false)}
/>
</>
</Modal>
)}
</>
);
}}
</Formik>
</Card>
<div className={`mt-4 grid grid-cols-3 w-full `}>
<button
className="border-border-dark border mr-auto flex gap-x-1 items-center text-text py-2.5 px-3.5 text-sm font-regular rounded-sm"
onClick={() => prevFormStep()}
>
<ArrowLeft />
Previous
</button>
<button
className="enabled:cursor-pointer ml-auto disabled:bg-accent/50 bg-accent flex mx-auto gap-x-1 items-center text-white py-2.5 px-3.5 text-sm font-regular rounded-sm"
onClick={async () => {
await createConnector();
}}
>
Create Connector
<FiPlus className="text-white h-4 w-4" />
</button>
)}
</Card>
)}
{formStep == 1 && (
<Card className="w-full py-8 flex gap-y-6 flex-col max-w-3xl px-12 mx-auto">
<DynamicConnectionForm
values={formikProps.values}
config={configuration}
setSelectedFiles={setSelectedFiles}
selectedFiles={selectedFiles}
/>
<IsPublicGroupSelector
removeIndent
formikProps={formikProps}
objectName="Connector"
/>
</Card>
)}
{formStep === 2 && (
<Card>
<AdvancedFormPage />
</Card>
)}
<NavigationRow
activatedCredential={credentialActivated != null}
isValid={formikProps.isValid}
onSubmit={formikProps.handleSubmit}
noCredentials={noCredentials}
noAdvanced={connector !== "file"}
/>
</div>
</>
)}
</div>
);
}}
</Formik>
);
}

View File

@@ -0,0 +1,91 @@
import { useFormContext } from "@/components/context/FormContext";
import { ArrowLeft, ArrowRight } from "@phosphor-icons/react";
import { FiPlus } from "react-icons/fi";
const NavigationRow = ({
noAdvanced,
noCredentials,
activatedCredential,
onSubmit,
isValid,
}: {
isValid: boolean;
onSubmit: () => void;
noAdvanced: boolean;
noCredentials: boolean;
activatedCredential: boolean;
}) => {
const { formStep, prevFormStep, nextFormStep } = useFormContext();
const SquareNavigationButton = ({
onClick,
disabled,
className,
children,
}: {
onClick: () => void;
disabled?: boolean;
className: string;
children: React.ReactNode;
}) => (
<button
className={`flex items-center gap-1 text-sm rounded-sm ${className}`}
onMouseDown={() => !disabled && onClick()}
disabled={disabled}
>
{children}
</button>
);
return (
<div className="mt-4 w-full grid grid-cols-3">
<div>
{formStep > 0 && !noCredentials && (
<SquareNavigationButton
className="border border-text-400 mr-auto p-2.5"
onClick={prevFormStep}
>
<ArrowLeft />
Previous
</SquareNavigationButton>
)}
</div>
<div className="flex justify-center">
{(formStep > 0 || noCredentials) && (
<SquareNavigationButton
className="bg-accent text-white py-2.5 px-3.5 disabled:opacity-50"
disabled={!isValid}
onClick={onSubmit}
>
Create Connector
<FiPlus className="h-4 w-4" />
</SquareNavigationButton>
)}
</div>
<div className="flex justify-end">
{formStep === 0 && (
<SquareNavigationButton
className="bg-blue-400 text-white py-2.5 px-3.5 disabled:bg-blue-200"
disabled={!activatedCredential}
onClick={nextFormStep}
>
Continue
<ArrowRight />
</SquareNavigationButton>
)}
{noAdvanced && formStep === 1 && (
<SquareNavigationButton
className="text-text-600 disabled:text-text-400 py-2.5 px-3.5"
disabled={!isValid}
onClick={nextFormStep}
>
Advanced
<ArrowRight />
</SquareNavigationButton>
)}
</div>
</div>
);
};
export default NavigationRow;

View File

@@ -1,66 +1,47 @@
import React, { Dispatch, forwardRef, SetStateAction } from "react";
import { Formik, Form, FormikProps } from "formik";
import * as Yup from "yup";
import React from "react";
import NumberInput from "./ConnectorInput/NumberInput";
import { TextFormField } from "@/components/admin/connectors/Field";
import { TrashIcon } from "@/components/icons/icons";
interface AdvancedFormPageProps {
formikProps: FormikProps<{
indexingStart: string | null;
pruneFreq: number;
refreshFreq: number;
}>;
}
const AdvancedFormPage = () => {
return (
<div className="py-4 flex flex-col gap-y-6 rounded-lg max-w-2xl mx-auto">
<h2 className="text-2xl font-bold mb-4 text-text-800">
Advanced Configuration
</h2>
const AdvancedFormPage = forwardRef<FormikProps<any>, AdvancedFormPageProps>(
({ formikProps }, ref) => {
const { indexingStart, refreshFreq, pruneFreq } = formikProps.values;
<NumberInput
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.
`}
label="Prune Frequency (days)"
name="pruneFreq"
/>
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>
<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."
label="Refresh Frequency (minutes)"
name="refreshFreq"
/>
<Form>
<div key="prune_freq">
<NumberInput
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}
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>
<TextFormField
type="date"
subtext="Documents prior to this date will not be pulled in"
optional
label="Indexing Start Date"
name="indexingStart"
/>
<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 ">
<TrashIcon size={20} className="text-white" />
<div className="w-full items-center gap-x-2 flex">Reset</div>
</button>
</div>
);
}
);
</div>
);
};
AdvancedFormPage.displayName = "AdvancedFormPage";
export default AdvancedFormPage;

View File

@@ -3,23 +3,17 @@ import { Field, useFormikContext } from "formik";
export default function NumberInput({
label,
value,
optional,
description,
name,
showNeverIfZero,
onChange,
}: {
value?: number;
label: string;
name: string;
optional?: boolean;
description?: string;
showNeverIfZero?: boolean;
onChange?: (value: number) => void;
}) {
const { setFieldValue } = useFormikContext();
return (
<div className="w-full flex flex-col">
<label className="block text-base font-medium text-text-700 mb-1">
@@ -32,15 +26,6 @@ export default function NumberInput({
type="number"
name={name}
min="-1"
value={value === 0 && showNeverIfZero ? "Never" : value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const newValue =
e.target.value === "Never" ? 0 : Number(e.target.value);
setFieldValue(name, newValue);
if (onChange) {
onChange(newValue);
}
}}
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

View File

@@ -1,37 +1,42 @@
import CredentialSubText from "@/components/credentials/CredentialFields";
import { ListOption, SelectOption } from "@/lib/connectors/connectors";
import {
ListOption,
SelectOption,
StringWithDescription,
} from "@/lib/connectors/connectors";
import { Field } from "formik";
export default function SelectInput({
field,
value,
name,
optional,
description,
options,
label,
}: {
field: SelectOption;
value: any;
name: string;
optional?: boolean;
description?: string;
options: StringWithDescription[];
label?: string;
}) {
return (
<>
<label
htmlFor={field.name}
htmlFor={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}
{optional && <span className="text-text-500 ml-1">(optional)</span>}
</label>
{field.description && (
<CredentialSubText>{field.description}</CredentialSubText>
)}
{description && <CredentialSubText>{description}</CredentialSubText>}
<Field
as="select"
value={value}
name={field.name}
name={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) => (
{options?.map((option: any) => (
<option key={option.name} value={option.name}>
{option.name}
</option>

View File

@@ -1,28 +1,9 @@
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 React, { Dispatch, FC, SetStateAction } from "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";
@@ -63,6 +44,7 @@ const DynamicConnectionForm: FC<DynamicConnectionFormProps> = ({
<div key={field.name}>
{field.type == "file" ? (
<FileUpload
name={field.name}
selectedFiles={selectedFiles}
setSelectedFiles={setSelectedFiles}
/>
@@ -78,11 +60,16 @@ const DynamicConnectionForm: FC<DynamicConnectionFormProps> = ({
) : field.type === "list" ? (
<ListInput field={field} />
) : field.type === "select" ? (
<SelectInput field={field} value={values[field.name]} />
<SelectInput
name={field.name}
optional={field.optional}
description={field.description}
options={field.options || []}
label={field.label}
/>
) : field.type === "number" ? (
<NumberInput
label={field.label}
value={values[field.name]}
optional={field.optional}
description={field.description}
name={field.name}

View File

@@ -2,14 +2,12 @@ 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 { AdvancedConfigFinal } from "../../AddConnectorPage";
export const submitFiles = async (
selectedFiles: File[],
setPopup: (popup: PopupSpec) => void,
setSelectedFiles: (files: File[]) => void,
name: string,
advancedConfig: AdvancedConfigFinal,
isPublic: boolean,
groups?: number[]
) => {

View File

@@ -2,13 +2,14 @@ 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 { AdvancedConfigFinal } from "../../AddConnectorPage";
export const submitGoogleSite = async (
selectedFiles: File[],
base_url: any,
setPopup: (popup: PopupSpec) => void,
advancedConfig: AdvancedConfigFinal,
refreshFreq: number,
pruneFreq: number,
indexingStart: Date,
name?: string
) => {
const uploadCreateAndTriggerConnector = async () => {
@@ -41,9 +42,9 @@ export const submitGoogleSite = async (
base_url: base_url,
zip_path: filePaths[0],
},
refresh_freq: advancedConfig.refreshFreq,
prune_freq: advancedConfig.pruneFreq,
indexing_start: advancedConfig.indexingStart,
refresh_freq: refreshFreq,
prune_freq: pruneFreq,
indexing_start: indexingStart,
});
if (connectorErrorMsg || !connector) {
setPopup({

View File

@@ -66,14 +66,7 @@ const RerankingDetailsForm = forwardRef<
>
{({ values, setFieldValue, resetForm }) => {
const resetRerankingValues = () => {
setRerankingDetails({
...values,
rerank_provider_type: null!,
rerank_model_name: null,
});
setFieldValue("rerank_provider_type", null);
setFieldValue("rerank_model_name", null);
setFieldValue("rerank_api_key", null);
resetForm();
};
return (
@@ -346,7 +339,7 @@ const RerankingDetailsForm = forwardRef<
}}
type="password"
label="Cohere API Key"
name="api_key"
name="rerank_api_key"
/>
<div className="flex w-full justify-end mt-4">
<Button

View File

@@ -1,18 +1,19 @@
import { EmbeddingProvider } from "@/components/embedding/interfaces";
import { NonNullChain } from "typescript";
// This is a slightly differnte interface than used in the backend
// but is always used in conjunction with `AdvancedSearchConfiguration`
export interface RerankingDetails {
rerank_model_name: string | null;
rerank_provider_type: RerankerProvider | null;
rerank_api_key: string | null;
rerank_api_url: string | null;
num_rerank: number;
}
export enum RerankerProvider {
COHERE = "cohere",
LITELLM = "litellm",
}
export interface AdvancedSearchConfiguration {
model_name: string;
model_dim: number;
@@ -24,19 +25,12 @@ export interface AdvancedSearchConfiguration {
multilingual_expansion: string[];
disable_rerank_for_streaming: boolean;
api_url: string | null;
num_rerank: number;
}
export interface SavedSearchSettings extends RerankingDetails {
model_name: string;
model_dim: number;
normalize: boolean;
query_prefix: string;
passage_prefix: string;
index_name: string | null;
multipass_indexing: boolean;
multilingual_expansion: string[];
disable_rerank_for_streaming: boolean;
api_url: string | null;
export interface SavedSearchSettings
extends RerankingDetails,
AdvancedSearchConfiguration {
provider_type: EmbeddingProvider | null;
}

View File

@@ -158,14 +158,7 @@ export function ProviderCreationModal({
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({
values,
errors,
touched,
isSubmitting,
handleSubmit,
setFieldValue,
}) => (
{({ isSubmitting, handleSubmit, setFieldValue }) => (
<Form onSubmit={handleSubmit} className="space-y-4">
<Text className="text-lg mb-2">
You are setting the credentials for this provider. To access

View File

@@ -14,169 +14,102 @@ interface AdvancedEmbeddingFormPageProps {
value: any
) => void;
advancedEmbeddingDetails: AdvancedSearchConfiguration;
numRerank: number;
updateNumRerank: (value: number) => void;
}
const AdvancedEmbeddingFormPage = forwardRef<
FormikProps<any>,
AdvancedEmbeddingFormPageProps
>(
(
{
updateAdvancedEmbeddingDetails,
advancedEmbeddingDetails,
numRerank,
updateNumRerank,
},
ref
) => {
return (
<div className="py-4 rounded-lg max-w-4xl px-4 mx-auto">
<h2 className="text-2xl font-bold mb-4 text-text-800">
Advanced Configuration
</h2>
<Formik
innerRef={ref}
initialValues={{
multilingual_expansion:
advancedEmbeddingDetails.multilingual_expansion,
multipass_indexing: advancedEmbeddingDetails.multipass_indexing,
disable_rerank_for_streaming:
advancedEmbeddingDetails.disable_rerank_for_streaming,
num_rerank: numRerank,
}}
validationSchema={Yup.object().shape({
multilingual_expansion: Yup.array().of(Yup.string()),
multipass_indexing: Yup.boolean(),
disable_rerank_for_streaming: Yup.boolean(),
})}
onSubmit={async (_, { setSubmitting }) => {
setSubmitting(false);
}}
enableReinitialize={true}
>
{({ values, setFieldValue }) => (
<Form>
<FieldArray name="multilingual_expansion">
{({ push, remove }) => (
<div>
<label
htmlFor="multilingual_expansion"
className="block text-sm font-medium text-text-700 mb-1"
>
Multilingual Expansion
<span className="text-text-500 ml-1">(optional)</span>
</label>
<CredentialSubText>
List of languages for multilingual expansion. Leave empty
for no additional expansion.
</CredentialSubText>
{values.multilingual_expansion.map(
(_: any, index: number) => (
<div key={index} className="w-full flex mb-4">
<Field
name={`multilingual_expansion.${index}`}
className={`w-full bg-input text-sm p-2 border border-border-medium rounded-md
>(({ updateAdvancedEmbeddingDetails, advancedEmbeddingDetails }, ref) => {
return (
<div className="py-4 rounded-lg max-w-4xl px-4 mx-auto">
<h2 className="text-2xl font-bold mb-4 text-text-800">
Advanced Configuration
</h2>
<Formik
innerRef={ref}
initialValues={advancedEmbeddingDetails}
validationSchema={Yup.object().shape({
multilingual_expansion: Yup.array().of(Yup.string()),
multipass_indexing: Yup.boolean(),
disable_rerank_for_streaming: Yup.boolean(),
num_rerank: Yup.number(),
})}
onSubmit={async (_, { setSubmitting }) => {
setSubmitting(false);
}}
validate={(values) => {
// Call updateAdvancedEmbeddingDetails for each changed field
Object.entries(values).forEach(([key, value]) => {
updateAdvancedEmbeddingDetails(
key as keyof AdvancedSearchConfiguration,
value
);
});
}}
enableReinitialize={true}
>
{({ values }) => (
<Form>
<FieldArray name="multilingual_expansion">
{({ push, remove }) => (
<div className="w-full">
{values.multilingual_expansion.map(
(_: any, index: number) => (
<div key={index} className="w-full flex mb-4">
<Field
name={`multilingual_expansion.${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.multilingual_expansion,
];
newValue[index] = e.target.value;
setFieldValue("multilingual_expansion", newValue);
updateAdvancedEmbeddingDetails(
"multilingual_expansion",
newValue
);
}}
value={values.multilingual_expansion[index]}
/>
<button
type="button"
onClick={() => {
remove(index);
const newValue =
values.multilingual_expansion.filter(
(_: any, i: number) => i !== index
);
setFieldValue("multilingual_expansion", newValue);
updateAdvancedEmbeddingDetails(
"multilingual_expansion",
newValue
);
}}
className={`p-2 my-auto bg-input flex-none rounded-md
/>
<button
type="button"
onClick={() => remove(index)}
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 flex items-center
>
<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 flex items-center
hover:bg-rose-600 focus:outline-none focus:ring-2 focus:ring-rose-500 focus:ring-opacity-50`}
>
<FaPlus className="mr-2" />
Add Language
</button>
</div>
)}
</FieldArray>
>
<FaPlus className="mr-2" />
Add Language
</button>
</div>
)}
</FieldArray>
<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
onChange={(value: number) => {
updateNumRerank(value);
setFieldValue("num_rerank", value);
}}
description="Number of results to rerank"
optional={false}
value={values.num_rerank}
label="Number of Results to Rerank"
name="num_rerank"
/>
</Form>
)}
</Formik>
</div>
);
}
);
<BooleanFormField
subtext="Enable multipass indexing for both mini and large chunks."
optional
label="Multipass Indexing"
name="multipass_indexing"
/>
<BooleanFormField
subtext="Disable reranking for streaming to improve response time."
optional
label="Disable Rerank for Streaming"
name="disable_rerank_for_streaming"
/>
<NumberInput
description="Number of results to rerank"
optional={false}
label="Number of Results to Rerank"
name="num_rerank"
/>
</Form>
)}
</Formik>
</div>
);
});
export default AdvancedEmbeddingFormPage;
AdvancedEmbeddingFormPage.displayName = "AdvancedEmbeddingFormPage";
export default AdvancedEmbeddingFormPage;

View File

@@ -41,11 +41,11 @@ export default function EmbeddingForm() {
multilingual_expansion: [],
disable_rerank_for_streaming: false,
api_url: null,
num_rerank: 0,
});
const [rerankingDetails, setRerankingDetails] = useState<RerankingDetails>({
rerank_api_key: "",
num_rerank: 0,
rerank_provider_type: null,
rerank_model_name: "",
rerank_api_url: null,
@@ -117,11 +117,12 @@ export default function EmbeddingForm() {
multilingual_expansion: searchSettings.multilingual_expansion,
disable_rerank_for_streaming:
searchSettings.disable_rerank_for_streaming,
num_rerank: searchSettings.num_rerank,
api_url: null,
});
setRerankingDetails({
rerank_api_key: searchSettings.rerank_api_key,
num_rerank: searchSettings.num_rerank,
rerank_provider_type: searchSettings.rerank_provider_type,
rerank_model_name: searchSettings.rerank_model_name,
rerank_api_url: searchSettings.rerank_api_url,
@@ -132,14 +133,12 @@ export default function EmbeddingForm() {
const originalRerankingDetails: RerankingDetails = searchSettings
? {
rerank_api_key: searchSettings.rerank_api_key,
num_rerank: searchSettings.num_rerank,
rerank_provider_type: searchSettings.rerank_provider_type,
rerank_model_name: searchSettings.rerank_model_name,
rerank_api_url: searchSettings.rerank_api_url,
}
: {
rerank_api_key: "",
num_rerank: 0,
rerank_provider_type: null,
rerank_model_name: "",
rerank_api_url: null,
@@ -422,13 +421,6 @@ export default function EmbeddingForm() {
<>
<Card>
<AdvancedEmbeddingFormPage
updateNumRerank={(newNumRerank: number) =>
setRerankingDetails({
...rerankingDetails,
num_rerank: newNumRerank,
})
}
numRerank={rerankingDetails.num_rerank}
advancedEmbeddingDetails={advancedEmbeddingDetails}
updateAdvancedEmbeddingDetails={updateAdvancedEmbeddingDetails}
/>

View File

@@ -29,7 +29,7 @@ export const UserGroupCreationForm = ({
const isUpdate = existingUserGroup !== undefined;
return (
<Modal onOutsideClick={onClose}>
<Modal className="w-fit" onOutsideClick={onClose}>
<div className="px-8 py-6 bg-background">
<h2 className="text-xl font-bold flex">
{isUpdate ? "Update a User Group" : "Create a new User Group"}

View File

@@ -1,3 +1,4 @@
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
import React, { useState, useEffect } from "react";
import { FormikProps, FieldArray, ArrayHelpers, ErrorMessage } from "formik";
import { Text, Divider } from "@tremor/react";
@@ -12,6 +13,8 @@ export type IsPublicGroupSelectorFormType = {
groups: number[];
};
// This should be included for all forms that require groups / public access
// to be set, and access to this / permissioning should be handled within this component itself.
export const IsPublicGroupSelector = <T extends IsPublicGroupSelectorFormType>({
formikProps,
objectName,
@@ -27,10 +30,11 @@ export const IsPublicGroupSelector = <T extends IsPublicGroupSelectorFormType>({
}) => {
const { data: userGroups, isLoading: userGroupsIsLoading } = useUserGroups();
const { isAdmin, user, isLoadingUser, isCurator } = useUser();
const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled();
const [shouldHideContent, setShouldHideContent] = useState(false);
useEffect(() => {
if (user && userGroups) {
if (user && userGroups && isPaidEnterpriseFeaturesEnabled) {
const isUserAdmin = user.role === UserRole.ADMIN;
if (!isUserAdmin) {
formikProps.setFieldValue("is_public", false);
@@ -55,6 +59,9 @@ export const IsPublicGroupSelector = <T extends IsPublicGroupSelectorFormType>({
if (isLoadingUser || userGroupsIsLoading) {
return <div>Loading...</div>;
}
if (!isPaidEnterpriseFeaturesEnabled) {
return null;
}
if (shouldHideContent && enforceGroupSelection) {
return (

View File

@@ -104,7 +104,6 @@ export function TextFormField({
subtext,
placeholder,
value,
onChange,
type = "text",
optional,
includeRevert,
@@ -121,6 +120,8 @@ export function TextFormField({
explanationLink,
small,
removeLabel,
min,
onChange,
}: {
value?: string;
name: string;
@@ -128,7 +129,6 @@ export function TextFormField({
label: string;
subtext?: string | JSX.Element;
placeholder?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
includeRevert?: boolean;
optional?: boolean;
type?: string;
@@ -144,11 +144,14 @@ export function TextFormField({
explanationText?: string;
explanationLink?: string;
small?: boolean;
min?: number;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}) {
let heightString = defaultHeight || "";
if (isTextArea && !heightString) {
heightString = "h-28";
}
const [field, , helpers] = useField(name);
const { setValue } = helpers;
@@ -186,6 +189,8 @@ export function TextFormField({
{subtext && <SubLabel>{subtext}</SubLabel>}
<div className={`w-full flex ${includeRevert && "gap-x-2"}`}>
<Field
onChange={handleChange}
min={min}
as={isTextArea ? "textarea" : "input"}
type={type}
defaultValue={value}
@@ -211,26 +216,7 @@ export function TextFormField({
disabled={disabled}
placeholder={placeholder}
autoComplete={autoCompleteDisabled ? "off" : undefined}
onChange={handleChange}
/>
{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 && (

View File

@@ -1,3 +1,4 @@
import { useFormikContext } from "formik";
import { FC, useState } from "react";
import React from "react";
import Dropzone from "react-dropzone";
@@ -6,14 +7,17 @@ interface FileUploadProps {
selectedFiles: File[];
setSelectedFiles: (files: File[]) => void;
message?: string;
name?: string;
}
export const FileUpload: FC<FileUploadProps> = ({
name,
selectedFiles,
setSelectedFiles,
message,
}) => {
const [dragActive, setDragActive] = useState(false);
const { setFieldValue } = useFormikContext();
return (
<div>
@@ -21,6 +25,9 @@ export const FileUpload: FC<FileUploadProps> = ({
onDrop={(acceptedFiles) => {
setSelectedFiles(acceptedFiles);
setDragActive(false);
if (name) {
setFieldValue(name, acceptedFiles);
}
}}
onDragLeave={() => setDragActive(false)}
onDragEnter={() => setDragActive(true)}

View File

@@ -1,10 +1,10 @@
import React, { useState, useEffect } from "react";
import React, { useState } from "react";
import { Button, Card } from "@tremor/react";
import { ValidSources } from "@/lib/types";
import { FaAccusoft } from "react-icons/fa";
import { submitCredential } from "@/components/admin/connectors/CredentialForm";
import { TextFormField } from "@/components/admin/connectors/Field";
import { Form, Formik, FormikHelpers, FormikProps } from "formik";
import { Form, Formik, FormikHelpers } from "formik";
import { PopupSpec } from "@/components/admin/connectors/Popup";
import { getSourceDocLink } from "@/lib/sources";
import GDriveMain from "@/app/admin/connectors/[connector]/pages/gdrive/GoogleDrivePage";
@@ -178,7 +178,7 @@ export default function CreateCredential({
initialValues={
{
name: "",
is_public: isAdmin,
is_public: isAdmin || !isPaidEnterpriseFeaturesEnabled,
groups: [],
} as formType
}
@@ -232,7 +232,7 @@ export default function CreateCredential({
setShowAdvancedOptions={setShowAdvancedOptions}
/>
)}
{(showAdvancedOptions || !isAdmin) && (
{showAdvancedOptions && (
<IsPublicGroupSelector
formikProps={formikProps}
objectName="credential"

View File

@@ -1,7 +1,7 @@
import React from "react";
import { Button, Text, Card } from "@tremor/react";
import { FaNewspaper } from "react-icons/fa";
import { FaNewspaper, FaPaperPlane, FaTractor, FaTrash } from "react-icons/fa";
import { TextFormField } from "@/components/admin/connectors/Field";
import { Form, Formik, FormikHelpers } from "formik";
import { PopupSpec } from "@/components/admin/connectors/Popup";
@@ -48,7 +48,7 @@ const EditCredential = ({
};
return (
<div className="mb-4">
<div className="flex flex-col gap-y-6">
<Text>
Ensure that you update to a credential with the proper permissions!
</Text>
@@ -58,37 +58,37 @@ const EditCredential = ({
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({ isSubmitting, setFieldValue }) => (
{({ isSubmitting, resetForm }) => (
<Form>
<Card className="mt-4 flex flex-col gap-y-4">
<TextFormField
includeRevert
name="name"
placeholder={credential.name || ""}
label="Name (optional):"
/>
{Object.entries(credential.credential_json).map(([key, value]) => (
<TextFormField
includeRevert
onChange={(e) => setFieldValue("name", e.target.value)}
name="name"
placeholder={credential.name || ""}
label="Name (optional):"
key={key}
name={key}
placeholder={value}
label={getDisplayNameForCredentialKey(key)}
type={
key.toLowerCase().includes("token") ||
key.toLowerCase().includes("password")
? "password"
: "text"
}
/>
{Object.entries(credential.credential_json).map(
([key, value]) => (
<TextFormField
includeRevert
key={key}
onChange={(e) => setFieldValue(key, e.target.value)}
name={key}
placeholder={value}
label={getDisplayNameForCredentialKey(key)}
type={
key.toLowerCase().includes("token") ||
key.toLowerCase().includes("password")
? "password"
: "text"
}
/>
)
)}
</Card>
<div className="flex mt-8 justify-end">
))}
<div className="flex justify-between w-full">
<Button type="button" onClick={() => resetForm()}>
<div className="flex gap-x-2 items-center w-full border-none">
<FaTrash />
<p>Reset Changes</p>
</div>
</Button>
<Button
type="submit"
disabled={isSubmitting}

View File

@@ -45,7 +45,7 @@ export function CustomModelForm({
});
}}
>
{({ isSubmitting, setFieldValue }) => (
{({ isSubmitting }) => (
<Form>
<TextFormField
name="model_name"
@@ -61,15 +61,10 @@ export function CustomModelForm({
subtext="The dimensionality of the embeddings generated by the model"
placeholder="E.g. '768'"
autoCompleteDisabled={true}
onChange={(e) => {
const value = e.target.value;
// Allow only integer values
if (value === "" || /^[0-9]+$/.test(value)) {
setFieldValue("model_dim", value);
}
}}
type="number"
/>
<TextFormField
min={-1}
name="description"
label="Description:"
subtext="Description of your model"

View File

@@ -1,3 +1,5 @@
import * as Yup from "yup";
import { IsPublicGroupSelectorFormType } from "@/components/IsPublicGroupSelector";
import { ConfigurableSources, ValidInputTypes, ValidSources } from "../types";
export type InputType =
@@ -808,6 +810,68 @@ For example, specifying .*-support.* as a "channel" will cause the connector to
],
},
};
export function createConnectorInitialValues(
connector: ConfigurableSources
): Record<string, any> & IsPublicGroupSelectorFormType {
const configuration = connectorConfigs[connector];
return {
name: "",
groups: [],
is_public: true,
...configuration.values.reduce(
(acc, field) => {
if (field.type === "select") {
acc[field.name] = null;
} else 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 }
),
};
}
export function createConnectorValidationSchema(
connector: ConfigurableSources
): Yup.ObjectSchema<Record<string, any>> {
const configuration = connectorConfigs[connector];
return Yup.object().shape({
name: Yup.string().required("Connector Name is required"),
...configuration.values.reduce(
(acc, field) => {
let schema: any =
field.type === "select"
? Yup.string()
: field.type === "list"
? Yup.array().of(Yup.string())
: field.type === "checkbox"
? Yup.boolean()
: field.type === "file"
? Yup.mixed()
: Yup.string();
if (!field.optional) {
schema = schema.required(`${field.label} is required`);
}
acc[field.name] = schema;
return acc;
},
{} as Record<string, any>
),
// These are advanced settings
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"),
});
}
// CONNECTORS
export interface ConnectorBase<T> {