diff --git a/frontend/src/components/Input.tsx b/frontend/src/components/Input.tsx index 17e60190..f7b1765e 100644 --- a/frontend/src/components/Input.tsx +++ b/frontend/src/components/Input.tsx @@ -7,6 +7,7 @@ const Input = ({ value, isAutoFocused = false, placeholder, + label, maxLength, className, colorVariant = 'silver', @@ -26,21 +27,30 @@ const Input = ({ thick: 'border-2', }; return ( - - {children} - +
+ + {children} + + {label && ( +
+ + {label} + +
+ )} +
); }; diff --git a/frontend/src/components/ToggleSwitch.tsx b/frontend/src/components/ToggleSwitch.tsx new file mode 100644 index 00000000..061d2563 --- /dev/null +++ b/frontend/src/components/ToggleSwitch.tsx @@ -0,0 +1,58 @@ +import React from 'react'; + +type ToggleSwitchProps = { + checked: boolean; + onChange: (checked: boolean) => void; + className?: string; + label?: string; + disabled?: boolean; + activeColor?: string; + inactiveColor?: string; + id?: string; +}; + +const ToggleSwitch: React.FC = ({ + checked, + onChange, + className = '', + label, + disabled = false, + activeColor = 'bg-purple-30', + inactiveColor = 'bg-transparent', + id, +}) => { + return ( + + ); +}; + +export default ToggleSwitch; diff --git a/frontend/src/components/types/index.ts b/frontend/src/components/types/index.ts index 7af1c545..a7ff5405 100644 --- a/frontend/src/components/types/index.ts +++ b/frontend/src/components/types/index.ts @@ -8,6 +8,7 @@ export type InputProps = { maxLength?: number; name?: string; placeholder?: string; + label?: string; className?: string; children?: React.ReactElement; onChange: ( diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx index b1c45156..725ecdb1 100644 --- a/frontend/src/upload/Upload.tsx +++ b/frontend/src/upload/Upload.tsx @@ -8,6 +8,7 @@ import FileUpload from '../assets/file_upload.svg'; import WebsiteCollect from '../assets/website_collect.svg'; import Dropdown from '../components/Dropdown'; import Input from '../components/Input'; +import ToggleSwitch from '../components/ToggleSwitch'; import { ActiveState, Doc } from '../models/misc'; import { getDocs } from '../preferences/preferenceApi'; import { @@ -16,6 +17,27 @@ import { selectSourceDocs, } from '../preferences/preferenceSlice'; import WrapperModal from '../modals/WrapperModal'; +import { + IngestorType, + IngestorConfig, + RedditIngestorConfig, + GithubIngestorConfig, + CrawlerIngestorConfig, + UrlIngestorConfig, + IngestorFormSchemas, + FormField, +} from './types/ingestor'; +import { IngestorDefaultConfigs } from '../upload/types/ingestor'; + +type IngestorState = { + type: IngestorType; + name: string; + config: + | RedditIngestorConfig + | GithubIngestorConfig + | CrawlerIngestorConfig + | UrlIngestorConfig; +}; function Upload({ receivedFile = [], @@ -33,18 +55,106 @@ function Upload({ onSuccessfulUpload?: () => void; }) { const [docName, setDocName] = useState(receivedFile[0]?.name); - const [urlName, setUrlName] = useState(''); - const [url, setUrl] = useState(''); - const [repoUrl, setRepoUrl] = useState(''); // P3f93 - const [redditData, setRedditData] = useState({ - client_id: '', - client_secret: '', - user_agent: '', - search_queries: [''], - number_posts: 10, - }); - const [activeTab, setActiveTab] = useState(renderTab); const [files, setfiles] = useState(receivedFile); + const [activeTab, setActiveTab] = useState(renderTab); + + const renderFormFields = () => { + const schema = IngestorFormSchemas[ingestor.type]; + + return schema.map((field: FormField) => { + switch (field.type) { + case 'string': + return ( +
+ + handleIngestorChange(field.name, e.target.value) + } + borderVariant="thin" + label={field.label} + colorVariant="gray" + /> +
+ ); + case 'number': + return ( +
+ + handleIngestorChange(field.name, parseInt(e.target.value)) + } + borderVariant="thin" + label={field.label} + colorVariant="gray" + /> +
+ ); + case 'enum': + return ( +
+ { + const value = + typeof selected === 'string' ? selected : selected.value; + handleIngestorChange(field.name, value); + }} + size="w-full" + rounded="3xl" + placeholder={field.label} + border="border" + borderColor="gray-5000" + /> +
+ ); + case 'boolean': + return ( +
+ { + const syntheticEvent = { + target: { + name: field.name, + value: checked, + }, + } as unknown as React.ChangeEvent; + handleIngestorChange(field.name, syntheticEvent.target.value); + }} + className="mt-2" + /> +
+ ); + default: + return null; + } + }); + }; + + // New unified ingestor state + const [ingestor, setIngestor] = useState(() => { + const defaultType: IngestorType = 'crawler'; + const defaultConfig = IngestorDefaultConfigs[defaultType]; + return { + type: defaultType, + name: defaultConfig.name, + config: defaultConfig.config, + }; + }); + const [progress, setProgress] = useState<{ type: 'UPLOAD' | 'TRAINING'; percentage: number; @@ -55,12 +165,11 @@ function Upload({ const { t } = useTranslation(); const setTimeoutRef = useRef(); - const urlOptions: { label: string; value: string }[] = [ - { label: `Crawler`, value: 'crawler' }, - // { label: t('modals.uploadDoc.sitemap'), value: 'sitemap' }, - { label: `Link`, value: 'url' }, - { label: `GitHub`, value: 'github' }, - { label: `Reddit`, value: 'reddit' }, + const urlOptions: { label: string; value: IngestorType }[] = [ + { label: 'Crawler', value: 'crawler' }, + { label: 'Link', value: 'url' }, + { label: 'GitHub', value: 'github' }, + { label: 'Reddit', value: 'reddit' }, ]; const [urlType, setUrlType] = useState<{ label: string; value: string }>({ @@ -264,7 +373,8 @@ function Upload({ files.forEach((file) => { formData.append('file', file); }); - formData.append('name', docName); + + formData.append('name', activeTab === 'file' ? docName : ingestor.name); formData.append('user', 'local'); const apiHost = import.meta.env.VITE_API_HOST; const xhr = new XMLHttpRequest(); @@ -284,22 +394,27 @@ function Upload({ const uploadRemote = () => { const formData = new FormData(); - formData.append('name', urlName); + formData.append('name', ingestor.name); formData.append('user', 'local'); - if (urlType !== null) { - formData.append('source', urlType?.value); - } - formData.append('data', url); - if ( - redditData.client_id.length > 0 && - redditData.client_secret.length > 0 - ) { - formData.set('name', 'other'); - formData.set('data', JSON.stringify(redditData)); - } - if (urlType.value === 'github') { - formData.append('repo_url', repoUrl); // Pdeac + formData.append('source', ingestor.type); + + if (ingestor.type === 'reddit') { + const redditConfig = ingestor.config as RedditIngestorConfig; + redditConfig.name = ingestor.name; + formData.set('data', JSON.stringify(redditConfig)); + } else if (ingestor.type === 'github') { + const githubConfig = ingestor.config as GithubIngestorConfig; + githubConfig.name = ingestor.name; + formData.append('repo_url', githubConfig.repo_url); + formData.append('data', githubConfig.repo_url); + } else { + const urlBasedConfig = ingestor.config as + | CrawlerIngestorConfig + | UrlIngestorConfig; + urlBasedConfig.name = ingestor.name; + formData.append('data', urlBasedConfig.url); } + const apiHost = import.meta.env.VITE_API_HOST; const xhr = new XMLHttpRequest(); xhr.upload.addEventListener('progress', (event) => { @@ -346,20 +461,50 @@ function Upload({ }, }); - const handleChange = ( - e: React.ChangeEvent, - ) => { - const { name, value } = e.target; - if (name === 'search_queries' && value.length > 0) { - setRedditData({ - ...redditData, - [name]: value.split(',').map((item) => item.trim()), - }); - } else - setRedditData({ - ...redditData, - [name]: name === 'number_posts' ? parseInt(value) : value, - }); + const isUploadDisabled = () => { + if (activeTab === 'file') { + return !docName || files.length === 0; + } + + if (activeTab !== 'remote') return false; + + if (!ingestor.name) return true; + + return Object.values(ingestor.config).some((value) => { + if (Array.isArray(value)) { + return value.length === 0; + } + return !value; + }); + }; + + const handleIngestorChange = (key: string, value: any) => { + setIngestor((prevState: IngestorConfig): IngestorConfig => { + if (key === 'name') { + return { + ...prevState, + name: value, + }; + } + + return { + ...prevState, + config: { + ...(prevState.config as any), + [key]: value, + }, + }; + }); + }; + + const handleIngestorTypeChange = (type: IngestorType) => { + const defaultConfig = IngestorDefaultConfigs[type]; + + setIngestor({ + type, + name: defaultConfig.name, + config: defaultConfig.config, + }); }; let view; @@ -455,146 +600,29 @@ function Upload({ - setUrlType(value) + selectedValue={ + urlOptions.find((opt) => opt.value === ingestor.type) || null + } + onSelect={(selected: { label: string; value: string }) => + handleIngestorTypeChange(selected.value as IngestorType) } size="w-full" rounded="3xl" /> - {urlType.label !== 'Reddit' && urlType.label !== 'GitHub' ? ( - <> - setUrlName(e.target.value)} - borderVariant="thin" - > -
- - {t('modals.uploadDoc.name')} - -
- setUrl(e.target.value)} - borderVariant="thin" - > -
- - {t('modals.uploadDoc.link')} - -
- - ) : urlType.label === 'GitHub' ? ( // P3f93 - <> - setUrlName(e.target.value)} - borderVariant="thin" - > -
- - {t('modals.uploadDoc.name')} - -
- setRepoUrl(e.target.value)} - borderVariant="thin" - > -
- - {t('modals.uploadDoc.repoUrl')} - -
- - ) : ( -
-
- -
- - {t('modals.uploadDoc.reddit.id')} - -
-
-
- -
- - {t('modals.uploadDoc.reddit.secret')} - -
-
-
- -
- - {t('modals.uploadDoc.reddit.agent')} - -
-
-
- -
- - {t('modals.uploadDoc.reddit.searchQueries')} - -
-
-
- -
- - {t('modals.uploadDoc.reddit.numberOfPosts')} - -
-
-
- )} + {/* Dynamically render form fields based on schema */} + + + setIngestor({ ...ingestor, name: e.target.value }) + } + borderVariant="thin" + placeholder="Name" + label="Name" + /> + {renderFormFields()} )}
@@ -615,33 +643,8 @@ function Upload({ uploadRemote(); } }} - disabled={ - (activeTab === 'file' && (!files.length || !docName)) || - (activeTab === 'remote' && - ((urlType.label !== 'Reddit' && - urlType.label !== 'GitHub' && - (!url || !urlName)) || - (urlType.label === 'GitHub' && !repoUrl) || - (urlType.label === 'Reddit' && - (!redditData.client_id || - !redditData.client_secret || - !redditData.user_agent || - !redditData.search_queries || - !redditData.number_posts)))) - } className={`rounded-3xl px-4 py-2 font-medium ${ - (activeTab === 'file' && (!files.length || !docName)) || - (activeTab === 'remote' && - ((urlType.label !== 'Reddit' && - urlType.label !== 'GitHub' && - (!url || !urlName)) || - (urlType.label === 'GitHub' && !repoUrl) || - (urlType.label === 'Reddit' && - (!redditData.client_id || - !redditData.client_secret || - !redditData.user_agent || - !redditData.search_queries || - !redditData.number_posts)))) + isUploadDisabled() ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'cursor-pointer bg-purple-30 text-white hover:bg-purple-40' }`} diff --git a/frontend/src/upload/types/ingestor.ts b/frontend/src/upload/types/ingestor.ts new file mode 100644 index 00000000..83c74bfd --- /dev/null +++ b/frontend/src/upload/types/ingestor.ts @@ -0,0 +1,136 @@ +export interface BaseIngestorConfig { + name: string; +} + +export interface RedditIngestorConfig extends BaseIngestorConfig { + client_id: string; + client_secret: string; + user_agent: string; + search_queries: string; + number_posts: number; +} + +export interface GithubIngestorConfig extends BaseIngestorConfig { + repo_url: string; +} + +export interface CrawlerIngestorConfig extends BaseIngestorConfig { + url: string; +} + +export interface UrlIngestorConfig extends BaseIngestorConfig { + url: string; +} + +export type IngestorType = 'crawler' | 'github' | 'reddit' | 'url'; + +export interface IngestorConfig { + type: IngestorType; + name: string; + config: + | RedditIngestorConfig + | GithubIngestorConfig + | CrawlerIngestorConfig + | UrlIngestorConfig; +} + +export type IngestorFormData = { + name: string; + user: string; + source: IngestorType; + data: string; +}; + +export type FieldType = 'string' | 'number' | 'enum' | 'boolean'; + +export interface FormField { + name: keyof BaseIngestorConfig | string; + label: string; + type: FieldType; + options?: { label: string; value: string }[]; +} + +export const IngestorFormSchemas: Record = { + crawler: [ + { + name: 'url', + label: 'URL', + type: 'string', + }, + ], + url: [ + { + name: 'url', + label: 'URL', + type: 'string', + }, + ], + reddit: [ + { + name: 'client_id', + label: 'Client ID', + type: 'string', + }, + { + name: 'client_secret', + label: 'Client Secret', + type: 'string', + }, + { + name: 'user_agent', + label: 'User Agent', + type: 'string', + }, + { + name: 'search_queries', + label: 'Search Queries', + type: 'string', + }, + { + name: 'number_posts', + label: 'Number of Posts', + type: 'number', + }, + ], + github: [ + { + name: 'repo_url', + label: 'Repository URL', + type: 'string', + }, + ], +}; + +export const IngestorDefaultConfigs: Record< + IngestorType, + Omit +> = { + crawler: { + name: '', + config: { + url: '', + } as CrawlerIngestorConfig, + }, + url: { + name: '', + config: { + url: '', + } as UrlIngestorConfig, + }, + reddit: { + name: '', + config: { + client_id: '', + client_secret: '', + user_agent: '', + search_queries: '', + number_posts: 10, + } as RedditIngestorConfig, + }, + github: { + name: '', + config: { + repo_url: '', + } as GithubIngestorConfig, + }, +};