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:
pablodanswer 2024-08-27 09:01:57 -07:00 committed by GitHub
parent 97ba71e1b3
commit b36cd4937f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 1896 additions and 1960 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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} />

View File

@ -20,6 +20,7 @@ export default async function Page() {
<Card>
<AssistantEditor
{...values}
admin
defaultPublic={true}
redirectType={SuccessfulPersonaUpdateRedirectType.ADMIN}
/>

View File

@ -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?"

View File

@ -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?"

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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>
);
}

View File

@ -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}
/>
</>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
</>
);
}

View File

@ -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;

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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[]
) => {

View File

@ -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 () => {

View File

@ -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

View File

@ -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 ${

View File

@ -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"

View File

@ -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>

View File

@ -416,7 +416,6 @@ export default function EmbeddingForm() {
<Card>
<AdvancedEmbeddingFormPage
numRerank={rerankingDetails.num_rerank}
setRerankingDetails={setRerankingDetails}
advancedEmbeddingDetails={advancedEmbeddingDetails}
updateAdvancedEmbeddingDetails={updateAdvancedEmbeddingDetails}
/>

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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">

View File

@ -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

View File

@ -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}
/>

View File

@ -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}

View File

@ -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")
}

View File

@ -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>

View File

@ -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

View File

@ -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"

View File

@ -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 && (

View File

@ -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>
);

View File

@ -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

View File

@ -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"

View File

@ -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 ||

View File

@ -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>

View File

@ -94,7 +94,7 @@ const UserRoleDropdown = ({
if (isEditable) {
return (
<div className="w-40 ">
<div className="w-40">
<Select
value={localRole}
onValueChange={handleChange}

View File

@ -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">

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -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">

View File

@ -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

View File

@ -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>
)}

View File

@ -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>
);

View File

@ -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>
) : (

View File

@ -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>
</>
);
}

View File

@ -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>

View File

@ -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>
);
};

View File

@ -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.."

View File

@ -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") ||

View File

@ -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>

View File

@ -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

View File

@ -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>
);
};

View 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>
);
};

View File

@ -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>&quot;{chatSessionName.slice(0, 30)}&quot;</b>
<b>&quot;{entityName}&quot;</b>
</p>
<div className="flex">
<div className="mx-auto">

View File

@ -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">

View File

@ -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"

View File

@ -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}`}

View File

@ -24,5 +24,6 @@ export function orderAssistantsForUser(
return orderA - orderB;
});
}
return assistants;
}

View File

@ -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",

View File

@ -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: {