feat: dashboard: add file stores to assistants (#2809)
Co-authored-by: Hassan Ben Jobrane <hsanbenjobrane@gmail.com>
This commit is contained in:
5
.changeset/thin-bugs-visit.md
Normal file
5
.changeset/thin-bugs-visit.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@nhost/dashboard': minor
|
||||
---
|
||||
|
||||
feat: dashboard: add support for storage buckets to AI assistants
|
||||
@@ -20,8 +20,7 @@ interface AINavLinkProps extends ListItemButtonProps {
|
||||
*/
|
||||
href: string;
|
||||
/**
|
||||
* Determines whether or not the link should be active if it's href exactly
|
||||
* matches the current route.
|
||||
* Determines whether or not the link should be active if href matches the current route.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
@@ -87,7 +86,7 @@ export default function AISidebar({ className, ...props }: AISidebarProps) {
|
||||
<>
|
||||
<Backdrop
|
||||
open={expanded}
|
||||
className="absolute top-0 left-0 bottom-0 right-0 z-[34] md:hidden"
|
||||
className="absolute bottom-0 left-0 right-0 top-0 z-[34] md:hidden"
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
onClick={() => setExpanded(false)}
|
||||
@@ -104,7 +103,7 @@ export default function AISidebar({ className, ...props }: AISidebarProps) {
|
||||
<Box
|
||||
component="aside"
|
||||
className={twMerge(
|
||||
'absolute top-0 z-[35] h-full w-full overflow-auto border-r-1 px-2 pt-2 pb-17 motion-safe:transition-transform md:relative md:z-0 md:h-full md:py-2.5 md:transition-none',
|
||||
'absolute top-0 z-[35] h-full w-full overflow-auto border-r-1 px-2 pb-17 pt-2 motion-safe:transition-transform md:relative md:z-0 md:h-full md:py-2.5 md:transition-none',
|
||||
expanded ? 'translate-x-0' : '-translate-x-full md:translate-x-0',
|
||||
className,
|
||||
)}
|
||||
@@ -119,6 +118,7 @@ export default function AISidebar({ className, ...props }: AISidebarProps) {
|
||||
>
|
||||
Auto-Embeddings
|
||||
</AINavLink>
|
||||
|
||||
<AINavLink href="/assistants" exact={false} onClick={handleSelect}>
|
||||
Assistants
|
||||
</AINavLink>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { IconProps } from '@/components/ui/v2/icons';
|
||||
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
|
||||
|
||||
function FileStoresIcon(props: IconProps) {
|
||||
return (
|
||||
<SvgIcon
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
aria-label="FileStores Icon"
|
||||
{...props}
|
||||
>
|
||||
<path d="M12 22v-9" />
|
||||
<path d="M15.17 2.21a1.67 1.67 0 0 1 1.63 0L21 4.57a1.93 1.93 0 0 1 0 3.36L8.82 14.79a1.655 1.655 0 0 1-1.64 0L3 12.43a1.93 1.93 0 0 1 0-3.36z" />
|
||||
<path d="M20 13v3.87a2.06 2.06 0 0 1-1.11 1.83l-6 3.08a1.93 1.93 0 0 1-1.78 0l-6-3.08A2.06 2.06 0 0 1 4 16.87V13" />
|
||||
<path d="M21 12.43a1.93 1.93 0 0 0 0-3.36L8.83 2.2a1.64 1.64 0 0 0-1.63 0L3 4.57a1.93 1.93 0 0 0 0 3.36l12.18 6.86a1.636 1.636 0 0 0 1.63 0z" />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
|
||||
FileStoresIcon.displayName = 'FileStoresIcon';
|
||||
|
||||
export default FileStoresIcon;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as FileStoresIcon } from './FileStoresIcon';
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
@@ -10,14 +11,14 @@ import { Text } from '@/components/ui/v2/Text';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import { GraphqlDataSourcesFormSection } from '@/features/ai/AssistantForm/components/GraphqlDataSourcesFormSection';
|
||||
import { WebhooksDataSourcesFormSection } from '@/features/ai/AssistantForm/components/WebhooksDataSourcesFormSection';
|
||||
import { useAdminApolloClient } from '@/features/projects/common/hooks/useAdminApolloClient';
|
||||
import { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient'
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { removeTypename, type DeepRequired } from '@/utils/helpers';
|
||||
import {
|
||||
useInsertAssistantMutation,
|
||||
useUpdateAssistantMutation,
|
||||
} from '@/utils/__generated__/graphite.graphql';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { removeTypename, type DeepRequired } from '@/utils/helpers';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useEffect } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
@@ -28,6 +29,7 @@ export const validationSchema = Yup.object({
|
||||
description: Yup.string(),
|
||||
instructions: Yup.string().required('The instructions are required'),
|
||||
model: Yup.string().required('The model is required'),
|
||||
fileStore: Yup.string().label('File Store'),
|
||||
graphql: Yup.array().of(
|
||||
Yup.object().shape({
|
||||
name: Yup.string().required(),
|
||||
@@ -64,14 +66,14 @@ export type AssistantFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export interface AssistantFormProps extends DialogFormProps {
|
||||
/**
|
||||
* To use in conjunction with initialData to allow for updating the autoEmbeddingsConfiguration
|
||||
* To use in conjunction with initialData to allow for updating the Assistant Configuration
|
||||
*/
|
||||
assistantId?: string;
|
||||
|
||||
/**
|
||||
* if there is initialData then it's an update operation
|
||||
*/
|
||||
initialData?: AssistantFormValues;
|
||||
initialData?: AssistantFormValues
|
||||
|
||||
/**
|
||||
* Function to be called when the operation is cancelled.
|
||||
@@ -114,26 +116,26 @@ export default function AssistantForm({
|
||||
} = form;
|
||||
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
const createOrUpdateAutoEmbeddings = async (
|
||||
values: DeepRequired<AssistantFormValues> & { assistantID: string },
|
||||
const createOrUpdateAssistant = async (
|
||||
values: DeepRequired<AssistantFormValues> & {
|
||||
assistantID: string;
|
||||
},
|
||||
) => {
|
||||
// remove any __typename from the form values
|
||||
const payload = removeTypename(values);
|
||||
|
||||
if (values.webhooks.length === 0) {
|
||||
if (values.webhooks?.length === 0) {
|
||||
delete payload.webhooks;
|
||||
}
|
||||
|
||||
if (values.graphql.length === 0) {
|
||||
if (values.graphql?.length === 0) {
|
||||
delete payload.graphql;
|
||||
}
|
||||
|
||||
// remove assistantId because the update mutation fails otherwise
|
||||
delete payload.assistantID;
|
||||
|
||||
// If the assistantId is set then we do an update
|
||||
@@ -158,11 +160,13 @@ export default function AssistantForm({
|
||||
};
|
||||
|
||||
const handleSubmit = async (
|
||||
values: DeepRequired<AssistantFormValues> & { assistantID: string },
|
||||
values: DeepRequired<AssistantFormValues> & {
|
||||
assistantID: string;
|
||||
},
|
||||
) => {
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await createOrUpdateAutoEmbeddings(values);
|
||||
await createOrUpdateAssistant(values);
|
||||
onSubmit?.();
|
||||
},
|
||||
{
|
||||
@@ -282,6 +286,7 @@ export default function AssistantForm({
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<GraphqlDataSourcesFormSection />
|
||||
<WebhooksDataSourcesFormSection />
|
||||
</div>
|
||||
|
||||
@@ -15,12 +15,12 @@ import { type Assistant } from 'pages/[workspaceSlug]/[appSlug]/ai/assistants';
|
||||
|
||||
interface AssistantsListProps {
|
||||
/**
|
||||
* The run services fetched from entering the users page.
|
||||
* The list of assistants.
|
||||
*/
|
||||
assistants: Assistant[];
|
||||
|
||||
/**
|
||||
* Function to be called after a submitting the form for either creating or updating a service.
|
||||
* Function to be called after a submitting the form for either creating or updating an assistant.
|
||||
*
|
||||
* @example onDelete={() => refetch()}
|
||||
*/
|
||||
|
||||
@@ -128,6 +128,9 @@ export default function AISidebar({ className, ...props }: AISidebarProps) {
|
||||
<AINavLink href="/assistants" exact={false} onClick={handleSelect}>
|
||||
Assistants
|
||||
</AINavLink>
|
||||
<AINavLink href="/file-stores" exact={false} onClick={handleSelect}>
|
||||
File Stores
|
||||
</AINavLink>
|
||||
</List>
|
||||
</nav>
|
||||
</Box>
|
||||
|
||||
@@ -14,21 +14,27 @@ import { WebhooksDataSourcesFormSection } from '@/features/orgs/projects/ai/Assi
|
||||
import { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import { removeTypename, type DeepRequired } from '@/utils/helpers';
|
||||
import {
|
||||
useInsertAssistantMutation,
|
||||
useUpdateAssistantMutation,
|
||||
} from '@/utils/__generated__/graphite.graphql';
|
||||
import { removeTypename, type DeepRequired } from '@/utils/helpers';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useEffect } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import { ControlledSelect } from '@/components/form/ControlledSelect';
|
||||
import { Option } from '@/components/ui/v2/Option';
|
||||
import { useIsFileStoreSupported } from '@/features/orgs/projects/common/hooks/useIsFileStoreSupported';
|
||||
import { type GraphiteFileStore } from '@/pages/orgs/[orgSlug]/projects/[appSubdomain]/ai/file-stores';
|
||||
|
||||
export const validationSchema = Yup.object({
|
||||
name: Yup.string().required('The name is required.'),
|
||||
description: Yup.string(),
|
||||
instructions: Yup.string().required('The instructions are required'),
|
||||
model: Yup.string().required('The model is required'),
|
||||
fileStore: Yup.string().label('File Store'),
|
||||
graphql: Yup.array().of(
|
||||
Yup.object().shape({
|
||||
name: Yup.string().required(),
|
||||
@@ -65,14 +71,17 @@ export type AssistantFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export interface AssistantFormProps extends DialogFormProps {
|
||||
/**
|
||||
* To use in conjunction with initialData to allow for updating the autoEmbeddingsConfiguration
|
||||
* To use in conjunction with initialData to allow for updating the Assistant Configuration
|
||||
*/
|
||||
assistantId?: string;
|
||||
|
||||
/**
|
||||
* if there is initialData then it's an update operation
|
||||
*/
|
||||
initialData?: AssistantFormValues;
|
||||
initialData?: AssistantFormValues & {
|
||||
fileStores?: string[];
|
||||
};
|
||||
fileStores?: GraphiteFileStore[];
|
||||
|
||||
/**
|
||||
* Function to be called when the operation is cancelled.
|
||||
@@ -87,6 +96,7 @@ export interface AssistantFormProps extends DialogFormProps {
|
||||
export default function AssistantForm({
|
||||
assistantId,
|
||||
initialData,
|
||||
fileStores,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
location,
|
||||
@@ -103,8 +113,27 @@ export default function AssistantForm({
|
||||
client: adminClient,
|
||||
});
|
||||
|
||||
const isFileStoreSupported = useIsFileStoreSupported();
|
||||
|
||||
const fileStoresOptions = fileStores
|
||||
? fileStores.map((fileStore: GraphiteFileStore) => ({
|
||||
label: fileStore.name,
|
||||
value: fileStore.name,
|
||||
id: fileStore.id,
|
||||
}))
|
||||
: [];
|
||||
|
||||
const assistantFileStore = initialData?.fileStores
|
||||
? fileStores?.find((fileStore: GraphiteFileStore) =>
|
||||
fileStore.id === initialData?.fileStores[0]
|
||||
)
|
||||
: null;
|
||||
|
||||
const formDefaultValues = { ...initialData, fileStores: [] };
|
||||
formDefaultValues.fileStore = assistantFileStore ? assistantFileStore.id : '';
|
||||
|
||||
const form = useForm<AssistantFormValues>({
|
||||
defaultValues: initialData,
|
||||
defaultValues: formDefaultValues,
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
@@ -120,22 +149,32 @@ export default function AssistantForm({
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
const createOrUpdateAutoEmbeddings = async (
|
||||
values: DeepRequired<AssistantFormValues> & { assistantID: string },
|
||||
const createOrUpdateAssistant = async (
|
||||
values: DeepRequired<AssistantFormValues> & {
|
||||
assistantID: string;
|
||||
},
|
||||
) => {
|
||||
// remove any __typename from the form values
|
||||
const payload = removeTypename(values);
|
||||
|
||||
if (values.webhooks.length === 0) {
|
||||
if (values.webhooks?.length === 0) {
|
||||
delete payload.webhooks;
|
||||
}
|
||||
|
||||
if (values.graphql.length === 0) {
|
||||
if (values.graphql?.length === 0) {
|
||||
delete payload.graphql;
|
||||
}
|
||||
|
||||
if (isFileStoreSupported && values.fileStore) {
|
||||
payload.fileStores = [values.fileStore];
|
||||
}
|
||||
if (!isFileStoreSupported) {
|
||||
delete payload.fileStores;
|
||||
}
|
||||
|
||||
// remove assistantId because the update mutation fails otherwise
|
||||
delete payload.assistantID;
|
||||
delete payload.fileStore;
|
||||
|
||||
// If the assistantId is set then we do an update
|
||||
if (assistantId) {
|
||||
@@ -152,7 +191,7 @@ export default function AssistantForm({
|
||||
await insertAssistantMutation({
|
||||
variables: {
|
||||
data: {
|
||||
...values,
|
||||
...payload,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -163,7 +202,7 @@ export default function AssistantForm({
|
||||
) => {
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await createOrUpdateAutoEmbeddings(values);
|
||||
await createOrUpdateAssistant(values);
|
||||
onSubmit?.();
|
||||
},
|
||||
{
|
||||
@@ -175,6 +214,10 @@ export default function AssistantForm({
|
||||
);
|
||||
};
|
||||
|
||||
const fileStoreTooltip = isFileStoreSupported
|
||||
? "If specified, all text documents in this file store will be available to the assistant."
|
||||
: "Please upgrade Graphite to its latest version in order to use file stores.";
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form
|
||||
@@ -285,6 +328,36 @@ export default function AssistantForm({
|
||||
/>
|
||||
<GraphqlDataSourcesFormSection />
|
||||
<WebhooksDataSourcesFormSection />
|
||||
<ControlledSelect
|
||||
slotProps={{
|
||||
popper: { disablePortal: false, className: 'z-[10000]' },
|
||||
}}
|
||||
id="fileStore"
|
||||
name="fileStore"
|
||||
label={
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text>File Store</Text>
|
||||
<Tooltip title={fileStoreTooltip}>
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
className="h-4 w-4"
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
}
|
||||
fullWidth
|
||||
error={!!errors?.model?.message}
|
||||
helperText={errors?.model?.message}
|
||||
disabled={!isFileStoreSupported}
|
||||
>
|
||||
<Option value="" />
|
||||
{fileStoresOptions.map((fileStore) => (
|
||||
<Option key={fileStore.id} value={fileStore.id}>
|
||||
{fileStore.label}
|
||||
</Option>
|
||||
))}
|
||||
</ControlledSelect>
|
||||
</div>
|
||||
|
||||
<Box className="flex flex-row justify-between w-full p-4 border-t rounded">
|
||||
|
||||
@@ -11,16 +11,22 @@ import { Text } from '@/components/ui/v2/Text';
|
||||
import { AssistantForm } from '@/features/orgs/projects/ai/AssistantForm';
|
||||
import { DeleteAssistantModal } from '@/features/orgs/projects/ai/DeleteAssistantModal';
|
||||
import { type Assistant } from '@/pages/orgs/[orgSlug]/projects/[appSubdomain]/ai/assistants';
|
||||
import { type GraphiteFileStore } from '@/pages/orgs/[orgSlug]/projects/[appSubdomain]/ai/file-stores';
|
||||
import { copy } from '@/utils/copy';
|
||||
|
||||
interface AssistantsListProps {
|
||||
/**
|
||||
* The run services fetched from entering the users page.
|
||||
* The list of assistants
|
||||
*/
|
||||
assistants: Assistant[];
|
||||
|
||||
/**
|
||||
* Function to be called after a submitting the form for either creating or updating a service.
|
||||
* The list of file stores
|
||||
*/
|
||||
fileStores: GraphiteFileStore[];
|
||||
|
||||
/**
|
||||
* Function to be called after a submitting the form for either creating or updating an assistant.
|
||||
*
|
||||
* @example onDelete={() => refetch()}
|
||||
*/
|
||||
@@ -35,6 +41,7 @@ interface AssistantsListProps {
|
||||
|
||||
export default function AssistantsList({
|
||||
assistants,
|
||||
fileStores,
|
||||
onCreateOrUpdate,
|
||||
onDelete,
|
||||
}: AssistantsListProps) {
|
||||
@@ -49,6 +56,7 @@ export default function AssistantsList({
|
||||
initialData={{
|
||||
...assistant,
|
||||
}}
|
||||
fileStores={fileStores}
|
||||
onSubmit={() => onCreateOrUpdate()}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Checkbox } from '@/components/ui/v2/Checkbox';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { type GraphiteFileStore } from '@/pages/orgs/[orgSlug]/projects/[appSubdomain]/ai/file-stores';
|
||||
import { useDeleteFileStoreMutation } from '@/utils/__generated__/graphite.graphql';
|
||||
import { useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface DeleteFileStoreModalProps {
|
||||
fileStore: GraphiteFileStore;
|
||||
onDelete?: () => Promise<any>;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export default function DeleteFileStoreModal({
|
||||
fileStore,
|
||||
onDelete,
|
||||
close,
|
||||
}: DeleteFileStoreModalProps) {
|
||||
const [remove, setRemove] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { adminClient } = useAdminApolloClient();
|
||||
|
||||
const [deleteFileStoreMutation] = useDeleteFileStoreMutation({
|
||||
client: adminClient,
|
||||
});
|
||||
|
||||
const deleteFileStore = async () => {
|
||||
await deleteFileStoreMutation({
|
||||
variables: {
|
||||
id: fileStore.id,
|
||||
},
|
||||
});
|
||||
await onDelete?.();
|
||||
close();
|
||||
};
|
||||
|
||||
async function handleClick() {
|
||||
setLoading(true);
|
||||
|
||||
await execPromiseWithErrorToast(deleteFileStore, {
|
||||
loadingMessage: 'Deleting the file store...',
|
||||
successMessage: 'The file store has been deleted successfully.',
|
||||
errorMessage:
|
||||
'An error occurred while deleting the file store. Please try again.',
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className={twMerge('w-full rounded-lg p-6 text-left')}>
|
||||
{' '}
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
{' '}
|
||||
<Text variant="h3" component="h2">
|
||||
{' '}
|
||||
Delete File Store {fileStore?.name}{' '}
|
||||
</Text>{' '}
|
||||
<Text variant="subtitle2">
|
||||
{' '}
|
||||
Are you sure you want to delete this File Store?{' '}
|
||||
</Text>{' '}
|
||||
<Text
|
||||
variant="subtitle2"
|
||||
className="font-bold"
|
||||
sx={{ color: (theme) => `${theme.palette.error.main} !important` }}
|
||||
>
|
||||
This cannot be undone.
|
||||
</Text>
|
||||
<Box className="my-4">
|
||||
<Checkbox
|
||||
id="accept-1"
|
||||
label={`I'm sure I want to delete ${fileStore?.name}`}
|
||||
className="py-2"
|
||||
checked={remove}
|
||||
onChange={(_event, checked) => setRemove(checked)}
|
||||
aria-label="Confirm Delete File Store"
|
||||
/>
|
||||
</Box>
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button
|
||||
color="error"
|
||||
onClick={handleClick}
|
||||
disabled={!remove}
|
||||
loading={loading}
|
||||
>
|
||||
Delete File Store
|
||||
</Button>
|
||||
|
||||
<Button variant="outlined" color="secondary" onClick={close}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as DeleteFileStoreModal } from './DeleteFileStoreModal';
|
||||
@@ -0,0 +1,217 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { ArrowsClockwise } from '@/components/ui/v2/icons/ArrowsClockwise';
|
||||
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
|
||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient'
|
||||
import { useRemoteApplicationGQLClient } from '@/features/orgs/hooks/useRemoteApplicationGQLClient';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import {
|
||||
useInsertFileStoreMutation,
|
||||
useUpdateFileStoreMutation,
|
||||
} from '@/utils/__generated__/graphite.graphql';
|
||||
import { useGetBucketsQuery } from '@/utils/__generated__/graphql';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { removeTypename, type DeepRequired } from '@/utils/helpers';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useEffect } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export const validationSchema = Yup.object({
|
||||
name: Yup.string().required('The name is required'),
|
||||
buckets: Yup.array()
|
||||
.of(
|
||||
Yup.object({
|
||||
label: Yup.string(),
|
||||
value: Yup.string(),
|
||||
}),
|
||||
)
|
||||
.label('Buckets')
|
||||
.required('At least one bucket is required'),
|
||||
});
|
||||
|
||||
export type FileStoreFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export interface FileStoreFormProps extends DialogFormProps {
|
||||
id?: string;
|
||||
initialData?: Omit<FileStoreFormValues, 'buckets'> & { buckets: string[] };
|
||||
onSubmit?: VoidFunction | ((args?: any) => Promise<any>);
|
||||
onCancel?: VoidFunction;
|
||||
}
|
||||
|
||||
export default function FileStoreForm({
|
||||
id,
|
||||
initialData,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
location,
|
||||
}: FileStoreFormProps) {
|
||||
const { onDirtyStateChange } = useDialog();
|
||||
|
||||
const { adminClient } = useAdminApolloClient();
|
||||
|
||||
const [insertFileStore] = useInsertFileStoreMutation({
|
||||
client: adminClient,
|
||||
});
|
||||
|
||||
const [updateFileStore] = useUpdateFileStoreMutation({
|
||||
client: adminClient,
|
||||
});
|
||||
|
||||
const remoteProjectGQLClient = useRemoteApplicationGQLClient();
|
||||
const { data: buckets } = useGetBucketsQuery({
|
||||
client: remoteProjectGQLClient,
|
||||
});
|
||||
|
||||
const bucketOptions = buckets
|
||||
? buckets.buckets.map((bucket) => ({
|
||||
label: bucket.id,
|
||||
value: bucket.id,
|
||||
}))
|
||||
: [];
|
||||
|
||||
const formDefaultValues = { ...initialData, buckets: [] };
|
||||
formDefaultValues.buckets = initialData?.buckets
|
||||
? initialData.buckets.map((bucket) => ({
|
||||
label: bucket,
|
||||
value: bucket,
|
||||
}))
|
||||
: [];
|
||||
|
||||
const form = useForm<FileStoreFormValues>({
|
||||
defaultValues: formDefaultValues,
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isSubmitting, dirtyFields },
|
||||
} = form;
|
||||
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
const createOrUpdateFileStore = async (
|
||||
values: DeepRequired<FileStoreFormValues> & { id: string },
|
||||
) => {
|
||||
const payload = removeTypename(values);
|
||||
delete payload.id;
|
||||
delete payload.vectorStoreID;
|
||||
|
||||
if (id) {
|
||||
await updateFileStore({
|
||||
variables: {
|
||||
id,
|
||||
object: { ...payload, buckets: values.buckets.map((b) => b.value) },
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await insertFileStore({
|
||||
variables: {
|
||||
object: { ...values, buckets: values.buckets.map((b) => b.value) },
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (
|
||||
values: DeepRequired<FileStoreFormValues> & { id: string },
|
||||
) => {
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await createOrUpdateFileStore(values);
|
||||
onSubmit?.();
|
||||
},
|
||||
{
|
||||
loadingMessage: 'Creating File Store...',
|
||||
successMessage: 'The File Store has been created successfully.',
|
||||
errorMessage:
|
||||
'An error occurred while creating the File Store. Please try again.',
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex h-full flex-col overflow-hidden border-t"
|
||||
>
|
||||
<div className="flex flex-1 flex-col space-y-4 overflow-auto p-4">
|
||||
<Input
|
||||
{...register('name')}
|
||||
id="name"
|
||||
label={
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text>Name</Text>
|
||||
<Tooltip title="Name of the file store">
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
className="h-4 w-4"
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
}
|
||||
placeholder=""
|
||||
hideEmptyHelperText
|
||||
error={!!errors.name}
|
||||
helperText={errors?.name?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<ControlledAutocomplete
|
||||
id="buckets"
|
||||
name="buckets"
|
||||
label={
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text>Buckets</Text>
|
||||
<Tooltip title="One or more buckets from storage from which documents can be used by Assistants">
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
className="h-4 w-4"
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
}
|
||||
fullWidth
|
||||
multiple
|
||||
aria-label="Buckets"
|
||||
error={!!errors.buckets}
|
||||
options={bucketOptions}
|
||||
helperText={errors?.buckets?.message}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Box className="flex w-full flex-row justify-between rounded border-t p-4">
|
||||
<Button variant="outlined" color="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
startIcon={id ? <ArrowsClockwise /> : <PlusIcon />}
|
||||
>
|
||||
{id ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as FileStoreForm } from './FileStoreForm';
|
||||
@@ -0,0 +1,157 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Divider } from '@/components/ui/v2/Divider';
|
||||
import { Dropdown } from '@/components/ui/v2/Dropdown';
|
||||
import { IconButton } from '@/components/ui/v2/IconButton';
|
||||
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
|
||||
import { DotsHorizontalIcon } from '@/components/ui/v2/icons/DotsHorizontalIcon';
|
||||
import { FileStoresIcon } from '@/components/ui/v2/icons/FileStoresIcon';
|
||||
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
|
||||
import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { DeleteFileStoreModal } from '@/features/orgs/projects/ai/DeleteFileStoreModal';
|
||||
import { FileStoreForm } from '@/features/orgs/projects/ai/FileStoreForm';
|
||||
import { type GraphiteFileStore } from '@/pages/orgs/[orgSlug]/projects/[appSubdomain]/ai/file-stores';
|
||||
import { copy } from '@/utils/copy';
|
||||
|
||||
interface FileStoresListProps {
|
||||
/**
|
||||
* List of File Stores to be displayed.
|
||||
*/
|
||||
fileStores: GraphiteFileStore[];
|
||||
|
||||
/**
|
||||
* Function to be called after a submitting the form for either creating or updating a File Store.
|
||||
*
|
||||
* @example onDelete={() => refetch()}
|
||||
*/
|
||||
onCreateOrUpdate?: () => Promise<any>;
|
||||
|
||||
/**
|
||||
* Function to be called after a successful delete action.
|
||||
*
|
||||
*/
|
||||
onDelete?: () => Promise<any>;
|
||||
}
|
||||
|
||||
export default function FileStoresList({
|
||||
fileStores,
|
||||
onCreateOrUpdate,
|
||||
onDelete,
|
||||
}: FileStoresListProps) {
|
||||
const { openDrawer, openDialog, closeDialog } = useDialog();
|
||||
|
||||
const viewFileStore = async (fileStore: GraphiteFileStore) => {
|
||||
openDrawer({
|
||||
title: fileStore.name,
|
||||
component: (
|
||||
<FileStoreForm
|
||||
id={fileStore.id}
|
||||
initialData={{ ...fileStore }}
|
||||
onSubmit={() => onCreateOrUpdate()}
|
||||
/>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const deleteFileStore = async (fileStore: GraphiteFileStore) => {
|
||||
openDialog({
|
||||
component: (
|
||||
<DeleteFileStoreModal
|
||||
fileStore={fileStore}
|
||||
close={closeDialog}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="flex flex-col">
|
||||
{fileStores.map((fileStore) => (
|
||||
<Box
|
||||
key={fileStore.id}
|
||||
className="flex h-[64px] w-full cursor-pointer items-center justify-between space-x-4 border-b-1 px-4 py-2 transition-colors"
|
||||
sx={{
|
||||
[`&:hover`]: {
|
||||
backgroundColor: 'action.hover',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
onClick={() => viewFileStore(fileStore)}
|
||||
className="flex w-full flex-row justify-between"
|
||||
sx={{ backgroundColor: 'transparent' }}
|
||||
>
|
||||
<div className="flex flex-1 flex-row items-center space-x-4">
|
||||
<FileStoresIcon className="h-5 w-5" />
|
||||
<div className="flex flex-col">
|
||||
<Text variant="h4" className="font-semibold">
|
||||
{fileStore?.name ?? 'unset'}
|
||||
</Text>
|
||||
<div className="hidden flex-row items-center space-x-2 md:flex">
|
||||
<Text variant="subtitle1" className="font-mono text-xs">
|
||||
{fileStore.id}
|
||||
</Text>
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
onClick={(event) => {
|
||||
copy(fileStore.id, 'File Store Id');
|
||||
event.stopPropagation();
|
||||
}}
|
||||
aria-label="Service Id"
|
||||
>
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<Dropdown.Root>
|
||||
<Dropdown.Trigger
|
||||
asChild
|
||||
hideChevron
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
aria-label="More options"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<DotsHorizontalIcon />
|
||||
</IconButton>
|
||||
</Dropdown.Trigger>
|
||||
<Dropdown.Content
|
||||
menu
|
||||
PaperProps={{ className: 'w-auto' }}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
>
|
||||
<Dropdown.Item
|
||||
onClick={() => viewFileStore(fileStore)}
|
||||
className="z-50 grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
>
|
||||
<UserIcon className="h-4 w-4" />
|
||||
<Text className="font-medium">View {fileStore?.name}</Text>
|
||||
</Dropdown.Item>
|
||||
<Divider component="li" />
|
||||
<Dropdown.Item
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
sx={{ color: 'error.main' }}
|
||||
onClick={() => deleteFileStore(fileStore)}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<Text className="font-medium" color="error">
|
||||
Delete {fileStore?.name}
|
||||
</Text>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as FileStoresList } from './FileStoresList';
|
||||
@@ -0,0 +1 @@
|
||||
export { default as useIsFileStoreSupported } from './useIsFileStoreSupported';
|
||||
@@ -0,0 +1,44 @@
|
||||
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { useGetConfiguredVersionsQuery } from '@/utils/__generated__/graphql';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
function compareSemver(v1: string, v2: string): number {
|
||||
const parse = (v: string) => v.split('.').map(Number);
|
||||
const [a, b] = [parse(v1), parse(v2)];
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
if (a[i] > b[i]) { return 1; }
|
||||
if (a[i] < b[i]) { return -1; }
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const MIN_VERSION_WITH_FILE_STORE_SUPPORT = '0.6.2';
|
||||
|
||||
export default function useIsFileStoreSupported() {
|
||||
const [isFileStoreSupported, setIsFileStoreSupported] = useState<boolean | null>(null);
|
||||
const { project } = useProject();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const isPlatform = useIsPlatform();
|
||||
|
||||
const { data, loading, error } = useGetConfiguredVersionsQuery({
|
||||
variables: { appId: project?.id },
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && data?.config?.ai?.version) {
|
||||
setIsFileStoreSupported(compareSemver(data.config.ai.version, MIN_VERSION_WITH_FILE_STORE_SUPPORT) >= 0);
|
||||
}
|
||||
}, [data, loading]);
|
||||
|
||||
return {
|
||||
isFileStoreSupported,
|
||||
version: data?.config?.ai?.version,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
query getAssistants {
|
||||
query getAssistants($isFileStoresSupported: Boolean!) {
|
||||
graphite {
|
||||
assistants {
|
||||
assistantID
|
||||
@@ -28,6 +28,7 @@ query getAssistants {
|
||||
required
|
||||
}
|
||||
}
|
||||
fileStores @include(if: $isFileStoresSupported)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation deleteFileStore($id: uuid!) {
|
||||
graphite {
|
||||
deleteFileStore(id: $id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
query getGraphiteFileStores {
|
||||
graphite {
|
||||
fileStores {
|
||||
id
|
||||
name
|
||||
vectorStoreID
|
||||
buckets
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mutation insertFileStore($object: graphiteFileStoreInput!) {
|
||||
graphite {
|
||||
insertFileStore(object: $object) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mutation updateFileStore($id: uuid!, $object: graphiteFileStoreInput!) {
|
||||
graphite {
|
||||
updateFileStore(id: $id, object: $object) {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import { useIsGraphiteEnabled } from '@/features/projects/common/hooks/useIsGrap
|
||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
import {
|
||||
useGetAssistantsQuery,
|
||||
type GetAssistantsQuery,
|
||||
type GetAssistantsQuery
|
||||
} from '@/utils/__generated__/graphite.graphql';
|
||||
import { useMemo, type ReactElement } from 'react';
|
||||
|
||||
@@ -29,21 +29,29 @@ export type Assistant = Omit<
|
||||
export default function AssistantsPage() {
|
||||
const { openDrawer } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
|
||||
const { currentWorkspace, currentProject } = useCurrentWorkspaceAndProject();
|
||||
const { adminClient } = useAdminApolloClient();
|
||||
const { isGraphiteEnabled } = useIsGraphiteEnabled();
|
||||
|
||||
const { data, loading, refetch } = useGetAssistantsQuery({
|
||||
const {
|
||||
data,
|
||||
loading,
|
||||
refetch,
|
||||
} = useGetAssistantsQuery({
|
||||
client: adminClient,
|
||||
});
|
||||
|
||||
const assistants = useMemo(() => data?.graphite?.assistants || [], [data]);
|
||||
|
||||
const assistants = useMemo(
|
||||
() => data?.graphite?.assistants || [],
|
||||
[data],
|
||||
);
|
||||
|
||||
const openCreateAssistantForm = () => {
|
||||
openDrawer({
|
||||
title: 'Create a new Assistant',
|
||||
component: <AssistantForm onSubmit={refetch} />,
|
||||
component: (
|
||||
<AssistantForm onSubmit={refetch} />
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -97,7 +105,11 @@ export default function AssistantsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (data?.graphite?.assistants.length === 0 && !loading) {
|
||||
if (loading) {
|
||||
return <Box className="p-4">Loading...</Box>;
|
||||
}
|
||||
|
||||
if (assistants.length === 0) {
|
||||
return (
|
||||
<Box
|
||||
className="w-full p-6"
|
||||
@@ -141,13 +153,11 @@ export default function AssistantsPage() {
|
||||
New
|
||||
</Button>
|
||||
</Box>
|
||||
<div>
|
||||
<AssistantsList
|
||||
assistants={assistants}
|
||||
onDelete={() => refetch()}
|
||||
onCreateOrUpdate={() => refetch()}
|
||||
/>
|
||||
</div>
|
||||
<AssistantsList
|
||||
assistants={assistants}
|
||||
onDelete={() => refetch()}
|
||||
onCreateOrUpdate={() => refetch()}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Text } from '@/components/ui/v2/Text';
|
||||
|
||||
import {
|
||||
useGetAssistantsQuery,
|
||||
useGetGraphiteFileStoresQuery,
|
||||
type GetAssistantsQuery,
|
||||
} from '@/utils/__generated__/graphite.graphql';
|
||||
import { useMemo, type ReactElement } from 'react';
|
||||
@@ -21,6 +22,7 @@ import { AISidebar } from '@/features/orgs/layout/AISidebar';
|
||||
import { ProjectLayout } from '@/features/orgs/layout/ProjectLayout';
|
||||
import { AssistantForm } from '@/features/orgs/projects/ai/AssistantForm';
|
||||
import { AssistantsList } from '@/features/orgs/projects/ai/AssistantsList';
|
||||
import { useIsFileStoreSupported } from '@/features/orgs/projects/common/hooks/useIsFileStoreSupported';
|
||||
import { useIsGraphiteEnabled } from '@/features/orgs/projects/common/hooks/useIsGraphiteEnabled';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient';
|
||||
@@ -43,24 +45,46 @@ export default function AssistantsPage() {
|
||||
const { isGraphiteEnabled, loading: loadingGraphite } =
|
||||
useIsGraphiteEnabled();
|
||||
|
||||
const {
|
||||
data,
|
||||
loading: loadingAssistants,
|
||||
refetch,
|
||||
} = useGetAssistantsQuery({
|
||||
client: adminClient,
|
||||
});
|
||||
const { isFileStoreSupported, loading: fileStoreLoading } =
|
||||
useIsFileStoreSupported();
|
||||
|
||||
const assistants = useMemo(() => data?.graphite?.assistants || [], [data]);
|
||||
const {
|
||||
data: assistantsData,
|
||||
loading: assistantsLoading,
|
||||
refetch: assistantsRefetch,
|
||||
} = useGetAssistantsQuery({
|
||||
client: adminClient,
|
||||
variables: {
|
||||
isFileStoresSupported: isFileStoreSupported ?? false,
|
||||
},
|
||||
skip: isFileStoreSupported === null || fileStoreLoading,
|
||||
});
|
||||
const { data: fileStoresData } = useGetGraphiteFileStoresQuery({
|
||||
client: adminClient,
|
||||
});
|
||||
|
||||
const assistants = useMemo(
|
||||
() => assistantsData?.graphite?.assistants || [],
|
||||
[assistantsData],
|
||||
);
|
||||
const fileStores = useMemo(
|
||||
() => fileStoresData?.graphite?.fileStores || [],
|
||||
[fileStoresData],
|
||||
);
|
||||
|
||||
const openCreateAssistantForm = () => {
|
||||
openDrawer({
|
||||
title: 'Create a new Assistant',
|
||||
component: <AssistantForm onSubmit={refetch} />,
|
||||
component: (
|
||||
<AssistantForm
|
||||
onSubmit={assistantsRefetch}
|
||||
fileStores={isFileStoreSupported ? fileStores : undefined}
|
||||
/>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
if (loadingOrg || loadingProject || loadingGraphite || loadingAssistants) {
|
||||
if (loadingOrg || loadingProject || loadingGraphite || assistantsLoading) {
|
||||
return (
|
||||
<Box className="flex items-center justify-center w-full h-full">
|
||||
<ActivityIndicator
|
||||
@@ -114,7 +138,7 @@ export default function AssistantsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (data?.graphite?.assistants.length === 0 && !loadingAssistants) {
|
||||
if (assistants.length === 0 && !assistantsLoading) {
|
||||
return (
|
||||
<Box
|
||||
className="w-full p-6"
|
||||
@@ -161,8 +185,9 @@ export default function AssistantsPage() {
|
||||
<div>
|
||||
<AssistantsList
|
||||
assistants={assistants}
|
||||
onDelete={() => refetch()}
|
||||
onCreateOrUpdate={() => refetch()}
|
||||
fileStores={isFileStoreSupported ? fileStores : undefined}
|
||||
onDelete={() => assistantsRefetch()}
|
||||
onCreateOrUpdate={() => assistantsRefetch()}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
@@ -12,14 +12,13 @@ import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import { Link } from '@/components/ui/v2/Link';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { AISidebar } from '@/features/orgs/layout/AISidebar';
|
||||
// import AILayout from '@/features/orgs/layout/AILayout/AILayout';
|
||||
import { ProjectLayout } from '@/features/orgs/layout/ProjectLayout';
|
||||
import { AutoEmbeddingsForm } from '@/features/orgs/projects/ai/AutoEmbeddingsForm';
|
||||
import { AutoEmbeddingsList } from '@/features/orgs/projects/ai/AutoEmbeddingsList';
|
||||
import { useIsGraphiteEnabled } from '@/features/orgs/projects/common/hooks/useIsGraphiteEnabled';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient';
|
||||
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import {
|
||||
useGetGraphiteAutoEmbeddingsConfigurationsQuery,
|
||||
@@ -36,9 +35,11 @@ export type AutoEmbeddingsConfiguration = Omit<
|
||||
export default function AutoEmbeddingsPage() {
|
||||
const limit = useRef(25);
|
||||
const router = useRouter();
|
||||
|
||||
const { openDrawer } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { currentOrg: org } = useOrgs();
|
||||
|
||||
const { org } = useCurrentOrg();
|
||||
const { project } = useProject();
|
||||
|
||||
const { adminClient } = useAdminApolloClient();
|
||||
@@ -128,7 +129,7 @@ export default function AutoEmbeddingsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (data?.graphiteAutoEmbeddingsConfigurations.length === 0 && !loading) {
|
||||
if (autoEmbeddingsConfigurations.length === 0 && !loading) {
|
||||
return (
|
||||
<Box
|
||||
className="w-full p-6"
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { UpgradeToProBanner } from '@/components/common/UpgradeToProBanner';
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { FileStoresIcon } from '@/components/ui/v2/icons/FileStoresIcon';
|
||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import { Link } from '@/components/ui/v2/Link';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { ProjectLayout } from '@/features/orgs/layout/ProjectLayout';
|
||||
import { FileStoreForm } from '@/features/orgs/projects/ai/FileStoreForm';
|
||||
import { FileStoresList } from '@/features/orgs/projects/ai/FileStoresList';
|
||||
import { useIsFileStoreSupported } from '@/features/orgs/projects/common/hooks/useIsFileStoreSupported';
|
||||
import { useIsGraphiteEnabled } from '@/features/orgs/projects/common/hooks/useIsGraphiteEnabled';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import {
|
||||
useGetGraphiteFileStoresQuery,
|
||||
type GetGraphiteFileStoresQuery
|
||||
} from '@/utils/__generated__/graphite.graphql';
|
||||
import { useMemo, type ReactElement } from 'react';
|
||||
|
||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { AISidebar } from '@/features/orgs/layout/AISidebar';
|
||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||
|
||||
export type GraphiteFileStore = Omit<
|
||||
GetGraphiteFileStoresQuery['graphite']['fileStores'][0],
|
||||
'__typename'
|
||||
>;
|
||||
|
||||
export default function FileStoresPage() {
|
||||
const { openDrawer } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
|
||||
const { org, loading: loadingOrg } = useCurrentOrg();
|
||||
const { project, loading: loadingProject } = useProject();
|
||||
|
||||
const { adminClient } = useAdminApolloClient();
|
||||
const { isGraphiteEnabled } = useIsGraphiteEnabled();
|
||||
const { isFileStoreSupported } = useIsFileStoreSupported();
|
||||
|
||||
const { data, loading, refetch } = useGetGraphiteFileStoresQuery({
|
||||
client: adminClient,
|
||||
});
|
||||
|
||||
const fileStores = useMemo(() => data?.graphite.fileStores || [], [data]);
|
||||
|
||||
const openCreateFileStoreForm = () => {
|
||||
openDrawer({
|
||||
title: 'Create a new File Store',
|
||||
component: <FileStoreForm onSubmit={refetch} />,
|
||||
});
|
||||
};
|
||||
|
||||
if (loadingOrg || loadingProject || loading) {
|
||||
return (
|
||||
<Box className="flex items-center justify-center w-full h-full">
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading File Stores..."
|
||||
className="justify-center"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (isPlatform && org?.plan?.isFree) {
|
||||
return (
|
||||
<Box className="p-4" sx={{ backgroundColor: 'background.default' }}>
|
||||
<UpgradeToProBanner
|
||||
title="Upgrade to Nhost Pro."
|
||||
description={
|
||||
<Text>
|
||||
Graphite is an addon to the Pro plan. To unlock it, please upgrade
|
||||
to Pro first.
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(isPlatform &&
|
||||
!org?.plan?.isFree &&
|
||||
!project.config?.ai) ||
|
||||
!isGraphiteEnabled
|
||||
) {
|
||||
return (
|
||||
<Box
|
||||
className="w-full p-4"
|
||||
sx={{ backgroundColor: 'background.default' }}
|
||||
>
|
||||
<Alert className="grid w-full grid-flow-col place-content-between items-center gap-2">
|
||||
<Text className="grid grid-flow-row justify-items-start gap-0.5">
|
||||
<Text component="span">
|
||||
To enable graphite, configure the service first in{' '}
|
||||
<Link
|
||||
href={`/orgs/${org?.slug}/projects/${project?.subdomain}/settings/ai`}
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
>
|
||||
AI Settings
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</Text>
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (fileStores.length === 0 && !loading) {
|
||||
return (
|
||||
<Box
|
||||
className="w-full p-6"
|
||||
sx={{ backgroundColor: 'background.default' }}
|
||||
>
|
||||
<Box className="flex flex-col items-center justify-center space-y-5 rounded-lg border px-48 py-12 shadow-sm">
|
||||
<FileStoresIcon className="h-10 w-10" />
|
||||
|
||||
<div className="flex flex-col space-y-1">
|
||||
<Text className="text-center font-medium" variant="h3">
|
||||
No File Stores are configured
|
||||
</Text>
|
||||
<Text variant="subtitle1" className="text-center">
|
||||
File Stores are used to share storage documents with your
|
||||
AI assistants.
|
||||
</Text>
|
||||
{!isFileStoreSupported && (
|
||||
<Box className="px-4 pb-4">
|
||||
<Alert className="mt-2 text-left">
|
||||
Please upgrade Graphite to its latest version in order to use
|
||||
file stores.
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row place-content-between rounded-lg">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className="w-full"
|
||||
onClick={openCreateFileStoreForm}
|
||||
startIcon={<PlusIcon className="h-4 w-4" />}
|
||||
disabled={!isFileStoreSupported}
|
||||
>
|
||||
Add a new File Store
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="flex flex-col w-full overflow-hidden">
|
||||
<Box className="flex flex-row place-content-end border-b-1 p-4">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={openCreateFileStoreForm}
|
||||
startIcon={<PlusIcon className="h-4 w-4" />}
|
||||
>
|
||||
New
|
||||
</Button>
|
||||
</Box>
|
||||
<div>
|
||||
<FileStoresList
|
||||
fileStores={fileStores}
|
||||
onDelete={() => refetch()}
|
||||
onCreateOrUpdate={() => refetch()}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
FileStoresPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<ProjectLayout
|
||||
mainContainerProps={{ className: 'flex flex-row w-full h-full' }}
|
||||
>
|
||||
<AISidebar className="w-full max-w-sidebar" />
|
||||
<RetryableErrorBoundary>{page}</RetryableErrorBoundary>
|
||||
</ProjectLayout>
|
||||
);
|
||||
|
||||
};
|
||||
@@ -290,10 +290,10 @@ function TicketPage() {
|
||||
<ControlledAutocomplete
|
||||
id="services"
|
||||
name="services"
|
||||
label="services"
|
||||
label="Services"
|
||||
fullWidth
|
||||
multiple
|
||||
aria-label="Enabled APIs"
|
||||
aria-label="Services"
|
||||
options={[
|
||||
'Dashboard',
|
||||
'Database',
|
||||
|
||||
22227
dashboard/src/utils/__generated__/graphite.graphql.ts
generated
22227
dashboard/src/utils/__generated__/graphite.graphql.ts
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user