diff --git a/.gitignore b/.gitignore index 09f4f040b..f35f8a89f 100644 --- a/.gitignore +++ b/.gitignore @@ -172,4 +172,5 @@ google-cloud-cli-linux-x86_64.tar.gz .vennv newenv files - +startupbackend.sh +startupfrontend.sh \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 6761a63c7..72bdc0d67 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,63 +1,63 @@ asyncio==3.4.3 -boto3==1.36.2 -botocore==1.36.2 -certifi==2024.8.30 -fastapi==0.115.6 +boto3==1.37.11 +botocore==1.37.11 +certifi==2025.1.31 +fastapi==0.115.11 fastapi-health==0.4.0 -google-api-core==2.24.0 -google-auth==2.37.0 +google-api-core==2.24.2 +google-auth==2.38.0 google_auth_oauthlib==1.2.1 -google-cloud-core==2.4.1 -json-repair==0.30.2 +google-cloud-core==2.4.3 +json-repair==0.30.3 pip-install==1.3.5 -langchain==0.3.15 -langchain-aws==0.2.11 -langchain-anthropic==0.3.3 -langchain-fireworks==0.2.6 -langchain-community==0.3.15 -langchain-core==0.3.31 +langchain==0.3.20 +langchain-aws==0.2.15 +langchain-anthropic==0.3.9 +langchain-fireworks==0.2.7 +langchain-community==0.3.19 +langchain-core==0.3.45 langchain-experimental==0.3.4 -langchain-google-vertexai==2.0.11 -langchain-groq==0.2.3 -langchain-openai==0.3.1 -langchain-text-splitters==0.3.5 +langchain-google-vertexai==2.0.15 +langchain-groq==0.2.5 +langchain-openai==0.3.8 +langchain-text-splitters==0.3.6 langchain-huggingface==0.1.2 langdetect==1.0.9 -langsmith==0.2.11 +langsmith==0.3.13 langserve==0.3.1 neo4j-rust-ext nltk==3.9.1 -openai==1.59.9 -opencv-python==4.10.0.84 -psutil==6.1.0 -pydantic==2.9.2 +openai==1.66.2 +opencv-python==4.11.0.86 +psutil==7.0.0 +pydantic==2.10.6 python-dotenv==1.0.1 python-magic==0.4.27 PyPDF2==3.0.1 -PyMuPDF==1.24.14 -starlette==0.41.3 -sse-starlette==2.1.3 +PyMuPDF==1.25.3 +starlette==0.46.1 +sse-starlette==2.2.1 starlette-session==0.4.3 tqdm==4.67.1 unstructured[all-docs] -unstructured==0.16.11 -unstructured-client==0.28.1 -unstructured-inference==0.8.1 -urllib3==2.2.2 -uvicorn==0.32.1 +unstructured==0.16.25 +unstructured-client==0.31.1 +unstructured-inference==0.8.9 +urllib3==2.3.0 +uvicorn==0.34.0 gunicorn==23.0.0 wikipedia==1.4.0 -wrapt==1.16.0 -yarl==1.9.4 -youtube-transcript-api==0.6.3 -zipp==3.17.0 -sentence-transformers==3.3.1 -google-cloud-logging==3.11.3 -pypandoc==1.13 -graphdatascience==1.12 -Secweb==1.11.0 -ragas==0.2.11 +wrapt==1.17.2 +yarl==1.18.3 +youtube-transcript-api==1.0.0 +zipp==3.21.0 +sentence-transformers==3.4.1 +google-cloud-logging==3.11.4 +pypandoc==1.15 +graphdatascience==1.14 +Secweb==1.18.1 +ragas==0.2.14 rouge_score==0.1.2 -langchain-neo4j==0.3.0 +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/backend/score.py b/backend/score.py index e668788c3..75181e096 100644 --- a/backend/score.py +++ b/backend/score.py @@ -576,9 +576,10 @@ async def upload_large_file_into_chunks(file:UploadFile = File(...), chunkNumber result = await asyncio.to_thread(upload_file, graph, model, file, chunkNumber, totalChunks, originalname, uri, CHUNK_DIR, MERGED_DIR) end = time.time() elapsed_time = end - start - json_obj = {'api_name':'upload','db_url':uri,'userName':userName, 'database':database, 'chunkNumber':chunkNumber,'totalChunks':totalChunks, - 'original_file_name':originalname,'model':model, 'logging_time': formatted_time(datetime.now(timezone.utc)), 'elapsed_api_time':f'{elapsed_time:.2f}','email':email} - logger.log_struct(json_obj, "INFO") + if int(chunkNumber) == int(totalChunks): + json_obj = {'api_name':'upload','db_url':uri,'userName':userName, 'database':database, 'chunkNumber':chunkNumber,'totalChunks':totalChunks, + 'original_file_name':originalname,'model':model, 'logging_time': formatted_time(datetime.now(timezone.utc)), 'elapsed_api_time':f'{elapsed_time:.2f}','email':email} + logger.log_struct(json_obj, "INFO") if int(chunkNumber) == int(totalChunks): return create_api_response('Success',data=result, message='Source Node Created Successfully') else: @@ -894,7 +895,7 @@ async def retry_processing(uri=Form(None), userName=Form(None), password=Form(No try: start = time.time() graph = create_graph_database_connection(uri, userName, password, database) - chunks = graph.query(QUERY_TO_GET_CHUNKS, params={"filename":file_name}) + chunks = execute_graph_query(graph,QUERY_TO_GET_CHUNKS,params={"filename":file_name}) end = time.time() elapsed_time = end - start json_obj = {'api_name':'retry_processing', 'db_url':uri, 'userName':userName, 'database':database, 'file_name':file_name,'retry_condition':retry_condition, diff --git a/backend/src/QA_integration.py b/backend/src/QA_integration.py index 42a800851..1d3afb8e8 100644 --- a/backend/src/QA_integration.py +++ b/backend/src/QA_integration.py @@ -380,7 +380,7 @@ def create_retriever(neo_db, document_names, chat_mode_settings,search_k, score_ retriever = neo_db.as_retriever( search_type="similarity_score_threshold", search_kwargs={ - 'k': search_k, + 'top_k': search_k, 'effective_search_ratio': ef_ratio, 'score_threshold': score_threshold, 'filter': {'fileName': {'$in': document_names}} @@ -390,7 +390,7 @@ def create_retriever(neo_db, document_names, chat_mode_settings,search_k, score_ else: retriever = neo_db.as_retriever( search_type="similarity_score_threshold", - search_kwargs={'k': search_k,'effective_search_ratio': ef_ratio, 'score_threshold': score_threshold} + search_kwargs={'top_k': search_k,'effective_search_ratio': ef_ratio, 'score_threshold': score_threshold} ) logging.info(f"Successfully created retriever with search_k={search_k}, score_threshold={score_threshold}") return retriever diff --git a/backend/src/document_sources/youtube.py b/backend/src/document_sources/youtube.py index 82e9a9219..1c60a9b85 100644 --- a/backend/src/document_sources/youtube.py +++ b/backend/src/document_sources/youtube.py @@ -1,6 +1,7 @@ from langchain.docstore.document import Document from src.shared.llm_graph_builder_exception import LLMGraphBuilderException from youtube_transcript_api import YouTubeTranscriptApi +from youtube_transcript_api.proxies import GenericProxyConfig import logging from urllib.parse import urlparse,parse_qs from difflib import SequenceMatcher @@ -12,8 +13,10 @@ def get_youtube_transcript(youtube_id): try: proxy = os.environ.get("YOUTUBE_TRANSCRIPT_PROXY") - proxies = { 'https': proxy } - transcript_pieces = YouTubeTranscriptApi.get_transcript(youtube_id, proxies = proxies) + proxy_config = GenericProxyConfig(http_url=proxy, https_url=proxy) if proxy else None + youtube_api = YouTubeTranscriptApi(proxy_config=proxy_config) + transcript_pieces = youtube_api.fetch(youtube_id, preserve_formatting=True) + transcript_pieces = transcript_pieces.to_raw_data() return transcript_pieces except Exception as e: message = f"Youtube transcript is not available for youtube Id: {youtube_id}" diff --git a/backend/src/graphDB_dataAccess.py b/backend/src/graphDB_dataAccess.py index 6f5365498..397227a9a 100644 --- a/backend/src/graphDB_dataAccess.py +++ b/backend/src/graphDB_dataAccess.py @@ -1,5 +1,7 @@ import logging import os +import time +from neo4j.exceptions import TransientError from langchain_neo4j import Neo4jGraph from src.shared.common_fn import create_gcs_bucket_folder_name_hashed, delete_uploaded_local_file, load_embedding_model from src.document_sources.gcs_bucket import delete_file_from_gcs @@ -16,7 +18,7 @@ class graphDBdataAccess: def __init__(self, graph: Neo4jGraph): self.graph = graph - def update_exception_db(self, file_name, exp_msg, retry_condition): + def update_exception_db(self, file_name, exp_msg, retry_condition=None): try: job_status = "Failed" result = self.get_current_status_document_node(file_name) @@ -254,8 +256,20 @@ def connection_check_and_get_vector_dimensions(self,database): else: return {'message':"Connection Successful","gds_status": gds_status,"write_access":write_access} - def execute_query(self, query, param=None): - return self.graph.query(query, param) + def execute_query(self, query, param=None,max_retries=3, delay=2): + retries = 0 + while retries < max_retries: + try: + return self.graph.query(query, param) + except TransientError as e: + if "DeadlockDetected" in str(e): + retries += 1 + logging.info(f"Deadlock detected. Retrying {retries}/{max_retries} in {delay} seconds...") + time.sleep(delay) # Wait before retrying + else: + raise + logging.error("Failed to execute query after maximum retries due to persistent deadlocks.") + raise RuntimeError("Query execution failed after multiple retries due to deadlock.") def get_current_status_document_node(self, file_name): query = """ diff --git a/backend/src/main.py b/backend/src/main.py index c21e26f5a..41e69e6f4 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -400,7 +400,7 @@ async def processing_source(uri, userName, password, database, model, file_name, obj_source_node.processing_time = processed_time obj_source_node.processed_chunk = select_chunks_upto+select_chunks_with_retry if retry_condition == START_FROM_BEGINNING: - result = graph.query(QUERY_TO_GET_NODES_AND_RELATIONS_OF_A_DOCUMENT, params={"filename":file_name}) + result = execute_graph_query(graph,QUERY_TO_GET_NODES_AND_RELATIONS_OF_A_DOCUMENT, params={"filename":file_name}) obj_source_node.node_count = result[0]['nodes'] obj_source_node.relationship_count = result[0]['rels'] else: @@ -503,21 +503,10 @@ async def processing_chunks(chunkId_chunkDoc_list,graph,uri, userName, password, logging.info(f'Time taken to create relationship between chunk and entities: {elapsed_relationship:.2f} seconds') latency_processing_chunk["relationship_between_chunk_entity"] = f'{elapsed_relationship:.2f}' - distinct_nodes = set() - relations = [] - for graph_document in graph_documents: - #get distinct nodes - for node in graph_document.nodes: - node_id = node.id - node_type= node.type - if (node_id, node_type) not in distinct_nodes: - distinct_nodes.add((node_id, node_type)) - #get all relations - for relation in graph_document.relationships: - relations.append(relation.type) - - node_count += len(distinct_nodes) - rel_count += len(relations) + graphDb_data_Access = graphDBdataAccess(graph) + count_response = graphDb_data_Access.update_node_relationship_count(file_name) + node_count = count_response[file_name].get('nodeCount',"0") + rel_count = count_response[file_name].get('relationshipCount',"0") return node_count,rel_count,latency_processing_chunk def get_chunkId_chunkDoc_list(graph, file_name, pages, token_chunk_size, chunk_overlap, retry_condition): @@ -539,7 +528,7 @@ def get_chunkId_chunkDoc_list(graph, file_name, pages, token_chunk_size, chunk_o else: chunkId_chunkDoc_list=[] - chunks = graph.query(QUERY_TO_GET_CHUNKS, params={"filename":file_name}) + chunks = execute_graph_query(graph,QUERY_TO_GET_CHUNKS, params={"filename":file_name}) if chunks[0]['text'] is None or chunks[0]['text']=="" or not chunks : raise LLMGraphBuilderException(f"Chunks are not created for {file_name}. Please re-upload file and try again.") @@ -550,13 +539,13 @@ def get_chunkId_chunkDoc_list(graph, file_name, pages, token_chunk_size, chunk_o if retry_condition == START_FROM_LAST_PROCESSED_POSITION: logging.info(f"Retry : start_from_last_processed_position") - starting_chunk = graph.query(QUERY_TO_GET_LAST_PROCESSED_CHUNK_POSITION, params={"filename":file_name}) + starting_chunk = execute_graph_query(graph,QUERY_TO_GET_LAST_PROCESSED_CHUNK_POSITION, params={"filename":file_name}) if starting_chunk and starting_chunk[0]["position"] < len(chunkId_chunkDoc_list): return len(chunks), chunkId_chunkDoc_list[starting_chunk[0]["position"] - 1:] elif starting_chunk and starting_chunk[0]["position"] == len(chunkId_chunkDoc_list): - starting_chunk = graph.query(QUERY_TO_GET_LAST_PROCESSED_CHUNK_WITHOUT_ENTITY, params={"filename":file_name}) + starting_chunk = execute_graph_query(graph,QUERY_TO_GET_LAST_PROCESSED_CHUNK_WITHOUT_ENTITY, params={"filename":file_name}) return len(chunks), chunkId_chunkDoc_list[starting_chunk[0]["position"] - 1:] else: @@ -741,7 +730,7 @@ def set_status_retry(graph, file_name, retry_condition): if retry_condition == DELETE_ENTITIES_AND_START_FROM_BEGINNING or retry_condition == START_FROM_BEGINNING: obj_source_node.processed_chunk=0 if retry_condition == DELETE_ENTITIES_AND_START_FROM_BEGINNING: - graph.query(QUERY_TO_DELETE_EXISTING_ENTITIES, params={"filename":file_name}) + execute_graph_query(graph,QUERY_TO_DELETE_EXISTING_ENTITIES, params={"filename":file_name}) obj_source_node.node_count=0 obj_source_node.relationship_count=0 logging.info(obj_source_node) diff --git a/backend/src/make_relationships.py b/backend/src/make_relationships.py index bccfa1ddd..a07f29c9c 100644 --- a/backend/src/make_relationships.py +++ b/backend/src/make_relationships.py @@ -1,6 +1,6 @@ from langchain_neo4j import Neo4jGraph from langchain.docstore.document import Document -from src.shared.common_fn import load_embedding_model +from src.shared.common_fn import load_embedding_model,execute_graph_query import logging from typing import List import os @@ -33,7 +33,7 @@ def merge_relationship_between_chunk_and_entites(graph: Neo4jGraph, graph_docume CALL apoc.merge.node([data.node_type], {id: data.node_id}) YIELD node AS n MERGE (c)-[:HAS_ENTITY]->(n) """ - graph.query(unwind_query, params={"batch_data": batch_data}) + execute_graph_query(graph,unwind_query, params={"batch_data": batch_data}) def create_chunk_embeddings(graph, chunkId_chunkDoc_list, file_name): @@ -59,7 +59,7 @@ def create_chunk_embeddings(graph, chunkId_chunkDoc_list, file_name): SET c.embedding = row.embeddings MERGE (c)-[:PART_OF]->(d) """ - graph.query(query_to_create_embedding, params={"fileName":file_name, "data":data_for_query}) + execute_graph_query(graph,query_to_create_embedding, params={"fileName":file_name, "data":data_for_query}) def create_relation_between_chunks(graph, file_name, chunks: List[Document])->list: logging.info("creating FIRST_CHUNK and NEXT_CHUNK relationships between chunks") @@ -127,7 +127,7 @@ def create_relation_between_chunks(graph, file_name, chunks: List[Document])->li MATCH (d:Document {fileName: data.f_name}) MERGE (c)-[:PART_OF]->(d) """ - graph.query(query_to_create_chunk_and_PART_OF_relation, params={"batch_data": batch_data}) + execute_graph_query(graph,query_to_create_chunk_and_PART_OF_relation, params={"batch_data": batch_data}) query_to_create_FIRST_relation = """ UNWIND $relationships AS relationship @@ -136,7 +136,7 @@ def create_relation_between_chunks(graph, file_name, chunks: List[Document])->li FOREACH(r IN CASE WHEN relationship.type = 'FIRST_CHUNK' THEN [1] ELSE [] END | MERGE (d)-[:FIRST_CHUNK]->(c)) """ - graph.query(query_to_create_FIRST_relation, params={"f_name": file_name, "relationships": relationships}) + execute_graph_query(graph,query_to_create_FIRST_relation, params={"f_name": file_name, "relationships": relationships}) query_to_create_NEXT_CHUNK_relation = """ UNWIND $relationships AS relationship @@ -145,17 +145,16 @@ def create_relation_between_chunks(graph, file_name, chunks: List[Document])->li MATCH (pc:Chunk {id: relationship.previous_chunk_id}) FOREACH(r IN CASE WHEN relationship.type = 'NEXT_CHUNK' THEN [1] ELSE [] END | MERGE (c)<-[:NEXT_CHUNK]-(pc)) - """ - graph.query(query_to_create_NEXT_CHUNK_relation, params={"relationships": relationships}) - + """ + execute_graph_query(graph,query_to_create_NEXT_CHUNK_relation, params={"relationships": relationships}) return lst_chunks_including_hash def create_chunk_vector_index(graph): start_time = time.time() try: - vector_index = graph.query("SHOW INDEXES YIELD * WHERE labelsOrTypes = ['Chunk'] and type = 'VECTOR' AND name = 'vector' return options") - + vector_index_query = "SHOW INDEXES YIELD * WHERE labelsOrTypes = ['Chunk'] and type = 'VECTOR' AND name = 'vector' return options" + vector_index = execute_graph_query(graph,vector_index_query) if not vector_index: vector_store = Neo4jVector(embedding=EMBEDDING_FUNCTION, graph=graph, diff --git a/backend/src/post_processing.py b/backend/src/post_processing.py index cdc8b06d3..0865c5ad3 100644 --- a/backend/src/post_processing.py +++ b/backend/src/post_processing.py @@ -4,7 +4,7 @@ from langchain_neo4j import Neo4jGraph import os from src.graph_query import get_graphDB_driver -from src.shared.common_fn import load_embedding_model +from src.shared.common_fn import load_embedding_model,execute_graph_query from langchain_core.output_parsers import JsonOutputParser from langchain_core.prompts import ChatPromptTemplate from src.shared.constants import GRAPH_CLEANUP_PROMPT @@ -179,8 +179,8 @@ def fetch_entities_for_embedding(graph): MATCH (e) WHERE NOT (e:Chunk OR e:Document OR e:`__Community__`) AND e.embedding IS NULL AND e.id IS NOT NULL RETURN elementId(e) AS elementId, e.id + " " + coalesce(e.description, "") AS text - """ - result = graph.query(query) + """ + result = execute_graph_query(graph,query) return [{"elementId": record["elementId"], "text": record["text"]} for record in result] def update_embeddings(rows, graph): @@ -194,7 +194,7 @@ def update_embeddings(rows, graph): MATCH (e) WHERE elementId(e) = row.elementId CALL db.create.setNodeVectorProperty(e, "embedding", row.embedding) """ - return graph.query(query,params={'rows':rows}) + return execute_graph_query(graph,query,params={'rows':rows}) def graph_schema_consolidation(graph): graphDb_data_Access = graphDBdataAccess(graph) @@ -223,14 +223,14 @@ def graph_schema_consolidation(graph): SET n:`{new_label}` REMOVE n:`{old_label}` """ - graph.query(query) - + execute_graph_query(graph,query) + for old_label, new_label in relation_mapping.items(): query = f""" MATCH (n)-[r:`{old_label}`]->(m) CREATE (n)-[r2:`{new_label}`]->(m) DELETE r """ - graph.query(query) + execute_graph_query(graph,query) return None diff --git a/backend/src/shared/common_fn.py b/backend/src/shared/common_fn.py index d95626bb3..6f394bb24 100644 --- a/backend/src/shared/common_fn.py +++ b/backend/src/shared/common_fn.py @@ -5,10 +5,12 @@ from langchain_google_vertexai import VertexAIEmbeddings from langchain_openai import OpenAIEmbeddings from langchain_neo4j import Neo4jGraph +from neo4j.exceptions import TransientError from langchain_community.graphs.graph_document import GraphDocument from typing import List import re import os +import time from pathlib import Path from urllib.parse import urlparse import boto3 @@ -90,10 +92,22 @@ def load_embedding_model(embedding_model_name: str): logging.info(f"Embedding: Using Langchain HuggingFaceEmbeddings , Dimension:{dimension}") return embeddings, dimension -def save_graphDocuments_in_neo4j(graph:Neo4jGraph, graph_document_list:List[GraphDocument]): - graph.add_graph_documents(graph_document_list, baseEntityLabel=True) - # graph.add_graph_documents(graph_document_list) - +def save_graphDocuments_in_neo4j(graph: Neo4jGraph, graph_document_list: List[GraphDocument], max_retries=3, delay=1): + retries = 0 + while retries < max_retries: + try: + graph.add_graph_documents(graph_document_list, baseEntityLabel=True) + return + except TransientError as e: + if "DeadlockDetected" in str(e): + retries += 1 + logging.info(f"Deadlock detected. Retrying {retries}/{max_retries} in {delay} seconds...") + time.sleep(delay) # Wait before retrying + else: + raise + logging.error("Failed to execute query after maximum retries due to persistent deadlocks.") + raise RuntimeError("Query execution failed after multiple retries due to deadlock.") + def handle_backticks_nodes_relationship_id_type(graph_document_list:List[GraphDocument]): for graph_document in graph_document_list: # Clean node id and types @@ -114,6 +128,21 @@ def handle_backticks_nodes_relationship_id_type(graph_document_list:List[GraphDo graph_document.nodes = cleaned_nodes return graph_document_list +def execute_graph_query(graph: Neo4jGraph, query, params=None, max_retries=3, delay=2): + retries = 0 + while retries < max_retries: + try: + return graph.query(query, params) + except TransientError as e: + if "DeadlockDetected" in str(e): + retries += 1 + logging.info(f"Deadlock detected. Retrying {retries}/{max_retries} in {delay} seconds...") + time.sleep(delay) # Wait before retrying + else: + raise + logging.error("Failed to execute query after maximum retries due to persistent deadlocks.") + raise RuntimeError("Query execution failed after multiple retries due to deadlock.") + def delete_uploaded_local_file(merged_file_path, file_name): file_path = Path(merged_file_path) if file_path.exists(): diff --git a/backend/src/shared/constants.py b/backend/src/shared/constants.py index 806f58541..30cedfdfd 100644 --- a/backend/src/shared/constants.py +++ b/backend/src/shared/constants.py @@ -358,12 +358,12 @@ WITH CASE - WHEN e.embedding IS NULL OR ({embedding_match_min} <= vector.similarity.cosine($embedding, e.embedding) AND vector.similarity.cosine($embedding, e.embedding) <= {embedding_match_max}) THEN + WHEN e.embedding IS NULL OR ({embedding_match_min} <= vector.similarity.cosine($query_vector, e.embedding) AND vector.similarity.cosine($query_vector, e.embedding) <= {embedding_match_max}) THEN collect {{ OPTIONAL MATCH path=(e)(()-[rels:!HAS_ENTITY&!PART_OF]-()){{0,1}}(:!Chunk&!Document&!__Community__) RETURN path LIMIT {entity_limit_minmax_case} }} - WHEN e.embedding IS NOT NULL AND vector.similarity.cosine($embedding, e.embedding) > {embedding_match_max} THEN + WHEN e.embedding IS NOT NULL AND vector.similarity.cosine($query_vector, e.embedding) > {embedding_match_max} THEN collect {{ OPTIONAL MATCH path=(e)(()-[rels:!HAS_ENTITY&!PART_OF]-()){{0,2}}(:!Chunk&!Document&!__Community__) RETURN path LIMIT {entity_limit_max_case} diff --git a/frontend/package.json b/frontend/package.json index f0da5dd67..311560023 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,11 +21,11 @@ "@neo4j-ndl/base": "^3.2.9", "@neo4j-ndl/react": "^3.2.18", "@neo4j-nvl/base": "^0.3.6", - "@neo4j-nvl/react": "^0.3.6", + "@neo4j-nvl/react": "^0.3.7", "@react-oauth/google": "^0.12.1", "@tanstack/react-table": "^8.20.5", "@types/uuid": "^9.0.7", - "axios": "^1.7.9", + "axios": "^1.8.3", "clsx": "^2.1.1", "eslint-plugin-react": "^7.37.4", "re-resizable": "^6.11.2", @@ -38,8 +38,8 @@ "uuid": "^9.0.1" }, "devDependencies": { - "@tailwindcss/postcss": "^4.0.7", - "@types/node": "^22.13.9", + "@tailwindcss/postcss": "^4.0.12", + "@types/node": "^22.13.10", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@typescript-eslint/eslint-plugin": "^7.0.0", @@ -52,7 +52,7 @@ "husky": "^9.1.7", "lint-staged": "^15.4.3", "postcss": "^8.5.3", - "prettier": "^2.7.1", + "prettier": "^3.5.3", "react-dropzone": "^14.3.8", "tailwindcss": "^4.0.7", "typescript": "^5.7.3", diff --git a/frontend/src/App.css b/frontend/src/App.css index 260bc07aa..b7a92bc0b 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -415,4 +415,5 @@ .enhancement-btn__wrapper{ padding-right: 12px; + display: flex; } \ No newline at end of file diff --git a/frontend/src/Home.tsx b/frontend/src/Home.tsx index dc2ec1cbb..4b30a0f40 100644 --- a/frontend/src/Home.tsx +++ b/frontend/src/Home.tsx @@ -5,7 +5,7 @@ import QuickStarter from './components/QuickStarter'; import { GoogleOAuthProvider } from '@react-oauth/google'; import { APP_SOURCES } from './utils/Constants'; import ErrorBoundary from './components/UI/ErrroBoundary'; -import { Toaster } from '@neo4j-ndl/react'; +import { Toaster, SpotlightProvider } from '@neo4j-ndl/react'; const Home: React.FC = () => { return ( <> @@ -13,7 +13,9 @@ const Home: React.FC = () => { - + + + @@ -21,7 +23,9 @@ const Home: React.FC = () => { ) : ( - + + + diff --git a/frontend/src/components/Auth/Auth.tsx b/frontend/src/components/Auth/Auth.tsx index aa4ac0511..44c67ab02 100644 --- a/frontend/src/components/Auth/Auth.tsx +++ b/frontend/src/components/Auth/Auth.tsx @@ -1,6 +1,8 @@ -import React from 'react'; + +import React, { useEffect } from 'react'; import { AppState, Auth0Provider, useAuth0 } from '@auth0/auth0-react'; -import { Navigate, useNavigate } from 'react-router'; +import { useNavigate } from 'react-router'; + const domain = process.env.VITE_AUTH0_DOMAIN; const clientId = process.env.VITE_AUTH0_CLIENT_ID; const Auth0ProviderWithHistory: React.FC<{ children: React.ReactNode }> = ({ children }) => { @@ -24,13 +26,18 @@ const Auth0ProviderWithHistory: React.FC<{ children: React.ReactNode }> = ({ chi }; export const AuthenticationGuard: React.FC<{ component: React.ComponentType }> = ({ component }) => { - const { isAuthenticated } = useAuth0(); + + const { isAuthenticated, isLoading } = useAuth0(); const Component = component; + const navigate = useNavigate(); + useEffect(() => { + if (!isLoading && !isAuthenticated) { + localStorage.setItem('isReadOnlyMode', 'true'); + navigate('/readonly', { replace: true }); + } + }, [isLoading, isAuthenticated]); + - if (!isAuthenticated) { - localStorage.setItem('isReadOnlyMode', 'true'); - return ; - } return ; }; diff --git a/frontend/src/components/ChatBot/Chatbot.tsx b/frontend/src/components/ChatBot/Chatbot.tsx index e565f3858..59617a7c6 100644 --- a/frontend/src/components/ChatBot/Chatbot.tsx +++ b/frontend/src/components/ChatBot/Chatbot.tsx @@ -10,6 +10,7 @@ import { Flex, Box, TextLink, + SpotlightTarget, } from '@neo4j-ndl/react'; import { ArrowDownTrayIconOutline, XMarkIconOutline } from '@neo4j-ndl/react/icons'; import ChatBotAvatar from '../../assets/images/chatbot-ai.png'; @@ -560,17 +561,19 @@ const Chatbot: FC = (props) => { name: 'chatbot-input', }} /> - - {buttonCaptions.ask}{' '} - {selectedFileNames != undefined && selectedFileNames.length > 0 && `(${selectedFileNames.length})`} - + + + {buttonCaptions.ask}{' '} + {selectedFileNames != undefined && selectedFileNames.length > 0 && `(${selectedFileNames.length})`} + + }> diff --git a/frontend/src/components/ChatBot/ChunkInfo.tsx b/frontend/src/components/ChatBot/ChunkInfo.tsx index 44daf0516..6c6756549 100644 --- a/frontend/src/components/ChatBot/ChunkInfo.tsx +++ b/frontend/src/components/ChatBot/ChunkInfo.tsx @@ -47,7 +47,7 @@ const ChunkInfo: FC = ({ loading, chunks, mode }) => {
  • {chunk?.page_number ? ( <> -
    +
    <> = ({ loading, chunks, mode }) => { ) : chunk?.url && chunk?.start_time ? ( <> -
    +
    = ({ loading, chunks, mode }) => { mode !== chatModeLables['entity search+vector'] && mode !== chatModeLables.graph && ( <> - + Similarity Score: {chunk?.score} = ({ loading, chunks, mode }) => { - - {/*
    */} - - {/* handleChunkClick(chunk.element_id, 'Chunk'), - }} - > - {'View Graph'} - */} - {/*
    */} )} ) : chunk?.url && new URL(chunk.url).host === 'wikipedia.org' ? ( <> -
    +
    {chunk?.fileName}
    @@ -151,22 +138,13 @@ const ChunkInfo: FC = ({ loading, chunks, mode }) => { -
    - {/* handleChunkClick(chunk.element_id, 'Chunk'), - }} - > - {'View Graph'} - */} -
    +
    )} ) : chunk?.url && new URL(chunk.url).host === 'storage.googleapis.com' ? ( <> -
    +
    {chunk?.fileName}
    @@ -186,23 +164,12 @@ const ChunkInfo: FC = ({ loading, chunks, mode }) => { - - {/*
    - handleChunkClick(chunk.element_id, 'Chunk'), - }} - > - {'View Graph'} - -
    */} )} ) : chunk?.url && chunk?.url.startsWith('s3://') ? ( <> -
    +
    {chunk?.fileName}
    @@ -222,16 +189,6 @@ const ChunkInfo: FC = ({ loading, chunks, mode }) => { -
    - {/* handleChunkClick(chunk.element_id, 'Chunk'), - }} - > - {'View Graph'} - */} -
    )} @@ -239,7 +196,7 @@ const ChunkInfo: FC = ({ loading, chunks, mode }) => { !chunk?.url.startsWith('s3://') && !isAllowedHost(chunk?.url, ['storage.googleapis.com', 'wikipedia.org', 'youtube.com']) ? ( <> -
    +
    {chunk?.url} @@ -261,22 +218,12 @@ const ChunkInfo: FC = ({ loading, chunks, mode }) => { -
    - {/* handleChunkClick(chunk.element_id, 'Chunk'), - }} - > - {'View Graph'} - */} -
    )} ) : ( <> -
    +
    {chunk.fileSource === 'local file' ? ( @@ -306,19 +253,7 @@ const ChunkInfo: FC = ({ loading, chunks, mode }) => { - <> - {/*
    */} - - {/* handleChunkClick(chunk.element_id, 'Chunk'), - }} - > - {'View Graph'} - */} - {/*
    */} - + <>
    )} diff --git a/frontend/src/components/ChatBot/SourcesInfo.tsx b/frontend/src/components/ChatBot/SourcesInfo.tsx index 99c9ee2f9..6016b511b 100644 --- a/frontend/src/components/ChatBot/SourcesInfo.tsx +++ b/frontend/src/components/ChatBot/SourcesInfo.tsx @@ -37,8 +37,8 @@ const SourcesInfo: FC = ({ loading, mode, chunks, sources }) => { .map((c) => ({ fileName: c.fileName, fileSource: c.fileSource })) .map((s, index) => { return ( -
  • -
    +
  • +
    {s.fileSource === 'local file' ? ( ) : ( @@ -59,11 +59,11 @@ const SourcesInfo: FC = ({ loading, mode, chunks, sources }) => {
      {sources.map((link, index) => { return ( -
    • +
    • {link?.startsWith('http') || link?.startsWith('https') ? ( <> {isAllowedHost(link, ['wikipedia.org']) && ( -
      +
      Wikipedia Logo @@ -78,7 +78,7 @@ const SourcesInfo: FC = ({ loading, mode, chunks, sources }) => {
      )} {isAllowedHost(link, ['storage.googleapis.com']) && ( -
      +
      Google Cloud Storage Logo = ({ loading, mode, chunks, sources }) => { )} {youtubeLinkValidation(link) && ( <> -
      +
      @@ -107,7 +107,7 @@ const SourcesInfo: FC = ({ loading, mode, chunks, sources }) => { )} {!link?.startsWith('s3://') && !isAllowedHost(link, ['storage.googleapis.com', 'wikipedia.org', 'www.youtube.com']) && ( -
      +
      {link} @@ -116,7 +116,7 @@ const SourcesInfo: FC = ({ loading, mode, chunks, sources }) => { )} ) : link?.startsWith('s3://') ? ( -
      +
      S3 Logo = ({ loading, mode, chunks, sources }) => {
      ) : ( -
      +
      import('./Popups/LargeFilePopUp/ConfirmationDialog')); let afterFirstRender = false; - const Content: React.FC = ({ showEnhancementDialog, toggleEnhancementDialog, @@ -76,7 +85,8 @@ const Content: React.FC = ({ const graphbtnRef = useRef(null); const chunksTextAbortController = useRef(); const { colorMode } = useContext(ThemeWrapperContext); - + const { isAuthenticated } = useAuth0(); + const { setIsOpen } = useSpotlightContext(); const [alertStateForRetry, setAlertStateForRetry] = useState({ showAlert: false, alertType: 'neutral', @@ -207,7 +217,14 @@ const Content: React.FC = ({ } afterFirstRender = true; }, [queue.items.length, userCredentials]); - + const isFirstTimeUser = useMemo(() => { + return localStorage.getItem('neo4j.connection') === null; + }, []); + useEffect(() => { + if (!isAuthenticated && !connectionStatus && isFirstTimeUser) { + setIsOpen(true); + } + }, [connectionStatus, isAuthenticated, isFirstTimeUser]); const handleDropdownChange = (selectedOption: OptionType | null | void) => { if (selectedOption?.value) { setModel(selectedOption?.value); @@ -252,7 +269,7 @@ const Content: React.FC = ({ }; const extractHandler = async (fileItem: CustomFile, uid: string) => { - queue.remove(fileItem.name as string); + queue.remove((item) => item.name === fileItem.name); try { setFilesData((prevfiles) => prevfiles.map((curfile) => { @@ -337,7 +354,7 @@ const Content: React.FC = ({ return prev + 1; }); const { message, fileName } = error; - queue.remove(fileName); + queue.remove((item) => item.name === fileName); const errorMessage = error.message; showErrorToast(message); setFilesData((prevfiles) => @@ -920,13 +937,20 @@ const Content: React.FC = ({ Graph Enhancement {!connectionStatus ? ( - + + ) : ( showDisconnectButton && (
      - - {buttonCaptions.generateGraph}{' '} - {selectedfileslength && !disableCheck && newFilecheck ? `(${newFilecheck})` : ''} - + + + {buttonCaptions.generateGraph}{' '} + {selectedfileslength && !disableCheck && newFilecheck ? `(${newFilecheck})` : ''} + + + = ({ {buttonCaptions.deleteFiles} {selectedfileslength != undefined && selectedfileslength > 0 && `(${selectedfileslength})`} - - -
      { - setIsGraphBtnMenuOpen((old) => !old); - e.stopPropagation(); - }} - ref={graphbtnRef} - > - {!isGraphBtnMenuOpen ? ( - - ) : ( - - )} -
      -
      + + + +
      { + setIsGraphBtnMenuOpen((old) => !old); + e.stopPropagation(); + }} + ref={graphbtnRef} + > + {!isGraphBtnMenuOpen ? ( + + ) : ( + + )} +
      +
      +
      = ({ openModal, isLargeDesktop = true }) => { +const S3Component: React.FC = ({ openModal, isLargeDesktop = true, isDisabled = false }) => { return ( = ({ openModal, isLargeDesktop = logo={s3logo} wrapperclassName='' className={!isLargeDesktop ? 'widthunset' : ''} + isDisabled={isDisabled} /> ); }; diff --git a/frontend/src/components/DataSources/GCS/GCSButton.tsx b/frontend/src/components/DataSources/GCS/GCSButton.tsx index 1ad31fdfd..2a7c3a747 100644 --- a/frontend/src/components/DataSources/GCS/GCSButton.tsx +++ b/frontend/src/components/DataSources/GCS/GCSButton.tsx @@ -2,7 +2,7 @@ import gcslogo from '../../../assets/images/gcs.webp'; import { DataComponentProps } from '../../../types'; import { buttonCaptions } from '../../../utils/Constants'; import CustomButton from '../../UI/CustomButton'; -const GCSButton: React.FC = ({ openModal, isLargeDesktop = true }) => { +const GCSButton: React.FC = ({ openModal, isLargeDesktop = true, isDisabled = false }) => { return ( = ({ openModal, isLargeDesktop = t logo={gcslogo} wrapperclassName='' className={!isLargeDesktop ? 'widthunset' : ''} + isDisabled={isDisabled} /> ); }; diff --git a/frontend/src/components/DataSources/Local/DropZone.tsx b/frontend/src/components/DataSources/Local/DropZone.tsx index 90cd2ca81..88d200267 100644 --- a/frontend/src/components/DataSources/Local/DropZone.tsx +++ b/frontend/src/components/DataSources/Local/DropZone.tsx @@ -1,4 +1,4 @@ -import { Dropzone, Flex, Typography } from '@neo4j-ndl/react'; +import { Dropzone, Flex, SpotlightTarget, Typography } from '@neo4j-ndl/react'; import { useState, FunctionComponent, useEffect } from 'react'; import Loader from '../../../utils/Loader'; import { v4 as uuidv4 } from 'uuid'; @@ -17,7 +17,6 @@ const DropZone: FunctionComponent = () => { const [isClicked, setIsClicked] = useState(false); const { userCredentials } = useCredentials(); const [selectedFiles, setSelectedFiles] = useState([]); - const onDropHandler = (f: Partial[]) => { setIsClicked(true); setSelectedFiles(f.map((f) => f as File)); @@ -197,57 +196,65 @@ const DropZone: FunctionComponent = () => { return ( <> - } - isTesting={true} - className='bg-none! dropzoneContainer' - supportedFilesDescription={ - - - {buttonCaptions.dropzoneSpan} -
      - - - Microsoft Office (.docx, .pptx, .xls, .xlsx) - PDF (.pdf) - Images (.jpeg, .jpg, .png, .svg) - Text (.html, .txt , .md) - - - } - > - - -
      -
      -
      - } - dropZoneOptions={{ - accept: { - 'application/pdf': ['.pdf'], - 'image/*': ['.jpeg', '.jpg', '.png', '.svg'], - 'text/html': ['.html'], - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], - 'text/plain': ['.txt'], - 'application/vnd.ms-powerpoint': ['.pptx'], - 'application/vnd.ms-excel': ['.xls'], - 'text/markdown': ['.md'], - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], - }, - onDrop: (f: Partial[]) => { - onDropHandler(f); - }, - onDropRejected: (e) => { - if (e.length) { - showErrorToast('Failed To Upload, Unsupported file extention'); - } - }, - }} - /> + + } + isTesting={true} + className='bg-none! dropzoneContainer' + supportedFilesDescription={ + + + {buttonCaptions.dropzoneSpan} +
      + + + Microsoft Office (.docx, .pptx, .xls, .xlsx) + PDF (.pdf) + Images (.jpeg, .jpg, .png, .svg) + Text (.html, .txt , .md) + + + } + > + + +
      +
      +
      + } + dropZoneOptions={{ + accept: { + 'application/pdf': ['.pdf'], + 'image/*': ['.jpeg', '.jpg', '.png', '.svg'], + 'text/html': ['.html'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], + 'text/plain': ['.txt'], + 'application/vnd.ms-powerpoint': ['.pptx'], + 'application/vnd.ms-excel': ['.xls'], + 'text/markdown': ['.md'], + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], + }, + onDrop: (f: Partial[]) => { + onDropHandler(f); + }, + onDropRejected: (e) => { + if (e.length) { + showErrorToast('Failed To Upload, Unsupported file extention'); + } + }, + }} + /> +
      ); }; diff --git a/frontend/src/components/DataSources/Local/DropZoneForSmallLayouts.tsx b/frontend/src/components/DataSources/Local/DropZoneForSmallLayouts.tsx index 13fe74745..31147767c 100644 --- a/frontend/src/components/DataSources/Local/DropZoneForSmallLayouts.tsx +++ b/frontend/src/components/DataSources/Local/DropZoneForSmallLayouts.tsx @@ -14,7 +14,7 @@ export default function DropZoneForSmallLayouts() { const { filesData, setFilesData, model } = useFileContext(); const [isLoading, setIsLoading] = useState(false); const [isClicked, setIsClicked] = useState(false); - const { userCredentials } = useCredentials(); + const { userCredentials, connectionStatus, isReadOnlyUser } = useCredentials(); const [selectedFiles, setSelectedFiles] = useState([]); const uploadFileInChunks = (file: File) => { @@ -218,7 +218,7 @@ export default function DropZoneForSmallLayouts() { return ( <>
      - + {isLoading ? : }
      diff --git a/frontend/src/components/DataSources/Web/WebButton.tsx b/frontend/src/components/DataSources/Web/WebButton.tsx index 6e1d85f4b..a80475363 100644 --- a/frontend/src/components/DataSources/Web/WebButton.tsx +++ b/frontend/src/components/DataSources/Web/WebButton.tsx @@ -5,7 +5,7 @@ import { ThemeWrapperContext } from '../../../context/ThemeWrapper'; import internet from '../../../assets/images/web-search-svgrepo-com.svg'; import internetdarkmode from '../../../assets/images/web-search-darkmode-final2.svg'; -const WebButton: React.FC = ({ openModal, isLargeDesktop = true }) => { +const WebButton: React.FC = ({ openModal, isLargeDesktop = true, isDisabled }) => { const themeUtils = useContext(ThemeWrapperContext); return ( @@ -14,6 +14,7 @@ const WebButton: React.FC = ({ openModal, isLargeDesktop = t logo={themeUtils.colorMode === 'dark' ? internetdarkmode : internet} wrapperclassName='my-2' className={isLargeDesktop ? 'webImg' : 'widthunset'} + isDisabled={isDisabled} /> ); }; diff --git a/frontend/src/components/Dropdown.tsx b/frontend/src/components/Dropdown.tsx index 8eee984f5..8f43e3f3f 100644 --- a/frontend/src/components/Dropdown.tsx +++ b/frontend/src/components/Dropdown.tsx @@ -1,4 +1,4 @@ -import { Tooltip, useMediaQuery, Select } from '@neo4j-ndl/react'; +import { Tooltip, useMediaQuery, Select, SpotlightTarget } from '@neo4j-ndl/react'; import { OptionType, ReusableDropdownProps } from '../types'; import { memo, useMemo } from 'react'; import { capitalize, capitalizeWithUnderscore } from '../utils/Utils'; @@ -27,50 +27,53 @@ const DropdownComponent: React.FC = ({ return ( <>
      - + LLM Model for Processing & Chat +
      + } + selectProps={{ + onChange: handleChange, + // @ts-ignore + options: allOptions?.map((option) => { + const label = typeof option === 'string' ? capitalizeWithUnderscore(option) : capitalize(option.label); + const value = typeof option === 'string' ? option : option.value; + const isModelSupported = !isProdEnv || prodllms?.includes(value); + return { + label: !isModelSupported ? ( + + + {label} + + Available In Development Version + + ) : ( + {label} + ), + value, + isDisabled: !isModelSupported, + }; + }), + placeholder: placeholder || 'Select an option', + defaultValue: defaultValue + ? { label: capitalizeWithUnderscore(defaultValue), value: defaultValue } + : undefined, + menuPlacement: 'auto', + isDisabled: isDisabled, + value: value, + }} + size='medium' + isFluid + htmlAttributes={{ + 'aria-label': 'A selection dropdown', + }} + /> + + {children}
      diff --git a/frontend/src/components/FileTable.tsx b/frontend/src/components/FileTable.tsx index 0011ef4fc..1617d88a1 100644 --- a/frontend/src/components/FileTable.tsx +++ b/frontend/src/components/FileTable.tsx @@ -337,7 +337,7 @@ const FileTable: ForwardRefRenderFunction = (props, re columnHelper.accessor((row) => row.uploadProgress, { id: 'uploadprogess', cell: (info: CellContext) => { - if (parseInt(info.getValue()) === 100 || info.row.original?.status === 'New') { + if (Number(info.getValue()) === 100 || info.row.original?.status === 'New') { return (
      @@ -347,7 +347,7 @@ const FileTable: ForwardRefRenderFunction = (props, re
      ); } else if (info.row.original?.status === 'Uploading') { - return ; + return ; } else if (info.row.original?.status === 'Failed') { return (
      @@ -372,7 +372,7 @@ const FileTable: ForwardRefRenderFunction = (props, re }), columnHelper.accessor((row) => row.size, { id: 'fileSize', - cell: (info: CellContext) => {(parseInt(info?.getValue()) / 1000)?.toFixed(2)}, + cell: (info: CellContext) => {(Number(info?.getValue()) / 1000)?.toFixed(2)}, header: () => Size (KB), footer: (info) => info.column.id, }), @@ -714,7 +714,7 @@ const FileTable: ForwardRefRenderFunction = (props, re waitingQueue.length && waitingQueue.find((f: CustomFile) => f.name === item.fileName); if (isFileCompleted(waitingFile as CustomFile, item)) { setProcessedCount((prev) => calculateProcessedCount(prev, batchSize)); - queue.remove(item.fileName); + queue.remove((i) => i.name === item.fileName); } if (waitingFile && item.status === 'Completed') { setProcessedCount((prev) => { @@ -723,7 +723,7 @@ const FileTable: ForwardRefRenderFunction = (props, re } return prev + 1; }); - queue.remove(item.fileName); + queue.remove((i) => i.name === item.fileName); } prefiles.push({ name: item?.fileName, @@ -851,7 +851,7 @@ const FileTable: ForwardRefRenderFunction = (props, re } return prev + 1; }); - queue.remove(fileName); + queue.remove((i) => i.name === fileName); } else { let errorobj = { error: res.data.error, message: res.data.message, fileName }; throw new Error(JSON.stringify(errorobj)); @@ -925,7 +925,7 @@ const FileTable: ForwardRefRenderFunction = (props, re } return prev + 1; }); - queue.remove(fileName); + queue.remove((i) => i.name === fileName); } }; diff --git a/frontend/src/components/Graph/GraphViewModal.tsx b/frontend/src/components/Graph/GraphViewModal.tsx index f9775d3e2..28aede125 100644 --- a/frontend/src/components/Graph/GraphViewModal.tsx +++ b/frontend/src/components/Graph/GraphViewModal.tsx @@ -382,8 +382,8 @@ const GraphViewModal: React.FunctionComponent = ({ )} - -
      + +
      {loading ? (
      diff --git a/frontend/src/components/Layout/DrawerDropzone.tsx b/frontend/src/components/Layout/DrawerDropzone.tsx index f6e9b301f..a3c4db2ac 100644 --- a/frontend/src/components/Layout/DrawerDropzone.tsx +++ b/frontend/src/components/Layout/DrawerDropzone.tsx @@ -125,12 +125,20 @@ const DrawerDropzone: React.FC = ({ ) : ( - + { + loginWithRedirect(); + }, + }} + > {filesData.length === 0 - ? `It seems like you haven't ingested any data yet Please log in and connect to your database to proceed.` - : 'You must be logged in to process this data. Please log in and connect to your database to proceed.'} + ? `It seems like you haven't ingested any data yet Please log in to the main application.` + : 'You must be logged in to process this data. Please log in to the main application'}
      loginWithRedirect()}> diff --git a/frontend/src/components/Layout/Header.tsx b/frontend/src/components/Layout/Header.tsx index 91719f89c..80f06b1c0 100644 --- a/frontend/src/components/Layout/Header.tsx +++ b/frontend/src/components/Layout/Header.tsx @@ -10,8 +10,8 @@ import { ArrowLeftIconOutline, ArrowDownTrayIconOutline, } from '@neo4j-ndl/react/icons'; -import { Button, TextLink, Typography } from '@neo4j-ndl/react'; -import { Dispatch, memo, SetStateAction, useCallback, useContext, useRef, useState } from 'react'; +import { Button, SpotlightTarget, TextLink, Typography, useSpotlightContext } from '@neo4j-ndl/react'; +import { memo, useCallback, useContext, useEffect, useRef, useState, useMemo } from 'react'; import { IconButtonWithToolTip } from '../UI/IconButtonToolTip'; import { buttonCaptions, SKIP_AUTH, tooltips } from '../../utils/Constants'; import { ThemeWrapperContext } from '../../context/ThemeWrapper'; @@ -20,18 +20,11 @@ import { useLocation, useNavigate } from 'react-router'; import { useMessageContext } from '../../context/UserMessages'; import { RiChatSettingsLine } from 'react-icons/ri'; import ChatModeToggle from '../ChatBot/ChatModeToggle'; -import { connectionState } from '../../types'; +import { HeaderProp } from '../../types'; import { downloadClickHandler, getIsLoading } from '../../utils/Utils'; import Profile from '../User/Profile'; import { useAuth0 } from '@auth0/auth0-react'; -interface HeaderProp { - chatOnly?: boolean; - deleteOnClick?: () => void; - setOpenConnection?: Dispatch>; - showBackButton?: boolean; -} - const Header: React.FC = ({ chatOnly, deleteOnClick, setOpenConnection, showBackButton }) => { const { colorMode, toggleColorMode } = useContext(ThemeWrapperContext); const navigate = useNavigate(); @@ -41,11 +34,20 @@ const Header: React.FC = ({ chatOnly, deleteOnClick, setOpenConnecti }, []); const downloadLinkRef = useRef(null); const { loginWithRedirect } = useAuth0(); - + const firstTourTarget = useRef(null); const { connectionStatus } = useCredentials(); const chatAnchor = useRef(null); const { pathname } = useLocation(); const [showChatModeOption, setShowChatModeOption] = useState(false); + const { setIsOpen } = useSpotlightContext(); + const isFirstTimeUser = useMemo(() => { + return localStorage.getItem('neo4j.connection') === null; + }, []); + useEffect(() => { + if (!connectionStatus && isFirstTimeUser) { + setIsOpen(true); + } + }, []); const openChatPopout = useCallback(() => { let session = localStorage.getItem('neo4j.connection'); const isLoading = getIsLoading(messages); @@ -70,6 +72,7 @@ const Header: React.FC = ({ chatOnly, deleteOnClick, setOpenConnecti const onBackButtonClick = () => { navigate('/', { state: messages }); }; + return ( <>
      = ({ chatOnly, deleteOnClick, setOpenConnecti id='navigation' aria-label='main navigation' > -
      +
      = ({ chatOnly, deleteOnClick, setOpenConnecti {!SKIP_AUTH && } - {pathname === '/readonly' && ( - - )} + {pathname === '/readonly' && + (!connectionStatus ? ( + + + + ) : ( + + ))}
      diff --git a/frontend/src/components/Layout/PageLayout.tsx b/frontend/src/components/Layout/PageLayout.tsx index 5750b1d40..5b9cd2b91 100644 --- a/frontend/src/components/Layout/PageLayout.tsx +++ b/frontend/src/components/Layout/PageLayout.tsx @@ -7,7 +7,7 @@ import { clearChatAPI } from '../../services/QnaAPI'; import { useCredentials } from '../../context/UserCredentials'; import { connectionState } from '../../types'; import { useMessageContext } from '../../context/UserMessages'; -import { useMediaQuery } from '@mui/material'; +import { useMediaQuery, Spotlight, SpotlightTour, useSpotlightContext } from '@neo4j-ndl/react'; import { useFileContext } from '../../context/UsersFiles'; import SchemaFromTextDialog from '../Popups/Settings/SchemaFromText'; import useSpeechSynthesis from '../../hooks/useSpeech'; @@ -24,6 +24,123 @@ const S3Modal = lazy(() => import('../DataSources/AWS/S3Modal')); const GenericModal = lazy(() => import('../WebSources/GenericSourceModal')); const ConnectionModal = lazy(() => import('../Popups/ConnectionModal/ConnectionModal')); +const spotlightsforunauthenticated = [ + { + target: 'loginbutton', + children: ( + <> + Login with Neo4j + Using Google Account or Email Address + + ), + }, + { + target: 'connectbutton', + children: ( + <> + Connect To Neo4j Database + Fill out the neo4j credentials and click on connect + + ), + }, + { + target: 'dropzone', + children: ( + <> + Upload documents + Upload any unstructured files + + ), + }, + { + target: 'llmdropdown', + children: ( + <> + Choose The Desired LLM + + ), + }, + { + target: 'generategraphbtn', + children: ( + <> + Start The Extraction Process + Click On Generate Graph + + ), + }, + { + target: 'visualizegraphbtn', + children: ( + <> + Visualize The Knowledge Graph + Select At Least One or More Completed Files From The Table For Visualization + + ), + }, + { + target: 'chatbtn', + children: ( + <> + Ask Questions Related To Documents + + ), + }, +]; +const spotlights = [ + { + target: 'connectbutton', + children: ( + <> + Connect To Neo4j Database + Fill out the neo4j credentials and click on connect + + ), + }, + { + target: 'dropzone', + children: ( + <> + Upload documents + Upload any unstructured files + + ), + }, + { + target: 'llmdropdown', + children: ( + <> + Choose The Desired LLM + + ), + }, + { + target: 'generategraphbtn', + children: ( + <> + Start The Extraction Process + Click On Generate Graph + + ), + }, + { + target: 'visualizegraphbtn', + children: ( + <> + Visualize The Knowledge Graph + Select At Least One or More Completed Files From The Table For Visualization + + ), + }, + { + target: 'chatbtn', + children: ( + <> + Ask Questions Related To Documents + + ), + }, +]; const PageLayout: React.FC = () => { const [openConnection, setOpenConnection] = useState({ openPopUp: false, @@ -43,7 +160,6 @@ const PageLayout: React.FC = () => { setShowDisconnectButton, showDisconnectButton, setIsGCSActive, - // setChunksToBeProces, } = useCredentials(); const [isLeftExpanded, setIsLeftExpanded] = useState(Boolean(isLargeDesktop)); const [isRightExpanded, setIsRightExpanded] = useState(Boolean(isLargeDesktop)); @@ -58,6 +174,7 @@ const PageLayout: React.FC = () => { const { user, isAuthenticated } = useAuth0(); const navigate = useNavigate(); + const toggleLeftDrawer = () => { if (isLargeDesktop) { setIsLeftExpanded(!isLeftExpanded); @@ -87,7 +204,10 @@ const PageLayout: React.FC = () => { const { messages, setClearHistoryData, clearHistoryData, setMessages, setIsDeleteChatLoading } = useMessageContext(); const { setShowTextFromSchemaDialog, showTextFromSchemaDialog } = useFileContext(); const { cancel } = useSpeechSynthesis(); - + const { setActiveSpotlight } = useSpotlightContext(); + const isFirstTimeUser = useMemo(() => { + return localStorage.getItem('neo4j.connection') === null; + }, []); useEffect(() => { async function initializeConnection() { // Fetch backend health status @@ -111,11 +231,10 @@ const PageLayout: React.FC = () => { isReadonlyUser: !connectionData.data.write_access, isgdsActive: connectionData.data.gds_status, isGCSActive: connectionData.data.gcs_file_cache === 'True', - chunksTobeProcess: parseInt(connectionData.data.chunk_to_be_created), + chunksTobeProcess: Number(connectionData.data.chunk_to_be_created), email: user?.email ?? '', connection: 'backendApi', }; - // setChunksToBeProces(credentials.chunksTobeProcess); setIsGCSActive(credentials.isGCSActive); setUserCredentials(credentials); createDefaultFormData({ uri: credentials.uri, email: credentials.email ?? '' }); @@ -128,8 +247,13 @@ const PageLayout: React.FC = () => { if (storedCredentials) { const credentials = JSON.parse(storedCredentials); setUserCredentials({ ...credentials, password: atob(credentials.password) }); - createDefaultFormData({ ...credentials, password: atob(credentials.password) }); - // setChunksToBeProces(credentials.chunksTobeProcess); + createDefaultFormData({ + uri: credentials.uri, + database: credentials.database, + userName: credentials.userName, + password: atob(credentials.password), + email: credentials.email ?? '', + }); setIsGCSActive(credentials.isGCSActive); setGdsActive(credentials.isgdsActive); setConnectionStatus(Boolean(credentials.connection === 'connectAPI')); @@ -138,12 +262,10 @@ const PageLayout: React.FC = () => { } handleDisconnectButtonState(true); } else { - setOpenConnection((prev) => ({ ...prev, openPopUp: true })); handleDisconnectButtonState(true); } } else { setErrorMessage(backendApiResponse?.data?.error); - setOpenConnection((prev) => ({ ...prev, openPopUp: true })); handleDisconnectButtonState(true); console.log('from else cndition error is there'); } @@ -154,6 +276,13 @@ const PageLayout: React.FC = () => { } } initializeConnection(); + if (!isAuthenticated && isFirstTimeUser) { + setActiveSpotlight('loginbutton'); + } + + if (isAuthenticated && isFirstTimeUser) { + setActiveSpotlight('connectbutton'); + } }, [isAuthenticated]); const deleteOnClick = async () => { @@ -190,6 +319,39 @@ const PageLayout: React.FC = () => { return ( <> + {!isAuthenticated && isFirstTimeUser && ( + { + if (target == 'connectbutton' && action == 'next') { + if (!isLeftExpanded) { + toggleLeftDrawer(); + } + } + if (target === 'visualizegraphbtn' && action === 'next' && !isRightExpanded) { + toggleRightDrawer(); + } + console.log(`Action ${action} was performed in spotlight ${target}`); + }} + /> + )} + {isAuthenticated && isFirstTimeUser && ( + { + if (target == 'connectbutton' && action == 'next') { + if (!isLeftExpanded) { + toggleLeftDrawer(); + } + } + if (target === 'visualizegraphbtn' && action === 'next' && !isRightExpanded) { + toggleRightDrawer(); + } + console.log(`Action ${action} was performed in spotlight ${target}`); + }} + /> + )} + }> = ({ const { setMessages, isDeleteChatLoading } = useMessageContext(); const [showChatMode, setShowChatMode] = useState(false); const isLargeDesktop = useMediaQuery(`(min-width:1440px )`); - const { connectionStatus, isReadOnlyUser } = useCredentials(); + const { connectionStatus } = useCredentials(); const downloadLinkRef = useRef(null); const anchorMenuRef = useRef(null); @@ -80,9 +80,9 @@ const SideNav: React.FC = ({ const renderDataSourceItems = () => { const dataSourceItems = []; - if (!isLargeDesktop && !isReadOnlyUser && position === 'left') { - if (connectionStatus) { - dataSourceItems.push( + if (!isLargeDesktop && position === 'left') { + dataSourceItems.push( + = ({ } /> - ); - } + + ); - if (APP_SOURCES.includes('gcs') && connectionStatus && position === 'left') { + if (APP_SOURCES.includes('gcs') && position === 'left') { dataSourceItems.push( - + } /> ); } - if (APP_SOURCES.includes('s3') && connectionStatus && position === 'left') { + if (APP_SOURCES.includes('s3') && position === 'left') { dataSourceItems.push( - + } /> ); } - if (APP_SOURCES.includes('web') && connectionStatus && position === 'left') { + if (APP_SOURCES.includes('web') && position === 'left') { dataSourceItems.push( - + } /> @@ -164,14 +172,16 @@ const SideNav: React.FC = ({ )} {position === 'right' && !isExpanded && ( - - - - } - /> + + + + + } + /> + )} {renderDataSourceItems()} {position === 'right' && isExpanded && ( diff --git a/frontend/src/components/Popups/ConnectionModal/ConnectionModal.tsx b/frontend/src/components/Popups/ConnectionModal/ConnectionModal.tsx index 29ab5e9fd..fc4876a49 100644 --- a/frontend/src/components/Popups/ConnectionModal/ConnectionModal.tsx +++ b/frontend/src/components/Popups/ConnectionModal/ConnectionModal.tsx @@ -64,7 +64,6 @@ export default function ConnectionModal({ const databaseRef = useRef(null); const userNameRef = useRef(null); const passwordRef = useRef(null); - useEffect(() => { if (searchParams.has('connectURL')) { const url = searchParams.get('connectURL'); @@ -237,7 +236,7 @@ export default function ConnectionModal({ const isgdsActive = response.data.data.gds_status; const isReadOnlyUser = !response.data.data.write_access; const isGCSActive = response.data.data.gcs_file_cache === 'True'; - const chunksTobeProcess = parseInt(response.data.data.chunk_to_be_created); + const chunksTobeProcess = Number(response.data.data.chunk_to_be_created); setIsGCSActive(isGCSActive); setGdsActive(isgdsActive); setIsReadOnlyUser(isReadOnlyUser); @@ -361,7 +360,7 @@ export default function ConnectionModal({ Connect to Neo4j - + Don't have a Neo4j instance? Start for free today diff --git a/frontend/src/components/UI/CustomButton.tsx b/frontend/src/components/UI/CustomButton.tsx index 038adbe36..f22746e3e 100644 --- a/frontend/src/components/UI/CustomButton.tsx +++ b/frontend/src/components/UI/CustomButton.tsx @@ -1,8 +1,18 @@ import { CommonButtonProps } from '../../types'; -const CustomButton: React.FC = ({ openModal, wrapperclassName, logo, title, className }) => { +const CustomButton: React.FC = ({ + openModal, + wrapperclassName, + logo, + title, + className, + isDisabled = false, +}) => { return ( -
      +
      {title}
      diff --git a/frontend/src/components/User/Profile.tsx b/frontend/src/components/User/Profile.tsx index 38c26b93f..6c31c9f96 100644 --- a/frontend/src/components/User/Profile.tsx +++ b/frontend/src/components/User/Profile.tsx @@ -26,7 +26,7 @@ export default function Profile() { setShowOpen(false); }; if (isLoading) { - return
      Loading ...
      ; + return ; } if (isAuthenticated) { return ( diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 0a75e4c8f..72b6ae712 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -111,6 +111,7 @@ export interface CustomAlertProps { export interface DataComponentProps { openModal: () => void; isLargeDesktop?: boolean; + isDisabled?: boolean; } export interface S3ModalProps { @@ -204,6 +205,7 @@ export interface CommonButtonProps { className?: string; imgWidth?: number; imgeHeight?: number; + isDisabled?: boolean; } export interface Source { @@ -868,8 +870,8 @@ export interface FileContextType { setShowTextFromSchemaDialog: React.Dispatch>; postProcessingTasks: string[]; setPostProcessingTasks: React.Dispatch>; - queue: Queue; - setQueue: Dispatch>; + queue: Queue; + setQueue: Dispatch>>; processedCount: number; setProcessedCount: Dispatch>; postProcessingVal: boolean; @@ -952,3 +954,9 @@ export type FileTableHandle = React.ElementRef; export interface VisibilityProps { isVisible: boolean; } +export interface HeaderProp { + chatOnly?: boolean; + deleteOnClick?: () => void; + setOpenConnection?: Dispatch>; + showBackButton?: boolean; +} diff --git a/frontend/src/utils/Constants.ts b/frontend/src/utils/Constants.ts index 723e28ee7..1d319757c 100644 --- a/frontend/src/utils/Constants.ts +++ b/frontend/src/utils/Constants.ts @@ -13,7 +13,7 @@ export const llms = process.env?.VITE_LLM_MODELS?.trim() != '' ? (process.env.VITE_LLM_MODELS?.split(',') as string[]) : [ - 'openai_gpt_3.5', + 'openai_gpt_4.5', 'openai-gpt-o3-mini', 'openai_gpt_4o', 'openai_gpt_4o_mini', @@ -25,7 +25,7 @@ export const llms = 'azure_ai_gpt_4o', 'ollama_llama3', 'groq_llama3_70b', - 'anthropic_claude_3_5_sonnet', + 'anthropic_claude_3_7_sonnet', 'fireworks_llama_v3p2_90b', 'fireworks_qwen72b_instruct', 'bedrock_claude_3_5_sonnet', @@ -37,7 +37,7 @@ export const llms = ]; export const supportedLLmsForRagas = [ - 'openai_gpt_3.5', + 'openai_gpt_4.5', 'openai_gpt_4', 'openai_gpt_4o', 'openai_gpt_4o_mini', @@ -47,20 +47,20 @@ export const supportedLLmsForRagas = [ 'azure_ai_gpt_35', 'azure_ai_gpt_4o', 'groq_llama3_70b', - 'anthropic_claude_3_5_sonnet', + 'anthropic_claude_3_7_sonnet', 'fireworks_llama_v3_70b', 'bedrock_claude_3_5_sonnet', 'openai-gpt-o3-mini', ]; export const supportedLLmsForGroundTruthMetrics = [ - 'openai_gpt_3.5', + 'openai_gpt_4.5', 'openai_gpt_4', 'openai_gpt_4o', 'openai_gpt_4o_mini', 'azure_ai_gpt_35', 'azure_ai_gpt_4o', 'groq_llama3_70b', - 'anthropic_claude_3_5_sonnet', + 'anthropic_claude_3_7_sonnet', 'fireworks_llama_v3_70b', 'bedrock_claude_3_5_sonnet', 'openai-gpt-o3-mini', @@ -130,17 +130,17 @@ export const chatModes = }, ]; -export const chunkSize = process.env.VITE_CHUNK_SIZE ? parseInt(process.env.VITE_CHUNK_SIZE) : 1 * 1024 * 1024; -export const tokenchunkSize = process.env.VITE_TOKENS_PER_CHUNK ? parseInt(process.env.VITE_TOKENS_PER_CHUNK) : 100; -export const chunkOverlap = process.env.VITE_CHUNK_OVERLAP ? parseInt(process.env.VITE_CHUNK_OVERLAP) : 20; -export const chunksToCombine = process.env.VITE_CHUNK_TO_COMBINE ? parseInt(process.env.VITE_CHUNK_TO_COMBINE) : 1; +export const chunkSize = process.env.VITE_CHUNK_SIZE ? Number(process.env.VITE_CHUNK_SIZE) : 1 * 1024 * 1024; +export const tokenchunkSize = process.env.VITE_TOKENS_PER_CHUNK ? Number(process.env.VITE_TOKENS_PER_CHUNK) : 100; +export const chunkOverlap = process.env.VITE_CHUNK_OVERLAP ? Number(process.env.VITE_CHUNK_OVERLAP) : 20; +export const chunksToCombine = process.env.VITE_CHUNK_TO_COMBINE ? Number(process.env.VITE_CHUNK_TO_COMBINE) : 1; export const defaultTokenChunkSizeOptions = [50, 100, 200, 400, 1000]; export const defaultChunkOverlapOptions = [10, 20, 30, 40, 50]; export const defaultChunksToCombineOptions = [1, 2, 3, 4, 5, 6]; -export const timeperpage = process.env.VITE_TIME_PER_PAGE ? parseInt(process.env.VITE_TIME_PER_PAGE) : 50; +export const timeperpage = process.env.VITE_TIME_PER_PAGE ? Number(process.env.VITE_TIME_PER_PAGE) : 50; export const timePerByte = 0.2; export const largeFileSize = process.env.VITE_LARGE_FILE_SIZE - ? parseInt(process.env.VITE_LARGE_FILE_SIZE) + ? Number(process.env.VITE_LARGE_FILE_SIZE) : 5 * 1024 * 1024; export const tooltips = { @@ -237,7 +237,7 @@ export const RETRY_OPIONS = [ 'delete_entities_and_start_from_beginning', 'start_from_last_processed_position', ]; -export const batchSize: number = parseInt(process.env.VITE_BATCH_SIZE ?? '2'); +export const batchSize: number = Number(process.env.VITE_BATCH_SIZE ?? '2'); // Graph Constants export const document = `+ [docs]`; diff --git a/frontend/src/utils/Queue.ts b/frontend/src/utils/Queue.ts index 725918623..018a62163 100644 --- a/frontend/src/utils/Queue.ts +++ b/frontend/src/utils/Queue.ts @@ -1,42 +1,42 @@ -import { CustomFile } from '../types'; -class Queue { - items: CustomFile[] = []; +class Queue { + items: T[] = []; - constructor(items: CustomFile[]) { + constructor(items: T[] = []) { this.items = items; } - enqueue(item: CustomFile) { + enqueue(item: T) { this.items.push(item); } - dequeue() { + dequeue(): T | undefined { if (!this.isEmpty()) { return this.items.shift(); } } - peek() { + peek(): T | undefined { if (this.isEmpty()) { - return -1; + return undefined; } return this.items[0]; } - size() { + size(): number { return this.items.length; } - isEmpty() { + isEmpty(): boolean { return this.items.length === 0; } - remove(name: string) { - this.items = [...this.items.filter((f) => f.name != name)]; + remove(predicate: (item: T) => boolean) { + this.items = this.items.filter((item) => !predicate(item)); } clear() { this.items = []; } } + export default Queue; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 022731c35..b467d7583 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -791,12 +791,12 @@ tinycolor2 "1.6.0" usehooks-ts "3.1.0" -"@neo4j-nvl/base@0.3.6", "@neo4j-nvl/base@^0.3.6": - version "0.3.6" - resolved "https://registry.yarnpkg.com/@neo4j-nvl/base/-/base-0.3.6.tgz#e5f8020fc641412ccc638f32d25edeaa037113ae" - integrity sha512-mxWbg6Lsrp7jjR4O4eqIPVJBMpRTzQ8Jir8liJ2HOLVL2QvpEYGQc8NP/F5GfpkVSUHj/uE5dlWKhNaaB6Lo1g== +"@neo4j-nvl/base@0.3.7", "@neo4j-nvl/base@^0.3.6": + version "0.3.7" + resolved "https://registry.yarnpkg.com/@neo4j-nvl/base/-/base-0.3.7.tgz#0ec95c528324648bc55d50999e8f3c5c00a84aab" + integrity sha512-/3gKtk9eCAw3tW8p+uSs4HBkK9jn2KeDqbG6fM2a+QLgHGJNj0iMSbWpKJNJPN5VNrQ5asRx+MOKd8/OezTzvg== dependencies: - "@neo4j-nvl/layout-workers" "0.3.6" + "@neo4j-nvl/layout-workers" "0.3.7" "@segment/analytics-next" "^1.70.0" color-string "^1.9.1" d3-force "^3.0.0" @@ -810,19 +810,19 @@ tinycolor2 "1.6.0" uuid "^8.3.2" -"@neo4j-nvl/interaction-handlers@0.3.6": - version "0.3.6" - resolved "https://registry.yarnpkg.com/@neo4j-nvl/interaction-handlers/-/interaction-handlers-0.3.6.tgz#48d1db20c2febf095cb4084dc1bffa7dd825bcfe" - integrity sha512-ul99QZVwJ9h6btWXOTvMDWQJhs7qnZjBnwfq9gZ+QeBHhWT960Zw8VST39Qre8L7jLzLTa38HPfSbnabIiH2hg== +"@neo4j-nvl/interaction-handlers@0.3.7": + version "0.3.7" + resolved "https://registry.yarnpkg.com/@neo4j-nvl/interaction-handlers/-/interaction-handlers-0.3.7.tgz#22f4ae44c90884aa2e780d3af9bedeb531520212" + integrity sha512-U0Yh08R6ihn4b+pKIkLhMmSADSrrHwyR0N2/zP30N0ZRM59gZjpFbOpXYeKnwn+YWar3DHYKyCrb8xpqEtBaYg== dependencies: - "@neo4j-nvl/base" "0.3.6" + "@neo4j-nvl/base" "0.3.7" concaveman "^1.2.1" lodash "4.17.21" -"@neo4j-nvl/layout-workers@0.3.6": - version "0.3.6" - resolved "https://registry.yarnpkg.com/@neo4j-nvl/layout-workers/-/layout-workers-0.3.6.tgz#88f97a93d9f44e5efa5d7396108e0e30cf95b01c" - integrity sha512-yHq2FVPgOU8gGj42zgqXgH2BQyjW0xRozqFGNb7OutgP9jbJh/fATC9zyUTtBkO2KGjsmNnujujtzrI0yQ+Qsw== +"@neo4j-nvl/layout-workers@0.3.7": + version "0.3.7" + resolved "https://registry.yarnpkg.com/@neo4j-nvl/layout-workers/-/layout-workers-0.3.7.tgz#0bf534b376fa5630ef40c6bde5c5c369a35a0176" + integrity sha512-o+MY4BG9aNjTbDiqNOhWCgDTI9O5woKe361ub0YwWyr5bI9Hs8VjGuyjyxd+cqmRZQeSgogu+onkz2kritt3+w== dependencies: "@neo4j-bloom/dagre" "^0.8.14" bin-pack "^1.0.2" @@ -830,13 +830,13 @@ cytoscape-cose-bilkent "^4.1.0" graphlib "^2.1.8" -"@neo4j-nvl/react@^0.3.6": - version "0.3.6" - resolved "https://registry.yarnpkg.com/@neo4j-nvl/react/-/react-0.3.6.tgz#5f16ddc1f4c89dcc2ac0ae2c19cb14b4dd3af08f" - integrity sha512-tv2jkDxYDq9o5RN4FANPYMk3EAnTBmO4XNI6K9S6Km0EsINuWsG0EC/10rht3StDjubkJN6qyV8/he8T603fmg== +"@neo4j-nvl/react@^0.3.7": + version "0.3.7" + resolved "https://registry.yarnpkg.com/@neo4j-nvl/react/-/react-0.3.7.tgz#4d99dd8c7d5e6818c8d105c9119fbc931335fecc" + integrity sha512-HKwz7HySVloM58IzOcMsFY/uTeHHbtE4d36wU4n8pGCqZqR3MpHDDdOVVLqnDMMrW+H16A4/tZk8R6pI8Ztx7A== dependencies: - "@neo4j-nvl/base" "0.3.6" - "@neo4j-nvl/interaction-handlers" "0.3.6" + "@neo4j-nvl/base" "0.3.7" + "@neo4j-nvl/interaction-handlers" "0.3.7" lodash "4.17.21" react "^18.2.0" react-dom "^18.2.0" @@ -2003,98 +2003,98 @@ dependencies: tslib "^2.4.0" -"@tailwindcss/node@4.0.7": - version "4.0.7" - resolved "https://registry.yarnpkg.com/@tailwindcss/node/-/node-4.0.7.tgz#11211457bbe83ff3656c74bf0276e27e9ce87410" - integrity sha512-dkFXufkbRB2mu3FPsW5xLAUWJyexpJA+/VtQj18k3SUiJVLdpgzBd1v1gRRcIpEJj7K5KpxBKfOXlZxT3ZZRuA== +"@tailwindcss/node@4.0.12": + version "4.0.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/node/-/node-4.0.12.tgz#0f89ad2537eafe65a35ec4400b42142de918bafc" + integrity sha512-a6J11K1Ztdln9OrGfoM75/hChYPcHYGNYimqciMrvKXRmmPaS8XZTHhdvb5a3glz4Kd4ZxE1MnuFE2c0fGGmtg== dependencies: enhanced-resolve "^5.18.1" jiti "^2.4.2" - tailwindcss "4.0.7" + tailwindcss "4.0.12" -"@tailwindcss/oxide-android-arm64@4.0.7": - version "4.0.7" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.7.tgz#a0864f4831a4eca1a92207fd5ae726343fbb0554" - integrity sha512-5iQXXcAeOHBZy8ASfHFm1k0O/9wR2E3tKh6+P+ilZZbQiMgu+qrnfpBWYPc3FPuQdWiWb73069WT5D+CAfx/tg== +"@tailwindcss/oxide-android-arm64@4.0.12": + version "4.0.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.12.tgz#f79cf637bc11b0ae71e75b66d7c5ba5159bd3df3" + integrity sha512-dAXCaemu3mHLXcA5GwGlQynX8n7tTdvn5i1zAxRvZ5iC9fWLl5bGnjZnzrQqT7ttxCvRwdVf3IHUnMVdDBO/kQ== -"@tailwindcss/oxide-darwin-arm64@4.0.7": - version "4.0.7" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.7.tgz#a25e3831cfb0a9ab5b77ada620b8c714e0f41eab" - integrity sha512-7yGZtEc5IgVYylqK/2B0yVqoofk4UAbkn1ygNpIJZyrOhbymsfr8uUFCueTu2fUxmAYIfMZ8waWo2dLg/NgLgg== +"@tailwindcss/oxide-darwin-arm64@4.0.12": + version "4.0.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.12.tgz#6b730b5d19ff01748a27af5103966f534a899f07" + integrity sha512-vPNI+TpJQ7sizselDXIJdYkx9Cu6JBdtmRWujw9pVIxW8uz3O2PjgGGzL/7A0sXI8XDjSyRChrUnEW9rQygmJQ== -"@tailwindcss/oxide-darwin-x64@4.0.7": - version "4.0.7" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.7.tgz#1dae8648aa03c9783cd01cfd1249c781aaa49dd1" - integrity sha512-tPQDV20fBjb26yWbPqT1ZSoDChomMCiXTKn4jupMSoMCFyU7+OJvIY1ryjqBuY622dEBJ8LnCDDWsnj1lX9nNQ== +"@tailwindcss/oxide-darwin-x64@4.0.12": + version "4.0.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.12.tgz#2eb33ba40cbb78922004be9d6b81542347043fe5" + integrity sha512-RL/9jM41Fdq4Efr35C5wgLx98BirnrfwuD+zgMFK6Ir68HeOSqBhW9jsEeC7Y/JcGyPd3MEoJVIU4fAb7YLg7A== -"@tailwindcss/oxide-freebsd-x64@4.0.7": - version "4.0.7" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.7.tgz#904aaa135e5a2500faf729290b12133f15273500" - integrity sha512-sZqJpTyTZiknU9LLHuByg5GKTW+u3FqM7q7myequAXxKOpAFiOfXpY710FuMY+gjzSapyRbDXJlsTQtCyiTo5w== +"@tailwindcss/oxide-freebsd-x64@4.0.12": + version "4.0.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.12.tgz#317b980504b0bbcaee32c9e4e961f77cdadb02a6" + integrity sha512-7WzWiax+LguJcMEimY0Q4sBLlFXu1tYxVka3+G2M9KmU/3m84J3jAIV4KZWnockbHsbb2XgrEjtlJKVwHQCoRA== -"@tailwindcss/oxide-linux-arm-gnueabihf@4.0.7": - version "4.0.7" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.7.tgz#27b6483303218f545a971ec68f468417cab1f6be" - integrity sha512-PBgvULgeSswjd8cbZ91gdIcIDMdc3TUHV5XemEpxlqt9M8KoydJzkuB/Dt910jYdofOIaTWRL6adG9nJICvU4A== +"@tailwindcss/oxide-linux-arm-gnueabihf@4.0.12": + version "4.0.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.12.tgz#0f7153dec6e200327b76775ad2d1154706cef34d" + integrity sha512-X9LRC7jjE1QlfIaBbXjY0PGeQP87lz5mEfLSVs2J1yRc9PSg1tEPS9NBqY4BU9v5toZgJgzKeaNltORyTs22TQ== -"@tailwindcss/oxide-linux-arm64-gnu@4.0.7": - version "4.0.7" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.7.tgz#98126af54ff0262756e9a4f730a8b6f6585e13c2" - integrity sha512-By/a2yeh+e9b+C67F88ndSwVJl2A3tcUDb29FbedDi+DZ4Mr07Oqw9Y1DrDrtHIDhIZ3bmmiL1dkH2YxrtV+zw== +"@tailwindcss/oxide-linux-arm64-gnu@4.0.12": + version "4.0.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.12.tgz#50a3ec46600cc998f45a265ebdd5de2d7bb27ee0" + integrity sha512-i24IFNq2402zfDdoWKypXz0ZNS2G4NKaA82tgBlE2OhHIE+4mg2JDb5wVfyP6R+MCm5grgXvurcIcKWvo44QiQ== -"@tailwindcss/oxide-linux-arm64-musl@4.0.7": - version "4.0.7" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.7.tgz#55f0183a8521473cab1ca0414b57079efc145952" - integrity sha512-WHYs3cpPEJb/ccyT20NOzopYQkl7JKncNBUbb77YFlwlXMVJLLV3nrXQKhr7DmZxz2ZXqjyUwsj2rdzd9stYdw== +"@tailwindcss/oxide-linux-arm64-musl@4.0.12": + version "4.0.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.12.tgz#2de10c5239766e2fb810045fe5b75db54d481fd4" + integrity sha512-LmOdshJBfAGIBG0DdBWhI0n5LTMurnGGJCHcsm9F//ISfsHtCnnYIKgYQui5oOz1SUCkqsMGfkAzWyNKZqbGNw== -"@tailwindcss/oxide-linux-x64-gnu@4.0.7": - version "4.0.7" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.7.tgz#053b019201892b339c0d975092bfc8024be54226" - integrity sha512-7bP1UyuX9kFxbOwkeIJhBZNevKYPXB6xZI37v09fqi6rqRJR8elybwjMUHm54GVP+UTtJ14ueB1K54Dy1tIO6w== +"@tailwindcss/oxide-linux-x64-gnu@4.0.12": + version "4.0.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.12.tgz#f89a24ffb0f14ea23d110efcbf58baa1db783582" + integrity sha512-OSK667qZRH30ep8RiHbZDQfqkXjnzKxdn0oRwWzgCO8CoTxV+MvIkd0BWdQbYtYuM1wrakARV/Hwp0eA/qzdbw== -"@tailwindcss/oxide-linux-x64-musl@4.0.7": - version "4.0.7" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.7.tgz#c4d753acf3d6ef617e91b53880b7c1b966677ca6" - integrity sha512-gBQIV8nL/LuhARNGeroqzXymMzzW5wQzqlteVqOVoqwEfpHOP3GMird5pGFbnpY+NP0fOlsZGrxxOPQ4W/84bQ== +"@tailwindcss/oxide-linux-x64-musl@4.0.12": + version "4.0.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.12.tgz#c53a5b494e241a744f067ba3a261adca80c20af4" + integrity sha512-uylhWq6OWQ8krV8Jk+v0H/3AZKJW6xYMgNMyNnUbbYXWi7hIVdxRKNUB5UvrlC3RxtgsK5EAV2i1CWTRsNcAnA== -"@tailwindcss/oxide-win32-arm64-msvc@4.0.7": - version "4.0.7" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.7.tgz#cceea5857939092d5149e6afc8da1b8d3a7464b2" - integrity sha512-aH530NFfx0kpQpvYMfWoeG03zGnRCMVlQG8do/5XeahYydz+6SIBxA1tl/cyITSJyWZHyVt6GVNkXeAD30v0Xg== +"@tailwindcss/oxide-win32-arm64-msvc@4.0.12": + version "4.0.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.12.tgz#63c0befad43355fb5cea6c64efd946a718acb06b" + integrity sha512-XDLnhMoXZEEOir1LK43/gHHwK84V1GlV8+pAncUAIN2wloeD+nNciI9WRIY/BeFTqES22DhTIGoilSO39xDb2g== -"@tailwindcss/oxide-win32-x64-msvc@4.0.7": - version "4.0.7" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.7.tgz#500cf333326a45078ca5b0fd68b56b1f0b434bfa" - integrity sha512-8Cva6bbJN7ZJx320k7vxGGdU0ewmpfS5A4PudyzUuofdi8MgeINuiiWiPQ0VZCda/GX88K6qp+6UpDZNVr8HMQ== +"@tailwindcss/oxide-win32-x64-msvc@4.0.12": + version "4.0.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.12.tgz#d8fabbdc86d323ca29f5ef298c20f651585ed402" + integrity sha512-I/BbjCLpKDQucvtn6rFuYLst1nfFwSMYyPzkx/095RE+tuzk5+fwXuzQh7T3fIBTcbn82qH/sFka7yPGA50tLw== -"@tailwindcss/oxide@4.0.7": - version "4.0.7" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide/-/oxide-4.0.7.tgz#b53573fc01b8b61af195ad36957d05c78278761d" - integrity sha512-yr6w5YMgjy+B+zkJiJtIYGXW+HNYOPfRPtSs+aqLnKwdEzNrGv4ZuJh9hYJ3mcA+HMq/K1rtFV+KsEr65S558g== +"@tailwindcss/oxide@4.0.12": + version "4.0.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide/-/oxide-4.0.12.tgz#2db7c930d2c6fe29ee14a996891b21401650189a" + integrity sha512-DWb+myvJB9xJwelwT9GHaMc1qJj6MDXRDR0CS+T8IdkejAtu8ctJAgV4r1drQJLPeS7mNwq2UHW2GWrudTf63A== optionalDependencies: - "@tailwindcss/oxide-android-arm64" "4.0.7" - "@tailwindcss/oxide-darwin-arm64" "4.0.7" - "@tailwindcss/oxide-darwin-x64" "4.0.7" - "@tailwindcss/oxide-freebsd-x64" "4.0.7" - "@tailwindcss/oxide-linux-arm-gnueabihf" "4.0.7" - "@tailwindcss/oxide-linux-arm64-gnu" "4.0.7" - "@tailwindcss/oxide-linux-arm64-musl" "4.0.7" - "@tailwindcss/oxide-linux-x64-gnu" "4.0.7" - "@tailwindcss/oxide-linux-x64-musl" "4.0.7" - "@tailwindcss/oxide-win32-arm64-msvc" "4.0.7" - "@tailwindcss/oxide-win32-x64-msvc" "4.0.7" - -"@tailwindcss/postcss@^4.0.7": - version "4.0.7" - resolved "https://registry.yarnpkg.com/@tailwindcss/postcss/-/postcss-4.0.7.tgz#b8bc02b5e23248ac7cbac1970ed807850e03261c" - integrity sha512-zXcKs1uGssVDlnsQ+iwrkul5GPKvsXPynGCuk/eXLx3DVhHlQKMpA6tXN2oO28x2ki1xRBTfadKiHy2taVvp7g== + "@tailwindcss/oxide-android-arm64" "4.0.12" + "@tailwindcss/oxide-darwin-arm64" "4.0.12" + "@tailwindcss/oxide-darwin-x64" "4.0.12" + "@tailwindcss/oxide-freebsd-x64" "4.0.12" + "@tailwindcss/oxide-linux-arm-gnueabihf" "4.0.12" + "@tailwindcss/oxide-linux-arm64-gnu" "4.0.12" + "@tailwindcss/oxide-linux-arm64-musl" "4.0.12" + "@tailwindcss/oxide-linux-x64-gnu" "4.0.12" + "@tailwindcss/oxide-linux-x64-musl" "4.0.12" + "@tailwindcss/oxide-win32-arm64-msvc" "4.0.12" + "@tailwindcss/oxide-win32-x64-msvc" "4.0.12" + +"@tailwindcss/postcss@^4.0.12": + version "4.0.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/postcss/-/postcss-4.0.12.tgz#eea9e278317338db58fd5752b6982215ad37a77e" + integrity sha512-r59Sdr8djCW4dL3kvc4aWU8PHdUAVM3O3te2nbYzXsWwKLlHPCuUoZAc9FafXb/YyNDZOMI7sTbKTKFmwOrMjw== dependencies: "@alloc/quick-lru" "^5.2.0" - "@tailwindcss/node" "4.0.7" - "@tailwindcss/oxide" "4.0.7" + "@tailwindcss/node" "4.0.12" + "@tailwindcss/oxide" "4.0.12" lightningcss "^1.29.1" postcss "^8.4.41" - tailwindcss "4.0.7" + tailwindcss "4.0.12" "@tanstack/react-table@^8.20.5": version "8.20.5" @@ -2201,10 +2201,10 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== -"@types/node@^22.13.9": - version "22.13.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.9.tgz#5d9a8f7a975a5bd3ef267352deb96fb13ec02eca" - integrity sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw== +"@types/node@^22.13.10": + version "22.13.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.10.tgz#df9ea358c5ed991266becc3109dc2dc9125d77e4" + integrity sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw== dependencies: undici-types "~6.20.0" @@ -2583,10 +2583,10 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" -axios@^1.7.9: - version "1.7.9" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a" - integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw== +axios@^1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.3.tgz#9ebccd71c98651d547162a018a1a95a4b4ed4de8" + integrity sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" @@ -4545,8 +4545,8 @@ iterator.prototype@^1.1.4: get-proto "^1.0.0" has-symbols "^1.1.0" set-function-name "^2.0.2" -jiti@^2.4.2: +jiti@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.4.2.tgz#d19b7732ebb6116b06e2038da74a55366faef560" integrity sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A== @@ -5494,10 +5494,10 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@^2.7.1: - version "2.8.8" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" - integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== +prettier@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.3.tgz#4fc2ce0d657e7a02e602549f053b239cb7dfe1b5" + integrity sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw== prismjs@^1.27.0: version "1.29.0" @@ -6420,10 +6420,11 @@ tabbable@^6.0.0: resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97" integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew== -tailwindcss@4.0.7, tailwindcss@^4.0.7: - version "4.0.7" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.0.7.tgz#b3e26a5dda77651808a873f1b535cc8c39fcb0ae" - integrity sha512-yH5bPPyapavo7L+547h3c4jcBXcrKwybQRjwdEIVAd9iXRvy/3T1CC6XSQEgZtRySjKfqvo3Cc0ZF1DTheuIdA== +tailwindcss@4.0.12, tailwindcss@^4.0.7: + version "4.0.12" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.0.12.tgz#71ab22c78810303b1156354bcc561a747169b5bd" + integrity sha512-bT0hJo91FtncsAMSsMzUkoo/iEU0Xs5xgFgVC9XmdM9bw5MhZuQFjPNl6wxAE0SiQF/YTZJa+PndGWYSDtuxAg== + tapable@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"