diff --git a/frontend/src/components/Layout/PageLayout.tsx b/frontend/src/components/Layout/PageLayout.tsx index 5d544f4f8..a92a64c01 100644 --- a/frontend/src/components/Layout/PageLayout.tsx +++ b/frontend/src/components/Layout/PageLayout.tsx @@ -23,6 +23,7 @@ import PredefinedSchemaDialog from '../Popups/GraphEnhancementDialog/EnitityExtr import { SKIP_AUTH } from '../../utils/Constants'; import { useNavigate } from 'react-router'; import { deduplicateByFullPattern, deduplicateNodeByValue } from '../../utils/Utils'; +import DataImporterSchemaDailog from '../Popups/GraphEnhancementDialog/EnitityExtraction/DataImporter'; const GCSModal = lazy(() => import('../DataSources/GCS/GCSModal')); const S3Modal = lazy(() => import('../DataSources/AWS/S3Modal')); @@ -193,6 +194,11 @@ const PageLayout: React.FC = () => { allPatterns, selectedNodes, selectedRels, + dataImporterSchemaDialog, + setDataImporterSchemaDialog, + setImporterPattern, + setImporterNodes, + setImporterRels, } = useFileContext(); const navigate = useNavigate(); const { user, isAuthenticated } = useAuth0(); @@ -465,6 +471,45 @@ const PageLayout: React.FC = () => { [] ); + const handleImporterApply = useCallback( + ( + newPatterns: string[], + nodes: OptionType[], + rels: OptionType[], + updatedSource: OptionType[], + updatedTarget: OptionType[], + updatedType: OptionType[] + ) => { + setImporterPattern((prevPatterns: string[]) => { + const uniquePatterns = Array.from(new Set([...newPatterns, ...prevPatterns])); + return uniquePatterns; + }); + setCombinedPatternsVal((prevPatterns: string[]) => { + const uniquePatterns = Array.from(new Set([...newPatterns, ...prevPatterns])); + return uniquePatterns; + }); + setDataImporterSchemaDialog({ + triggeredFrom: 'importerSchemaApply', + show: true, + }); + setSchemaView('importer'); + setImporterNodes(nodes); + setCombinedNodesVal((prevNodes: OptionType[]) => { + const combined = [...nodes, ...prevNodes]; + return deduplicateNodeByValue(combined); + }); + setImporterRels(rels); + setCombinedRelsVal((prevRels: OptionType[]) => { + const combined = [...rels, ...prevRels]; + return deduplicateByFullPattern(combined); + }); + localStorage.setItem(LOCAL_KEYS.source, JSON.stringify(updatedSource)); + localStorage.setItem(LOCAL_KEYS.type, JSON.stringify(updatedType)); + localStorage.setItem(LOCAL_KEYS.target, JSON.stringify(updatedTarget)); + }, + [] + ); + const openPredefinedSchema = useCallback(() => { setPredefinedSchemaDialog({ triggeredFrom: 'predefinedDialog', show: true }); }, []); @@ -477,6 +522,10 @@ const PageLayout: React.FC = () => { setShowTextFromSchemaDialog({ triggeredFrom: 'schemadialog', show: true }); }, []); + const openDataImporterSchema = useCallback(() => { + setDataImporterSchemaDialog({ triggeredFrom: 'schemadialog', show: true }); + }, []); + const openChatBot = useCallback(() => setShowChatBot(true), []); return ( @@ -565,6 +614,20 @@ const PageLayout: React.FC = () => { }} onApply={handlePredinedApply} > + { + setDataImporterSchemaDialog({ triggeredFrom: '', show: false }); + switch (dataImporterSchemaDialog.triggeredFrom) { + case 'enhancementtab': + toggleEnhancementDialog(); + break; + default: + break; + } + }} + onApply={handleImporterApply} + > {isLargeDesktop ? (
{ openTextSchema={openTextSchema} openLoadSchema={openLoadSchema} openPredefinedSchema={openPredefinedSchema} + openDataImporterSchema={openDataImporterSchema} showEnhancementDialog={showEnhancementDialog} toggleEnhancementDialog={toggleEnhancementDialog} setOpenConnection={setOpenConnection} @@ -670,6 +734,7 @@ const PageLayout: React.FC = () => { openTextSchema={openTextSchema} openLoadSchema={openLoadSchema} openPredefinedSchema={openPredefinedSchema} + openDataImporterSchema={openDataImporterSchema} showEnhancementDialog={showEnhancementDialog} toggleEnhancementDialog={toggleEnhancementDialog} setOpenConnection={setOpenConnection} diff --git a/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/DataImporter.tsx b/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/DataImporter.tsx new file mode 100644 index 000000000..bedfd181c --- /dev/null +++ b/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/DataImporter.tsx @@ -0,0 +1,179 @@ +import { Button, Dialog } from '@neo4j-ndl/react'; +import { useState } from 'react'; +import { OptionType, TupleType } from '../../../../types'; +import { extractOptions, updateSourceTargetTypeOptions } from '../../../../utils/Utils'; +import { useFileContext } from '../../../../context/UsersFiles'; +import ImporterInput from './ImporterInput'; +import SchemaViz from '../../../Graph/SchemaViz'; +import PatternContainer from './PatternContainer'; +import UploadJsonData from './UploadJsonData'; + +interface DataImporterDialogProps { + open: boolean; + onClose: () => void; + onApply: ( + patterns: string[], + nodeLabels: OptionType[], + relationshipLabels: OptionType[], + updatedSource: OptionType[], + updatedTarget: OptionType[], + updatedType: OptionType[] + ) => void; +} + +const DataImporterSchemaDailog = ({ open, onClose, onApply }: DataImporterDialogProps) => { + const { + importerPattern, + setImporterPattern, + importerNodes, + setImporterNodes, + importerRels, + setImporterRels, + sourceOptions, + setSourceOptions, + targetOptions, + setTargetOptions, + typeOptions, + setTypeOptions, + } = useFileContext(); + + const [openGraphView, setOpenGraphView] = useState(false); + const [viewPoint, setViewPoint] = useState(''); + const handleCancel = () => { + onClose(); + setImporterPattern([]); + setImporterNodes([]); + setImporterRels([]); + }; + + const handleImporterCheck = async () => { + const [newSourceOptions, newTargetOptions, newTypeOptions] = await updateSourceTargetTypeOptions({ + patterns: importerPattern.map((label) => ({ label, value: label })), + currentSourceOptions: sourceOptions, + currentTargetOptions: targetOptions, + currentTypeOptions: typeOptions, + setSourceOptions, + setTargetOptions, + setTypeOptions, + }); + onApply(importerPattern, importerNodes, importerRels, newSourceOptions, newTargetOptions, newTypeOptions); + onClose(); + }; + + const handleRemovePattern = (patternToRemove: string) => { + const updatedPatterns = importerPattern.filter((p) => p !== patternToRemove); + if (updatedPatterns.length === 0) { + setImporterPattern([]); + setImporterNodes([]); + setImporterRels([]); + return; + } + const updatedTuples: TupleType[] = updatedPatterns + .map((item: string) => { + const matchResult = item.match(/^(.+?)-\[:([A-Z_]+)\]->(.+)$/); + if (matchResult) { + const [source, rel, target] = matchResult.slice(1).map((s) => s.trim()); + return { + value: `${source},${rel},${target}`, + label: `${source} -[:${rel}]-> ${target}`, + source, + target, + type: rel, + }; + } + return null; + }) + .filter(Boolean) as TupleType[]; + const { nodeLabelOptions, relationshipTypeOptions } = extractOptions(updatedTuples); + setImporterPattern(updatedPatterns); + setImporterNodes(nodeLabelOptions); + setImporterRels(relationshipTypeOptions); + }; + + const handleSchemaView = () => { + setOpenGraphView(true); + setViewPoint('showSchemaView'); + }; + + return ( + <> + + Entity Graph Extraction Settings + + + { + const nodeLabelMap = Object.fromEntries(nodeLabels.map((n) => [n.$id, n.token])); + const relTypeMap = Object.fromEntries(relationshipTypes.map((r) => [r.$id, r.token])); + const nodeIdToLabel: Record = {}; + nodeObjectTypes.forEach((nodeObj: any) => { + const labelRef = nodeObj.labels?.[0]?.$ref; + if (labelRef && nodeLabelMap[labelRef.slice(1)]) { + nodeIdToLabel[nodeObj.$id] = nodeLabelMap[labelRef.slice(1)]; + } + }); + + const patterns = relationshipObjectTypes.map((relObj) => { + const fromId = relObj.from.$ref.slice(1); + const toId = relObj.to.$ref.slice(1); + const relId = relObj.type.$ref.slice(1); + const fromLabel = nodeIdToLabel[fromId] || 'source'; + const toLabel = nodeIdToLabel[toId] || 'target'; + const relLabel = relTypeMap[relId] || 'type'; + const pattern = `${fromLabel} -[:${relLabel}]-> ${toLabel}`; + return pattern; + }); + + const importerTuples = patterns + .map((p) => { + const match = p.match(/^(.+?) -\[:(.+?)\]-> (.+)$/); + if (!match) { + return null; + } + const [_, source, type, target] = match; + return { + label: `${source} -[:${type}]-> ${target}`, + value: `${source},${type},${target}`, + source, + target, + type, + }; + }) + .filter(Boolean) as TupleType[]; + const { nodeLabelOptions, relationshipTypeOptions } = extractOptions(importerTuples); + setImporterNodes(nodeLabelOptions); + setImporterRels(relationshipTypeOptions); + setImporterPattern(patterns); + }} + /> + + + + + + + + {openGraphView && ( + + )} + + ); +}; + +export default DataImporterSchemaDailog; diff --git a/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/ImporterInput.tsx b/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/ImporterInput.tsx new file mode 100644 index 000000000..69d4288c5 --- /dev/null +++ b/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/ImporterInput.tsx @@ -0,0 +1,67 @@ +import { useState, useCallback, KeyboardEvent, ChangeEvent, FocusEvent } from 'react'; +import { Box, TextInput, Button } from '@neo4j-ndl/react'; +import { importerValidation } from '../../../../utils/Utils'; + +const ImporterInput = () => { + const [value, setValue] = useState(''); + const [isValid, setIsValid] = useState(false); + const [isFocused, setIsFocused] = useState(false); + const handleChange = (e: ChangeEvent) => { + const newValue = e.target.value; + setValue(newValue); + setIsValid(importerValidation(newValue)); + }; + const handleBlur = () => { + setIsFocused(false); + setIsValid(importerValidation(value)); + }; + const handleFocus = () => { + setIsFocused(true); + }; + const handleSubmit = useCallback(() => { + if (importerValidation(value)) { + window.open(value, '_blank'); + } + }, [value]); + const handleCancel = () => { + setValue(''); + setIsValid(false); + }; + const handleKeyDown = (e: KeyboardEvent) => { + if (e.code === 'Enter' && isValid) { + handleSubmit(); + } + }; + const isEmpty = value.trim() === ''; + return ( + +
+ +
+
+ + +
+
+ ); +}; + +export default ImporterInput; diff --git a/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/NewEntityExtractionSetting.tsx b/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/NewEntityExtractionSetting.tsx index 727f27d4c..842818075 100644 --- a/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/NewEntityExtractionSetting.tsx +++ b/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/NewEntityExtractionSetting.tsx @@ -33,6 +33,7 @@ export default function NewEntityExtractionSetting({ setCombinedNodes, combinedRels, setCombinedRels, + openDataImporterSchema, }: { view: 'Dialog' | 'Tabs'; open?: boolean; @@ -49,6 +50,7 @@ export default function NewEntityExtractionSetting({ setCombinedNodes: Dispatch>; combinedRels: OptionType[]; setCombinedRels: Dispatch>; + openDataImporterSchema: () => void; }) { const { setSelectedRels, @@ -288,6 +290,16 @@ export default function NewEntityExtractionSetting({ openLoadSchema(); }, []); + const onDataImporterSchemaCLick: MouseEventHandler = useCallback(async () => { + if (view === 'Dialog' && onClose != undefined) { + onClose(); + } + if (view === 'Tabs' && closeEnhanceGraphSchemaDialog != undefined) { + closeEnhanceGraphSchemaDialog(); + } + openDataImporterSchema(); + }, []); + return (
@@ -363,6 +375,14 @@ export default function NewEntityExtractionSetting({ } onClick={onSchemaFromTextCLick} /> + + Data Importer JSON + + } + onClick={onDataImporterSchemaCLick} + /> diff --git a/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/UploadJsonData.tsx b/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/UploadJsonData.tsx new file mode 100644 index 000000000..535133244 --- /dev/null +++ b/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/UploadJsonData.tsx @@ -0,0 +1,104 @@ +import { Dropzone, Flex, Typography } from '@neo4j-ndl/react'; +import { useState } from 'react'; +import { IconButtonWithToolTip } from '../../../UI/IconButtonToolTip'; +import { InformationCircleIconOutline } from '@neo4j-ndl/react/icons'; +import { showErrorToast } from '../../../../utils/Toasts'; +import { buttonCaptions } from '../../../../utils/Constants'; +import Loader from '../../../../utils/Loader'; + +interface GraphSchema { + nodeLabels: any[]; + relationshipTypes: any[]; + relationshipObjectTypes: any[]; + nodeObjectTypes: any[]; +} +interface UploadJsonDataProps { + onSchemaExtracted: (schema: GraphSchema) => void; +} +const UploadJsonData = ({ onSchemaExtracted }: UploadJsonDataProps) => { + const [isLoading, setIsLoading] = useState(false); + const onDropHandler = async (files: Partial[]) => { + const file = files[0]; + if (!file) { + return; + } + setIsLoading(true); + try { + const fileReader = new FileReader(); + fileReader.onload = (event) => { + try { + const jsonText = event.target?.result as string; + const parsed = JSON.parse(jsonText); + const graphSchema = parsed?.dataModel?.graphSchemaRepresentation?.graphSchema; + if ( + graphSchema && + Array.isArray(graphSchema.nodeLabels) && + Array.isArray(graphSchema.relationshipTypes) && + Array.isArray(graphSchema.relationshipObjectTypes) && + Array.isArray(graphSchema.nodeObjectTypes) + ) { + onSchemaExtracted({ + nodeLabels: graphSchema.nodeLabels, + relationshipTypes: graphSchema.relationshipTypes, + relationshipObjectTypes: graphSchema.relationshipObjectTypes, + nodeObjectTypes: graphSchema.nodeObjectTypes, + }); + } else { + showErrorToast('Invalid graphSchema format'); + } + } catch (err) { + console.error(err); + showErrorToast('Failed to parse JSON file.'); + } finally { + setIsLoading(false); + } + }; + fileReader.readAsText(file as File); + } catch (err) { + console.error(err); + showErrorToast('Error reading file.'); + setIsLoading(false); + } + }; + return ( + } + isTesting={true} + className='bg-none! dropzoneContainer' + supportedFilesDescription={ + + + {buttonCaptions.importDropzoneSpan} +
+ + + JSON (.json) + + + } + > + + +
+
+
+ } + dropZoneOptions={{ + accept: { + 'application/json': ['.json'], + }, + onDrop: onDropHandler, + onDropRejected: (e) => { + if (e.length) { + showErrorToast('Failed To Upload, Unsupported file extension.'); + } + }, + }} + /> + ); +}; +export default UploadJsonData; diff --git a/frontend/src/components/Popups/GraphEnhancementDialog/index.tsx b/frontend/src/components/Popups/GraphEnhancementDialog/index.tsx index 46114f765..323b66b0d 100644 --- a/frontend/src/components/Popups/GraphEnhancementDialog/index.tsx +++ b/frontend/src/components/Popups/GraphEnhancementDialog/index.tsx @@ -50,6 +50,7 @@ export default function GraphEnhancementDialog({ setPreDefinedPattern, setSelectedPreDefOption, allPatterns, + setDataImporterSchemaDialog, } = useFileContext(); const isTablet = useMediaQuery(`(min-width:${breakpoints.xs}) and (max-width: ${breakpoints.lg})`); @@ -193,6 +194,9 @@ export default function GraphEnhancementDialog({ setCombinedNodes={setCombinedNodes} combinedRels={combinedRels} setCombinedRels={setCombinedRels} + openDataImporterSchema={() => { + setDataImporterSchemaDialog({ triggeredFrom: 'enhancementtab', show: true }); + }} />
diff --git a/frontend/src/context/UsersFiles.tsx b/frontend/src/context/UsersFiles.tsx index 8b082e3d3..ab05093b5 100644 --- a/frontend/src/context/UsersFiles.tsx +++ b/frontend/src/context/UsersFiles.tsx @@ -7,6 +7,7 @@ import { showTextFromSchemaDialogType, schemaLoadDialogType, predefinedSchemaDialogType, + dataImporterSchemaDialogType, } from '../types'; import { chatModeLables, @@ -69,6 +70,11 @@ const FileContextProvider: FC = ({ children }) => { show: false, }); + const [dataImporterSchemaDialog, setDataImporterSchemaDialog] = useState({ + triggeredFrom: '', + show: false, + }); + const [postProcessingTasks, setPostProcessingTasks] = useState([ 'materialize_text_chunk_similarities', 'enable_hybrid_search_and_fulltext_search_in_bloom', @@ -97,6 +103,11 @@ const FileContextProvider: FC = ({ children }) => { const [typeOptions, setTypeOptions] = useState(initialTypeOptions); const [targetOptions, setTargetOptions] = useState(initialTargetOptions); + // Importer schema + const [importerNodes, setImporterNodes] = useState([]); + const [importerRels, setImporterRels] = useState([]); + const [importerPattern, setImporterPattern] = useState([]); + useEffect(() => { if (selectedNodeLabelstr != null) { const selectedNodeLabel = JSON.parse(selectedNodeLabelstr); @@ -213,6 +224,14 @@ const FileContextProvider: FC = ({ children }) => { setTypeOptions, targetOptions, setTargetOptions, + dataImporterSchemaDialog, + setDataImporterSchemaDialog, + importerNodes, + setImporterNodes, + importerRels, + setImporterRels, + importerPattern, + setImporterPattern, }; return {children}; }; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index d544ed46c..36b476957 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -165,6 +165,7 @@ export interface ContentProps { setCombinedNodes: Dispatch>; combinedRels: OptionType[]; setCombinedRels: Dispatch>; + openDataImporterSchema: () => void; } export interface FileTableProps { @@ -870,6 +871,12 @@ export interface predefinedSchemaDialogType { onApply?: (selectedPattern: string[], nodes: OptionType[], rels: OptionType[]) => void; } +export interface dataImporterSchemaDialogType { + triggeredFrom: string; + show: boolean; + onApply?: (selectedPattern: string[], nodes: OptionType[], rels: OptionType[]) => void; +} + export interface FileContextType { files: (File | null)[] | []; filesData: CustomFile[] | []; @@ -956,6 +963,16 @@ export interface FileContextType { setTypeOptions: Dispatch>; targetOptions: OptionType[]; setTargetOptions: Dispatch>; + + // importer defined schema + dataImporterSchemaDialog: dataImporterSchemaDialogType; + setDataImporterSchemaDialog: React.Dispatch>; + importerNodes: OptionType[]; + setImporterNodes: Dispatch>; + importerRels: OptionType[]; + setImporterRels: Dispatch>; + importerPattern: string[]; + setImporterPattern: Dispatch>; } export declare type Side = 'top' | 'right' | 'bottom' | 'left'; diff --git a/frontend/src/utils/Constants.ts b/frontend/src/utils/Constants.ts index 8a13a7b01..0b7c14d2b 100644 --- a/frontend/src/utils/Constants.ts +++ b/frontend/src/utils/Constants.ts @@ -188,6 +188,7 @@ export const tooltips = { visualizeGraph: 'Visualize Graph Schema', additionalInstructions: 'Analyze instructions for schema', predinedSchema: 'Predefined Schema', + dataImporterJson: 'Data Importer JSON', }; export const PRODMODLES = ['openai_gpt_4o', 'openai_gpt_4o_mini', 'diffbot', 'gemini_1.5_flash']; export const buttonCaptions = { @@ -215,6 +216,7 @@ export const buttonCaptions = { provideAdditionalInstructions: 'Provide Additional Instructions for Entity Extractions', analyzeInstructions: 'Analyze Instructions', helpInstructions: 'Provide specific instructions for entity extraction, such as focusing on the key topics.', + importDropzoneSpan: 'JSON Documents', }; export const POST_PROCESSING_JOBS: { title: string; description: string }[] = [ @@ -372,6 +374,7 @@ export const appLabels = { chunkingConfiguration: 'Select a Chunking Configuration', graphPatternTuple: 'Graph Pattern', selectedPatterns: 'Selected Patterns', + dataImporterSchema: 'Schema from Data Importer', }; export const LLMDropdownLabel = { diff --git a/frontend/src/utils/Utils.ts b/frontend/src/utils/Utils.ts index 0996932d4..393df4c36 100644 --- a/frontend/src/utils/Utils.ts +++ b/frontend/src/utils/Utils.ts @@ -892,3 +892,7 @@ export const deduplicateByFullPattern = (arrays: { value: string; label: string }); return result; }; + +export const importerValidation = (url: string) => { + return url.trim() !== '' && /^https:\/\/console-preview\.neo4j\.io\/tools\/import\/models(\/.*)?$/i.test(url); +};