mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-13 04:14:42 +00:00
Group Add Source and Add Tool by where things come from
Add Source splits into Upload and web, and From a connection. Connector tiles are always shown and say whether an account is connected or the server still needs setup, instead of disappearing when a VITE_ client id is unset; without the Google Picker key, Drive falls back to the server-side file browser. The name follows the first picked item and the button reads Add source. Add Tool splits into Built in and From a service. The MCP and API tool cards move to Connectors > Add custom connector.
This commit is contained in:
1 parent
aad43c2fd0
commit
45c4d928c4
7 files changed
+389
-116
No files matched your search
@@ -75,6 +75,8 @@ interface CloudFilePickerProps {
|
||||
selectedFolderIds?: string[],
|
||||
) => void;
|
||||
onDisconnect?: () => void;
|
||||
/** Called with the first item's name when the selection goes from empty to one. */
|
||||
onFirstPickName?: (name: string) => void;
|
||||
provider: string;
|
||||
token: string | null;
|
||||
initialSelectedFiles?: string[];
|
||||
@@ -84,6 +86,7 @@ interface CloudFilePickerProps {
|
||||
export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
onSelectionChange,
|
||||
onDisconnect,
|
||||
onFirstPickName,
|
||||
provider,
|
||||
token,
|
||||
initialSelectedFiles = [],
|
||||
@@ -404,6 +407,14 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
};
|
||||
|
||||
const handleFileSelect = (fileId: string, isFolder: boolean) => {
|
||||
if (
|
||||
selectedFiles.length === 0 &&
|
||||
selectedFolders.length === 0 &&
|
||||
onFirstPickName
|
||||
) {
|
||||
const picked = files.find((file) => file.id === fileId);
|
||||
if (picked) onFirstPickName(picked.name);
|
||||
}
|
||||
if (isFolder) {
|
||||
const newSelectedFolders = selectedFolders.includes(fileId)
|
||||
? selectedFolders.filter((id) => id !== fileId)
|
||||
|
||||
@@ -33,11 +33,14 @@ interface PickerFile {
|
||||
interface GoogleDrivePickerProps {
|
||||
token: string | null;
|
||||
onSelectionChange: (fileIds: string[], folderIds?: string[]) => void;
|
||||
/** Called with the first item's name when the selection goes from empty to one. */
|
||||
onFirstPickName?: (name: string) => void;
|
||||
}
|
||||
|
||||
const GoogleDrivePicker: React.FC<GoogleDrivePickerProps> = ({
|
||||
token,
|
||||
onSelectionChange,
|
||||
onFirstPickName,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedFiles, setSelectedFiles] = useState<PickerFile[]>([]);
|
||||
@@ -191,6 +194,13 @@ const GoogleDrivePicker: React.FC<GoogleDrivePickerProps> = ({
|
||||
);
|
||||
return [...prevFolders, ...uniqueNewFolders];
|
||||
});
|
||||
if (
|
||||
selectedFiles.length === 0 &&
|
||||
selectedFolders.length === 0 &&
|
||||
docs.length > 0
|
||||
) {
|
||||
onFirstPickName?.(docs[0].name);
|
||||
}
|
||||
onSelectionChange(
|
||||
[...selectedFiles, ...newFiles].map((file) => file.id),
|
||||
[...selectedFolders, ...newFolders].map((folder) => folder.id),
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { ArrowRight, Plus } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
import SkeletonLoader from '../components/SkeletonLoader';
|
||||
import ToolIcon from '../components/ToolIcon';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardDescription, CardTitle } from '../components/ui/card';
|
||||
import { Modal } from '../components/ui/modal';
|
||||
import { SectionHeader } from '../components/ui/section-header';
|
||||
import { useLoaderState } from '../hooks';
|
||||
import PairDeviceModal from '../settings/PairDeviceModal';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import ConfigToolModal from './ConfigToolModal';
|
||||
import MCPServerModal from './MCPServerModal';
|
||||
import { AvailableToolType } from './types';
|
||||
|
||||
export default function AddToolModal({
|
||||
@@ -39,8 +42,6 @@ export default function AddToolModal({
|
||||
React.useState<AvailableToolType | null>(null);
|
||||
const [configModalState, setConfigModalState] =
|
||||
React.useState<ActiveState>('INACTIVE');
|
||||
const [mcpModalState, setMcpModalState] =
|
||||
React.useState<ActiveState>('INACTIVE');
|
||||
const [pairModalState, setPairModalState] =
|
||||
React.useState<ActiveState>('INACTIVE');
|
||||
const [loading, setLoading] = useLoaderState(false);
|
||||
@@ -53,7 +54,11 @@ export default function AddToolModal({
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
setAvailableTools(data.data);
|
||||
setAvailableTools(
|
||||
(data.data as AvailableToolType[]).filter(
|
||||
(tool) => tool.group !== 'custom',
|
||||
),
|
||||
);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
@@ -96,9 +101,6 @@ export default function AddToolModal({
|
||||
.catch((error) => {
|
||||
console.error('Failed to create tool:', error);
|
||||
});
|
||||
} else if (tool.name === 'mcp_tool') {
|
||||
setModalState('INACTIVE');
|
||||
setMcpModalState('ACTIVE');
|
||||
} else {
|
||||
setModalState('INACTIVE');
|
||||
setConfigModalState('ACTIVE');
|
||||
@@ -109,11 +111,6 @@ export default function AddToolModal({
|
||||
if (modalState === 'ACTIVE') getAvailableTools();
|
||||
}, [modalState]);
|
||||
|
||||
const handleMcpServerAdded = () => {
|
||||
getUserTools();
|
||||
setMcpModalState('INACTIVE');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
@@ -122,62 +119,88 @@ export default function AddToolModal({
|
||||
title={t('settings.tools.selectToolSetup')}
|
||||
size="xl"
|
||||
>
|
||||
<div className="flex h-full flex-col">
|
||||
<div>
|
||||
<div className="mt-5 px-3 py-px">
|
||||
{loading ? (
|
||||
<div className="grid auto-rows-fr grid-cols-1 gap-4 pb-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<SkeletonLoader component="addToolCards" count={6} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid auto-rows-fr grid-cols-1 gap-4 pb-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{availableTools.map((tool, index) => (
|
||||
<Card
|
||||
asChild
|
||||
key={index}
|
||||
variant="outline"
|
||||
padding="lg"
|
||||
interactive
|
||||
className="h-52 w-full justify-between"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedTool(tool);
|
||||
handleAddTool(tool);
|
||||
}}
|
||||
>
|
||||
<div className="w-full">
|
||||
<div className="flex w-full items-center justify-between px-1">
|
||||
<ToolIcon
|
||||
name={tool.name}
|
||||
className="size-6"
|
||||
title={t('settings.tools.toolIconTitle', {
|
||||
name: tool.name,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-[9px] px-1">
|
||||
<CardTitle
|
||||
title={tool.displayName}
|
||||
className="truncate capitalize"
|
||||
>
|
||||
{tool.displayName}
|
||||
</CardTitle>
|
||||
<CardDescription
|
||||
size="xs"
|
||||
className="mt-1 h-24 overflow-auto"
|
||||
>
|
||||
{tool.description}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-6">
|
||||
{loading ? (
|
||||
<div className="grid auto-rows-fr grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<SkeletonLoader component="addToolCards" count={6} />
|
||||
</div>
|
||||
) : (
|
||||
(['built_in', 'service'] as const).map((group) => {
|
||||
const tools = availableTools.filter(
|
||||
(tool) => (tool.group ?? 'built_in') === group,
|
||||
);
|
||||
if (tools.length === 0) return null;
|
||||
return (
|
||||
<section key={group} className="flex flex-col gap-3">
|
||||
<SectionHeader
|
||||
as="h3"
|
||||
size="sm"
|
||||
title={
|
||||
group === 'built_in'
|
||||
? t('settings.tools.groupBuiltIn')
|
||||
: t('settings.tools.groupService')
|
||||
}
|
||||
/>
|
||||
<div className="grid auto-rows-fr grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{tools.map((tool) => (
|
||||
<Card
|
||||
asChild
|
||||
key={tool.name}
|
||||
variant="outline"
|
||||
padding="lg"
|
||||
interactive
|
||||
className="h-44 w-full"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedTool(tool);
|
||||
handleAddTool(tool);
|
||||
}}
|
||||
>
|
||||
<ToolIcon
|
||||
name={tool.name}
|
||||
className="size-6"
|
||||
title={t('settings.tools.toolIconTitle', {
|
||||
name: tool.displayName,
|
||||
})}
|
||||
/>
|
||||
<CardTitle
|
||||
title={tool.displayName}
|
||||
className="truncate capitalize"
|
||||
>
|
||||
{tool.displayName}
|
||||
</CardTitle>
|
||||
<CardDescription size="xs" className="line-clamp-3">
|
||||
{tool.description}
|
||||
</CardDescription>
|
||||
</button>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})
|
||||
)}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<Button variant="link" size="inline" asChild>
|
||||
<Link
|
||||
to="/settings/connectors"
|
||||
onClick={() => setModalState('INACTIVE')}
|
||||
>
|
||||
{t('settings.tools.browseConnectors')}
|
||||
<ArrowRight className="size-3" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline-primary" shape="pill" size="sm" asChild>
|
||||
<Link
|
||||
to="/settings/connectors?filter=custom"
|
||||
onClick={() => setModalState('INACTIVE')}
|
||||
>
|
||||
<Plus />
|
||||
{t('settings.connectors.addCustom')}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -187,11 +210,6 @@ export default function AddToolModal({
|
||||
tool={selectedTool}
|
||||
getUserTools={getUserTools}
|
||||
/>
|
||||
<MCPServerModal
|
||||
modalState={mcpModalState}
|
||||
setModalState={setMcpModalState}
|
||||
onServerSaved={handleMcpServerAdded}
|
||||
/>
|
||||
<PairDeviceModal
|
||||
modalState={pairModalState}
|
||||
setModalState={setPairModalState}
|
||||
|
||||
@@ -24,4 +24,7 @@ export type AvailableToolType = {
|
||||
description: string;
|
||||
parameters: object;
|
||||
}[];
|
||||
/** `built_in`, `service` (a connector's tool) or `custom` (MCP / OpenAPI). */
|
||||
group?: 'built_in' | 'service' | 'custom';
|
||||
connector_key?: string | null;
|
||||
};
|
||||
@@ -5,8 +5,29 @@ vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
const connectorsState = vi.hoisted(() => ({
|
||||
catalog: [] as Record<string, unknown>[],
|
||||
connections: [] as Record<string, unknown>[],
|
||||
}));
|
||||
|
||||
vi.mock('react-redux', () => ({
|
||||
useSelector: () => null,
|
||||
useSelector: (selector: (state: unknown) => unknown) => {
|
||||
const state = {
|
||||
preference: { token: null, selectedDocs: [] },
|
||||
connectors: {
|
||||
catalog: connectorsState.catalog,
|
||||
connections: connectorsState.connections,
|
||||
loaded: true,
|
||||
loading: false,
|
||||
failed: false,
|
||||
},
|
||||
};
|
||||
try {
|
||||
return selector(state);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
useDispatch: () => vi.fn(),
|
||||
useStore: () => ({ getState: () => ({}) }),
|
||||
}));
|
||||
@@ -84,6 +105,70 @@ describe('Upload source-type tiles', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('groups upload and web apart from connections', async () => {
|
||||
await render();
|
||||
expect(document.body.textContent).toContain(
|
||||
'modals.uploadDoc.groupUploadWeb',
|
||||
);
|
||||
expect(document.body.textContent).toContain(
|
||||
'modals.uploadDoc.groupConnection',
|
||||
);
|
||||
});
|
||||
|
||||
it('shows connector tiles even when the server is not set up', async () => {
|
||||
connectorsState.catalog = [
|
||||
{
|
||||
key: 'google_drive',
|
||||
icon: 'drive',
|
||||
sync_ingestor: 'google_drive',
|
||||
available: false,
|
||||
missing_settings: ['GOOGLE_CLIENT_ID'],
|
||||
},
|
||||
];
|
||||
await render();
|
||||
const drive = tiles().find((tile) =>
|
||||
tile.textContent?.includes(
|
||||
'modals.uploadDoc.ingestors.google_drive.label',
|
||||
),
|
||||
);
|
||||
expect(drive).toBeDefined();
|
||||
expect(drive!.textContent).toContain(
|
||||
'settings.connectors.status.needsAdminSetup',
|
||||
);
|
||||
await act(async () => drive!.click());
|
||||
expect(document.body.textContent).toContain('GOOGLE_CLIENT_ID');
|
||||
connectorsState.catalog = [];
|
||||
});
|
||||
|
||||
it('names the connected account on a connection tile', async () => {
|
||||
connectorsState.catalog = [
|
||||
{
|
||||
key: 'confluence',
|
||||
icon: 'confluence',
|
||||
sync_ingestor: 'confluence',
|
||||
available: true,
|
||||
missing_settings: [],
|
||||
},
|
||||
];
|
||||
connectorsState.connections = [
|
||||
{
|
||||
id: 'c1',
|
||||
connector_key: 'confluence',
|
||||
status: 'connected',
|
||||
account_label: 'alex@example.com',
|
||||
},
|
||||
];
|
||||
await render();
|
||||
const confluence = tiles().find((tile) =>
|
||||
tile.textContent?.includes('modals.uploadDoc.ingestors.confluence.label'),
|
||||
);
|
||||
expect(confluence!.textContent).toContain(
|
||||
'modals.uploadDoc.tileConnectedAs',
|
||||
);
|
||||
connectorsState.catalog = [];
|
||||
connectorsState.connections = [];
|
||||
});
|
||||
|
||||
it('leaves the disabled Train button on the default variant', async () => {
|
||||
await render();
|
||||
const crawler = tiles().find((tile) =>
|
||||
|
||||
+173
-29
@@ -1,4 +1,4 @@
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import { ChevronLeft, CircleAlert } from 'lucide-react';
|
||||
import { envVar } from '@/env';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
@@ -28,6 +28,16 @@ import { Textarea } from '../components/ui/textarea';
|
||||
import { Modal } from '../components/ui/modal';
|
||||
import { Separator } from '../components/ui/separator';
|
||||
import { OptionCard } from '../components/ui/option-card';
|
||||
import { SectionHeader } from '../components/ui/section-header';
|
||||
import { Alert, AlertDescription, AlertTitle } from '../components/ui/alert';
|
||||
import ConnectorIcon from '../connectors/ConnectorIcon';
|
||||
import {
|
||||
loadConnectors,
|
||||
selectConnections,
|
||||
selectConnectorCatalog,
|
||||
selectConnectorsLoaded,
|
||||
} from '../connectors/connectorsSlice';
|
||||
import type { AppDispatch } from '../store';
|
||||
import { ActiveState, Doc } from '../models/misc';
|
||||
|
||||
import { getDocs } from '../preferences/preferenceApi';
|
||||
@@ -38,10 +48,12 @@ import {
|
||||
setSourceDocs,
|
||||
} from '../preferences/preferenceSlice';
|
||||
import {
|
||||
CONNECTION_INGESTORS,
|
||||
IngestorDefaultConfigs,
|
||||
IngestorFormSchemas,
|
||||
getIngestorSchema,
|
||||
IngestorOption,
|
||||
UPLOAD_AND_WEB_INGESTORS,
|
||||
} from '../upload/types/ingestor';
|
||||
import { addUploadTask, updateUploadTask } from './uploadSlice';
|
||||
|
||||
@@ -65,6 +77,7 @@ function Upload({
|
||||
close,
|
||||
onSuccessfulUpload = () => undefined,
|
||||
selectUploadedDoc = true,
|
||||
initialIngestor,
|
||||
}: {
|
||||
receivedFile: File[];
|
||||
setModalState: (state: ActiveState) => void;
|
||||
@@ -79,9 +92,14 @@ function Upload({
|
||||
* uploading never repoints the conversation the user left open.
|
||||
*/
|
||||
selectUploadedDoc?: boolean;
|
||||
/** Open straight on this source type's form (Connect from a connector card). */
|
||||
initialIngestor?: IngestorType;
|
||||
}) {
|
||||
const token = useSelector(selectToken);
|
||||
const selectedDocs = useSelector(selectSelectedDocs);
|
||||
const connectorCatalog = useSelector(selectConnectorCatalog);
|
||||
const connections = useSelector(selectConnections);
|
||||
const connectorsLoaded = useSelector(selectConnectorsLoaded);
|
||||
|
||||
const [files, setfiles] = useState<File[]>(receivedFile);
|
||||
const [activeTab, setActiveTab] = useState<boolean>(true);
|
||||
@@ -334,6 +352,7 @@ function Upload({
|
||||
setSelectedFiles(selectedFileIds);
|
||||
setSelectedFolders(selectedFolderIds);
|
||||
}}
|
||||
onFirstPickName={prefillName}
|
||||
provider={ingestor.type as unknown as string}
|
||||
token={token}
|
||||
initialSelectedFiles={selectedFiles}
|
||||
@@ -341,7 +360,9 @@ function Upload({
|
||||
/>
|
||||
);
|
||||
case 'google_drive_picker':
|
||||
return (
|
||||
// Google's own picker needs the client id in the browser; without it
|
||||
// the server-side file browser lists the same Drive.
|
||||
return envVar('VITE_GOOGLE_CLIENT_ID') ? (
|
||||
<GoogleDrivePicker
|
||||
key={field.name}
|
||||
onSelectionChange={(
|
||||
@@ -351,8 +372,25 @@ function Upload({
|
||||
setSelectedFiles(selectedFileIds);
|
||||
setSelectedFolders(selectedFolderIds);
|
||||
}}
|
||||
onFirstPickName={prefillName}
|
||||
token={token}
|
||||
/>
|
||||
) : (
|
||||
<FilePicker
|
||||
key={field.name}
|
||||
onSelectionChange={(
|
||||
selectedFileIds: string[],
|
||||
selectedFolderIds: string[] = [],
|
||||
) => {
|
||||
setSelectedFiles(selectedFileIds);
|
||||
setSelectedFolders(selectedFolderIds);
|
||||
}}
|
||||
onFirstPickName={prefillName}
|
||||
provider="google_drive"
|
||||
token={token}
|
||||
initialSelectedFiles={selectedFiles}
|
||||
initialSelectedFolders={selectedFolders}
|
||||
/>
|
||||
);
|
||||
case 'share_point_picker':
|
||||
return (
|
||||
@@ -365,6 +403,7 @@ function Upload({
|
||||
setSelectedFiles(selectedFileIds);
|
||||
setSelectedFolders(selectedFolderIds);
|
||||
}}
|
||||
onFirstPickName={prefillName}
|
||||
provider="share_point"
|
||||
token={token}
|
||||
initialSelectedFiles={selectedFiles}
|
||||
@@ -382,6 +421,7 @@ function Upload({
|
||||
setSelectedFiles(selectedFileIds);
|
||||
setSelectedFolders(selectedFolderIds);
|
||||
}}
|
||||
onFirstPickName={prefillName}
|
||||
provider="confluence"
|
||||
token={token}
|
||||
initialSelectedFiles={selectedFiles}
|
||||
@@ -402,17 +442,39 @@ function Upload({
|
||||
const [nameTouched, setNameTouched] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const store = useStore<RootState>();
|
||||
|
||||
const ingestorOptions: IngestorOption[] = IngestorFormSchemas.filter(
|
||||
(schema) => (schema.validate ? schema.validate() : true),
|
||||
).map((schema) => ({
|
||||
label: schema.label,
|
||||
value: schema.key,
|
||||
icon: schema.icon,
|
||||
heading: schema.heading,
|
||||
}));
|
||||
useEffect(() => {
|
||||
if (!connectorsLoaded) dispatch(loadConnectors({ token }));
|
||||
}, [connectorsLoaded, dispatch, token]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialIngestor) handleIngestorTypeChange(initialIngestor);
|
||||
// Only the type the modal opened with; later picks are the user's.
|
||||
}, []);
|
||||
|
||||
/** The name field follows the first picked item until the user edits it. */
|
||||
const prefillName = (name: string) => {
|
||||
if (nameTouched || !name) return;
|
||||
setIngestor((prev) => (prev.name ? prev : { ...prev, name }));
|
||||
};
|
||||
|
||||
const connectorFor = (type: IngestorType | null) =>
|
||||
type && CONNECTION_INGESTORS.includes(type)
|
||||
? connectorCatalog.find((c) => c.sync_ingestor === type)
|
||||
: undefined;
|
||||
const selectedConnector = connectorFor(ingestor.type);
|
||||
const needsSetup = !!selectedConnector && !selectedConnector.available;
|
||||
|
||||
const ingestorOptions: IngestorOption[] = IngestorFormSchemas.map(
|
||||
(schema) => ({
|
||||
label: schema.label,
|
||||
value: schema.key,
|
||||
icon: schema.icon,
|
||||
heading: schema.heading,
|
||||
}),
|
||||
);
|
||||
|
||||
const resetUploaderState = useCallback(() => {
|
||||
setIngestor({ type: null, name: '', config: {} });
|
||||
@@ -923,6 +985,7 @@ function Upload({
|
||||
if (!isPrescreenConfigValid(retrievalOptions)) return true;
|
||||
|
||||
if (!ingestor.type) return true;
|
||||
if (needsSetup) return true;
|
||||
const ingestorSchemaForValidation = getIngestorSchema(
|
||||
ingestor.type as IngestorType,
|
||||
);
|
||||
@@ -1026,28 +1089,109 @@ function Upload({
|
||||
}
|
||||
};
|
||||
|
||||
const connectionTileState = (type: IngestorType) => {
|
||||
const connector = connectorFor(type);
|
||||
if (!connector) return undefined;
|
||||
if (!connector.available)
|
||||
return t('settings.connectors.status.needsAdminSetup');
|
||||
const accounts = connections.filter(
|
||||
(c) => c.connector_key === connector.key && c.status === 'connected',
|
||||
);
|
||||
if (accounts.length === 1)
|
||||
return t('modals.uploadDoc.tileConnectedAs', {
|
||||
account: accounts[0].account_label,
|
||||
interpolation: { escapeValue: false },
|
||||
});
|
||||
if (accounts.length > 1)
|
||||
return t('settings.connectors.status.connectedCount', {
|
||||
count: accounts.length,
|
||||
});
|
||||
return t('settings.connectors.status.connect');
|
||||
};
|
||||
|
||||
const renderIngestorSelection = () => {
|
||||
const optionsFor = (types: IngestorType[]) =>
|
||||
types
|
||||
.map((type) => ingestorOptions.find((o) => o.value === type))
|
||||
.filter((option): option is IngestorOption => !!option);
|
||||
return (
|
||||
<div className="grid w-full grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||
{ingestorOptions.map((option) => (
|
||||
<OptionCard
|
||||
key={option.value}
|
||||
icon={
|
||||
<img
|
||||
src={option.icon}
|
||||
alt={option.label}
|
||||
className="size-6 dark:invert"
|
||||
/>
|
||||
}
|
||||
title={t(`modals.uploadDoc.ingestors.${option.value}.label`)}
|
||||
onClick={() =>
|
||||
handleIngestorTypeChange(option.value as IngestorType)
|
||||
}
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<section className="flex flex-col gap-3">
|
||||
<SectionHeader
|
||||
as="h3"
|
||||
size="sm"
|
||||
title={t('modals.uploadDoc.groupUploadWeb')}
|
||||
/>
|
||||
))}
|
||||
<div className="grid w-full grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||
{optionsFor(UPLOAD_AND_WEB_INGESTORS).map((option) => (
|
||||
<OptionCard
|
||||
key={option.value}
|
||||
icon={
|
||||
<img
|
||||
src={option.icon}
|
||||
alt=""
|
||||
className="size-6 dark:invert"
|
||||
/>
|
||||
}
|
||||
title={t(`modals.uploadDoc.ingestors.${option.value}.label`)}
|
||||
onClick={() => handleIngestorTypeChange(option.value)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section className="flex flex-col gap-3">
|
||||
<SectionHeader
|
||||
as="h3"
|
||||
size="sm"
|
||||
title={t('modals.uploadDoc.groupConnection')}
|
||||
/>
|
||||
<div className="grid w-full grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||
{optionsFor(CONNECTION_INGESTORS).map((option) => {
|
||||
const connector = connectorFor(option.value);
|
||||
return (
|
||||
<OptionCard
|
||||
key={option.value}
|
||||
icon={
|
||||
<ConnectorIcon
|
||||
icon={connector?.icon ?? option.value}
|
||||
className="size-6"
|
||||
/>
|
||||
}
|
||||
title={t(`modals.uploadDoc.ingestors.${option.value}.label`)}
|
||||
description={connectionTileState(option.value)}
|
||||
onClick={() => handleIngestorTypeChange(option.value)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSetupNotice = () =>
|
||||
selectedConnector && needsSetup ? (
|
||||
<Alert variant="warning">
|
||||
<CircleAlert />
|
||||
<AlertTitle>
|
||||
{t('settings.connectors.status.needsAdminSetup')}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<div className="flex flex-col gap-2">
|
||||
{selectedConnector.missing_settings.length > 0 ? (
|
||||
<>
|
||||
<span>{t('settings.connectors.setupSettings')}</span>
|
||||
<code className="font-mono text-xs wrap-anywhere">
|
||||
{selectedConnector.missing_settings.join(', ')}
|
||||
</code>
|
||||
</>
|
||||
) : (
|
||||
<span>{t('settings.connectors.askAdmin')}</span>
|
||||
)}
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null;
|
||||
return (
|
||||
<Modal
|
||||
open={true}
|
||||
@@ -1114,8 +1258,8 @@ function Upload({
|
||||
required={true}
|
||||
className="w-full"
|
||||
/>
|
||||
{renderFormFields()}
|
||||
{ingestor.type !== 'wiki' && (
|
||||
{needsSetup ? renderSetupNotice() : renderFormFields()}
|
||||
{ingestor.type !== 'wiki' && !needsSetup && (
|
||||
<RetrievalOptions
|
||||
value={retrievalOptions}
|
||||
onChange={setRetrievalOptions}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { envVar } from '@/env';
|
||||
import CrawlerIcon from '../../assets/crawler.svg';
|
||||
import FileUploadIcon from '../../assets/file_upload.svg';
|
||||
import UrlIcon from '../../assets/url.svg';
|
||||
@@ -63,10 +62,25 @@ export interface IngestorSchema {
|
||||
label: string;
|
||||
icon: string;
|
||||
heading: string;
|
||||
validate?: () => boolean;
|
||||
fields: FormField[];
|
||||
}
|
||||
|
||||
/** Add Source groups: what needs no account, and what comes from a connection. */
|
||||
export const UPLOAD_AND_WEB_INGESTORS: IngestorType[] = [
|
||||
'local_file',
|
||||
'url',
|
||||
'crawler',
|
||||
'github',
|
||||
'wiki',
|
||||
];
|
||||
export const CONNECTION_INGESTORS: IngestorType[] = [
|
||||
'google_drive',
|
||||
'share_point',
|
||||
'confluence',
|
||||
's3',
|
||||
'reddit',
|
||||
];
|
||||
|
||||
export const IngestorFormSchemas: IngestorSchema[] = [
|
||||
{
|
||||
key: 'local_file',
|
||||
@@ -175,10 +189,6 @@ export const IngestorFormSchemas: IngestorSchema[] = [
|
||||
label: 'Google Drive',
|
||||
icon: DriveIcon,
|
||||
heading: 'Upload from Google Drive',
|
||||
validate: () => {
|
||||
const googleClientId = envVar('VITE_GOOGLE_CLIENT_ID');
|
||||
return !!googleClientId;
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'files',
|
||||
@@ -243,10 +253,6 @@ export const IngestorFormSchemas: IngestorSchema[] = [
|
||||
label: 'Share Point',
|
||||
icon: SharePoint,
|
||||
heading: 'Upload from Share Point',
|
||||
validate: () => {
|
||||
const sharePointClientId = envVar('VITE_SHARE_POINT_CLIENT_ID');
|
||||
return !!sharePointClientId;
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'files',
|
||||
@@ -261,10 +267,6 @@ export const IngestorFormSchemas: IngestorSchema[] = [
|
||||
label: 'Confluence',
|
||||
icon: ConfluenceIcon,
|
||||
heading: 'Upload from Confluence',
|
||||
validate: () => {
|
||||
const confluenceClientId = envVar('VITE_CONFLUENCE_CLIENT_ID');
|
||||
return !!confluenceClientId;
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'files',
|
||||
|
||||
Reference in new issue
Block a user