diff --git a/backend/example.env b/backend/example.env index 2822c92e7..c0c24a90e 100644 --- a/backend/example.env +++ b/backend/example.env @@ -37,15 +37,14 @@ LLM_MODEL_CONFIG_openai_gpt_o3_mini="o3-mini-2025-01-31,openai_api_key" LLM_MODEL_CONFIG_gemini_1.5_pro="gemini-1.5-pro-002" LLM_MODEL_CONFIG_gemini_1.5_flash="gemini-1.5-flash-002" LLM_MODEL_CONFIG_gemini_2.0_flash="gemini-2.0-flash-001" -LLM_MODEL_CONFIG_gemini_2.5_pro="gemini-2.5-pro-exp-03-25" +LLM_MODEL_CONFIG_gemini_2.5_pro="gemini-2.5-pro" LLM_MODEL_CONFIG_diffbot="diffbot,diffbot_api_key" LLM_MODEL_CONFIG_azure_ai_gpt_35="azure_deployment_name,azure_endpoint or base_url,azure_api_key,api_version" LLM_MODEL_CONFIG_azure_ai_gpt_4o="gpt-4o,https://YOUR-ENDPOINT.openai.azure.com/,azure_api_key,api_version" LLM_MODEL_CONFIG_groq_llama3_70b="model_name,base_url,groq_api_key" -LLM_MODEL_CONFIG_anthropic_claude_3_5_sonnet="model_name,anthropic_api_key" +LLM_MODEL_CONFIG_anthropic_claude_4_sonnet="model_name,anthropic_api_key" #model_name="claude-sonnet-4-20250514" LLM_MODEL_CONFIG_fireworks_llama4_maverick="model_name,fireworks_api_key" -LLM_MODEL_CONFIG_bedrock_claude_3_5_sonnet="model_name,aws_access_key_id,aws_secret__access_key,region_name" -LLM_MODEL_CONFIG_ollama_llama3="model_name,model_local_url" +LLM_MODEL_CONFIG_ollama_llama3="llama3_model_name,model_local_url" YOUTUBE_TRANSCRIPT_PROXY="https://user:pass@domain:port" EFFECTIVE_SEARCH_RATIO=5 GRAPH_CLEANUP_MODEL="openai_gpt_4o" diff --git a/backend/requirements.txt b/backend/requirements.txt index 7ced40181..914bbf19d 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,64 +1,65 @@ -accelerate==1.6.0 +accelerate==1.7.0 asyncio==3.4.3 -boto3==1.37.29 -botocore==1.37.29 -certifi==2025.1.31 -fastapi==0.115.11 +boto3==1.38.36 +botocore==1.38.36 +certifi==2025.6.15 +fastapi==0.115.12 fastapi-health==0.4.0 -google-api-core==2.24.2 -google-auth==2.38.0 -google_auth_oauthlib==1.2.1 +fireworks-ai==0.15.12 +google-api-core==2.25.1 +google-auth==2.40.3 +google_auth_oauthlib==1.2.2 google-cloud-core==2.4.3 json-repair==0.39.1 pip-install==1.3.5 -langchain==0.3.23 -langchain-aws==0.2.18 -langchain-anthropic==0.3.9 -langchain-fireworks==0.2.9 -langchain-community==0.3.19 -langchain-core==0.3.51 +langchain==0.3.25 +langchain-aws==0.2.25 +langchain-anthropic==0.3.15 +langchain-fireworks==0.3.0 +langchain-community==0.3.25 +langchain-core==0.3.65 langchain-experimental==0.3.4 -langchain-google-vertexai==2.0.19 -langchain-groq==0.2.5 -langchain-openai==0.3.12 +langchain-google-vertexai==2.0.25 +langchain-groq==0.3.2 +langchain-openai==0.3.23 langchain-text-splitters==0.3.8 -langchain-huggingface==0.1.2 +langchain-huggingface==0.3.0 langdetect==1.0.9 -langsmith==0.3.26 +langsmith==0.3.45 langserve==0.3.1 -neo4j-rust-ext +neo4j-rust-ext==5.28.1.0 nltk==3.9.1 -openai==1.71.0 +openai==1.86.0 opencv-python==4.11.0.86 psutil==7.0.0 -pydantic==2.10.6 -python-dotenv==1.0.1 +pydantic==2.11.7 +python-dotenv==1.1.0 python-magic==0.4.27 PyPDF2==3.0.1 -PyMuPDF==1.25.5 -starlette==0.46.1 -sse-starlette==2.2.1 +PyMuPDF==1.26.1 +starlette==0.46.2 +sse-starlette==2.3.6 starlette-session==0.4.3 tqdm==4.67.1 unstructured[all-docs] unstructured==0.17.2 -unstructured-client==0.32.3 -unstructured-inference==0.8.10 -urllib3==2.3.0 -uvicorn==0.34.0 +unstructured-client==0.36.0 +unstructured-inference==1.0.5 +urllib3==2.4.0 +uvicorn==0.34.3 gunicorn==23.0.0 wikipedia==1.4.0 wrapt==1.17.2 -yarl==1.18.3 -youtube-transcript-api==1.0.3 -zipp==3.21.0 -sentence-transformers==4.0.2 -google-cloud-logging==3.11.4 +yarl==1.20.1 +youtube-transcript-api==1.1.0 +zipp==3.23.0 +sentence-transformers==4.1.0 +google-cloud-logging==3.12.1 pypandoc==1.15 -graphdatascience==1.14 +graphdatascience==1.15.1 Secweb==1.18.1 -ragas==0.2.14 +ragas==0.2.15 rouge_score==0.1.2 langchain-neo4j==0.4.0 pypandoc-binary==1.15 -chardet==5.2.0 +chardet==5.2.0 \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index ecd2d4368..ed0b788ef 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,12 +16,12 @@ "@auth0/auth0-react": "^2.2.4", "@emotion/styled": "^11.14.0", "@mui/material": "^5.15.10", - "@mui/styled-engine": "^7.0.2", + "@mui/styled-engine": "^7.1.0", "@neo4j-devtools/word-color": "^0.0.8", "@neo4j-ndl/base": "^3.2.9", "@neo4j-ndl/react": "^3.2.18", "@neo4j-nvl/base": "^0.3.6", - "@neo4j-nvl/react": "^0.3.7", + "@neo4j-nvl/react": "^0.3.8", "@react-oauth/google": "^0.12.1", "@tanstack/react-table": "^8.20.5", "@types/uuid": "^10.0.0", @@ -46,7 +46,7 @@ "@types/react-dom": "^18.2.7", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^6.0.0", - "@vitejs/plugin-react": "^4.0.3", + "@vitejs/plugin-react": "^4.5.0", "eslint": "^8.45.0", "eslint-config-prettier": "^10.1.1", "eslint-plugin-react-hooks": "^5.1.0", diff --git a/frontend/src/components/Layout/PageLayout.tsx b/frontend/src/components/Layout/PageLayout.tsx index 3acc3bf18..a7dfa1c0a 100644 --- a/frontend/src/components/Layout/PageLayout.tsx +++ b/frontend/src/components/Layout/PageLayout.tsx @@ -16,13 +16,14 @@ import { envConnectionAPI } from '../../services/ConnectAPI'; import { healthStatus } from '../../services/HealthStatus'; import { useAuth0 } from '@auth0/auth0-react'; import { showErrorToast } from '../../utils/Toasts'; -import { APP_SOURCES, LOCAL_KEYS } from '../../utils/Constants'; +import { APP_SOURCES } from '../../utils/Constants'; import { createDefaultFormData } from '../../API/Index'; import LoadDBSchemaDialog from '../Popups/GraphEnhancementDialog/EnitityExtraction/LoadExistingSchema'; import PredefinedSchemaDialog from '../Popups/GraphEnhancementDialog/EnitityExtraction/PredefinedSchemaDialog'; import { SKIP_AUTH } from '../../utils/Constants'; import { useNavigate } from 'react-router'; import { deduplicateByFullPattern, deduplicateNodeByValue } from '../../utils/Utils'; +import DataImporterSchemaDialog from '../Popups/GraphEnhancementDialog/EnitityExtraction/DataImporter'; const GCSModal = lazy(() => import('../DataSources/GCS/GCSModal')); @@ -187,13 +188,20 @@ const PageLayout: React.FC = () => { setSchemaValRels, setDbNodes, setDbRels, - setSchemaView, setPreDefinedNodes, setPreDefinedRels, setPreDefinedPattern, allPatterns, selectedNodes, selectedRels, + dataImporterSchemaDialog, + setDataImporterSchemaDialog, + setImporterPattern, + setImporterNodes, + setImporterRels, + setSourceOptions, + setTargetOptions, + setTypeOptions, } = useFileContext(); const navigate = useNavigate(); const { user, isAuthenticated } = useAuth0(); @@ -381,10 +389,9 @@ const PageLayout: React.FC = () => { const combined = [...rels, ...prevRels]; return deduplicateByFullPattern(combined); }); - setSchemaView('text'); - localStorage.setItem(LOCAL_KEYS.source, JSON.stringify(updatedSource)); - localStorage.setItem(LOCAL_KEYS.type, JSON.stringify(updatedType)); - localStorage.setItem(LOCAL_KEYS.target, JSON.stringify(updatedTarget)); + setSourceOptions((prev) => [...prev, ...updatedSource]); + setTargetOptions((prev) => [...prev, ...updatedTarget]); + setTypeOptions((prev) => [...prev, ...updatedType]); }, [] ); @@ -410,7 +417,6 @@ const PageLayout: React.FC = () => { triggeredFrom: 'loadExistingSchemaApply', show: true, }); - setSchemaView('db'); setDbNodes(nodes); setCombinedNodesVal((prevNodes: OptionType[]) => { const combined = [...nodes, ...prevNodes]; @@ -421,9 +427,9 @@ const PageLayout: React.FC = () => { 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)); + setSourceOptions((prev) => [...prev, ...updatedSource]); + setTargetOptions((prev) => [...prev, ...updatedTarget]); + setTypeOptions((prev) => [...prev, ...updatedType]); }, [] ); @@ -448,7 +454,6 @@ const PageLayout: React.FC = () => { triggeredFrom: 'predefinedSchemaApply', show: true, }); - setSchemaView('preDefined'); setPreDefinedNodes(nodes); setCombinedNodesVal((prevNodes: OptionType[]) => { const combined = [...nodes, ...prevNodes]; @@ -459,9 +464,47 @@ const PageLayout: React.FC = () => { 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)); + setSourceOptions((prev) => [...prev, ...updatedSource]); + setTargetOptions((prev) => [...prev, ...updatedTarget]); + setTypeOptions((prev) => [...prev, ...updatedType]); + }, + [] + ); + + 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, + }); + setImporterNodes(nodes); + setCombinedNodesVal((prevNodes: OptionType[]) => { + const combined = [...nodes, ...prevNodes]; + return deduplicateNodeByValue(combined); + }); + setImporterRels(rels); + setCombinedRelsVal((prevRels: OptionType[]) => { + const combined = [...rels, ...prevRels]; + return deduplicateByFullPattern(combined); + }); + setSourceOptions((prev) => [...prev, ...updatedSource]); + setTargetOptions((prev) => [...prev, ...updatedTarget]); + setTypeOptions((prev) => [...prev, ...updatedType]); }, [] ); @@ -478,6 +521,10 @@ const PageLayout: React.FC = () => { setShowTextFromSchemaDialog({ triggeredFrom: 'schemadialog', show: true }); }, []); + const openDataImporterSchema = useCallback(() => { + setDataImporterSchemaDialog({ triggeredFrom: 'schemadialog', show: true }); + }, []); + const openChatBot = useCallback(() => setShowChatBot(true), []); return ( @@ -566,6 +613,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} @@ -671,6 +733,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..061b3af4e --- /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 DataImporterSchemaDialog = ({ 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 ( + <> + + JSON Data 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 DataImporterSchemaDialog; diff --git a/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/GraphPattern.tsx b/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/GraphPattern.tsx index 1d05a9852..b6d9bd982 100644 --- a/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/GraphPattern.tsx +++ b/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/GraphPattern.tsx @@ -2,8 +2,9 @@ import React, { useState, useRef, useEffect } from 'react'; import { Select } from '@neo4j-ndl/react'; import ButtonWithToolTip from '../../../UI/ButtonWithToolTip'; import { OptionType, TupleCreationProps } from '../../../../types'; -import { appLabels, LOCAL_KEYS } from '../../../../utils/Constants'; +import { appLabels } from '../../../../utils/Constants'; import { useFileContext } from '../../../../context/UsersFiles'; +import { useCredentials } from '../../../../context/UserCredentials'; interface IErrorState { showError: boolean; errorMessage: string; @@ -15,8 +16,16 @@ const GraphPattern: React.FC = ({ onPatternChange, onAddPattern, }) => { - const { sourceOptions, setSourceOptions, typeOptions, setTypeOptions, targetOptions, setTargetOptions } = - useFileContext(); + const { + sourceOptions, + setSourceOptions, + typeOptions, + setTypeOptions, + targetOptions, + setTargetOptions, + setSelectedRels, + selectedRels, + } = useFileContext(); const [inputValues, setInputValues] = useState<{ source: string; type: string; target: string }>({ source: '', type: '', @@ -28,19 +37,41 @@ const GraphPattern: React.FC = ({ target: { showError: false, errorMessage: '' }, }); const sourceRef = useRef(null); + const { userCredentials } = useCredentials(); useEffect(() => { - const savedSources = JSON.parse(localStorage.getItem('customSourceOptions') ?? 'null'); - const savedTypes = JSON.parse(localStorage.getItem('customTypeOptions') ?? 'null'); - const savedTargets = JSON.parse(localStorage.getItem('customTargetOptions') ?? 'null'); - if (savedSources) { - setSourceOptions(savedSources); - } - if (savedTypes) { - setTypeOptions(savedTypes); + const isGlobalStateSet = + selectedRels.length > 0 || sourceOptions.length > 0 || typeOptions.length > 0 || targetOptions.length > 0; + if (isGlobalStateSet) { + return; } - if (savedTargets) { - setTargetOptions(savedTargets); + const selectedNodeRelsStr = localStorage.getItem('selectedRelationshipLabels'); + if (selectedNodeRelsStr != null) { + const selectedGraphOptions = JSON.parse(selectedNodeRelsStr); + if (userCredentials?.uri === selectedGraphOptions.db) { + const rels = selectedGraphOptions.selectedOptions; + const sourceSet = new Set(); + const typeSet = new Set(); + const targetSet = new Set(); + const mappedRels = rels.map((rel: { value: string }) => { + const [sourceVal, typeVal, targetVal] = rel.value.split(','); + sourceSet.add(sourceVal); + typeSet.add(typeVal); + targetSet.add(targetVal); + return { + source: { value: sourceVal, label: sourceVal }, + type: { value: typeVal, label: typeVal }, + target: { value: targetVal, label: targetVal }, + }; + }); + const savedSources: OptionType[] = Array.from(sourceSet).map((val) => ({ value: val, label: val })); + const savedTypes: OptionType[] = Array.from(typeSet).map((val) => ({ value: val, label: val })); + const savedTargets: OptionType[] = Array.from(targetSet).map((val) => ({ value: val, label: val })); + setSelectedRels(mappedRels); + setSourceOptions(savedSources); + setTypeOptions(savedTypes); + setTargetOptions(savedTargets); + } } }, []); @@ -92,9 +123,6 @@ const GraphPattern: React.FC = ({ const handleAddPattern = () => { onAddPattern(); - localStorage.setItem(LOCAL_KEYS.source, JSON.stringify(sourceOptions)); - localStorage.setItem(LOCAL_KEYS.type, JSON.stringify(typeOptions)); - localStorage.setItem(LOCAL_KEYS.target, JSON.stringify(targetOptions)); setTimeout(() => { const selectInput = sourceRef.current?.querySelector('input'); selectInput?.focus(); 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..35b8e95a9 --- /dev/null +++ b/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/ImporterInput.tsx @@ -0,0 +1,12 @@ +import { Box, TextLink } from '@neo4j-ndl/react'; + +const ImporterInput = () => { + return ( + + + Aura Data Models + + + ); +}; +export default ImporterInput; diff --git a/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/LoadExistingSchema.tsx b/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/LoadExistingSchema.tsx index 76b823763..54d62107c 100644 --- a/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/LoadExistingSchema.tsx +++ b/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/LoadExistingSchema.tsx @@ -36,6 +36,7 @@ const LoadDBSchemaDialog = ({ open, onClose, onApply }: LoadDBSchemaDialogProps) } = useFileContext(); const [openGraphView, setOpenGraphView] = useState(false); const [viewPoint, setViewPoint] = useState(''); + const [message, setMessage] = useState(''); useEffect(() => { if (open) { @@ -48,6 +49,13 @@ const LoadDBSchemaDialog = ({ open, onClose, onApply }: LoadDBSchemaDialogProps) try { const response = await getNodeLabelsAndRelTypes(); const schemaData: string[] = response.data.data.triplets; + if (!schemaData || schemaData.length === 0) { + setDbNodes([]); + setDbRels([]); + setDbPattern([]); + setMessage('No data found'); + return; + } const schemaTuples: TupleType[] = schemaData .map((item: string) => { const matchResult = item.match(/^(.+?)-([A-Z_]+)->(.+)$/); @@ -142,6 +150,7 @@ const LoadDBSchemaDialog = ({ open, onClose, onApply }: LoadDBSchemaDialogProps) onCancel={handleCancel} nodes={dbNodes} rels={dbRels} + message={message} /> {openGraphView && ( >; combinedRels: OptionType[]; setCombinedRels: Dispatch>; + openDataImporterSchema: () => void; }) { const { setSelectedRels, @@ -70,6 +72,10 @@ export default function NewEntityExtractionSetting({ setPreDefinedRels, preDefinedPattern, setPreDefinedPattern, + setImporterNodes, + setImporterRels, + setImporterPattern, + importerPattern, } = useFileContext(); const { userCredentials } = useCredentials(); const [openGraphView, setOpenGraphView] = useState(false); @@ -122,6 +128,10 @@ export default function NewEntityExtractionSetting({ updateLocalStorage(userCredentials!, 'selectedRelationshipLabels', []); updateLocalStorage(userCredentials!, 'selectedPattern', []); showNormalToast(`Successfully Removed the Schema settings`); + // Importer clear + setImporterNodes([]); + setImporterRels([]); + setImporterPattern([]); }; const handleFinalApply = (pattern: string[], nodeLables: OptionType[], relationshipLabels: OptionType[]) => { @@ -234,12 +244,6 @@ export default function NewEntityExtractionSetting({ }; const handleRemovePattern = (patternToRemove: string) => { - const match = patternToRemove.match(/(.*?) -\[:(.*?)\]-> (.*)/); - if (!match) { - return; - } - const [, source, type, target] = match.map((s) => s.trim()); - if (userDefinedPattern.includes(patternToRemove)) { updateStore(userDefinedPattern, patternToRemove, setUserDefinedPattern, setUserDefinedNodes, setUserDefinedRels); } @@ -252,9 +256,30 @@ export default function NewEntityExtractionSetting({ if (schemaTextPattern.includes(patternToRemove)) { updateStore(schemaTextPattern, patternToRemove, setSchemaTextPattern, setSchemaValNodes, setSchemaValRels); } - setCombinedPatterns((prev) => prev.filter((p) => p !== patternToRemove)); - setCombinedNodes((prev) => prev.filter((n) => n.value !== source && n.value !== target)); - setCombinedRels((prev) => prev.filter((r) => r.value !== type)); + if (importerPattern.includes(patternToRemove)) { + updateStore(importerPattern, patternToRemove, setImporterPattern, setImporterNodes, setImporterRels); + } + const updatedCombinedPatterns = combinedPatterns.filter((p) => p !== patternToRemove); + setCombinedPatterns(updatedCombinedPatterns); + const updatedTuples: TupleType[] = updatedCombinedPatterns + .map((item) => { + const parts = item.match(/(.*?) -\[:(.*?)\]-> (.*)/); + if (!parts) { + return null; + } + const [src, rel, tgt] = parts.slice(1).map((s) => s.trim()); + return { + value: `${src},${rel},${tgt}`, + label: `${src} -[:${rel}]-> ${tgt}`, + source: src, + target: tgt, + type: rel, + }; + }) + .filter(Boolean) as TupleType[]; + const { nodeLabelOptions, relationshipTypeOptions } = extractOptions(updatedTuples); + setCombinedNodes(nodeLabelOptions); + setCombinedRels(relationshipTypeOptions); setTupleOptions((prev) => prev.filter((t) => t.label !== patternToRemove)); }; @@ -288,6 +313,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 +398,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..61de6ab91 100644 --- a/frontend/src/components/Popups/GraphEnhancementDialog/index.tsx +++ b/frontend/src/components/Popups/GraphEnhancementDialog/index.tsx @@ -50,6 +50,10 @@ export default function GraphEnhancementDialog({ setPreDefinedPattern, setSelectedPreDefOption, allPatterns, + setDataImporterSchemaDialog, + setImporterNodes, + setImporterPattern, + setImporterRels, } = useFileContext(); const isTablet = useMediaQuery(`(min-width:${breakpoints.xs}) and (max-width: ${breakpoints.lg})`); @@ -85,7 +89,14 @@ export default function GraphEnhancementDialog({ setPreDefinedNodes([]); setPreDefinedRels([]); setPreDefinedPattern([]); + // combined Nodes and rels + setCombinedNodes([]); + setCombinedRels([]); setCombinedPatterns([]); + // Data Importer + setImporterNodes([]); + setImporterPattern([]); + setImporterRels([]); setSelectedPreDefOption(null); onClose(); }; @@ -193,6 +204,9 @@ export default function GraphEnhancementDialog({ setCombinedNodes={setCombinedNodes} combinedRels={combinedRels} setCombinedRels={setCombinedRels} + openDataImporterSchema={() => { + setDataImporterSchemaDialog({ triggeredFrom: 'enhancementtab', show: true }); + }} />
diff --git a/frontend/src/components/UI/SchemaSelectionPopup.tsx b/frontend/src/components/UI/SchemaSelectionPopup.tsx index 6612ced37..94aa04cd1 100644 --- a/frontend/src/components/UI/SchemaSelectionPopup.tsx +++ b/frontend/src/components/UI/SchemaSelectionPopup.tsx @@ -1,4 +1,4 @@ -import { Dialog, Button, LoadingSpinner } from '@neo4j-ndl/react'; +import { Dialog, Button, LoadingSpinner, Typography } from '@neo4j-ndl/react'; import PatternContainer from '../Popups/GraphEnhancementDialog/EnitityExtraction/PatternContainer'; import { SchemaSelectionProps } from '../../types'; @@ -14,6 +14,7 @@ const SchemaSelectionDialog = ({ highlightPattern, onApply, onCancel, + message, }: SchemaSelectionProps) => { return ( @@ -23,17 +24,19 @@ const SchemaSelectionDialog = ({
+ ) : pattern.length !== 0 ? ( + ) : ( - pattern.length !== 0 && ( - - ) +
+ {message} +
)}