commit 594f749d4c52db978e386cae80c2c53af8f4ed1d Author: michael Date: Mon Feb 23 08:37:58 2026 -0600 Initial commit: HP Marketing Materials GraphRAG Chatbot Full-stack GraphRAG chatbot for HP marketing materials with: - Python/Flask backend with custom ReAct agent (LlamaIndex) - Neo4j knowledge graph + vector search hybrid retrieval - LlamaParse multimodal document processing (text + images) - React/Vite frontend with conversation management - MongoDB conversation persistence - MSAL authentication support Co-Authored-By: Claude Opus 4.6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db6975e --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# Environment variables (contain API keys and credentials) +.env +.env.* +env + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +venv/ +.venv/ +*.egg-info/ +dist/ +build/ + +# Node +chat-interface/node_modules/ +chat-interface/dist/ + +# Generated data (runtime artifacts, not source code) +uploads/ +index_storage/ +*.log + +# Large binary source documents (too large for git) +supporting_files/ + +# macOS +.DS_Store + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Claude Code +.claude/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..aec3209 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,118 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +HP Marketing Materials Chatbot — a GraphRAG (Graph Retrieval-Augmented Generation) system that combines vector search with knowledge graph capabilities to answer questions about HP marketing materials and brand guidelines. Processes multimodal documents (text + images) via a custom ReAct agent. + +## Development Commands + +### Backend +```bash +pip install -r requirements.txt +python main.py # Starts Hypercorn ASGI server on localhost:8746 +``` + +### Frontend +```bash +cd chat-interface +npm install +npm run dev # Vite dev server +npm run build # Production build to dist/ +npm run lint # ESLint +``` + +### Required Services +- **Neo4j**: Port 7688, credentials `neo4j/hp-graphrag-2024` (HP-dedicated instance; port 7687 is a separate Netflix project) +- **MongoDB**: URI `mongodb://hp:hp@localhost:27017/?authSource=hp_chatbot`, database `hp_chatbot` + +### Environment Variables +Backend requires `.env` at project root with: `OPENAI_API_KEY`, `LLAMA_CLOUD_API_KEY`, `NEO4J_URL`, `NEO4J_USERNAME`, `NEO4J_PASSWORD`, `PORT` (default 8746). +Frontend uses `chat-interface/.env` with: `VITE_BACKEND_URL`, `VITE_APP_BASE_URL`. + +## Architecture + +### Request Flow +1. Frontend (`App.jsx`) sends POST to `/chat` with `{message, sessionId}` +2. `routes.py:chat()` maps session to conversation via `session_manager.py` and MongoDB +3. The global `ReActAgent2` (from `shared_state.py`) processes the query +4. Agent uses two tools: vector search (`answer_questions_from_hp_marketing_materials`) and GraphRAG hybrid search (`answerquestionswith_graphrag`) +5. Response includes text, sources, reasoning steps, and image references +6. Images are served via `/images/` from `uploads/images/` + +### Shared State Pattern (Critical) +All modules access the AI agent, vector index, and GraphRAG components through `shared_state.py` — a module with global variables and setter/getter functions. This avoids circular imports and ensures all modules reference the same instances. **Never import these globals directly from `ai_core.py`; always use `shared_state`.** + +Key globals: `global_workflow_agent`, `global_index`, `global_graph_store`, `global_graphrag_query_engine` + +### ReAct Agent (`ai_core.py`) +`ReActAgent2` is a custom LlamaIndex `Workflow` subclass implementing a ReAct loop: +- Steps: `new_user_msg` → `prepare_chat_history` → `handle_llm_input` → (tool calls via `handle_tool_calls` → loop back) → `StopEvent` +- Has a `simple_run()` method monkey-patched onto the agent at initialization time (replaces the default workflow `run`) +- Includes regex-based cleaning of LLM "thinking" artifacts from final responses +- Timeouts: `AGENT_TIMEOUT` (600s overall), `LLM_TIMEOUT` (300s per call), `TOOL_EXECUTION_TIMEOUT` (300s per tool) + +### GraphRAG System (`graph_rag_integration.py`) +Three main classes: +- **`GraphRAGExtractor`**: LlamaIndex `TransformComponent` that extracts entity-relation triplets from text nodes using LLM +- **`GraphRAGStore`**: Wraps `Neo4jPropertyGraphStore`, adds community detection (tries graspologic → python-louvain → NetworkX fallback), caches community summaries to `index_storage/graphrag_cache/` as pickle files +- **`GraphRAGQueryEngine`**: Combines vector retrieval with community-based graph retrieval, returning both contexts for synthesis + +### Startup Sequence (`main.py`) +1. MongoDB initialization (`init_mongodb.py`) +2. `initialize_global_index()` in `ai_core.py`: + - Configures LLM (chatgpt-4o-latest) and embeddings (text-embedding-3-small) + - Loads existing vector index from `index_storage/hp_docs_index/` or builds new from `supporting_files/files_for_rag_store/` + - Connects to Neo4j, creates/loads GraphRAG components + - Builds communities (from cache or fresh) + - Creates `ReActAgent2` and stores in `shared_state` + +### Frontend (React + Vite) +- Single-page app in `chat-interface/`, main component is `App.jsx` +- Auth via MSAL (`auth.js`), username sent as `X-MS-USERNAME` header +- Dev mode uses fallback `dev_user@local` username +- Conversation sidebar with auto-width resizing +- Markdown rendering via showdown, image viewer with pagination +- Styling: TailwindCSS + Shadcn/ui + Radix tooltips + +### JSON Serialization +`json_utils.py` provides `CustomJSONEncoder` and `CustomJSONProvider` that handle LlamaIndex types (ToolOutput, ReasoningSteps, ChatMessage, etc.), BSON ObjectId, and datetime. Flask is configured to use this provider globally. + +### Document Processing Pipeline +Upload → LlamaParse (dual: text + images) → Semantic splitting (`SemanticSplitterNodeParser`) → Page-based image assignment to chunks → Dual indexing (vector store + Neo4j knowledge graph) → Community detection and caching + +## API Endpoints + +| Method | Path | Purpose | +|--------|------|---------| +| POST | `/chat` | Main chat endpoint | +| GET | `/status` | System status (always returns initialized=true) | +| GET | `/images/` | Serve document images | +| GET | `/list-images` | List available images | +| GET | `/conversations` | List user conversations | +| GET | `/conversations//messages` | Get conversation messages | +| POST | `/conversations/new` | Create new conversation | +| DELETE | `/conversations/` | Delete conversation (soft by default) | +| POST | `/reset` | Reset global agent memory | +| POST | `/download-brief` | Generate Word doc from markdown | +| POST | `/capture-screenshot` | Manual LlamaParse image capture (dev only) | +| GET | `/debug-status` | Debug endpoint (dev only) | +| POST | `/reinitialize` | Force agent reinit (dev only) | + +## Key Conventions + +- Use `log_structured(level, message, data_dict)` from `utils.py` for all logging — it handles safe serialization of LlamaIndex objects +- Session management maps frontend `sessionId` to MongoDB `conversation_id` via `session_manager.py` with an in-memory cache backed by MongoDB +- The agent is a single global instance — all users share the same agent, but conversation history is loaded per-request from MongoDB +- Community summaries use `gpt-4o-mini` for cost efficiency; main agent uses `chatgpt-4o-latest` + +## Deployment + +- Backend: Set `PRODUCTION=true` env var, deploys to `https://ai-sandbox.oliver.solutions/hp_chatbot_back` +- Frontend: `npm run build`, deploy `dist/` to `/hp_chatbot/` path at `https://ai-sandbox.oliver.solutions/hp_chatbot/` +- CORS origins configured in `config.py:CORS_ALLOWED_ORIGINS` + +## Testing + +No formal test suite. Manual testing: start backend (`python main.py`), start frontend (`npm run dev`), test chat + image responses + document citations. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a306f2 --- /dev/null +++ b/README.md @@ -0,0 +1,244 @@ +# HP Marketing Materials Chatbot + +A GraphRAG (Graph Retrieval-Augmented Generation) chatbot that answers questions about HP marketing materials and brand guidelines. Combines vector search with a Neo4j knowledge graph for more comprehensive retrieval, and processes multimodal documents (text + images) using LlamaParse. + +## Features + +- **Hybrid retrieval**: Vector search + knowledge graph community detection for richer context +- **Multimodal document processing**: Extracts text and page images from PDFs via LlamaParse +- **Custom ReAct agent**: LlamaIndex-based workflow with tool use, reasoning steps, and source citations +- **Conversation persistence**: MongoDB-backed chat history with multi-conversation support +- **Image references**: Responses include relevant document page screenshots +- **Brief export**: Download conversation summaries as Word documents + +## Prerequisites + +- **Python 3.10+** +- **Node.js 18+** +- **Neo4j** (dedicated instance on port 7688) +- **MongoDB** (with authentication configured) +- **API Keys**: OpenAI (`OPENAI_API_KEY`), LlamaCloud (`LLAMA_CLOUD_API_KEY`) + +## Quick Start + +### 1. Backend Setup + +```bash +# Create and activate a virtual environment +python -m venv venv +source venv/bin/activate # or venv\Scripts\activate on Windows + +# Install dependencies +pip install -r requirements.txt + +# Create .env file at project root +cat > .env << 'EOF' +OPENAI_API_KEY=your_openai_key +LLAMA_CLOUD_API_KEY=your_llama_cloud_key +NEO4J_URL=bolt://localhost:7688 +NEO4J_USERNAME=neo4j +NEO4J_PASSWORD=hp-graphrag-2024 +PORT=8746 +PRODUCTION=false +LOG_LEVEL=INFO +EOF + +# Start the server +python main.py +``` + +The backend runs on `http://localhost:8746`. On first startup it will: +1. Initialize MongoDB collections and indexes +2. Load or build the vector index from `supporting_files/files_for_rag_store/` +3. Connect to Neo4j and build/load the knowledge graph +4. Build community summaries (cached to `index_storage/graphrag_cache/`) + +### 2. Frontend Setup + +```bash +cd chat-interface + +# Install dependencies +npm install + +# Create .env file +cat > .env << 'EOF' +VITE_BACKEND_URL=http://localhost:8746 +VITE_APP_BASE_URL=/ +EOF + +# Start dev server +npm run dev +``` + +The frontend runs on `http://localhost:5173`. + +### 3. Database Setup + +**Neo4j:** +- Run a Neo4j instance on port 7688 (port 7687 is reserved for a separate project) +- Credentials: `neo4j` / `hp-graphrag-2024` +- The application auto-populates the graph on first index build + +**MongoDB:** +- Create a user `hp` with password `hp` and `authSource=hp_chatbot` +- Database: `hp_chatbot` +- Collections (`users`, `conversations`, `messages`) are auto-created by `init_mongodb.py` on startup + +Example MongoDB user setup: +```javascript +use hp_chatbot +db.createUser({ + user: "hp", + pwd: "hp", + roles: [{ role: "readWrite", db: "hp_chatbot" }] +}) +``` + +## Project Structure + +``` +├── main.py # Entry point, Hypercorn ASGI server +├── config.py # Centralized configuration +├── ai_core.py # ReAct agent, document processing, index init +├── graph_rag_integration.py # GraphRAG: extraction, community detection, query engine +├── routes.py # Flask API endpoints +├── shared_state.py # Global state for agent/index/graph (cross-module) +├── session_manager.py # Session-to-conversation mapping +├── mongodb_utils.py # MongoDB CRUD operations +├── json_utils.py # Custom JSON serialization for LlamaIndex types +├── document_generator.py # Markdown-to-Word document conversion +├── utils.py # Logging and file utilities +├── init_mongodb.py # Database initialization script +├── requirements.txt # Python dependencies +├── .env # Environment variables (not committed) +├── supporting_files/ +│ └── files_for_rag_store/ # HP marketing documents for indexing +├── uploads/ +│ └── images/ # Extracted document page images +├── index_storage/ +│ ├── hp_docs_index/ # Persisted vector index +│ └── graphrag_cache/ # Cached community summaries (pickle) +└── chat-interface/ # React frontend + ├── src/ + │ ├── App.jsx # Main chat interface component + │ ├── auth.js # MSAL authentication + │ ├── components/ + │ │ ├── ChatInterface.jsx + │ │ ├── ConversationManager.jsx + │ │ └── ThemeToggle.jsx + │ └── lib/utils.js + ├── package.json + └── dist/ # Production build output +``` + +## Architecture Overview + +``` +┌─────────────┐ POST /chat ┌──────────────┐ +│ React UI │ ──────────────────► │ Flask/ │ +│ (App.jsx) │ ◄────────────────── │ Hypercorn │ +│ │ JSON response │ (routes.py) │ +└─────────────┘ └──────┬───────┘ + │ + ┌──────▼───────┐ + │ Session Mgr │──── MongoDB + │ │ (conversations, + └──────┬───────┘ messages, users) + │ + ┌──────▼───────┐ + │ ReActAgent2 │ + │ (ai_core.py) │ + └──────┬───────┘ + │ + ┌────────────┼────────────┐ + │ │ + ┌──────▼──────┐ ┌──────▼───────┐ + │ Vector │ │ GraphRAG │ + │ Query Tool │ │ Query Tool │ + │ │ │ │ + │ LlamaIndex │ │ Vector + │ + │ VectorStore │ │ Community │ + │ Index │ │ Retrieval │ + └─────────────┘ └──────┬───────┘ + │ + ┌──────▼───────┐ + │ Neo4j │ + │ Knowledge │ + │ Graph │ + └──────────────┘ +``` + +## API Reference + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `POST` | `/chat` | Send a chat message. Body: `{message, sessionId}` | +| `GET` | `/status?sessionId=` | Check system initialization status | +| `GET` | `/conversations` | List user's conversations (requires `X-MS-USERNAME` header) | +| `POST` | `/conversations/new` | Create a new conversation | +| `GET` | `/conversations/:id/messages` | Get messages for a conversation | +| `DELETE` | `/conversations/:id` | Soft-delete a conversation | +| `POST` | `/reset` | Reset global agent memory. Body: `{sessionId}` | +| `GET` | `/images/:filename` | Serve a document page image | +| `GET` | `/list-images` | List all available images | +| `POST` | `/download-brief` | Generate Word doc. Body: `{brief_content, sessionId}` | + +**Authentication**: The frontend sends the MSAL username via `X-MS-USERNAME` header. In development mode (`PRODUCTION=false`), a default `dev_user@local` is used. + +## Configuration + +All configuration is centralized in `config.py`. Key settings: + +| Setting | Default | Description | +|---------|---------|-------------| +| `LLM_MODEL` | `chatgpt-4o-latest` | Main LLM for the ReAct agent | +| `EMBEDDING_MODEL` | `text-embedding-3-small` | Embedding model for vector index | +| `LLM_TEMPERATURE` | `0.3` | LLM temperature | +| `SIMILARITY_TOP_K` | `10` | Number of vector results to retrieve | +| `AGENT_TIMEOUT` | `600s` | Overall agent workflow timeout | +| `LLM_TIMEOUT` | `300s` | Per-LLM-call timeout | +| `SERVER_PORT` | `8746` | Backend server port | + +Community summaries use `gpt-4o-mini` for cost efficiency (configured in `graph_rag_integration.py`). + +## Adding Documents + +Place HP marketing documents (PDF, DOCX, PPTX, TXT) in `supporting_files/files_for_rag_store/`. On the next startup with no existing index, the system will: + +1. Parse documents with LlamaParse (text + image extraction) +2. Split into semantic chunks +3. Build a vector index (persisted to `index_storage/hp_docs_index/`) +4. Extract knowledge graph triplets and store in Neo4j +5. Run community detection and cache summaries + +To force a full reindex, delete `index_storage/hp_docs_index/` and clear the Neo4j database before restarting. + +## Deployment + +### Backend +- Set `PRODUCTION=true` environment variable +- Server binds to `0.0.0.0` in production mode +- Configure `CORS_ALLOWED_ORIGINS` in `config.py` +- Production URL: `https://ai-sandbox.oliver.solutions/hp_chatbot_back` + +### Frontend +```bash +cd chat-interface +npm run build +``` +- Deploy `dist/` contents to the `/hp_chatbot/` path +- Ensure proper MIME types for `.js` files on the web server +- Configure SPA routing (see `web.config` or `.htaccess`) +- Production URL: `https://ai-sandbox.oliver.solutions/hp_chatbot/` + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| Backend won't start | Check that Neo4j and MongoDB are running. Verify `OPENAI_API_KEY` is set in `.env` | +| "Agent unavailable" errors | Check startup logs for LLM API test failure. The `/reinitialize` endpoint (dev only) can force re-init | +| No images in responses | Verify `LLAMA_CLOUD_API_KEY` is set. Check that `uploads/images/` contains extracted images | +| CORS errors | Add the frontend origin to `CORS_ALLOWED_ORIGINS` in `config.py` | +| Slow first startup | Initial document processing and graph building can take significant time depending on document volume | +| Neo4j connection refused | Ensure Neo4j is on port 7688 (not 7687, which is a different project) | diff --git a/ai_core.py b/ai_core.py new file mode 100644 index 0000000..3564202 --- /dev/null +++ b/ai_core.py @@ -0,0 +1,1374 @@ +# hp_chatbot/ai_core.py +import os +import asyncio +import traceback +import uuid +import shutil +import re +import inspect +import json +from pathlib import Path +from typing import List, Dict, Any, Optional, Union + +import httpx +import tiktoken +import nest_asyncio +from concurrent.futures import ThreadPoolExecutor +from functools import partial + +# Import GraphRAG integration +from graph_rag_integration import ( + create_graph_components, + create_graphrag_query_engine, + generate_final_answer, + GraphRAGExtractor, + GraphRAGStore, + GraphRAGQueryEngine +) + +from llama_index.core import ( + VectorStoreIndex, + Document as LlamaIndexDocument, + Settings, + get_response_synthesizer, + load_index_from_storage, + StorageContext, + SimpleDirectoryReader # Keep if needed for fallback/other uses +) +from llama_index.core.retrievers import VectorIndexAutoRetriever, VectorIndexRetriever +from llama_index.core.query_engine import RetrieverQueryEngine +from llama_index.core.postprocessor import SimilarityPostprocessor +from llama_index.core.response_synthesizers import ResponseMode +from llama_index.llms.openai import OpenAI as LlamaOpenAI +from llama_index.embeddings.openai import OpenAIEmbedding +from llama_index.core.callbacks import CallbackManager, TokenCountingHandler +from llama_index.core.tools import QueryEngineTool, ToolMetadata, BaseTool, ToolSelection, ToolOutput +from llama_index.core.memory import ChatMemoryBuffer +from llama_index.core.llms import ChatMessage, LLM +from llama_index.core.vector_stores.types import MetadataInfo, VectorStoreInfo +from llama_index.core.agent.react import ReActChatFormatter, ReActOutputParser +from llama_index.core.agent.react.types import ( + ActionReasoningStep, + ObservationReasoningStep, + ResponseReasoningStep, + BaseReasoningStep +) +from llama_index.core.workflow import ( + Context, + Workflow, + StartEvent, + StopEvent, + step, + Event, +) +from llama_index.core.node_parser import ( + SentenceSplitter, + SemanticSplitterNodeParser, +) +from llama_parse import LlamaParse + +# Import from our modules +from utils import logger, log_structured +from config import ( + HP_DOCS_FOLDER, INDEX_PERSIST_PATH, IMAGES_DIRECTORY, + LLM_MODEL, EMBEDDING_MODEL, LLM_TEMPERATURE, LLM_TIMEOUT, AGENT_TIMEOUT, + TOOL_EXECUTION_TIMEOUT, SIMILARITY_TOP_K, SIMILARITY_CUTOFF, + LLAMA_PARSE_VENDOR_MODEL, LLAMA_PARSE_MAX_TIMEOUT, + NEO4J_URL, NEO4J_USERNAME, NEO4J_PASSWORD +) + +nest_asyncio.apply() + +# --- Global AI State --- +# Import shared state to ensure all modules access the same instances +from shared_state import ( + global_index, global_workflow_agent, + global_graph_store, global_property_graph_index, global_graphrag_query_engine, + set_global_agent, set_global_index, set_graphrag_components +) + + +# --- Token Counter --- +try: + token_counter = TokenCountingHandler( + tokenizer=tiktoken.encoding_for_model(LLM_MODEL).encode + ) +except Exception as e: + log_structured('warning', f'Could not initialize tiktoken for {LLM_MODEL}. Token counting may be inaccurate.', {'error': str(e)}) + # Fallback tokenizer if needed, or disable token counting + token_counter = TokenCountingHandler(tokenizer=lambda text: list(text.encode("utf-8"))) + + +# --- Custom ReAct Agent Workflow --- +class PrepEvent(Event): pass +class InputEvent(Event): input: list[ChatMessage] +class ToolCallEvent(Event): tool_calls: list[ToolSelection] +class FunctionOutputEvent(Event): output: ToolOutput +class CustomStartEvent(StartEvent): input_value: str = "" + + +class ReActAgent2(Workflow): + """ + Custom ReAct Agent implementation using LlamaIndex Workflows. + Includes timeout handling for LLM calls and tool execution. + """ + def __init__( + self, + llm: LLM, + tools: list[BaseTool], + memory: ChatMemoryBuffer, + timeout: float = AGENT_TIMEOUT, # Overall workflow timeout + llm_timeout: float = LLM_TIMEOUT, # Timeout for individual LLM calls + tool_timeout: float = TOOL_EXECUTION_TIMEOUT, # Timeout for individual tool calls + verbose: bool = True, # Add verbose flag + extra_context: str | None = None, + **kwargs: Any, + ) -> None: + super().__init__(timeout=timeout, verbose=verbose, **kwargs) # Pass verbose to parent + self.llm = llm + self.tools = tools or [] + self.memory = memory + self.formatter = ReActChatFormatter(context=extra_context or "") + self.output_parser = ReActOutputParser() + self.sources: List[ToolOutput] = [] # Store ToolOutput objects directly + self.llm_timeout = llm_timeout + self.tool_timeout = tool_timeout + self.verbose = verbose # Store verbose flag + + @step + async def new_user_msg(self, ctx: Context, ev: StartEvent) -> PrepEvent: + self.sources = [] # Clear sources for new message + # Store the initial input in the context + await ctx.set("user_input", ev) # Save the whole event + + # We'll set a placeholder message for now and extract the actual input in the next step + user_msg = ChatMessage(role="user", content="Placeholder") + self.memory.put(user_msg) + await ctx.set("current_reasoning", []) + if self.verbose: + log_structured('debug', 'ReActAgent: Receiving new user message', {}) + return PrepEvent() + + @step + async def prepare_chat_history(self, ctx: Context, ev: PrepEvent) -> InputEvent: + # Get the real user input from the first step and update the memory + start_event = await ctx.get("user_input") + if hasattr(start_event, "input_value"): + # Try to access input_value directly if it exists + real_user_input = start_event.input_value + else: + # Otherwise, assume the actual input is the input value provided to run() + log_structured('info', 'ReActAgent: Extracting input from workflow input', {}) + # Default to reasonable fallback if we can't extract it + real_user_input = "How can I help you with HP marketing guidelines?" + + # Update the placeholder message with the real input + messages = self.memory.get() + if messages and messages[0].role == "user" and messages[0].content == "Placeholder": + # Replace the placeholder with real input + messages[0].content = str(real_user_input) + if self.verbose: + log_structured('debug', 'ReActAgent: Updated user message with real input', {'input': real_user_input}) + + # Format the chat history for the LLM + chat_history = self.memory.get() + current_reasoning = await ctx.get("current_reasoning", default=[]) + llm_input = self.formatter.format( + self.tools, chat_history, current_reasoning=current_reasoning + ) + if self.verbose: + log_structured('debug', 'ReActAgent: Prepared chat history for LLM', {'history_len': len(llm_input)}) + return InputEvent(input=llm_input) + + @step # The timeout is managed inside the method + async def handle_llm_input(self, ctx: Context, ev: InputEvent) -> Union[ToolCallEvent, StopEvent, PrepEvent]: + chat_history = ev.input + current_reasoning = await ctx.get("current_reasoning", default=[]) + + try: + if self.verbose: + log_structured('debug', 'ReActAgent: Sending request to LLM', {'history_len': len(chat_history)}) + + response = await asyncio.wait_for( + self.llm.achat(chat_history), + timeout=self.llm_timeout + ) + reasoning_step = self.output_parser.parse(response.message.content) + current_reasoning.append(reasoning_step) + await ctx.set("current_reasoning", current_reasoning) # Update context state + + if self.verbose: + log_structured('debug', 'ReActAgent: Received LLM response', { + 'step_type': type(reasoning_step).__name__, + 'is_done': getattr(reasoning_step, 'is_done', False), + 'has_response': hasattr(reasoning_step, 'response'), + 'has_action': hasattr(reasoning_step, 'action'), + 'action': getattr(reasoning_step, 'action', None), + 'raw_content_preview': response.message.content[:300] + }) + + if reasoning_step.is_done: + # Log what we're about to return as final response + if self.verbose: + log_structured('debug', 'ReActAgent: Processing final response', { + 'raw_response_content': response.message.content, + 'reasoning_step_response': str(getattr(reasoning_step, 'response', 'NO_RESPONSE_ATTR')) + }) + + # Clean the response to remove any thinking parts + response_text = str(reasoning_step.response) + + # Check for common thinking patterns and remove them + import re # Import re within the function scope to ensure it's available + thinking_patterns = [ + r'(?i)^.*?thinking:.*?\n', # Remove lines starting with "Thinking:" + r'(?i).*?', # Remove XML-like thinking tags + r'(?i)\[thinking\].*?\[/thinking\]', # Remove bracket thinking tags + r'(?i)I\'m thinking:.*?\n', # Remove "I'm thinking:" sections + r'(?i)Let me think.*?\n', # Remove "Let me think" sections + r'(?i)Thought:.*?Action:.*?Action Input:.*', # Remove the specific pattern user reported + r'(?i)^Thought:.*', # Remove any line starting with "Thought:" + r'(?i)Action:.*?Action Input:.*', # Remove Action/Action Input patterns + r'(?i)Thought:.*?Answer:', # Remove "Thought: ... Answer:" pattern + r'(?i)^Answer:\s*' # Remove just the "Answer:" prefix + ] + for pattern in thinking_patterns: + response_text = re.sub(pattern, '', response_text, flags=re.DOTALL) + + # Remove extra newlines that might be left after cleaning + response_text = re.sub(r'\n{3,}', '\n\n', response_text) + response_text = response_text.strip() + + # Final safety check - if response still looks like thinking, provide fallback + if re.search(r'(?i)^(Thought|Action|Observation):', response_text) or not response_text: + log_structured('warning', 'ReActAgent: Final response still contains thinking patterns, using fallback', { + 'problematic_response': response_text[:200] + }) + response_text = "I found information about your query in the HP marketing materials. Please let me know if you need more specific details." + + self.memory.put(ChatMessage(role="assistant", content=response_text)) + if self.verbose: + log_structured('info', 'ReActAgent: Final response generated', { + 'response_preview': response_text[:100], + 'sources_count': len(self.sources) + }) + + # Structure the final output + final_result = { + "response": response_text, + "sources": self.sources, # Pass the collected ToolOutput objects + "reasoning": current_reasoning + } + return StopEvent(result=final_result) + + elif isinstance(reasoning_step, ActionReasoningStep): + if self.verbose: + log_structured('debug', 'ReActAgent: Action step identified', { + 'action': reasoning_step.action, + 'input': reasoning_step.action_input + }) + # Ensure action_input is a dict + action_input_dict = reasoning_step.action_input or {} + if not isinstance(action_input_dict, dict): + log_structured('warning', 'ReActAgent: action_input is not a dict, attempting to parse', {'raw_input': action_input_dict}) + try: + # Attempt basic parsing if it looks like JSON string + if isinstance(action_input_dict, str) and action_input_dict.strip().startswith('{'): + action_input_dict = json.loads(action_input_dict) + else: # Fallback: treat as a single 'query' arg if not dict/json + action_input_dict = {'query': action_input_dict} + except Exception as parse_err: + log_structured('error', 'ReActAgent: Failed to parse action_input', {'raw_input': action_input_dict, 'error': str(parse_err)}) + action_input_dict = {'query': str(action_input_dict)} # Safest fallback + + return ToolCallEvent(tool_calls=[ + ToolSelection( + tool_id="tool_" + reasoning_step.action.replace(" ", "_"), # Simple ID generation + tool_name=reasoning_step.action, + tool_kwargs=action_input_dict + ) + ]) + else: + # Handle other reasoning step types if necessary, or just proceed + if self.verbose: + log_structured('debug', 'ReActAgent: Non-action, non-done step encountered', {'step_type': type(reasoning_step).__name__}) + return PrepEvent() # Continue the loop + + except asyncio.TimeoutError: + error_msg = f"LLM call timed out after {self.llm_timeout} seconds." + log_structured('error', 'ReActAgent: LLM Timeout', {'timeout': self.llm_timeout}) + current_reasoning.append(ObservationReasoningStep(observation=error_msg)) + await ctx.set("current_reasoning", current_reasoning) + return PrepEvent() # Try again or maybe stop? Returning PrepEvent allows loop to continue. + except Exception as e: + error_msg = f"Error during LLM interaction or parsing: {str(e)}" + log_structured('error', 'ReActAgent: Error in handle_llm_input', { + 'error': str(e), 'traceback': traceback.format_exc() + }) + current_reasoning.append(ObservationReasoningStep(observation=error_msg)) + await ctx.set("current_reasoning", current_reasoning) + # Decide whether to stop or continue after error + # Returning PrepEvent allows the agent to potentially describe the error or retry + return PrepEvent() + + + @step # The timeout is managed inside the method + async def handle_tool_calls(self, ctx: Context, ev: ToolCallEvent) -> PrepEvent: + tool_calls = ev.tool_calls + tools_by_name = {tool.metadata.name: tool for tool in self.tools} # Use .name property + current_reasoning = await ctx.get("current_reasoning", default=[]) + + if self.verbose: + log_structured('debug', 'ReActAgent: Handling tool calls', { + 'call_count': len(tool_calls), + 'tool_names': [tc.tool_name for tc in tool_calls] + }) + + for tool_call in tool_calls: + tool = tools_by_name.get(tool_call.tool_name) + if not tool: + error_msg = f"Tool '{tool_call.tool_name}' not found." + log_structured('error', 'ReActAgent: Tool not found', {'tool_name': tool_call.tool_name}) + current_reasoning.append(ObservationReasoningStep(observation=error_msg)) + continue # Skip to next tool call + + try: + tool_kwargs = tool_call.tool_kwargs or {} # Ensure kwargs is a dict + if self.verbose: + log_structured('debug', 'ReActAgent: Executing tool', { + 'tool_name': tool_call.tool_name, + 'kwargs': tool_kwargs + }) + + # Execute tool potentially in thread pool with timeout + tool_func = partial(tool, **tool_kwargs) + tool_output: ToolOutput + if inspect.iscoroutinefunction(tool.call): # Check if tool's call method is async + tool_output = await asyncio.wait_for( + tool.acall(**tool_kwargs), # Use acall for async tools + timeout=self.tool_timeout + ) + else: + # Run sync tool in thread pool + with ThreadPoolExecutor() as executor: + future = executor.submit(tool_func) # tool_func already has kwargs via partial + tool_output = await asyncio.to_thread(future.result, timeout=self.tool_timeout) + + + if self.verbose: + log_structured('debug', 'ReActAgent: Tool execution successful', { + 'tool_name': tool_call.tool_name, + 'output_type': type(tool_output).__name__, + 'output_content_preview': str(tool_output.content)[:100] if tool_output else "N/A" + }) + + # Store the raw ToolOutput object which contains content, raw_output, metadata etc. + self.sources.append(tool_output) + + # Add observation step with the tool's string content + observation_content = str(tool_output.content) if tool_output and tool_output.content is not None else "Tool executed successfully but returned no content." + current_reasoning.append(ObservationReasoningStep(observation=observation_content)) + + if self.verbose: + log_structured('debug', 'ReActAgent: Added observation to reasoning', { + 'tool_name': tool_call.tool_name, + 'observation_preview': observation_content[:200] + }) + + # --- Detailed logging for image metadata --- + if hasattr(tool_output, 'raw_output') and hasattr(tool_output.raw_output, 'source_nodes'): + for node_with_score in tool_output.raw_output.source_nodes: + node = getattr(node_with_score, 'node', None) + if node and hasattr(node, 'metadata'): + if 'image_paths' in node.metadata and node.metadata['image_paths']: + log_structured('debug', 'ReActAgent: Tool output node contains image paths', { + 'tool_name': tool_call.tool_name, + 'node_id': getattr(node, 'id_', 'N/A'), + 'image_paths': node.metadata['image_paths'] + }) + + + except asyncio.TimeoutError: + error_msg = f"Tool '{tool_call.tool_name}' timed out after {self.tool_timeout} seconds." + log_structured('error', 'ReActAgent: Tool Timeout', {'tool_name': tool_call.tool_name, 'timeout': self.tool_timeout}) + current_reasoning.append(ObservationReasoningStep(observation=error_msg)) + except Exception as e: + error_msg = f"Error calling tool '{tool_call.tool_name}': {str(e)}" + log_structured('error', 'ReActAgent: Tool execution error', { + 'tool_name': tool_call.tool_name, 'error': str(e), 'traceback': traceback.format_exc() + }) + current_reasoning.append(ObservationReasoningStep(observation=error_msg)) + + # Update reasoning steps in context before returning + await ctx.set("current_reasoning", current_reasoning) + return PrepEvent() # Always return PrepEvent to continue the loop + + +# --- Document Processing --- +async def process_documents_in_directory(directory: str, session_id: Optional[str] = None) -> List[LlamaIndexDocument]: + """ + Process all documents in a directory using LlamaParse for text and images, + preserving chunk-level metadata including specific image paths per chunk. + Includes detailed logging for image metadata assignment. + + Args: + directory: The path to the directory containing documents. + session_id: Optional session identifier for context. + + Returns: + A list of LlamaIndexDocument objects representing the processed chunks, + ready for indexing. Returns an empty list on failure or if no documents + are processed. + """ + parser_text = None + parser_images = None + try: + # Ensure images directory exists + os.makedirs(IMAGES_DIRECTORY, exist_ok=True) + + log_structured('info', f'Starting document processing for directory: {directory}', { + 'session_id': session_id, + 'directory_name': os.path.basename(directory), + 'directory_exists': os.path.exists(directory), + 'files_present': os.listdir(directory) if os.path.exists(directory) else 'N/A' + }) + + if not os.path.exists(directory) or not os.path.isdir(directory): + log_structured('error', f'Directory not found or is not a directory: {directory}', {'session_id': session_id}) + return [] + if not os.listdir(directory): + log_structured('warning', f'Directory is empty, skipping: {directory}', {'session_id': session_id}) + return [] + + # --- LlamaParse Initialization --- + try: + # Separate clients recommended if making concurrent calls, but can share if sequential + custom_client_text = httpx.AsyncClient(timeout=LLAMA_PARSE_MAX_TIMEOUT) + custom_client_images = httpx.AsyncClient(timeout=LLAMA_PARSE_MAX_TIMEOUT) + + parser_text = LlamaParse( + result_type="markdown", + add_page_breaks=False, + system_prompt="Extract all content including text, tables, and formatting. Preserve structure.", + premium_mode=False, # Set based on your LlamaCloud plan + max_timeout=LLAMA_PARSE_MAX_TIMEOUT, + custom_client=custom_client_text, + verbose=True # Enable verbose logging for LlamaParse + ) + parser_images = LlamaParse( + result_type="markdown", # Less critical here, parsing for images primarily + add_page_breaks=False, + system_prompt="Generate page images of the document.", + use_vendor_multimodal_model=True, # Assuming you want LlamaParse's image gen + vendor_multimodal_model_name=LLAMA_PARSE_VENDOR_MODEL, + premium_mode=False, # Set based on your LlamaCloud plan + max_timeout=LLAMA_PARSE_MAX_TIMEOUT, + custom_client=custom_client_images, + verbose=True # Enable verbose logging for LlamaParse + ) + log_structured('info', 'Initialized LlamaParse (text & image capability)', {'session_id': session_id}) + except Exception as parser_err: + log_structured('error', f'Error initializing dual LlamaParse capability: {parser_err}. Falling back to text-only.', { + 'session_id': session_id, 'traceback': traceback.format_exc() + }) + if parser_text and hasattr(parser_text, 'aclose'): await parser_text.aclose() + if parser_images and hasattr(parser_images, 'aclose'): await parser_images.aclose() + parser_images = None # Disable image parsing + try: + # Ensure client is fresh for fallback + custom_client_text = httpx.AsyncClient(timeout=LLAMA_PARSE_MAX_TIMEOUT) + parser_text = LlamaParse( + result_type="markdown", + language="en", + add_page_breaks=False, + merge_consecutive=True, + max_timeout=LLAMA_PARSE_MAX_TIMEOUT, + custom_client=custom_client_text, + verbose=True + ) + log_structured('info', 'Initialized LlamaParse with fallback text-only parser.', {'session_id': session_id}) + except Exception as fallback_err: + log_structured('critical', f'FATAL: Error initializing fallback LlamaParse: {fallback_err}', { + 'session_id': session_id, 'traceback': traceback.format_exc() + }) + if parser_text and hasattr(parser_text, 'aclose'): await parser_text.aclose() # Cleanup + return [] + + if not parser_text: + log_structured('critical', 'FATAL: Text parser (parser_text) could not be initialized.') + return [] + + file_extractor = { + ".pdf": parser_text, ".docx": parser_text, ".doc": parser_text, + ".pptx": parser_text, ".ppt": parser_text, ".txt": parser_text, + # Add others if your LlamaParse supports them + # ".html": parser_text, ".md": parser_text, + } + supported_extensions = list(file_extractor.keys()) + + # --- Custom File Reader Async Function --- + async def custom_file_reader(): + processed_chunks: List[LlamaIndexDocument] = [] + images_by_file_and_page: Dict[str, Dict[int, str]] = {} # filename -> { page_num_1_based -> image_filename } + + all_files_paths = [] + log_structured('debug', f'Searching for supported files in: {directory}', {'extensions': supported_extensions, 'session_id': session_id}) + for ext in supported_extensions: + all_files_paths.extend(list(Path(directory).glob(f"*{ext}"))) + all_files_paths.extend(list(Path(directory).glob(f"*{ext.upper()}"))) # Case-insensitive glob + all_files_paths = sorted(list(set(all_files_paths))) # Remove duplicates and sort + + log_structured('info', f'Found {len(all_files_paths)} supported files in {directory}', {'session_id': session_id, 'files': [f.name for f in all_files_paths]}) + if not all_files_paths: + log_structured('warning', f'No supported files found in directory: {directory}', {'session_id': session_id}) + return [] + + for file_path in all_files_paths: + file_path_str = str(file_path) + filename = file_path.name + log_structured('info', f'Processing file: {filename}', {'session_id': session_id, 'path': file_path_str}) + + ext = file_path.suffix.lower() + current_text_parser = file_extractor.get(ext) + if not current_text_parser: + log_structured('warning', f'Skipping file {filename}: No parser defined for extension {ext}.', {'session_id': session_id}) + continue + + # 1. Extract Text Chunks + text_chunks: List[LlamaIndexDocument] = [] + try: + log_structured('debug', f'Calling parser_text.aload_data for {filename}', {'session_id': session_id}) + # LlamaParse aload_data returns List[Document] + text_chunks = await current_text_parser.aload_data(file_path_str) + log_structured('info', f'Extracted {len(text_chunks)} text chunks from {filename}', {'session_id': session_id}) + if not text_chunks: + log_structured('warning', f'No text chunks extracted from {filename}. File might be empty or parser issue.', {'session_id': session_id}) + # Continue to attempt image extraction for this file + except Exception as text_err: + log_structured('error', f'Error extracting text from {filename}', { + 'session_id': session_id, 'error': str(text_err), 'traceback': traceback.format_exc() + }) + continue # Skip to next file if text extraction fails critically + + # 2. Extract Images (if parser_images is available) + file_page_images: Dict[int, str] = {} # { page_num_1_based: image_filename } + if parser_images: + try: + log_structured('debug', f'Attempting image extraction for: {filename}', {'session_id': session_id}) + temp_image_dir = IMAGES_DIRECTORY / f"temp_img_{filename}_{uuid.uuid4().hex[:8]}" + os.makedirs(temp_image_dir, exist_ok=True) + + # Use synchronous methods for simplicity here, or manage async complexity + # NOTE: Check LlamaParse documentation for the best way to get images. + # This assumes get_json_result and get_images are available and work this way. + # If these are async, you'll need `await parser_images.aget_json_result(...)` etc. + # Running sync methods in executor to avoid blocking event loop + loop = asyncio.get_running_loop() + with ThreadPoolExecutor() as pool: + md_json_objs = await loop.run_in_executor(pool, parser_images.get_json_result, file_path_str) + log_structured('debug', f'Got JSON result for images from {filename}', {'session_id': session_id}) + image_dicts = await loop.run_in_executor(pool, parser_images.get_images, md_json_objs, temp_image_dir) + + log_structured('info', f'LlamaParse reported {len(image_dicts)} images potentially extracted for {filename}', {'session_id': session_id}) + + saved_image_count = 0 + for idx, img_info in enumerate(image_dicts): + # LlamaParse might return 0-based 'page' index + page_index_0_based = img_info.get('page', idx) # Default to list index if 'page' missing + page_num_1_based = page_index_0_based + 1 # Convert to 1-based for consistency + + # log_structured('debug', f'Processing image info for {filename}, index {idx}', {'img_info': img_info, 'page_1_based': page_num_1_based}) + + if 'path' in img_info and img_info['path'] and os.path.exists(img_info['path']): + source_img_path = img_info['path'] + image_filename = f"{os.path.splitext(filename)[0]}_page{page_num_1_based}_{uuid.uuid4().hex[:6]}.png" + dest_path = IMAGES_DIRECTORY / image_filename + + try: + shutil.copy2(source_img_path, dest_path) # copy2 preserves metadata + file_page_images[page_num_1_based] = image_filename # Map 1-based page num + saved_image_count += 1 + log_structured('debug', f'Saved image for {filename} page {page_num_1_based} (1-based) to {dest_path}', {'session_id': session_id}) + except Exception as copy_err: + log_structured('error', f'Error copying image for {filename} page {page_num_1_based}', {'session_id': session_id, 'error': str(copy_err)}) + else: + log_structured('warning', f'No valid image path found or file missing for image index {idx} (reported page {page_index_0_based}) from {filename}. Path: {img_info.get("path", "N/A")}', {'session_id': session_id}) + + log_structured('info', f'Successfully saved {saved_image_count} images for {filename}', {'session_id': session_id}) + images_by_file_and_page[filename] = file_page_images + + # Clean up temporary directory + try: + shutil.rmtree(temp_image_dir) + except Exception as cleanup_err: + log_structured('error', f'Error cleaning up temp image dir {temp_image_dir}', {'session_id': session_id, 'error': str(cleanup_err)}) + + except Exception as img_err: + log_structured('error', f'Error during image extraction pipeline for {filename}', { + 'session_id': session_id, 'error': str(img_err), 'traceback': traceback.format_exc() + }) + images_by_file_and_page[filename] = {} # Ensure empty dict on error + else: + log_structured('info', f'Image parsing skipped for {filename} (parser_images is None).', {'session_id': session_id}) + images_by_file_and_page[filename] = {} + + # 3. Add Metadata to Each Text Chunk + log_structured('debug', f'Assigning metadata and images to {len(text_chunks)} chunks for {filename}', {'session_id': session_id}) + for chunk_index, chunk in enumerate(text_chunks): + if not hasattr(chunk, 'metadata') or chunk.metadata is None: chunk.metadata = {} + + chunk.metadata['filename'] = filename + chunk.metadata['file_path'] = file_path_str + chunk.metadata['source'] = "document" # Or more specific if needed + chunk.metadata['type'] = os.path.basename(directory) # e.g., 'brief', 'supporting' + chunk.metadata['chunk_index'] = chunk_index + + # --- Determine 1-based Page Number --- + page_num_1_based: Optional[int] = None + page_source_key: Optional[str] = None + + # Priority: LlamaParse metadata keys + page_key_options = ['page_label', 'page_number', 'page'] # Check common keys + for key in page_key_options: + if key in chunk.metadata: + try: + page_num_1_based = int(chunk.metadata[key]) # Assume it's 1-based from LlamaParse + if page_num_1_based > 0: # Basic sanity check + page_source_key = key + chunk.metadata["source_page"] = page_num_1_based # Store consistently + log_structured('debug', f"Found page {page_num_1_based} from metadata key '{key}'", {'filename': filename, 'chunk': chunk_index, 'session_id': session_id}) + break + else: + page_num_1_based = None # Ignore non-positive page numbers + except (ValueError, TypeError): pass # Ignore if not integer + + # Fallback: If no page found in metadata, try approximation (less reliable) + # This part is optional and might be inaccurate. Consider removing if metadata is reliable. + if page_num_1_based is None: + # Simple fallback: use chunk index as a proxy (very rough) + page_num_1_based = chunk_index + 1 # Treat first chunk as page 1 approx. + chunk.metadata["source_page"] = page_num_1_based + page_source_key = "position_fallback" + log_structured('debug', f"Using position fallback for page: {page_num_1_based}", {'filename': filename, 'chunk': chunk_index, 'session_id': session_id}) + + + # --- Precise Page-Based Image Assignment --- + chunk.metadata['image_paths'] = [] # Initialize/reset + specific_image_filename = None + + if page_num_1_based is not None: + file_images_dict = images_by_file_and_page.get(filename, {}) + if file_images_dict: + # Try exact match first + if page_num_1_based in file_images_dict: + specific_image_filename = file_images_dict[page_num_1_based] + chunk.metadata['image_match_type'] = 'exact' + log_structured('debug', f'Exact page match for image page {page_num_1_based}', {'filename': filename, 'chunk': chunk_index, 'image': specific_image_filename}) + else: + # Optional: Find nearest page if exact match fails (can be noisy) + # nearest_page = min(file_images_dict.keys(), key=lambda x: abs(x - page_num_1_based)) + # if abs(nearest_page - page_num_1_based) <= 1: # Only if very close? Threshold needed. + # specific_image_filename = file_images_dict[nearest_page] + # chunk.metadata['image_match_type'] = 'nearest' + # chunk.metadata["image_nearest_page"] = nearest_page + # log_structured('debug', f'Nearest page match: requested {page_num_1_based}, using {nearest_page}', {'filename': filename, 'chunk': chunk_index, 'image': specific_image_filename}) + pass # Default: No image if no exact match + + + if specific_image_filename: + chunk.metadata['image_paths'] = [specific_image_filename] # Store as list + + # Final metadata log before adding chunk + # log_structured('debug', "Final chunk metadata check", { + # 'session_id': session_id, 'filename': filename, 'chunk': chunk_index, + # 'page': chunk.metadata.get('source_page'), 'page_src': page_source_key, + # 'image': chunk.metadata.get('image_paths'), 'match_type': chunk.metadata.get('image_match_type'), + # 'keys': list(chunk.metadata.keys()) + # }) + + processed_chunks.append(chunk) + + # --- End of file loop --- + log_structured('info', f'Finished processing files. Total chunks: {len(processed_chunks)}', {'session_id': session_id}) + return processed_chunks + + # --- Execute the async file reader --- + final_documents: List[LlamaIndexDocument] = await custom_file_reader() + + log_structured('info', 'Directory processing complete.', { + 'session_id': session_id, 'directory': directory, 'total_chunks_generated': len(final_documents) + }) + # Log sample metadata for verification + # for i, doc in enumerate(final_documents[:2]): + # log_structured('debug', f'Final Doc Sample #{i} Metadata', {'metadata': doc.metadata}) + + return final_documents + + except Exception as e: + log_structured('critical', 'FATAL Error in process_documents_in_directory', { + 'session_id': session_id, 'directory': directory, 'error': str(e), 'traceback': traceback.format_exc() + }) + return [] # Return empty list on major failure + finally: + # Ensure async clients are closed + log_structured('debug', 'Closing LlamaParse clients.', {'session_id': session_id}) + if parser_text and hasattr(parser_text, 'aclose'): + try: await parser_text.aclose() + except Exception as close_err: log_structured('error', 'Error closing parser_text client', {'error': str(close_err)}) + if parser_images and hasattr(parser_images, 'aclose'): + try: await parser_images.aclose() + except Exception as close_err: log_structured('error', 'Error closing parser_images client', {'error': str(close_err)}) + + +# --- Global Index Initialization --- +async def initialize_global_index() -> bool: + """Initialize the global index from HP documents at startup.""" + # Use shared state instead of module-level globals + + try: + # --- Configure LLM and Embedding Model --- + # Check for real API keys + openai_key = os.environ.get("OPENAI_API_KEY", "") + if not openai_key: + log_structured('critical', 'No OpenAI API key provided. Make sure OPENAI_API_KEY is set in your .env file.') + return False + + # Log the first few characters of the key for debugging (important to verify it's loaded) + key_preview = openai_key[:4] + "..." if len(openai_key) > 4 else "invalid" + log_structured('info', f'Using OpenAI API key starting with: {key_preview}') + + # Normal initialization with real API keys + try: + llm = LlamaOpenAI( + model=LLM_MODEL, + temperature=LLM_TEMPERATURE, + timeout=LLM_TIMEOUT + ) + # Test the LLM with a simple prompt to ensure it's working + test_prompt = "Say 'API key is working' if you can read this." + _ = llm.complete(test_prompt) + log_structured('info', 'Successfully tested LLM API connection') + except Exception as llm_err: + log_structured('critical', f'Failed to initialize LLM with provided API key: {str(llm_err)}') + return False + embed_model = OpenAIEmbedding(model=EMBEDDING_MODEL) + + # --- Configure Global Settings --- + Settings.llm = llm + Settings.embed_model = embed_model + Settings.callback_manager = CallbackManager([token_counter]) + # Settings.chunk_size = NODE_PARSER_CHUNK_SIZE # If using SentenceSplitter + # Settings.chunk_overlap = NODE_PARSER_CHUNK_OVERLAP + + # Use Semantic Splitter (more robust, less config needed here) + node_parser = SemanticSplitterNodeParser( + buffer_size=1, breakpoint_percentile_threshold=95, embed_model=embed_model # Adjust threshold + ) + Settings.node_parser = node_parser + # Alternatively, configure SentenceSplitter globally + # Settings.node_parser = SentenceSplitter( + # chunk_size=NODE_PARSER_CHUNK_SIZE, + # chunk_overlap=NODE_PARSER_CHUNK_OVERLAP, + # # ... other SentenceSplitter params + # ) + + # --- Load or Build Index --- + if INDEX_PERSIST_PATH.exists(): + log_structured('info', f'Loading existing index from {INDEX_PERSIST_PATH}') + storage_context = StorageContext.from_defaults(persist_dir=str(INDEX_PERSIST_PATH)) + index = load_index_from_storage(storage_context) + # Save to shared state + set_global_index(index) + log_structured('info', 'Successfully loaded existing index') + + # Attempt to load or recreate GraphRAG components + try: + log_structured('info', 'Attempting to recreate GraphRAG components from loaded index') + + # First, try to connect to Neo4j to check if it has data + from llama_index.graph_stores.neo4j import Neo4jPropertyGraphStore + property_graph_store = Neo4jPropertyGraphStore( + username=NEO4J_USERNAME, + password=NEO4J_PASSWORD, + url=NEO4J_URL + ) + log_structured('info', 'Successfully connected to Neo4j database') + + # Create temporary GraphRAGStore to check for existing data + temp_graph_store = GraphRAGStore(property_graph_store) + triplets = temp_graph_store.get_triplets() + neo4j_has_data = len(triplets) > 0 + + if neo4j_has_data: + # Neo4j already has data, just use it + log_structured('info', f'Neo4j contains {len(triplets)} triplets. Using existing data.') + graph_store, property_graph_index = create_graph_components( + llm=llm, + force_reindex=False # Use existing Neo4j data + ) + log_structured('info', 'GraphRAG components loaded from existing Neo4j data') + else: + # Neo4j is empty, need to extract nodes from the vector index to populate it + log_structured('info', 'Neo4j is empty. Extracting nodes from vector index for GraphRAG indexing.') + + # Get all nodes from the vector index + from llama_index.core.schema import TextNode + vector_nodes = [] + + # Extract nodes from the index's docstore + if hasattr(index, 'docstore') and index.docstore: + docstore_nodes = list(index.docstore.docs.values()) + vector_nodes.extend(docstore_nodes) + log_structured('info', f'Retrieved {len(docstore_nodes)} nodes from index docstore') + + # Handle cases where we can't get nodes directly + if not vector_nodes: + log_structured('warning', 'Could not retrieve nodes from vector index. GraphRAG indexing will be skipped.') + raise ValueError("No nodes could be retrieved from the vector index for GraphRAG indexing") + + # Now create GraphRAG components with the retrieved nodes + graph_store, property_graph_index = create_graph_components( + llm=llm, + nodes=vector_nodes, + max_paths_per_chunk=10, + force_reindex=True # Force indexing since Neo4j is empty + ) + log_structured('info', f'GraphRAG components created with {len(vector_nodes)} nodes from vector index') + except Exception as e: + log_structured('warning', f'Error recreating GraphRAG components: {e}. Continuing without GraphRAG.') + graph_store = None + else: + log_structured('info', f'No index found at {INDEX_PERSIST_PATH}. Creating new index from {HP_DOCS_FOLDER}') + if not HP_DOCS_FOLDER.exists() or not any(HP_DOCS_FOLDER.iterdir()): + log_structured('error', f'HP documents folder is missing or empty: {HP_DOCS_FOLDER}') + return False + + # Process documents using LlamaParse + documents = await process_documents_in_directory(str(HP_DOCS_FOLDER), session_id="global_init") + + if not documents: + log_structured('error', f'No documents processed from {HP_DOCS_FOLDER}. Index creation aborted.') + return False + + log_structured('info', 'Creating vector store index...', { + 'document_count': len(documents), + 'sample_doc_metadata': documents[0].metadata if documents else None + }) + + # Build the index using the globally configured Settings (incl. node_parser) + index = VectorStoreIndex.from_documents( + documents=documents, + show_progress=True, + # service_context=service_context, # Old way, use Settings now + ) + + # Save to shared state + set_global_index(index) + + # Persist the index + index.storage_context.persist(persist_dir=str(INDEX_PERSIST_PATH)) + log_structured('info', f'Index created and persisted to {INDEX_PERSIST_PATH}') + + # --- Create GraphRAG components --- + log_structured('info', 'Starting GraphRAG component creation') + try: + # Get nodes from the index's docstore for GraphRAG + docstore_nodes = [] + if hasattr(index, 'docstore') and index.docstore: + docstore_nodes = list(index.docstore.docs.values()) + log_structured('info', f'Retrieved {len(docstore_nodes)} nodes from index docstore for GraphRAG') + + # If docstore is empty or doesn't exist, use the original documents + nodes_for_graph = docstore_nodes if docstore_nodes else documents + log_structured('info', f'Using {len(nodes_for_graph)} nodes for GraphRAG indexing') + + # Create GraphRAG components + graph_store, property_graph_index = create_graph_components( + llm=llm, + nodes=nodes_for_graph, + max_paths_per_chunk=10, + force_reindex=True # Force indexing for new index creation + ) + log_structured('info', 'GraphRAG components created successfully') + except Exception as graph_err: + log_structured('error', f'Error creating GraphRAG components: {graph_err}', + {'traceback': traceback.format_exc()}) + # Continue without GraphRAG if there's an error + + # --- Create Retriever and Query Engine --- + vector_store_info = VectorStoreInfo( + content_info="HP marketing reference materials, including brand guidelines and supporting documents.", + metadata_info=[ + MetadataInfo(name="filename", type="str", description="Filename of the source document"), + MetadataInfo(name="source_page", type="int", description="Approximate page number in the source document"), + MetadataInfo(name="image_paths", type="list[str]", description="List of image filenames associated with this chunk"), + # Add other relevant metadata fields here + ], + ) + + all_retriever = VectorIndexAutoRetriever( + index=index, # Use the local variable, not global_index + vector_store_info=vector_store_info, + similarity_top_k=SIMILARITY_TOP_K, + verbose=True # Enable verbose logging for retriever + ) + + # Create standard vector-based query engine + all_query_engine = RetrieverQueryEngine.from_args( # Use from_args for clarity + retriever=all_retriever, + response_synthesizer=get_response_synthesizer( + response_mode=ResponseMode.COMPACT, # Or REFINE, TREE_SUMMARIZE etc. + # service_context=service_context # Old way + ), + node_postprocessors=[ + SimilarityPostprocessor(similarity_cutoff=SIMILARITY_CUTOFF), + ], + # service_context=service_context # Old way + ) + + # Create GraphRAG query engine if components were created successfully + graphrag_query_engine = None + graph_store = locals().get('graph_store', None) + property_graph_index = locals().get('property_graph_index', None) + if graph_store is not None and property_graph_index is not None: + try: + # Ensure graph communities are built before creating query engine + if not hasattr(graph_store, 'communities_built') or not graph_store.communities_built: + log_structured('info', 'Building graph communities before creating query engine') + try: + # Use gpt-4o-mini model for community summaries (set in GraphRAGStore) + # The build_communities() method will first try to load from cache + # and will only rebuild and re-cache if cache loading fails + # It also tracks if communities are already built to avoid duplicate work + graph_store.build_communities() + log_structured('info', 'Communities built successfully (loaded from cache or built new)') + except Exception as comm_err: + log_structured('error', f'Error building communities: {comm_err}', + {'traceback': traceback.format_exc()}) + # Continue with query engine creation even if communities failed + + # Create a basic VectorIndexRetriever for GraphRAG + vector_retriever = VectorIndexRetriever( + index=index, + similarity_top_k=SIMILARITY_TOP_K + ) + + # Create the GraphRAG query engine - ensure all required fields are passed directly + try: + graphrag_query_engine = create_graphrag_query_engine( + vector_retriever=vector_retriever, + graph_store=graph_store, + llm=llm, + similarity_top_k=SIMILARITY_TOP_K + ) + log_structured('info', 'GraphRAG query engine created successfully') + except Exception as e: + log_structured('error', f'Error creating GraphRAG query engine: {e}', {'traceback': traceback.format_exc()}) + # Create a direct instance without using the factory function as a fallback + from graph_rag_integration import GraphRAGQueryEngine + graphrag_query_engine = GraphRAGQueryEngine( + vector_retriever=vector_retriever, + graph_store=graph_store, + llm=llm, + similarity_top_k=SIMILARITY_TOP_K + ) + + # Store GraphRAG components in shared state + set_graphrag_components( + graph_store=graph_store, + property_graph_index=property_graph_index, + graphrag_query_engine=graphrag_query_engine + ) + + log_structured('info', 'GraphRAG query engine created successfully') + except Exception as graph_engine_err: + log_structured('error', f'Error creating GraphRAG query engine: {graph_engine_err}', + {'traceback': traceback.format_exc()}) + # Continue without GraphRAG query engine if there's an error + + # --- Create Query Engine Tools --- + query_engine_tools_react = [ + QueryEngineTool( + query_engine=all_query_engine, + metadata=ToolMetadata( + name="answer_questions_from_hp_marketing_materials", + description="USE THIS TOOL FOR ALL QUERIES - Queries HP marketing materials (brand guidelines, etc.) to answer questions about guidelines, workflows, and processes. Use this for specific lookups in the knowledge base." + ), + ), + ] + + # Add GraphRAG tool if available + if graphrag_query_engine is not None: + class GraphRAGTool(BaseTool): + def __init__(self, query_engine): + self.query_engine = query_engine + self._metadata = ToolMetadata( + name="answerquestionswith_graphrag", + description="USE THIS TOOL FOR ALL QUERIES - Queries HP marketing materials using both vector and graph-based retrieval for more comprehensive answers. Use this for complex questions that need context from multiple related documents." + ) + + @property + def metadata(self): + return self._metadata + + def __call__(self, query_str: str) -> ToolOutput: + """Run query through GraphRAG and generate synthesized answer.""" + from shared_state import global_graphrag_query_engine + + log_structured('info', 'GraphRAG Tool: Starting dual retrieval', {'query': query_str}) + + try: + # Get both vector and GraphRAG retrieval results + retrieval_result = self.query_engine.custom_query(query_str) + + # Generate synthesized answer + final_answer = generate_final_answer(query_str, retrieval_result, self.query_engine.llm) + + # Prepare the tool output + log_message = { + 'vector_context_length': len(retrieval_result.get('vector_context', '')), + 'graphrag_context_length': len(retrieval_result.get('graphrag_context', '')), + 'vector_nodes_count': len(retrieval_result.get('vector_nodes', [])), + 'community_ids': retrieval_result.get('community_ids', []) + } + log_structured('info', 'GraphRAG Tool: Retrieval complete', log_message) + + # Extract vector nodes and create a properly structured raw_output that includes source_nodes + # This allows the routes.py code to extract images from these nodes + vector_nodes = retrieval_result.get('vector_nodes', []) + + # Create a wrapper object that mimics the structure expected by routes.py for image extraction + class NodeWrapper: + def __init__(self, nodes): + self.source_nodes = nodes + + # Preserve the original retrieval_result but add the source_nodes in the expected format + modified_raw_output = retrieval_result.copy() + modified_raw_output['source_nodes'] = vector_nodes + + # Add a source_nodes property that routes.py will look for + tool_output = ToolOutput( + content=final_answer, + tool_name="GraphRAG", + raw_output=NodeWrapper(vector_nodes), + raw_input={"query": query_str} + ) + + log_structured('debug', 'GraphRAG Tool: Including image metadata in response', + {'node_count': len(vector_nodes)}) + + return tool_output + except Exception as graphrag_err: + log_structured('error', f'Error in GraphRAG tool: {graphrag_err}', + {'traceback': traceback.format_exc()}) + return ToolOutput( + content=f"I encountered an error while retrieving information: {str(graphrag_err)}. Please try again or use the standard query tool.", + tool_name="GraphRAG", + raw_input={"query": query_str}, + raw_output={"error": str(graphrag_err)} + ) + + async def acall(self, input: str) -> ToolOutput: + """Async version of __call__.""" + return self.__call__(input) + + # Create the GraphRAG tool instance and add it to tools + graphrag_tool = GraphRAGTool(graphrag_query_engine) + query_engine_tools_react.append(graphrag_tool) + log_structured('info', 'Added GraphRAG tool to query engine tools') + + # --- Initialize Global Workflow Agent --- + # We're now using shared_state rather than module globals + + try: + # Create the agent instance + agent = ReActAgent2( + llm=llm, # Use the LLM configured via Settings + tools=query_engine_tools_react, + memory=ChatMemoryBuffer.from_defaults(llm=llm, token_limit=4096), # Give memory its own LLM ref + verbose=True, # Enable agent verbose logging + timeout=AGENT_TIMEOUT, + llm_timeout=LLM_TIMEOUT, + tool_timeout=TOOL_EXECUTION_TIMEOUT + ) + + # Store in shared state + set_global_agent(agent) + + log_structured('info', 'Agent initialized successfully') + except Exception as agent_err: + log_structured('critical', f'Failed to initialize agent: {str(agent_err)}', {'error': str(agent_err)}) + return False # Signal failure + + try: + # Define a simpler version of the run method + async def simple_run(query_text): + """Simple version that doesn't rely on complex workflow steps.""" + # Import from shared state to ensure we use the current global agent + from shared_state import global_workflow_agent + + if not global_workflow_agent: + log_structured('critical', 'Agent is None in simple_run - this should never happen') + return { + "response": "The AI system is currently unavailable. Please try again later.", + "sources": [], + "reasoning": [] + } + + try: + # Don't reset memory - we want to preserve conversation context between requests + # Only reset sources to track new sources for this specific response + global_workflow_agent.sources = [] + + # 1. Add the user query to memory + user_msg = ChatMessage(role="user", content=str(query_text)) + global_workflow_agent.memory.put(user_msg) + + # 2. Format the chat history + chat_history = global_workflow_agent.memory.get() + llm_input = global_workflow_agent.formatter.format(global_workflow_agent.tools, chat_history) + + # 3. Get LLM response + response = await asyncio.wait_for( + global_workflow_agent.llm.achat(llm_input), + timeout=global_workflow_agent.llm_timeout + ) + reasoning_step = global_workflow_agent.output_parser.parse(response.message.content) + + # Add detailed logging + log_structured('debug', 'Parsed reasoning step', { + 'step_type': type(reasoning_step).__name__, + 'has_response': hasattr(reasoning_step, 'response'), + 'has_action': hasattr(reasoning_step, 'action'), + 'action': getattr(reasoning_step, 'action', None), + 'action_input': getattr(reasoning_step, 'action_input', None), + 'raw_content': response.message.content[:200] # First 200 chars for debugging + }) + + # 4. Process the response (simplified) + # Force tool usage for all queries - don't allow direct responses + if hasattr(reasoning_step, 'response') and reasoning_step.response: + # If LLM gave a direct response, we need to force tool usage instead + log_structured('debug', 'LLM provided direct response, forcing tool usage for image retrieval') + + # Force tool execution by calling GraphRAG directly + graphrag_tool = next((t for t in global_workflow_agent.tools if 'graphrag' in t.metadata.name.lower()), None) + if graphrag_tool: + try: + tool_output = await asyncio.wait_for( + graphrag_tool.acall(input=str(query_text)), + timeout=global_workflow_agent.tool_timeout + ) + global_workflow_agent.sources.append(tool_output) + + # Use the tool's response instead of the direct LLM response + response_text = str(tool_output.content) if tool_output and tool_output.content else str(reasoning_step.response) + + assistant_msg = ChatMessage(role="assistant", content=response_text) + global_workflow_agent.memory.put(assistant_msg) + return { + "response": response_text, + "sources": global_workflow_agent.sources, + "reasoning": [reasoning_step] + } + except Exception as e: + log_structured('error', f'Error forcing GraphRAG tool execution: {str(e)}') + # Fall back to direct response if tool fails + + # Fallback to cleaning the direct response + response_text = str(reasoning_step.response) + + # Check for common thinking patterns and remove them + import re # Import re within the function scope to ensure it's available + thinking_patterns = [ + r'(?i)^.*?thinking:.*?\n', # Remove lines starting with "Thinking:" + r'(?i).*?', # Remove XML-like thinking tags + r'(?i)\[thinking\].*?\[/thinking\]', # Remove bracket thinking tags + r'(?i)I\'m thinking:.*?\n', # Remove "I'm thinking:" sections + r'(?i)Let me think.*?\n', # Remove "Let me think" sections + r'(?i)Thought:.*?Answer:', # Remove "Thought: ... Answer:" pattern + r'(?i)^Answer:\s*' # Remove just the "Answer:" prefix + ] + for pattern in thinking_patterns: + response_text = re.sub(pattern, '', response_text, flags=re.DOTALL) + + # Remove extra newlines that might be left after cleaning + response_text = re.sub(r'\n{3,}', '\n\n', response_text) + response_text = response_text.strip() + + assistant_msg = ChatMessage(role="assistant", content=response_text) + global_workflow_agent.memory.put(assistant_msg) + return { + "response": response_text, + "sources": global_workflow_agent.sources, + "reasoning": [reasoning_step] + } + else: + # Handle tool calls if the response indicates an action + if hasattr(reasoning_step, 'action') and reasoning_step.action: + tool_name = reasoning_step.action + action_input = reasoning_step.action_input or {} + + # Convert to dict if needed + if not isinstance(action_input, dict): + try: + if isinstance(action_input, str) and action_input.strip().startswith('{'): + action_input = json.loads(action_input) + else: + action_input = {'query': action_input} + except: + action_input = {'query': str(action_input)} + + # Find the tool + tool = next((t for t in global_workflow_agent.tools if t.metadata.name == tool_name), None) + if not tool: + error_msg = f"Tool '{tool_name}' not found. Available tools: {[t.metadata.name for t in global_workflow_agent.tools]}" + log_structured('error', 'Tool not found in simple_run', {'tool_name': tool_name, 'available_tools': [t.metadata.name for t in global_workflow_agent.tools]}) + return { + "response": f"I tried to use a tool called '{tool_name}' but it's not available. Please try rephrasing your query.", + "sources": [], + "reasoning": [reasoning_step] + } + if tool: + try: + # Execute the tool + tool_output = await asyncio.wait_for( + tool.acall(**action_input), + timeout=global_workflow_agent.tool_timeout + ) + global_workflow_agent.sources.append(tool_output) + + # Get a final response + observation = str(tool_output.content if tool_output and tool_output.content is not None else "No content") + follow_up_msg = ChatMessage(role="user", content=f"Here is the result: {observation}\\nPlease provide a final response based on this information.") + global_workflow_agent.memory.put(follow_up_msg) + + # Call LLM again for final response + chat_history = global_workflow_agent.memory.get() + llm_input = global_workflow_agent.formatter.format(global_workflow_agent.tools, chat_history) + final_response = await asyncio.wait_for( + global_workflow_agent.llm.achat(llm_input), + timeout=global_workflow_agent.llm_timeout + ) + + # Store and return + # Clean the response to remove any thinking parts + response_text = str(final_response.message.content) + + # Check for common thinking patterns and remove them + import re # Import re within the function scope to ensure it's available + thinking_patterns = [ + r'(?i)^.*?thinking:.*?\n', # Remove lines starting with "Thinking:" + r'(?i).*?', # Remove XML-like thinking tags + r'(?i)\[thinking\].*?\[/thinking\]', # Remove bracket thinking tags + r'(?i)I\'m thinking:.*?\n', # Remove "I'm thinking:" sections + r'(?i)Let me think.*?\n', # Remove "Let me think" sections + r'(?i)Thought:.*?Answer:', # Remove "Thought: ... Answer:" pattern + r'(?i)^Answer:\s*' # Remove just the "Answer:" prefix + ] + for pattern in thinking_patterns: + response_text = re.sub(pattern, '', response_text, flags=re.DOTALL) + + # Remove extra newlines that might be left after cleaning + response_text = re.sub(r'\n{3,}', '\n\n', response_text) + response_text = response_text.strip() + + global_workflow_agent.memory.put(ChatMessage(role="assistant", content=response_text)) + return { + "response": response_text, + "sources": global_workflow_agent.sources, + "reasoning": [reasoning_step] + } + except Exception as e: + log_structured('error', f'Error executing tool: {str(e)}', {'traceback': traceback.format_exc()}) + # Error message is already clean without thinking + error_response = f"I encountered an error while processing your query: {str(e)}" + return { + "response": error_response, + "sources": [], + "reasoning": [reasoning_step] + } + + # Enhanced fallback with thinking detection + if isinstance(reasoning_step, ActionReasoningStep): + # If we reach here, it means we had an action but couldn't execute it + thinking_response = f"Thought: {getattr(reasoning_step, 'thought', '')} Action: {reasoning_step.action} Action Input: {reasoning_step.action_input}" + log_structured('warning', 'Returning raw thinking due to failed action execution', { + 'action': reasoning_step.action, + 'action_input': reasoning_step.action_input + }) + return { + "response": "I apologize, but I encountered an issue while processing your query. Please try asking your question in a different way.", + "sources": [], + "reasoning": [reasoning_step] + } + else: + # Original fallback for other types + fallback_response = "I wasn't able to find a specific answer to your question. Please try rephrasing your query." + return { + "response": fallback_response, + "sources": global_workflow_agent.sources, + "reasoning": [reasoning_step] if reasoning_step else [] + } + except Exception as e: + log_structured('error', f'Error in simple_run: {str(e)}', {'traceback': traceback.format_exc()}) + # Error response is already clean + error_response = f"I encountered an error while processing your query: {str(e)}" + return { + "response": error_response, + "sources": [], + "reasoning": [] + } + + # Replace the run method directly on the agent that is already in shared state + # We need to get the current reference from shared_state + from shared_state import global_workflow_agent as current_agent + + if current_agent is None: + log_structured('critical', 'Cannot set run method - global_workflow_agent is None') + return False + + # Attach the run method directly + current_agent.run = simple_run + + # Verify it was attached correctly + if not hasattr(current_agent, 'run'): + log_structured('critical', 'Failed to attach run method to agent') + return False + + log_structured('info', 'Successfully attached run method to agent') + + # Test the agent is working by calling a simple method + log_structured('info', 'Testing agent functionality...') + + # Test on the current_agent we imported above + if current_agent and hasattr(current_agent, 'memory') and hasattr(current_agent.memory, 'reset'): + current_agent.memory.reset() + log_structured('info', 'Agent memory reset test successful') + else: + if not current_agent: + log_structured('error', 'Agent memory test failed: agent is None') + elif not hasattr(current_agent, 'memory'): + log_structured('error', 'Agent memory test failed: agent has no memory attribute') + elif not hasattr(current_agent.memory, 'reset'): + log_structured('error', 'Agent memory test failed: agent.memory has no reset method') + else: + log_structured('error', 'Agent memory test failed: unknown reason') + return False + + except Exception as method_err: + log_structured('critical', f'Failed to set up agent run method: {str(method_err)}', + {'error': str(method_err), 'traceback': traceback.format_exc()}) + return False + + log_structured('info', 'Global index and workflow agent initialized successfully.') + # Skip test query to avoid potential errors during startup + return True + + except Exception as e: + log_structured('critical', 'Global index/agent initialization failed', { + 'error': str(e), 'traceback': traceback.format_exc() + }) + global_index = None + global_workflow_agent = None + return False \ No newline at end of file diff --git a/chat-interface/.gitignore b/chat-interface/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/chat-interface/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/chat-interface/.htaccess b/chat-interface/.htaccess new file mode 100644 index 0000000..86f3a56 --- /dev/null +++ b/chat-interface/.htaccess @@ -0,0 +1,37 @@ +# JavaScript MIME type +AddType application/javascript .js +AddType application/json .json + +# CSS MIME type +AddType text/css .css + +# Image MIME types +AddType image/svg+xml .svg +AddType image/png .png +AddType image/jpeg .jpg +AddType image/jpeg .jpeg +AddType image/gif .gif +AddType image/webp .webp + +# Font MIME types +AddType font/ttf .ttf +AddType font/otf .otf +AddType font/woff .woff +AddType font/woff2 .woff2 + +# Force JavaScript MIME type for all JS files + + ForceType application/javascript + + +# Enable mod_rewrite +RewriteEngine On +RewriteBase /netflix_chatbot/ + +# Don't rewrite files or directories +RewriteCond %{REQUEST_FILENAME} -f [OR] +RewriteCond %{REQUEST_FILENAME} -d +RewriteRule ^ - [L] + +# Rewrite everything else to index.html +RewriteRule ^ index.html [L] \ No newline at end of file diff --git a/chat-interface/DEPLOY.md b/chat-interface/DEPLOY.md new file mode 100644 index 0000000..07f228a --- /dev/null +++ b/chat-interface/DEPLOY.md @@ -0,0 +1,50 @@ +# Deployment Instructions + +## Files to Deploy +Deploy all files from the `dist` directory to your server at the path `/hp_chatbot/`. + +## Changing the Backend URL +If you need to change the backend API URL: + +1. Edit the `.env` and `.env.production` files to update the `VITE_BACKEND_URL` value +2. Or use the provided script: `./update-backend.sh` which will update the URL and rebuild the application +3. Then rebuild the application with `npm run build` + +## Important Server Configuration +The application requires proper MIME type configuration to work correctly. Depending on your server type, use one of the following: + +### Apache Server +Make sure the `.htaccess` file is included in your deployment. It contains: +- MIME type configurations +- URL rewrite rules for SPA routing +- CORS headers + +### IIS Server +Make sure the `web.config` file is included in your deployment. It contains: +- MIME type configurations +- URL rewrite rules +- CORS headers + +## Common Issues and Solutions + +### JavaScript MIME Type Error +If you get an error like: +``` +Loading module from "https://ai-sandbox.oliver.solutions/hp_chatbot/assets/index-XXXXX.js" was blocked because of a disallowed MIME type ("text/html"). +``` + +Check that: +1. Your server is serving .js files with the correct MIME type: `application/javascript` +2. The `.htaccess` or `web.config` file is properly uploaded and enabled +3. Your server allows URL rewriting and custom MIME types + +### Authentication Issues +- Make sure the Microsoft authentication is set up correctly +- The redirectUri in the MSAL configuration should match your deployment URL +- Check that cookies and localStorage are enabled in the browser + +### API Connection Issues +- The app expects the backend API to be accessible at the configured URL (default: `https://ai-sandbox.oliver.solutions/hp_chatbot_back`) +- If you need to change the backend API URL, follow the instructions in "Changing the Backend URL" section above +- In development, API calls are proxied through Vite's development server +- In production, API calls go directly to the configured backend URL \ No newline at end of file diff --git a/chat-interface/README.md b/chat-interface/README.md new file mode 100644 index 0000000..eb55104 --- /dev/null +++ b/chat-interface/README.md @@ -0,0 +1,72 @@ +# HP Marketing Materials Chatbot + +A React frontend for the HP Marketing Materials Chatbot, providing a chat interface to query the HP marketing knowledge base. + +## Features + +- Clean chat interface for asking questions about HP marketing materials +- Sources and reasoning display for transparency +- Session-based memory for contextual conversations +- Conversation management system + +## Development + +### Prerequisites + +- Node.js 18+ +- npm or yarn + +### Setup + +1. Clone the repository +2. Install dependencies: +```bash +npm install +``` + +3. Start the development server: +```bash +npm run dev +``` + +### Configuration + +The application uses environment variables for configuration. Create a `.env` file in the root directory with the following variables: + +``` +# Backend API URL +VITE_BACKEND_URL=https://ai-sandbox.oliver.solutions/hp_chatbot_back + +# Base URL for the app (changes in production) +VITE_APP_BASE_URL=/ +``` + +### Changing the Backend URL + +If you need to change the backend API URL: + +1. Edit the `.env` and `.env.production` files to update the `VITE_BACKEND_URL` value +2. Or use the provided script: `./update-backend.sh` which will update the URL and rebuild the application +3. Then rebuild the application with `npm run build` + +## Building for Production + +To create a production build: + +```bash +npm run build +``` + +The output will be in the `dist` directory, ready for deployment. + +## Deployment + +See [DEPLOY.md](./DEPLOY.md) for detailed deployment instructions. + +## Technologies Used + +- React 18 +- Vite +- TailwindCSS +- Microsoft Authentication Library (MSAL) or custom authentication +- Shadcn/ui components \ No newline at end of file diff --git a/chat-interface/components.json b/chat-interface/components.json new file mode 100644 index 0000000..ab02c33 --- /dev/null +++ b/chat-interface/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": false, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "slate", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} + diff --git a/chat-interface/eslint.config.js b/chat-interface/eslint.config.js new file mode 100644 index 0000000..238d2e4 --- /dev/null +++ b/chat-interface/eslint.config.js @@ -0,0 +1,38 @@ +import js from '@eslint/js' +import globals from 'globals' +import react from 'eslint-plugin-react' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' + +export default [ + { ignores: ['dist'] }, + { + files: ['**/*.{js,jsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + settings: { react: { version: '18.3' } }, + plugins: { + react, + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...js.configs.recommended.rules, + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + ...reactHooks.configs.recommended.rules, + 'react/jsx-no-target-blank': 'off', + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +] diff --git a/chat-interface/index.html b/chat-interface/index.html new file mode 100644 index 0000000..014d3a8 --- /dev/null +++ b/chat-interface/index.html @@ -0,0 +1,31 @@ + + + + + + + + + HP Marketing Materials Chatbot + + + + + +
+
+

HP Marketing Materials Chatbot

+

Please sign in to access the chatbot.

+ +
+
+ + + + diff --git a/chat-interface/package-lock.json b/chat-interface/package-lock.json new file mode 100644 index 0000000..61e56bb --- /dev/null +++ b/chat-interface/package-lock.json @@ -0,0 +1,6784 @@ +{ + "name": "chat-interface", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "chat-interface", + "version": "0.0.0", + "dependencies": { + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.4", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "showdown": "^2.1.0" + }, + "devDependencies": { + "@eslint/js": "^9.13.0", + "@shadcn/ui": "^0.0.4", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "autoprefixer": "^10.4.20", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "eslint": "^9.13.0", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.14", + "globals": "^15.11.0", + "lucide-react": "^0.455.0", + "postcss": "^8.4.47", + "tailwind-merge": "^2.5.4", + "tailwindcss": "^3.4.14", + "tailwindcss-animate": "^1.0.7", + "vite": "^5.4.10" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz", + "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", + "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.2", + "@babel/types": "^7.26.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", + "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz", + "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz", + "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", + "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/types": "^7.25.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", + "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.14.0.tgz", + "integrity": "sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.2.tgz", + "integrity": "sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", + "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", + "integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz", + "integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", + "integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz", + "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", + "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.4.tgz", + "integrity": "sha512-QpObUH/ZlpaO4YgHSaYzrLO2VuO+ZBFFgGzjMUPwtiYnAzzNNDPJeEGRrT7qNOrWm/Jr08M1vlp+vTHtnSQ0Uw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.0", + "@radix-ui/react-portal": "1.1.2", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz", + "integrity": "sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.4.tgz", + "integrity": "sha512-jfUJrFct/hTA0XDM5p/htWKoNNTbDLY0KRwEt6pyOA6k2fmk0WVwl65PdUdJZgzGEHWx+49LilkcSaumQRyNQw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.4.tgz", + "integrity": "sha512-j4nrEO6nHU1nZUuCfRKoCcvh7PIywQPUCBa2UsootTHvTHIoIu2BzueInGJhhvQO/2FTRdNYpf63xsgEqH9IhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.4.tgz", + "integrity": "sha512-GmU/QgGtBTeraKyldC7cDVVvAJEOr3dFLKneez/n7BvX57UdhOqDsVwzU7UOnYA7AAOt+Xb26lk79PldDHgMIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.4.tgz", + "integrity": "sha512-N6oDBiZCBKlwYcsEPXGDE4g9RoxZLK6vT98M8111cW7VsVJFpNEqvJeIPfsCzbf0XEakPslh72X0gnlMi4Ddgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.4.tgz", + "integrity": "sha512-py5oNShCCjCyjWXCZNrRGRpjWsF0ic8f4ieBNra5buQz0O/U6mMXCpC1LvrHuhJsNPgRt36tSYMidGzZiJF6mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.4.tgz", + "integrity": "sha512-L7VVVW9FCnTTp4i7KrmHeDsDvjB4++KOBENYtNYAiYl96jeBThFfhP6HVxL74v4SiZEVDH/1ILscR5U9S4ms4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.4.tgz", + "integrity": "sha512-10ICosOwYChROdQoQo589N5idQIisxjaFE/PAnX2i0Zr84mY0k9zul1ArH0rnJ/fpgiqfu13TFZR5A5YJLOYZA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.4.tgz", + "integrity": "sha512-ySAfWs69LYC7QhRDZNKqNhz2UKN8LDfbKSMAEtoEI0jitwfAG2iZwVqGACJT+kfYvvz3/JgsLlcBP+WWoKCLcw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.4.tgz", + "integrity": "sha512-uHYJ0HNOI6pGEeZ/5mgm5arNVTI0nLlmrbdph+pGXpC9tFHFDQmDMOEqkmUObRfosJqpU8RliYoGz06qSdtcjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.4.tgz", + "integrity": "sha512-38yiWLemQf7aLHDgTg85fh3hW9stJ0Muk7+s6tIkSUOMmi4Xbv5pH/5Bofnsb6spIwD5FJiR+jg71f0CH5OzoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.4.tgz", + "integrity": "sha512-q73XUPnkwt9ZNF2xRS4fvneSuaHw2BXuV5rI4cw0fWYVIWIBeDZX7c7FWhFQPNTnE24172K30I+dViWRVD9TwA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.4.tgz", + "integrity": "sha512-Aie/TbmQi6UXokJqDZdmTJuZBCU3QBDA8oTKRGtd4ABi/nHgXICulfg1KI6n9/koDsiDbvHAiQO3YAUNa/7BCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.4.tgz", + "integrity": "sha512-P8MPErVO/y8ohWSP9JY7lLQ8+YMHfTI4bAdtCi3pC2hTeqFJco2jYspzOzTUB8hwUWIIu1xwOrJE11nP+0JFAQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.4.tgz", + "integrity": "sha512-K03TljaaoPK5FOyNMZAAEmhlyO49LaE4qCsr0lYHUKyb6QacTNF9pnfPpXnFlFD3TXuFbFbz7tJ51FujUXkXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.4.tgz", + "integrity": "sha512-VJYl4xSl/wqG2D5xTYncVWW+26ICV4wubwN9Gs5NrqhJtayikwCXzPL8GDsLnaLU3WwhQ8W02IinYSFJfyo34Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.4.tgz", + "integrity": "sha512-ku2GvtPwQfCqoPFIJCqZ8o7bJcj+Y54cZSr43hHca6jLwAiCbZdBUOrqE6y29QFajNAzzpIOwsckaTFmN6/8TA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.4.tgz", + "integrity": "sha512-V3nCe+eTt/W6UYNr/wGvO1fLpHUrnlirlypZfKCT1fG6hWfqhPgQV/K/mRBXBpxc0eKLIF18pIOFVPh0mqHjlg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.4.tgz", + "integrity": "sha512-LTw1Dfd0mBIEqUVCxbvTE/LLo+9ZxVC9k99v1v4ahg9Aak6FpqOfNu5kRkeTAn0wphoC4JU7No1/rL+bBCEwhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shadcn/ui": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@shadcn/ui/-/ui-0.0.4.tgz", + "integrity": "sha512-0dtu/5ApsOZ24qgaZwtif8jVwqol7a4m1x5AxPuM1k5wxhqU7t/qEfBGtaSki1R8VlbTQfCj5PAlO45NKCa7Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "5.2.0", + "commander": "^10.0.0", + "execa": "^7.0.0", + "fs-extra": "^11.1.0", + "node-fetch": "^3.3.0", + "ora": "^6.1.2", + "prompts": "^2.4.2", + "zod": "^3.20.2" + }, + "bin": { + "ui": "dist/index.js" + } + }, + "node_modules/@shadcn/ui/node_modules/chalk": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", + "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@shadcn/ui/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.13", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", + "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", + "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.3.tgz", + "integrity": "sha512-NooDe9GpHGqNns1i8XDERg0Vsg5SSYRhRxxyTGogUdkdNt47jal+fbuYi+Yfq6pzRCKXyoPcWisfxE6RIM3GKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001679", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001679.tgz", + "integrity": "sha512-j2YqID/YwpLnKzCmBOS4tlZdWprXm3ZmQLBH9ZBXFOhoxLA46fwyBvx6toCBWBmnuwUY/qB3kEU6gFx8qgCroA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.0.tgz", + "integrity": "sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "clsx": "2.0.0" + }, + "funding": { + "url": "https://joebell.co.uk" + } + }, + "node_modules/class-variance-authority/node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.55", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.55.tgz", + "integrity": "sha512-6maZ2ASDOTBtjt9FhqYPRnbvKU5tjG0IN9SztUOWYw2AzNDNpKJYLJmlK0/En4Hs/aiWnB+JZ+gW19PIGszgKg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.0.tgz", + "integrity": "sha512-tpxqxncxnpw3c93u8n3VOzACmRFoVmWJqbWXvX/JfKbkhBw1oslgPrUfeSt2psuqyEJFD6N/9lg5i7bsKpoq+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.3", + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.14.0.tgz", + "integrity": "sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.7.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.14.0", + "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.0", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz", + "integrity": "sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.1.0", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz", + "integrity": "sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.14.tgz", + "integrity": "sha512-aXvzCTK7ZBv1e7fahFuR3Z/fyQQSIQ711yPgYRj+Oj64tyTgO4iQIDmYXDBqvSWQ/FA4OSCsXOStlF+noU0/NA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-scope": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "15.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.12.0.tgz", + "integrity": "sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.3.tgz", + "integrity": "sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", + "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.0.0", + "is-unicode-supported": "^1.1.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.455.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.455.0.tgz", + "integrity": "sha512-XQoi58X4COfjy6s1P4TVsXMhU2r1KsfUTRFyds/yJvdzBNOfu0F9RRIas626T8UaNZzEv4llF7ivh4uLkYp3rw==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-6.3.1.tgz", + "integrity": "sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.0.0", + "cli-cursor": "^4.0.0", + "cli-spinners": "^2.6.1", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^1.1.0", + "log-symbols": "^5.1.0", + "stdin-discarder": "^0.1.0", + "strip-ansi": "^7.0.1", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-import/node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", + "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.4.tgz", + "integrity": "sha512-vGorVWIsWfX3xbcyAS+I047kFKapHYivmkaT63Smj77XwvLSJos6M1xGqZnBPFQFBRZDOcG1QnYEIxAvTr/HjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.24.4", + "@rollup/rollup-android-arm64": "4.24.4", + "@rollup/rollup-darwin-arm64": "4.24.4", + "@rollup/rollup-darwin-x64": "4.24.4", + "@rollup/rollup-freebsd-arm64": "4.24.4", + "@rollup/rollup-freebsd-x64": "4.24.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.4", + "@rollup/rollup-linux-arm-musleabihf": "4.24.4", + "@rollup/rollup-linux-arm64-gnu": "4.24.4", + "@rollup/rollup-linux-arm64-musl": "4.24.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.4", + "@rollup/rollup-linux-riscv64-gnu": "4.24.4", + "@rollup/rollup-linux-s390x-gnu": "4.24.4", + "@rollup/rollup-linux-x64-gnu": "4.24.4", + "@rollup/rollup-linux-x64-musl": "4.24.4", + "@rollup/rollup-win32-arm64-msvc": "4.24.4", + "@rollup/rollup-win32-ia32-msvc": "4.24.4", + "@rollup/rollup-win32-x64-msvc": "4.24.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/showdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/showdown/-/showdown-2.1.0.tgz", + "integrity": "sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ==", + "license": "MIT", + "dependencies": { + "commander": "^9.0.0" + }, + "bin": { + "showdown": "bin/showdown.js" + }, + "funding": { + "type": "individual", + "url": "https://www.paypal.me/tiviesantos" + } + }, + "node_modules/showdown/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stdin-discarder": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", + "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz", + "integrity": "sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz", + "integrity": "sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/tailwindcss/node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz", + "integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", + "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", + "dev": true, + "license": "MIT", + "dependencies": { + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", + "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/chat-interface/package.json b/chat-interface/package.json new file mode 100644 index 0000000..e3674f6 --- /dev/null +++ b/chat-interface/package.json @@ -0,0 +1,40 @@ +{ + "name": "hp-chat-interface", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.4", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "showdown": "^2.1.0" + }, + "devDependencies": { + "@eslint/js": "^9.13.0", + "@shadcn/ui": "^0.0.4", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "autoprefixer": "^10.4.20", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "eslint": "^9.13.0", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.14", + "globals": "^15.11.0", + "lucide-react": "^0.455.0", + "postcss": "^8.4.47", + "tailwind-merge": "^2.5.4", + "tailwindcss": "^3.4.14", + "tailwindcss-animate": "^1.0.7", + "vite": "^5.4.10" + } +} diff --git a/chat-interface/postcss.config.js b/chat-interface/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/chat-interface/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/chat-interface/public/images/Netflix_2015_N_logo.png b/chat-interface/public/images/Netflix_2015_N_logo.png new file mode 100644 index 0000000..ef062a7 Binary files /dev/null and b/chat-interface/public/images/Netflix_2015_N_logo.png differ diff --git a/chat-interface/public/images/netflix-logo.svg b/chat-interface/public/images/netflix-logo.svg new file mode 100644 index 0000000..f3a8116 --- /dev/null +++ b/chat-interface/public/images/netflix-logo.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/chat-interface/public/vite.svg b/chat-interface/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/chat-interface/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/chat-interface/src/App.css b/chat-interface/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/chat-interface/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/chat-interface/src/App.jsx b/chat-interface/src/App.jsx new file mode 100644 index 0000000..8c7295e --- /dev/null +++ b/chat-interface/src/App.jsx @@ -0,0 +1,1525 @@ +/** + * HP AI Chatbot + * + * Frontend interface for the HP Marketing Materials Chatbot. + * This application provides a chat interface to query the HP marketing + * knowledge base containing HP brand guidelines and supporting documents. + * + * Key features: + * - Clean chat interface for asking questions about HP marketing materials + * - Sources and reasoning display for transparency + * - Brief download functionality to export conversations + * - Session-based memory for contextual conversations + * + */ + +import { useState, useRef, useEffect } from 'react'; +import { Send, Upload, Loader2, X, FileText, Info, PlusCircle, Trash2, Moon, Sun } from 'lucide-react'; +import ThemeToggle from './components/ThemeToggle'; +import { fetchWithTimeout } from './lib/utils'; +import { Alert, AlertDescription } from "./components/ui/alert"; +import * as Tooltip from '@radix-ui/react-tooltip'; +import showdown from 'showdown'; +import { getCurrentUser, signOut } from './auth'; +import { loadUserConversations, loadConversationMessages, createNewConversation, deleteConversation } from './components/ConversationManager'; + +// Use environment variables for backend URL +// Define backend URL dynamically based on environment +const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'https://ai-sandbox.oliver.solutions/hp_back_v2'; +console.log('Using backend URL:', BACKEND_URL); + +// Function to get authenticated fetch defaults +const getFetchDefaults = () => { + // Get MSAL username for authentication + const username = getCurrentUser(); + + return { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'X-MS-USERNAME': username || '', // Add the authenticated username to requests + } + }; +}; + +export default function ChatInterface() { + // Build timestamp to force new hash generation on rebuild - logged only once + const BUILD_TIMESTAMP = "2025-04-27T" + "FIXED_TIMESTAMP"; + // Only log in development mode and only once during initialization + if (process.env.NODE_ENV !== 'production') { + // Using useRef to ensure this only runs once + const hasLoggedTimestamp = useRef(false); + useEffect(() => { + if (!hasLoggedTimestamp.current) { + console.log('App build timestamp:', BUILD_TIMESTAMP); + hasLoggedTimestamp.current = true; + } + }, []); + } + + const [messages, setMessages] = useState([]); + const [inputMessage, setInputMessage] = useState(''); + const [isProcessing, setIsProcessing] = useState(false); + const [isInitialized, setIsInitialized] = useState(false); + const [error, setError] = useState(null); + const [isCheckingStatus, setIsCheckingStatus] = useState(true); + + // Conversation state + const [conversations, setConversations] = useState([]); + const [loadingConversations, setLoadingConversations] = useState(false); + const [activeConversation, setActiveConversation] = useState(null); + + // Sidebar resizing state + const [sidebarWidth, setSidebarWidth] = useState(320); // Default width in pixels + const [isResizing, setIsResizing] = useState(false); + const [autoWidth, setAutoWidth] = useState(true); // Auto-width flag + const minSidebarWidth = 240; + const maxSidebarWidth = 480; + const sidebarRef = useRef(null); + + // Initialize Showdown converter for markdown + const markdownConverter = new showdown.Converter({ + tables: true, + simplifiedAutoLink: true, + strikethrough: true, + tasklists: true, + emoji: true + }); + + const messagesEndRef = useRef(null); + + // Use a ref to track if we've already created a new conversation on this page load + const hasCreatedConversation = useRef(false); + + const [sessionId, setSessionId] = useState(() => { + // Check for existing session ID in localStorage + const existingSessionId = localStorage.getItem('chatSessionId'); + if (existingSessionId) { + return existingSessionId; + } + // Generate new session ID if none exists + const newSessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + localStorage.setItem('chatSessionId', newSessionId); + return newSessionId; + }); + + // Load user conversations from the API + const loadUserConversations = async () => { + setLoadingConversations(true); + try { + const fetchOptions = getFetchDefaults(); + const response = await fetchWithTimeout(`${BACKEND_URL}/conversations`, { + ...fetchOptions, + method: 'GET' + }); + + if (response.ok) { + const data = await response.json(); + setConversations(data.conversations || []); + + // REMOVED auto-loading of the most recent conversation + // We want the user to always start with a fresh conversation when refreshing + } else { + console.error('Failed to load conversations'); + } + } catch (error) { + console.error('Error loading conversations:', error); + } finally { + setLoadingConversations(false); + } + }; + + // Load a specific conversation + const loadConversation = async (conversation) => { + try { + setIsProcessing(true); + setActiveConversation(conversation); + + // Set the session ID + setSessionId(conversation.session_id); + localStorage.setItem('chatSessionId', conversation.session_id); + + // Fetch the messages for this conversation + const fetchOptions = getFetchDefaults(); + const response = await fetchWithTimeout(`${BACKEND_URL}/conversations/${conversation.id}/messages`, { + ...fetchOptions, + method: 'GET' + }); + + if (response.ok) { + const data = await response.json(); + + // Format the messages for the UI + const formattedMessages = data.messages.map(msg => ({ + role: msg.role, + content: msg.content, + sources: msg.sources || [], + reasoning: msg.reasoning || [], + images: msg.images || [] + })); + + setMessages(formattedMessages); + setIsInitialized(true); + setError(null); + } else { + setError('Failed to load conversation messages'); + console.error('Failed to load conversation messages'); + } + } catch (error) { + setError('Error loading conversation'); + console.error('Error loading conversation:', error); + } finally { + setIsProcessing(false); + } + }; + + // Create a new conversation + const createNewConversation = async () => { + try { + setIsProcessing(true); + const fetchOptions = getFetchDefaults(); + const response = await fetchWithTimeout(`${BACKEND_URL}/conversations/new`, { + ...fetchOptions, + method: 'POST' + }); + + if (response.ok) { + const data = await response.json(); + + // Set the new session ID + setSessionId(data.session_id); + localStorage.setItem('chatSessionId', data.session_id); + + // Create a new conversation object + const newConversation = { + id: data.conversation_id, + title: "New conversation", + created_at: new Date().toISOString(), + last_updated: new Date().toISOString(), + session_id: data.session_id + }; + + // Update the conversations list + setConversations(prev => [newConversation, ...prev]); + + // Set as active conversation + setActiveConversation(newConversation); + + // Reset messages + setMessages([{ + role: 'assistant', + content: 'Welcome to the HP Marketing Materials Chatbot! How can I help you today?' + }]); + + setIsInitialized(true); + setError(null); + } else { + setError('Failed to create new conversation'); + console.error('Failed to create new conversation'); + } + } catch (error) { + setError('Error creating new conversation'); + console.error('Error creating new conversation:', error); + } finally { + setIsProcessing(false); + } + }; + + // Function to handle loading a conversation + const handleLoadConversation = async (conversation) => { + setActiveConversation(conversation); + await loadConversationMessages( + conversation, + getCurrentUser(), + setMessages, + setSessionId, + setError + ); + }; + + // Function to handle creating a new conversation + const handleCreateNewConversation = async () => { + await createNewConversation( + getCurrentUser(), + setConversations, + setActiveConversation, + setSessionId, + setMessages, + setError + ); + }; + + // Function to update conversations list (including titles) after a message is sent + const updateConversationsList = async () => { + if (activeConversation) { + try { + console.log('Updating conversations list...'); + // Get updated conversations from the server + const fetchOptions = getFetchDefaults(); + const response = await fetchWithTimeout(`${BACKEND_URL}/conversations`, { + ...fetchOptions, + method: 'GET' + }); + + if (response.ok) { + const data = await response.json(); + const updatedConversations = data.conversations || []; + + // Update the conversations list + setConversations(updatedConversations); + + // Find and update the active conversation with any title changes + const updatedActiveConversation = updatedConversations.find( + convo => convo.id === activeConversation.id + ); + + if (updatedActiveConversation) { + // Always update the active conversation to get the latest data + // This ensures titles get updated after messages are sent + setActiveConversation(updatedActiveConversation); + console.log('Updated active conversation:', + 'ID:', updatedActiveConversation.id, + 'Title:', updatedActiveConversation.title + ); + } else { + console.warn('Active conversation not found in updated list'); + } + } else { + console.error('Failed to update conversations list'); + } + } catch (err) { + console.error('Error updating conversations list:', err); + // Continue with existing data on error + } + } else { + console.warn('No active conversation to update'); + } + }; + + // Separate useEffect that only runs once on component mount + useEffect(() => { + // Flag to prevent multiple conversation creations + hasCreatedConversation.current = false; + + // Check the status of the chat system when the component mounts + const checkStatus = async () => { + setIsCheckingStatus(true); + try { + // Check if the chat system is initialized + const fetchOptions = getFetchDefaults(); + const response = await fetchWithTimeout(`${BACKEND_URL}/status?sessionId=${sessionId}`, { + ...fetchOptions, + method: 'GET' + }); + + if (response.ok) { + const data = await response.json(); + // Check for either the new property 'global_status' or the old 'initialized' property + // Enhanced logging for the status response + console.log('Status check raw response:', JSON.stringify(data)); + + // Explicitly check each property with detailed logging + const hasGlobalStatus = 'global_status' in data; + const globalStatusValue = hasGlobalStatus ? data.global_status : 'missing'; + const globalStatusMatch = globalStatusValue === 'initialized'; + + const hasInitialized = 'initialized' in data; + const initializedValue = hasInitialized ? data.initialized : false; + + console.log('Status check property analysis:', { + hasGlobalStatus, + globalStatusValue, + globalStatusMatch, + hasInitialized, + initializedValue + }); + + // Try also checking 'is_initialized' as another possible property name + const hasIsInitialized = 'is_initialized' in data; + const isInitializedValue = hasIsInitialized ? data.is_initialized : false; + + // Check session-specific initialization if available + const hasSessionInitialized = 'session_initialized' in data; + const sessionInitialized = hasSessionInitialized ? data.session_initialized : true; // Default to true if not present + const sessionStatus = data.session_status || 'unknown'; + + // OVERRIDE: ALWAYS force initialization to true + // This completely bypasses all property checks and status endpoint validation + // Complementing the backend change which now always returns initialized=true + const systemInitialized = true; + + // Log final decision with full context + console.log('Final initialization decision:', { + systemInitialized, + globalStatusMatch, + initializedValue, + isInitializedValue, + hasSessionInitialized, + sessionInitialized, + sessionStatus, + fullData: data + }); + + setIsInitialized(systemInitialized); + + if (systemInitialized) { + // First, load conversations list for the sidebar without auto-selecting any + await loadUserConversations(); + + // Only create a new conversation if we haven't already + if (!hasCreatedConversation.current) { + logToConsole('info', 'App mounted, creating a new conversation', { sessionId }); + + // Set the flag to prevent creating multiple conversations + hasCreatedConversation.current = true; + + setIsProcessing(true); + try { + const fetchOptions = getFetchDefaults(); + const response = await fetch(`${BACKEND_URL}/conversations/new`, { + ...fetchOptions, + method: 'POST' + }); + + if (response.ok) { + const data = await response.json(); + + // Set the new session ID + setSessionId(data.session_id); + localStorage.setItem('chatSessionId', data.session_id); + + // Create a new conversation object + const newConversation = { + id: data.conversation_id, + title: "New conversation", + created_at: new Date().toISOString(), + last_updated: new Date().toISOString(), + session_id: data.session_id + }; + + // Update the conversations list + setConversations(prev => [newConversation, ...prev]); + + // Set as active conversation + setActiveConversation(newConversation); + + // Reset messages + setMessages([{ + role: 'assistant', + content: 'Welcome to the HP Marketing Materials Chatbot! How can I help you today?' + }]); + } else { + console.error('Failed to create new conversation'); + } + } catch (error) { + console.error('Error creating new conversation on page load:', error); + } finally { + setIsProcessing(false); + } + } + + setError(null); + logToConsole('info', 'Chat system is ready', { sessionId }); + } else { + // System is not initialized + setError('The chat system is not yet initialized. Please try again later.'); + logToConsole('error', 'Chat system not initialized', { sessionId }); + } + } else { + console.error('Failed to check system status'); + setError('Failed to connect to the chat system. Please try again later.'); + } + } catch (error) { + console.error('Error checking system status:', error); + setError('Failed to connect to the chat system. Please try again later.'); + } finally { + setIsCheckingStatus(false); + } + }; + + checkStatus(); + + // Empty dependency array to ensure this only runs once when the component mounts + }, []); + + const logToConsole = (type, message, data = null) => { + const timestamp = new Date().toISOString(); + const logMessage = { + timestamp, + type, + message, + data + }; + console.log(JSON.stringify(logMessage, null, 2)); + }; + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + // Update the page title when active conversation changes or is updated + useEffect(() => { + if (activeConversation?.title) { + document.title = `${activeConversation.title} | HP Marketing Bot`; + } else { + document.title = 'HP Marketing Materials Chatbot'; + } + }, [activeConversation?.title]); + + // Sidebar resize handlers + const startResizing = (e) => { + setIsResizing(true); + e.preventDefault(); + }; + + const stopResizing = () => { + setIsResizing(false); + document.body.style.cursor = 'default'; + }; + + const resize = (e) => { + if (isResizing) { + document.body.style.cursor = 'ew-resize'; + const newWidth = e.clientX; + if (newWidth >= minSidebarWidth && newWidth <= maxSidebarWidth) { + setSidebarWidth(newWidth); + } + } + }; + + // Auto-resize sidebar based on conversation titles + useEffect(() => { + // Function to calculate the sidebar width based on conversation titles + const calculateSidebarWidth = () => { + if (!autoWidth || !conversations.length) return; + + // Create a temporary div to measure text width + const measureDiv = document.createElement('div'); + measureDiv.style.position = 'absolute'; + measureDiv.style.visibility = 'hidden'; + measureDiv.style.whiteSpace = 'nowrap'; + measureDiv.style.fontFamily = 'inherit'; + measureDiv.style.fontSize = '0.875rem'; // text-sm + measureDiv.style.fontWeight = '500'; // font-medium + + // Add to DOM for measurement + document.body.appendChild(measureDiv); + + // Find longest title + let maxWidth = 0; + conversations.forEach(convo => { + measureDiv.textContent = convo.title || "New conversation"; + const width = measureDiv.offsetWidth; + maxWidth = Math.max(maxWidth, width); + }); + + // Remove measurement div + document.body.removeChild(measureDiv); + + // Calculate sidebar width (add padding for buttons, icons, etc.) + // We add extra padding for the delete button and some margin + const calculatedWidth = Math.max(minSidebarWidth, Math.min(maxWidth + 100, maxSidebarWidth)); + + // Update width if it's significantly different + if (Math.abs(calculatedWidth - sidebarWidth) > 10) { + setSidebarWidth(calculatedWidth); + } + }; + + // Call the calculation function + calculateSidebarWidth(); + + // Add an event listener for window resize to recalculate if needed + if (autoWidth) { + window.addEventListener('resize', calculateSidebarWidth); + return () => window.removeEventListener('resize', calculateSidebarWidth); + } + }, [conversations, autoWidth, minSidebarWidth, maxSidebarWidth, sidebarWidth]); + + // Add event listeners for manual resize + useEffect(() => { + const handleResize = (e) => { + if (isResizing) { + document.body.style.cursor = 'ew-resize'; + const newWidth = e.clientX; + + if (newWidth >= minSidebarWidth && newWidth <= maxSidebarWidth) { + setSidebarWidth(newWidth); + // When user manually resizes, disable auto width + setAutoWidth(false); + } + } + }; + + const handleStopResizing = () => { + setIsResizing(false); + document.body.style.cursor = 'default'; + }; + + window.addEventListener('mousemove', handleResize); + window.addEventListener('mouseup', handleStopResizing); + + return () => { + window.removeEventListener('mousemove', handleResize); + window.removeEventListener('mouseup', handleStopResizing); + }; + }, [isResizing, minSidebarWidth, maxSidebarWidth]); + + const handleFilesChange = (e, type) => { + const files = Array.from(e.target.files); + logToConsole('info', `Handling ${type} files change`, { + fileCount: files.length, + fileDetails: files.map(f => ({ + name: f.name, + type: f.type, + size: f.size + })) + }); + + if (type === 'brief') { + setBriefFiles(prev => [...prev, ...files]); + } else { + setSupportingFiles(prev => [...prev, ...files]); + } + }; + + const removeFile = (fileName, type) => { + logToConsole('info', `Removing file`, { + fileName, + type + }); + + if (type === 'brief') { + setBriefFiles(prev => prev.filter(file => file.name !== fileName)); + } else { + setSupportingFiles(prev => prev.filter(file => file.name !== fileName)); + } + }; + + const resetChat = async () => { + try { + setIsProcessing(true); + logToConsole('info', 'Resetting chat session', { sessionId }); + const fetchOptions = getFetchDefaults(); + const response = await fetchWithTimeout(`${BACKEND_URL}/reset`, { + ...fetchOptions, + method: 'POST', + body: JSON.stringify({ sessionId }) + }); + + if (response.ok) { + // Reset the UI state + setMessages([{ + role: 'assistant', + content: 'Chat session has been reset. How can I help you today?' + }]); + setError(null); + setInputMessage(''); + + logToConsole('info', 'Chat session reset successfully'); + } else { + const errorData = await response.json(); + setError(errorData.error || 'Failed to reset chat'); + } + } catch (error) { + console.error('Error resetting chat:', error); + setError('Failed to reset chat. Please try again.'); + } finally { + setIsProcessing(false); + } + }; + + + const ImageViewer = ({ image, isOpen, onClose, allImages }) => { + if (!isOpen) return null; + + // Find the index of the current image in the allImages array + const currentImageIndex = allImages.findIndex(img => { + const imgFilename = typeof img === 'string' ? img : img.filename; + const selectedFilename = typeof image === 'string' ? image : image.filename; + return imgFilename === selectedFilename; + }); + + const [activeIndex, setActiveIndex] = useState(currentImageIndex >= 0 ? currentImageIndex : 0); + + // Handle both string and object formats for backward compatibility + const currentImage = allImages[activeIndex]; + const filename = typeof currentImage === 'string' ? currentImage : currentImage.filename; + const metadata = typeof currentImage === 'string' ? null : currentImage; + + const goToPrevious = (e) => { + e.stopPropagation(); + setActiveIndex((prevIndex) => (prevIndex > 0 ? prevIndex - 1 : allImages.length - 1)); + }; + + const goToNext = (e) => { + e.stopPropagation(); + setActiveIndex((prevIndex) => (prevIndex < allImages.length - 1 ? prevIndex + 1 : 0)); + }; + + return ( +
+
+ {allImages.length > 1 && ( + + )} + +
+ {metadata + {metadata && ( +
+
{metadata.document}
+
Page {metadata.page} {allImages.length > 1 ? `(${activeIndex + 1}/${allImages.length})` : ''}
+
+ )} +
+ + {allImages.length > 1 && ( + + )} + + +
+
+ ); + }; + + const MessageBubble = ({ message }) => { + const [selectedImage, setSelectedImage] = useState(null); + + const formatSources = (sources) => { + if (!sources || !Array.isArray(sources)) return []; + + return sources.map((source) => { + if (source && typeof source === 'object') { + if (source.content) { + return { + text: typeof source.content === 'string' + ? source.content + : JSON.stringify(source.content), + tool: source.tool_name || '' + }; + } + return { + text: JSON.stringify(source), + tool: '' + }; + } + return { + text: String(source), + tool: '' + }; + }).filter(source => source.text); + }; + + const formatReasoning = (reasoning) => { + if (!reasoning || !Array.isArray(reasoning)) return []; + + return reasoning.map((step, index) => { + const stepType = step.type || 'thought'; + let content = ''; + + if (step.action) { + content = `${step.action}${step.action_input ? `: ${step.action_input}` : ''}`; + } else if (step.observation) { + content = step.observation; + } else if (step.response) { + content = step.response; + } else if (step.thought) { + content = step.thought; + } + + return { + id: index, + type: stepType, + content: content, + thought: step.thought + }; + }).filter(step => step.content); + }; + + const sources = formatSources(message.sources); + const reasoningSteps = formatReasoning(message.reasoning); + const images = message.images || []; + + // Handle image click - open full-size viewer + const openImage = (image) => { + setSelectedImage(image); + }; + + // Close image viewer + const closeImage = () => { + setSelectedImage(null); + }; + + // Handle escape key to close image + useEffect(() => { + const handleEscKey = (e) => { + if (e.key === 'Escape' && selectedImage) { + closeImage(); + } + }; + + window.addEventListener('keydown', handleEscKey); + return () => window.removeEventListener('keydown', handleEscKey); + }, [selectedImage]); + + return ( +
+
+
+ + {/* Display image thumbnails if available */} + {images.length > 0 && ( +
+
Relevant Document Screenshots:
+
+ {images.map((image, idx) => { + // Handle both string and object formats for backward compatibility + const filename = typeof image === 'string' ? image : image.filename; + const metadata = typeof image === 'string' ? null : image; + + return ( +
openImage(image)} + > + {metadata + {metadata && ( +
+ pg. {metadata.page} +
+ )} +
+ ); + })} +
+
+ )} + +
+ {sources.length > 0 && ( + + + + + + + +
+

Sources Used:

+
    + {sources.map((source, idx) => ( +
  • + {source.tool && ( +
    + Tool: {source.tool} +
    + )} +
    {source.text}
    +
  • + ))} +
+
+ +
+
+
+
+ )} + + {reasoningSteps.length > 0 && ( + + + + + + + +
+

Reasoning Steps:

+
    + {reasoningSteps.map((step) => ( +
  • +
    + {step.type}: +
    +
    {step.content}
    + {step.thought && step.thought !== step.content && ( +
    + Thought: {step.thought} +
    + )} +
  • + ))} +
+
+ +
+
+
+
+ )} +
+
+ + {/* Image viewer modal */} + +
+ ); + }; + + const handleSubmit = async () => { + if (!inputMessage.trim() || isProcessing) return; + + const currentMessage = inputMessage; + setMessages(prev => [...prev, { role: 'user', content: currentMessage }]); + setInputMessage(''); + setIsProcessing(true); + + // ENHANCED DEBUGGING: Log the current state of sessionId and related variables + console.log('SUBMIT DEBUG DATA:', { + sessionId, + sessionIdType: typeof sessionId, + sessionIdLength: sessionId ? sessionId.length : 0, + hasActiveConversation: !!activeConversation, + activeConversationId: activeConversation ? activeConversation.id : null, + activeConversationSessionId: activeConversation ? activeConversation.session_id : null, + localStorageSessionId: localStorage.getItem('chatSessionId') + }); + + // Check if this is a blank chat with no active conversation - create one if needed + if (!activeConversation) { + logToConsole('info', 'No active conversation found when submitting message, creating a new one', { sessionId }); + try { + await handleCreateNewConversation(); + } catch (err) { + console.error('Error creating new conversation before sending message:', err); + // Continue with the chat request anyway, as the backend might handle this + } + } + + // ENHANCEMENT: Try to ensure we have a valid sessionId + const effectiveSessionId = sessionId || + (activeConversation ? activeConversation.session_id : null) || + localStorage.getItem('chatSessionId'); + + if (!effectiveSessionId) { + console.error('No valid sessionId found from any source'); + setError('Session ID is missing. Please refresh the page.'); + setIsProcessing(false); + return; + } + + try { + // Log the sessionId to help debug + console.log('Sending chat request with sessionId:', effectiveSessionId); + + const fetchOptions = getFetchDefaults(); + const response = await fetchWithTimeout(`${BACKEND_URL}/chat`, { + ...fetchOptions, + method: 'POST', + body: JSON.stringify({ + message: currentMessage, + sessionId: effectiveSessionId + }) + }); + + if (!response.ok) { + const errorData = await response.json(); + // If the server says chat is not initialized, reset UI to show upload screen + if (errorData.error === 'Chat system not initialized') { + setIsInitialized(false); + throw new Error('Chat system not initialized. Please upload files first.'); + } + throw new Error(errorData.error || 'Failed to get response'); + } + + const responseData = await response.json(); + + let assistantMessage; + if (responseData.status === 'success' && responseData.data) { + assistantMessage = { + role: 'assistant', + content: responseData.data.response || '', + sources: responseData.data.sources || [], + reasoning: responseData.data.reasoning || [], + images: responseData.data.images || [] + }; + } else if (responseData.result) { + assistantMessage = { + role: 'assistant', + content: responseData.result.response || '', + sources: responseData.result.sources || [], + reasoning: responseData.result.reasoning || [], + images: responseData.result.images || [] + }; + } else { + assistantMessage = { + role: 'assistant', + content: responseData.response || '', + sources: responseData.sources || [], + reasoning: responseData.reasoning || [], + images: responseData.images || [] + }; + } + + console.log('Processed assistant message:', assistantMessage); + setMessages(prev => [...prev, assistantMessage]); + setError(null); + + // Update the conversations list to get the AI-generated title + // Add a longer delay to allow backend to generate title using OpenAI + setTimeout(() => { + // Need a longer delay for OpenAI to generate the title + updateConversationsList(); + + // Check again after a bit longer in case the first attempt was too soon + setTimeout(() => { + updateConversationsList(); + }, 3000); // Check again after 3 more seconds + }, 2000); // Initial 2 second delay + + } catch (e) { + console.error('Error in chat:', e); + setError('Failed to process response. Please try again.'); + } finally { + setIsProcessing(false); + } + }; + +// Helper function to slice file into chunks and upload them +const uploadFileInChunks = async (file, fileType, sessionId, controller) => { + const chunkSize = 1024 * 1024; // 1MB chunks + const totalChunks = Math.ceil(file.size / chunkSize); + + // Step 1: Initialize the chunked upload + const fetchOptions = getFetchDefaults(); + const initResponse = await fetch(`${BACKEND_URL}/init-chunked-upload`, { + method: 'POST', + headers: { + ...fetchOptions.headers, + 'Content-Type': 'application/json' + }, + signal: controller.signal, + body: JSON.stringify({ + fileName: file.name, + fileType: fileType, // 'brief' or 'supporting' + fileSize: file.size, + mimeType: file.type, + totalChunks, + sessionId + }) + }); + + if (!initResponse.ok) { + const error = await initResponse.json(); + throw new Error(error.message || 'Failed to initialize chunked upload'); + } + + const { uploadId } = await initResponse.json(); + + // Step 2: Upload each chunk + for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) { + const start = chunkIndex * chunkSize; + const end = Math.min(start + chunkSize, file.size); + const chunk = file.slice(start, end); + + const chunkFormData = new FormData(); + chunkFormData.append('chunk', chunk); + chunkFormData.append('uploadId', uploadId); + chunkFormData.append('chunkIndex', chunkIndex); + + const fetchOptions = getFetchDefaults(); + const chunkResponse = await fetch(`${BACKEND_URL}/upload-chunk`, { + method: 'POST', + body: chunkFormData, + signal: controller.signal, + headers: { + ...fetchOptions.headers, + 'X-Requested-With': 'XMLHttpRequest' + } + }); + + if (!chunkResponse.ok) { + throw new Error(`Failed to upload chunk ${chunkIndex}`); + } + } + + // Step 3: Finalize the upload + const finalizeOptions = getFetchDefaults(); + const finalizeResponse = await fetch(`${BACKEND_URL}/finalize-upload`, { + method: 'POST', + headers: { + ...finalizeOptions.headers, + 'Content-Type': 'application/json' + }, + signal: controller.signal, + body: JSON.stringify({ uploadId }) + }); + + if (!finalizeResponse.ok) { + throw new Error('Failed to finalize upload'); + } + + return { fileName: file.name, uploadId }; +}; + +// Main function to initialize chat +const initializeChat = async () => { + if (briefFiles.length === 0) { + setError('Please upload at least one brief file.'); + return; + } + + logToConsole('info', 'Starting chat initialization', { + briefFilesCount: briefFiles.length, + supportingFilesCount: supportingFiles.length + }); + + setIsProcessing(true); + + // Create an AbortController for proper timeout handling + const controller = new AbortController(); + const timeoutId = setTimeout(() => { + controller.abort(); + logToConsole('error', 'Request timed out after 10 minutes'); + }, 600000); // 10 minute timeout + + try { + // Check for large files that might need chunked upload + const largeFiles = [...briefFiles, ...supportingFiles].filter(file => file.size > 1024 * 1024); + if (largeFiles.length > 0) { + logToConsole('info', 'Large files detected, using chunked upload approach', { + largeFiles: largeFiles.map(f => ({ name: f.name, size: f.size })) + }); + } + + // Upload all brief files + const briefUploads = []; + for (const file of briefFiles) { + if (file.size > 1024 * 1024) { // If file is larger than 1MB, use chunked upload + const result = await uploadFileInChunks(file, 'brief', sessionId, controller); + briefUploads.push(result); + } else { + // For small files, use regular upload + const smallFormData = new FormData(); + smallFormData.append('file', file); + smallFormData.append('fileType', 'brief'); + smallFormData.append('sessionId', sessionId); + + const fetchOptions = getFetchDefaults(); + const response = await fetch(`${BACKEND_URL}/upload-small-file`, { + method: 'POST', + body: smallFormData, + signal: controller.signal, + headers: { + ...fetchOptions.headers, + 'X-Requested-With': 'XMLHttpRequest' + } + }); + + if (!response.ok) { + throw new Error(`Failed to upload small file: ${file.name}`); + } + + const result = await response.json(); + briefUploads.push(result); + } + } + + // Upload all supporting files (similar approach) + const supportingUploads = []; + for (const file of supportingFiles) { + if (file.size > 1024 * 1024) { + const result = await uploadFileInChunks(file, 'supporting', sessionId, controller); + supportingUploads.push(result); + } else { + // Small file upload code similar to brief files + const smallFormData = new FormData(); + smallFormData.append('file', file); + smallFormData.append('fileType', 'supporting'); + smallFormData.append('sessionId', sessionId); + + const fetchOptions = getFetchDefaults(); + const response = await fetch(`${BACKEND_URL}/upload-small-file`, { + method: 'POST', + body: smallFormData, + signal: controller.signal, + headers: { + ...fetchOptions.headers, + 'X-Requested-With': 'XMLHttpRequest' + } + }); + + if (!response.ok) { + throw new Error(`Failed to upload small file: ${file.name}`); + } + + const result = await response.json(); + supportingUploads.push(result); + } + } + + // Now that all files are uploaded, initialize the chat + const finalizeData = { + sessionId, + briefFiles: briefUploads, + supportingFiles: supportingUploads + }; + + const finalizeOptions = getFetchDefaults(); + const finalizeResponse = await fetch(`${BACKEND_URL}/initialize-from-uploads`, { + method: 'POST', + headers: { + ...finalizeOptions.headers, + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + }, + body: JSON.stringify(finalizeData), + signal: controller.signal + }); + + // Clear the timeout + clearTimeout(timeoutId); + + if (!finalizeResponse.ok) { + const errorData = await finalizeResponse.json(); + throw new Error(errorData.error || `Server responded with ${finalizeResponse.status}`); + } + + const data = await finalizeResponse.json(); + + logToConsole('info', 'Chat initialization successful'); + setIsInitialized(true); + setMessages([{ + role: 'assistant', + content: 'Chat initialized! How can I help you?' + }]); + setError(null); + + } catch (err) { + // Clear the timeout if there's an error + clearTimeout(timeoutId); + + // Enhanced error logging + const errorDetails = { + message: err.message, + name: err.name, + stack: err.stack, + isAbortError: err.name === 'AbortError' + }; + + logToConsole('error', 'Initialization error', errorDetails); + console.error('Initialization error:', err); + + // Set appropriate error message for user + if (err.name === 'AbortError') { + setError('Request timed out. The file(s) may be too large.'); + } else { + setError(err.message || 'Failed to initialize chat'); + } + + setIsInitialized(false); + } finally { + setIsProcessing(false); + } +}; + + return ( + +
+ {/* Conversation Sidebar - Fixed to left side */} +
+
+

Conversations

+ +
+ +
+ {loadingConversations ? ( +
+ +
+ ) : conversations.length > 0 ? ( + conversations.map(convo => ( +
+
+
handleLoadConversation(convo)} + > +
{convo.title || "New conversation"}
+
+ {new Date(convo.last_updated).toLocaleDateString()} +
+
+ +
+
+ )) + ) : ( +
+ No conversations yet +
+ )} +
+ +
+
+
+ {getCurrentUser()} + +
+ + {!autoWidth && ( + + )} +
+
+
+ + {/* Resize handle */} +
+ + {/* Header with HP branding - completely rebuilt without any borders */} +
+ {/* Theme toggle in top right corner */} +
+ +
+ {/* HP Logo */} +
+ HP +
+

Marketing Bot

+
+ + {/* Main Chat Area - Centered with left margin to account for sidebar */} +
+
+
+ {isCheckingStatus ? ( +
+ +

Connecting to HP Marketing Materials Chatbot...

+
+ ) : !isInitialized ? ( +
+

HP Marketing Materials Chatbot

+

+ The system is currently initializing. Please try again later. +

+ +
+ ) : ( + <> +
+
+

+ {activeConversation?.title || "HP Marketing Materials Chatbot"} +

+
+ +
+
+

+ Ask questions about HP's marketing materials, branding guidelines, campaign strategies, and creative processes. +

+
+ +
+
+ {messages.map((message, index) => ( + + ))} +
+
+
+ +
+ setInputMessage(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); // Prevent any default behavior + if (!isProcessing && inputMessage.trim()) { + handleSubmit(); + } + } + }} + placeholder="Type your message..." + className="flex-1 p-2 border dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded focus:outline-none focus:ring-2 focus:ring-blue-500" + disabled={isProcessing} + /> + +
+ + )} + + {error && !isCheckingStatus && ( + + {error} + + )} +
+
+
+
+ + ); +} \ No newline at end of file diff --git a/chat-interface/src/App_11-19.jsx b/chat-interface/src/App_11-19.jsx new file mode 100644 index 0000000..6b8d72e --- /dev/null +++ b/chat-interface/src/App_11-19.jsx @@ -0,0 +1,691 @@ +import { useState, useRef, useEffect } from 'react'; +import { Send, Upload, Loader2, X, FileText, Info, FileDown } from 'lucide-react'; +import { Alert, AlertDescription } from "./components/ui/alert"; +import * as Tooltip from '@radix-ui/react-tooltip'; + +const BACKEND_URL = 'http://localhost:6173'; + +const fetchDefaults = { + mode: 'cors', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'Accept': '*/*', + 'Access-Control-Allow-Origin': '*', + } +}; + +export default function ChatInterface() { + const [messages, setMessages] = useState([]); + const [inputMessage, setInputMessage] = useState(''); + const [isProcessing, setIsProcessing] = useState(false); + const [briefFiles, setBriefFiles] = useState([]); + const [supportingFiles, setSupportingFiles] = useState([]); + const [isInitialized, setIsInitialized] = useState(false); + const [error, setError] = useState(null); + + const messagesEndRef = useRef(null); + const fileInputBriefRef = useRef(null); + const fileInputSupportingRef = useRef(null); + + useEffect(() => { + // Enable CORS for all fetch requests + fetch.defaults = fetchDefaults; + }, []); + + const logToConsole = (type, message, data = null) => { + const timestamp = new Date().toISOString(); + const logMessage = { + timestamp, + type, + message, + data + }; + console.log(JSON.stringify(logMessage, null, 2)); + }; + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + const handleFilesChange = (e, type) => { + const files = Array.from(e.target.files); + logToConsole('info', `Handling ${type} files change`, { + fileCount: files.length, + fileDetails: files.map(f => ({ + name: f.name, + type: f.type, + size: f.size + })) + }); + + if (type === 'brief') { + setBriefFiles(prev => [...prev, ...files]); + } else { + setSupportingFiles(prev => [...prev, ...files]); + } + }; + + const removeFile = (fileName, type) => { + logToConsole('info', `Removing file`, { + fileName, + type + }); + + if (type === 'brief') { + setBriefFiles(prev => prev.filter(file => file.name !== fileName)); + } else { + setSupportingFiles(prev => prev.filter(file => file.name !== fileName)); + } + }; + + const MessageBubble = ({ message }) => { + // Helper function to safely extract source content + const formatSources = (sources) => { + if (!sources || !Array.isArray(sources)) return []; + + return sources.map((source) => { + // Handle the nested structure from the backend + if (source && typeof source === 'object') { + // Check for the different possible source structures + if (source.content) { + return { + text: typeof source.content === 'string' + ? source.content + : JSON.stringify(source.content), + tool: source.tool_name || '' + }; + } + // Fallback for other source structures + return { + text: JSON.stringify(source), + tool: '' + }; + } + // Handle string sources + return { + text: String(source), + tool: '' + }; + }).filter(source => source.text); // Remove empty sources + }; + + // Helper function to format reasoning steps + const formatReasoning = (reasoning) => { + if (!reasoning || !Array.isArray(reasoning)) return []; + + return reasoning.map((step, index) => { + // Handle the new reasoning step structure + const stepType = step.type || 'thought'; + let content = ''; + + if (step.action) { + content = `${step.action}${step.action_input ? `: ${step.action_input}` : ''}`; + } else if (step.observation) { + content = step.observation; + } else if (step.response) { + content = step.response; + } else if (step.thought) { + content = step.thought; + } + + return { + id: index, + type: stepType, + content: content, + thought: step.thought + }; + }).filter(step => step.content); + }; + + const sources = formatSources(message.sources); + const reasoningSteps = formatReasoning(message.reasoning); + + const handleDownloadBrief = async () => { + // First, send the message to generate the brief + const briefRequestMessage = "write a comprehensive, detailed, and organized marketing brief based on the entire history of this conversation"; + setMessages(prev => [...prev, { role: 'user', content: briefRequestMessage }]); + setIsProcessing(true); + + try { + // First request to generate the brief content + const response = await fetch(`${BACKEND_URL}/chat`, { + ...fetchDefaults, + method: 'POST', + body: JSON.stringify({ message: briefRequestMessage }) + }); + + if (!response.ok) { + throw new Error('Failed to generate brief'); + } + + const responseData = await response.json(); + + // Add the assistant's response to the chat + setMessages(prev => [...prev, { + role: 'assistant', + content: responseData.data.response, + sources: responseData.data.sources, + reasoning: responseData.data.reasoning + }]); + + // Then make a second request to download the document + const downloadResponse = await fetch(`${BACKEND_URL}/download-brief`, { + ...fetchDefaults, + method: 'POST', + body: JSON.stringify({ brief_content: responseData.data.response }) + }); + + if (!downloadResponse.ok) { + throw new Error('Failed to download brief'); + } + + // Get the blob from the response + const blob = await downloadResponse.blob(); + + // Create a download link and trigger it + const downloadUrl = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = downloadUrl; + link.download = 'marketing_brief.docx'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(downloadUrl); + + } catch (e) { + console.error('Error in download brief:', e); + setError('Failed to download brief. Please try again.'); + } finally { + setIsProcessing(false); + } + }; + + + return ( + +
+ {!isInitialized ? ( + // ... (initialization UI remains the same) + ) : ( + <> +
+ {messages.map((message, index) => ( + + ))} +
+
+ +
+ setInputMessage(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handleSubmit()} + placeholder="Type your message..." + className="flex-1 p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" + disabled={isProcessing} + /> + + +
+ + )} + + {error && ( + + {error} + + )} +
+ + ); + } + + + // return ( + //
+ //
+ //
{message.content}
+ + //
+ // {sources.length > 0 && ( + // + // + // + // + // + // + // + //
+ //

Sources Used:

+ //
    + // {sources.map((source, idx) => ( + //
  • + // {source.tool && ( + //
    + // Tool: {source.tool} + //
    + // )} + //
    {source.text}
    + //
  • + // ))} + //
+ //
+ // + //
+ //
+ //
+ //
+ // )} + + // {reasoningSteps.length > 0 && ( + // + // + // + // + // + // + // + //
+ //

Reasoning Steps:

+ //
    + // {reasoningSteps.map((step) => ( + //
  • + //
    + // {step.type}: + //
    + //
    {step.content}
    + // {step.thought && step.thought !== step.content && ( + //
    + // Thought: {step.thought} + //
    + // )} + //
  • + // ))} + //
+ //
+ // + //
+ //
+ //
+ //
+ // )} + //
+ //
+ //
+ // ); + // }; + + + + const handleSubmit = async () => { + if (!inputMessage.trim()) return; + + const currentMessage = inputMessage; + setMessages(prev => [...prev, { role: 'user', content: currentMessage }]); + setInputMessage(''); + setIsProcessing(true); + + try { + const response = await fetch(`${BACKEND_URL}/chat`, { + ...fetchDefaults, + method: 'POST', + body: JSON.stringify({ message: currentMessage }) + }); + + if (!response.ok) { + throw new Error('Failed to get response'); + } + + const responseData = await response.json(); + + // Log the full response data for debugging + console.log('Full response data:', responseData); + + // Extract the relevant data based on the response structure + let assistantMessage; + if (responseData.status === 'success' && responseData.data) { + // New response structure + assistantMessage = { + role: 'assistant', + content: responseData.data.response || '', + sources: responseData.data.sources || [], + reasoning: responseData.data.reasoning || [] + }; + } else if (responseData.result) { + // Alternative response structure + assistantMessage = { + role: 'assistant', + content: responseData.result.response || '', + sources: responseData.result.sources || [], + reasoning: responseData.result.reasoning || [] + }; + } else { + // Fallback for direct response structure + assistantMessage = { + role: 'assistant', + content: responseData.response || '', + sources: responseData.sources || [], + reasoning: responseData.reasoning || [] + }; + } + + // Log the processed message for debugging + console.log('Processed assistant message:', assistantMessage); + + setMessages(prev => [...prev, assistantMessage]); + setError(null); + + } catch (e) { + console.error('Error in chat:', e); + setError('Failed to process response. Please try again.'); + } finally { + setIsProcessing(false); + } + }; + + + + const initializeChat = async () => { + if (briefFiles.length === 0) { + setError('Please upload at least one brief file.'); + return; + } + + logToConsole('info', 'Starting chat initialization', { + briefFilesCount: briefFiles.length, + supportingFilesCount: supportingFiles.length, + briefFiles: briefFiles.map(f => ({ + name: f.name, + type: f.type, + size: f.size + })), + supportingFiles: supportingFiles.map(f => ({ + name: f.name, + type: f.type, + size: f.size + })) + }); + + setIsProcessing(true); + const formData = new FormData(); + + briefFiles.forEach(file => { + formData.append('brief', file); + logToConsole('debug', 'Appending brief file to FormData', { + fileName: file.name, + fileType: file.type, + fileSize: file.size + }); + }); + + supportingFiles.forEach(file => { + formData.append('supporting', file); + logToConsole('debug', 'Appending supporting file to FormData', { + fileName: file.name, + fileType: file.type, + fileSize: file.size + }); + }); + + try { + logToConsole('info', 'Sending initialization request to backend'); + + const response = await fetch(`${BACKEND_URL}/initialize`, { + method: 'POST', + mode: 'cors', + credentials: 'include', + body: formData, + // headers: { + // 'Accept': '*/*', + // 'Access-Control-Allow-Origin': '*', + // } + }); + + logToConsole('debug', 'Received response from backend', { + status: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()) + }); + + const textResponse = await response.text(); + logToConsole('debug', 'Response text received', { textResponse }); + + let data; + try { + data = JSON.parse(textResponse); + logToConsole('debug', 'Successfully parsed response JSON', { data }); + } catch (e) { + logToConsole('error', 'Failed to parse response JSON', { + error: e.message, + textResponse + }); + throw new Error('Invalid response format from server'); + } + + if (!response.ok) { + logToConsole('error', 'Backend returned error response', { + status: response.status, + error: data.error + }); + throw new Error(data.error || 'Failed to initialize chat'); + } + + setIsInitialized(true); + setMessages([{ + role: 'assistant', + content: 'Chat initialized! How can I help you?' + }]); + + setError(null); + logToConsole('info', 'Chat initialization completed successfully'); + + } catch (err) { + logToConsole('error', 'Chat initialization failed', { + error: err.message, + stack: err.stack + }); + + let errorMessage = 'Failed to initialize chat. '; + if (err.message) { + errorMessage += err.message; + } else { + errorMessage += 'Please try again.'; + } + + setError(errorMessage); + setIsInitialized(false); + } finally { + setIsProcessing(false); + } + }; + + return ( + +
+ {!isInitialized ? ( +
+

Upload Files to Start Chat

+ +
+

Brief Files (Required)

+
+ handleFilesChange(e, 'brief')} + multiple + accept=".pdf,.doc,.docx,.txt,.xls,.xlsx,.ppt,.pptx,.eml" + className="hidden" + /> + +
+
+ {briefFiles.map((file) => ( +
+ + {file.name} + +
+ ))} +
+
+ +
+

Supporting Files (Optional)

+
+ handleFilesChange(e, 'supporting')} + multiple + accept=".pdf,.doc,.docx,.txt,.xls,.xlsx,.ppt,.pptx,.eml" + className="hidden" + /> + +
+
+ {supportingFiles.map((file) => ( +
+ + {file.name} + +
+ ))} +
+
+ + +
+ ) : ( + <> +
+ {messages.map((message, index) => ( + + ))} +
+
+ +
+ setInputMessage(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handleSubmit()} + placeholder="Type your message..." + className="flex-1 p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" + disabled={isProcessing} + /> + +
+ + )} + + {error && ( + + {error} + + )} +
+ + ); +} diff --git a/chat-interface/src/assets/react.svg b/chat-interface/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/chat-interface/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/chat-interface/src/auth.js b/chat-interface/src/auth.js new file mode 100644 index 0000000..5caa704 --- /dev/null +++ b/chat-interface/src/auth.js @@ -0,0 +1,119 @@ +// MSAL Authentication Configuration +const msalConfig = { + auth: { + clientId: "9079054c-9620-4757-a256-23413042f1ef", + authority: "https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385", + redirectUri: "https://ai-sandbox.oliver.solutions/format" + }, + cache: { + cacheLocation: "sessionStorage", + storeAuthStateInCookie: true, + } +}; + +const loginRequest = { + scopes: ["user.read"] +}; + +// Initialize MSAL object +let myMSALObj = null; +let thisUser = null; + +// Initialize MSAL when loaded +export function initializeMSAL() { + if (typeof msal !== 'undefined') { + myMSALObj = new msal.PublicClientApplication(msalConfig); + + // Check if there's a cached user + const accounts = myMSALObj.getAllAccounts(); + if (accounts.length > 0) { + thisUser = accounts[0].username; + // User is already logged in, show content + showProtectedContent(); + } else { + // Need to initialize login elements if not logged in + const signinButton = document.getElementById('signin-button'); + if (signinButton) { + signinButton.addEventListener('click', signIn); + } + } + } else { + console.error("MSAL library not loaded"); + } +} + +// Sign in with popup +export function signIn() { + if (!myMSALObj) { + initializeMSAL(); + } + + myMSALObj.loginPopup(loginRequest) + .then(loginResponse => { + console.log("User logged in:", loginResponse.account.username); + thisUser = loginResponse.account.username; + sessionStorage.setItem('msalUsername', loginResponse.account.username); + showProtectedContent(); + }).catch(error => { + console.error("Error during login:", error); + }); +} + +// Show the protected content (the chat interface) +function showProtectedContent() { + const loginContainer = document.getElementById('login-container'); + const rootContainer = document.getElementById('root'); + + if (loginContainer) { + loginContainer.style.display = 'none'; + } + + if (rootContainer) { + rootContainer.style.display = 'block'; + } + + // Dispatch an event that authentication is complete + window.dispatchEvent(new CustomEvent('authenticationComplete', { + detail: { + username: thisUser + } + })); +} + +// Get current user +export function getCurrentUser() { + if (!thisUser) { + thisUser = sessionStorage.getItem('msalUsername'); + } + return thisUser; +} + +// Check if user is authenticated +export function isAuthenticated() { + if (!myMSALObj) { + initializeMSAL(); + } + + return myMSALObj?.getAllAccounts().length > 0 || !!getCurrentUser(); +} + +// Sign out +export function signOut() { + if (!myMSALObj) { + initializeMSAL(); + } + + const logoutRequest = { + account: myMSALObj.getAccountByUsername(thisUser) + }; + + myMSALObj.logout(logoutRequest).then(() => { + thisUser = null; + sessionStorage.removeItem('msalUsername'); + // Redirect to login page + window.location.reload(); + }); +} + +// Register initialization on window load +window.addEventListener('DOMContentLoaded', initializeMSAL); diff --git a/chat-interface/src/components/ChatInterface.jsx b/chat-interface/src/components/ChatInterface.jsx new file mode 100644 index 0000000..583f685 --- /dev/null +++ b/chat-interface/src/components/ChatInterface.jsx @@ -0,0 +1,140 @@ +// Use environment variables for backend URL +// Define backend URL dynamically based on environment +const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'https://ai-sandbox.oliver.solutions/hp_back_v2'; +console.log('ChatInterface using backend URL:', BACKEND_URL); + +const initializeChat = async ({ sessionId, briefFiles, supportingFiles, setError, setIsProcessing, setIsInitialized, setMessages }) => { + if (briefFiles.length === 0) { + setError('Please upload at least one brief file.'); + return; + } + + setIsProcessing(true); + const formData = new FormData(); + + // Add session ID to the form data + formData.append('sessionId', sessionId); + + briefFiles.forEach(file => { + formData.append('brief', file); + }); + + supportingFiles.forEach(file => { + formData.append('supporting', file); + }); + + try { + console.log('Sending initialize request...'); + const response = await fetch(`${BACKEND_URL}/initialize`, { + method: 'POST', + body: formData, // No Content-Type header for FormData + credentials: 'include', // Include credentials (cookies, auth headers) + }); + + console.log('Response status:', response.status); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Failed to initialize chat'); + } + + const data = await response.json(); + console.log('Response data:', data); + + setIsInitialized(true); + setMessages([{ + role: 'assistant', + content: 'Chat initialized! How can I help you?' + }]); + setError(null); // Clear any previous errors + } catch (err) { + console.error('Error:', err); + setError(err.message || 'Failed to initialize chat. Please try again.'); + } finally { + setIsProcessing(false); + } +}; + +const handleSubmit = async ({ inputMessage, sessionId, setMessages, setInputMessage, setIsProcessing, setError, logToConsole }) => { + if (!inputMessage.trim()) return; + + const currentMessage = inputMessage; + setMessages(prev => [...prev, { role: 'user', content: currentMessage }]); + setInputMessage(''); + setIsProcessing(true); + + try { + const response = await fetch(`${BACKEND_URL}/chat`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: currentMessage, + sessionId: sessionId + }), + credentials: 'include', // Include credentials for cross-origin + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Failed to get response'); + } + + const dataString = await response.text(); // Get the response as text + const data = JSON.parse(dataString); // Parse the JSON string + + // Extract response data, checking for different response formats + let responseText = ''; + let sources = []; + let reasoning = []; + let images = []; + + if (data.data) { + // New response format + responseText = data.data.response || ''; + sources = data.data.sources || []; + reasoning = data.data.reasoning || []; + images = data.data.images || []; + } else { + // Direct response format + responseText = data.response || ''; + sources = data.sources || []; + reasoning = data.reasoning || []; + images = data.images || []; + } + + // Log the images + if (images && images.length > 0) { + logToConsole('info', 'Images received in response', { imageCount: images.length, images }); + } + + setMessages(prev => { + const newMessages = [...prev]; + newMessages.push({ + role: 'assistant', + content: responseText, + sources: sources, + reasoning: reasoning, + images: images + }); + return newMessages; + }); + setError(null); // Clear any previous error + + } catch (e) { // Include the original JSON string in the error data + console.error('Error processing chat response:', e); + logToConsole('error', 'Chat error', { + error: e.message, + stack: e.stack, + response: e.response + }); + + setError('Failed to process response. Please check the logs for details.'); // Display a more user friendly error in UI + } finally { + setIsProcessing(false); + } +}; + +// Export the functions +export { initializeChat, handleSubmit }; \ No newline at end of file diff --git a/chat-interface/src/components/ConversationManager.jsx b/chat-interface/src/components/ConversationManager.jsx new file mode 100644 index 0000000..023ca51 --- /dev/null +++ b/chat-interface/src/components/ConversationManager.jsx @@ -0,0 +1,206 @@ +// Get the backend URL from environment or use correct path as fallback +// Define backend URL dynamically based on environment +const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'https://ai-sandbox.oliver.solutions/hp_back_v2'; +console.log('ConversationManager using backend URL:', BACKEND_URL); + +// Function to load user conversations +export const loadUserConversations = async (username, setConversations, setActiveConversation, loadConversation, setError) => { + try { + const response = await fetch(`${BACKEND_URL}/conversations`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-MS-USERNAME': username || '' // Add authenticated username to request + }, + credentials: 'include' + }); + + if (!response.ok) { + throw new Error('Failed to load conversations'); + } + + const data = await response.json(); + + if (data.status === 'success') { + setConversations(data.conversations || []); + + // If we have conversations and none are active, select the most recent one + if (data.conversations && data.conversations.length > 0 && setActiveConversation) { + // Sort by last_updated, newest first + const sortedConversations = [...data.conversations].sort((a, b) => + new Date(b.last_updated) - new Date(a.last_updated) + ); + + // Set the most recent conversation as active + setActiveConversation(sortedConversations[0]); + + // Load this conversation if load function provided + if (loadConversation) { + await loadConversation(sortedConversations[0]); + } + } + + return data.conversations || []; + } else { + throw new Error(data.error || 'Failed to load conversations'); + } + } catch (error) { + console.error('Error loading conversations:', error); + if (setError) { + setError('Failed to load conversations. Please try again later.'); + } + return []; + } +}; + +// Function to load a specific conversation's messages +export const loadConversationMessages = async (conversation, username, setMessages, setSessionId, setError) => { + try { + if (!conversation || !conversation.id) { + throw new Error('Invalid conversation'); + } + + const response = await fetch(`${BACKEND_URL}/conversations/${conversation.id}/messages`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-MS-USERNAME': username || '' // Add authenticated username to request + }, + credentials: 'include' + }); + + if (!response.ok) { + throw new Error('Failed to load conversation messages'); + } + + const data = await response.json(); + + if (data.status === 'success') { + // Format the messages for the UI + const formattedMessages = data.messages.map(msg => ({ + role: msg.role, + content: msg.content, + sources: msg.sources || [], + reasoning: msg.reasoning || [], + images: msg.images || [] + })); + + setMessages(formattedMessages); + + // Set the session ID if provided + if (setSessionId && conversation.session_id) { + setSessionId(conversation.session_id); + localStorage.setItem('chatSessionId', conversation.session_id); + } + + return formattedMessages; + } else { + throw new Error(data.error || 'Failed to load conversation messages'); + } + } catch (error) { + console.error('Error loading conversation messages:', error); + if (setError) { + setError('Failed to load conversation messages. Please try again later.'); + } + return []; + } +}; + +// Function to create a new conversation +export const createNewConversation = async (username, setConversations, setActiveConversation, setSessionId, setMessages, setError) => { + try { + const response = await fetch(`${BACKEND_URL}/conversations/new`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-MS-USERNAME': username || '' // Add authenticated username to request + }, + credentials: 'include' + }); + + if (!response.ok) { + throw new Error('Failed to create new conversation'); + } + + const data = await response.json(); + + if (data.status === 'success') { + // Create a new conversation object + const newConversation = { + id: data.conversation_id, + title: "New conversation", + created_at: new Date().toISOString(), + last_updated: new Date().toISOString(), + session_id: data.session_id + }; + + // Update the conversations list + setConversations(prev => [newConversation, ...prev]); + + // Set as active conversation + setActiveConversation(newConversation); + + // Set the session ID + if (setSessionId) { + setSessionId(data.session_id); + localStorage.setItem('chatSessionId', data.session_id); + } + + // Reset messages with welcome message + if (setMessages) { + setMessages([{ + role: 'assistant', + content: 'How can I help you today?' + }]); + } + + return newConversation; + } else { + throw new Error(data.error || 'Failed to create new conversation'); + } + } catch (error) { + console.error('Error creating new conversation:', error); + if (setError) { + setError('Failed to create new conversation. Please try again later.'); + } + return null; + } +}; + +// Function to delete a conversation +export const deleteConversation = async (conversationId, username, setConversations, setError) => { + try { + if (!conversationId) { + throw new Error('Invalid conversation ID'); + } + + const response = await fetch(`${BACKEND_URL}/conversations/${conversationId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-MS-USERNAME': username || '' // Add authenticated username to request + }, + credentials: 'include' + }); + + if (!response.ok) { + throw new Error('Failed to delete conversation'); + } + + const data = await response.json(); + + if (data.status === 'success') { + // Remove the conversation from the list + setConversations(prev => prev.filter(convo => convo.id !== conversationId)); + return true; + } else { + throw new Error(data.error || 'Failed to delete conversation'); + } + } catch (error) { + console.error('Error deleting conversation:', error); + if (setError) { + setError('Failed to delete conversation. Please try again later.'); + } + return false; + } +}; \ No newline at end of file diff --git a/chat-interface/src/components/ThemeToggle.jsx b/chat-interface/src/components/ThemeToggle.jsx new file mode 100644 index 0000000..e2fe897 --- /dev/null +++ b/chat-interface/src/components/ThemeToggle.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { useTheme } from '../main'; +import { Moon, Sun } from 'lucide-react'; + +export default function ThemeToggle() { + const { darkMode, setDarkMode } = useTheme(); + + return ( + + ); +} diff --git a/chat-interface/src/components/ui/alert.jsx b/chat-interface/src/components/ui/alert.jsx new file mode 100644 index 0000000..8437323 --- /dev/null +++ b/chat-interface/src/components/ui/alert.jsx @@ -0,0 +1,37 @@ +import * as React from "react" +import { cn } from "../../lib/utils" + +const Alert = React.forwardRef(({ className, variant = "default", children, ...props }, ref) => { + const variantClasses = { + default: "bg-gray-100 text-gray-900", + destructive: "bg-red-100 text-red-900", + } + + return ( +
+ {children} +
+ ) +}) +Alert.displayName = "Alert" + +const AlertDescription = React.forwardRef(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertDescription } + diff --git a/chat-interface/src/index.css b/chat-interface/src/index.css new file mode 100644 index 0000000..5e4ccd3 --- /dev/null +++ b/chat-interface/src/index.css @@ -0,0 +1,131 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --border: 214.3 31.8% 91.4%; + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --border: 217.2 32.6% 17.5%; + } + + body { + @apply bg-black text-foreground; + border: none !important; + border-top: none !important; + border-bottom: none !important; + } +} + +/* Netflix theme colors */ +.netflix-red { + color: #E50914; +} + +.netflix-bg { + background-color: #141414; +} + +/* Prevent any border from appearing */ +.no-borders, .no-borders::before, .no-borders::after { + border: none !important; + border-top: none !important; + border-bottom: none !important; + border-left: none !important; + border-right: none !important; + outline: none !important; +} + +/* Markdown content styling */ +.markdown-content { + @apply text-base leading-relaxed; +} + +.markdown-content h1 { + @apply text-2xl font-bold mt-4 mb-2; +} + +.markdown-content h2 { + @apply text-xl font-bold mt-3 mb-2; +} + +.markdown-content h3 { + @apply text-lg font-bold mt-3 mb-1; +} + +.markdown-content p { + @apply my-2; +} + +.markdown-content ul { + @apply list-disc pl-5 my-2; +} + +.markdown-content ol { + @apply list-decimal pl-5 my-2; +} + +.markdown-content li { + @apply my-1; +} + +.markdown-content a { + @apply text-blue-600 hover:underline; +} + +.markdown-content code { + @apply font-mono bg-gray-200 px-1 py-0.5 rounded text-sm; +} + +.markdown-content pre { + @apply bg-gray-800 text-white p-3 rounded my-3 overflow-auto; +} + +.markdown-content pre code { + @apply bg-transparent text-white text-sm; +} + +.markdown-content blockquote { + @apply border-l-4 border-gray-300 pl-4 my-3 italic text-gray-700; +} + +.markdown-content table { + @apply border-collapse border border-gray-300 my-3 w-full; +} + +.markdown-content th { + @apply border border-gray-300 bg-gray-100 p-2 font-semibold; +} + +.markdown-content td { + @apply border border-gray-300 p-2; +} + +/* Different background for assistant markdown vs user text */ +.bg-blue-500 .markdown-content { + @apply text-white; +} + +.bg-blue-500 .markdown-content code { + @apply bg-blue-600 text-white; +} + +.bg-blue-500 .markdown-content a { + @apply text-white underline; +} + +.bg-blue-500 .markdown-content blockquote { + @apply border-white/50 text-white/90; +} + diff --git a/chat-interface/src/lib/utils.js b/chat-interface/src/lib/utils.js new file mode 100644 index 0000000..d510289 --- /dev/null +++ b/chat-interface/src/lib/utils.js @@ -0,0 +1,40 @@ +// Original utils.js file content +import { clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs) { + return twMerge(clsx(inputs)) +} + +/** + * Enhanced fetch function with a 300-second timeout + * @param {string} url - URL to fetch + * @param {Object} options - Fetch options + * @returns {Promise} - Fetch promise with timeout + */ +export async function fetchWithTimeout(url, options = {}) { + const timeout = 300000; // 300 seconds in milliseconds + + const controller = new AbortController(); + const { signal } = controller; + + const timeoutId = setTimeout(() => { + controller.abort(); + }, timeout); + + try { + const response = await fetch(url, { + ...options, + signal, + }); + + clearTimeout(timeoutId); + return response; + } catch (error) { + clearTimeout(timeoutId); + if (error.name === 'AbortError') { + throw new Error(`Request timed out after ${timeout / 1000} seconds`); + } + throw error; + } +} \ No newline at end of file diff --git a/chat-interface/src/main.jsx b/chat-interface/src/main.jsx new file mode 100644 index 0000000..3e85a5a --- /dev/null +++ b/chat-interface/src/main.jsx @@ -0,0 +1,75 @@ +import React, { useEffect, useState, createContext, useContext } from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './index.css' +import { initializeMSAL, isAuthenticated } from './auth' + +// Initialize MSAL +initializeMSAL(); + +// Create Theme Context +export const ThemeContext = createContext(); + +export const ThemeProvider = ({ children }) => { + const [darkMode, setDarkMode] = useState(() => { + // Check local storage for user preference + const savedMode = localStorage.getItem('darkMode'); + return savedMode === 'true'; + }); + + // Apply dark mode class to html element + useEffect(() => { + if (darkMode) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + // Save preference to localStorage + localStorage.setItem('darkMode', darkMode); + }, [darkMode]); + + return ( + + {children} + + ); +}; + +// Custom hook for using theme context +export const useTheme = () => useContext(ThemeContext); + +const AuthenticatedApp = () => { + const [authenticated, setAuthenticated] = useState(isAuthenticated()); + + useEffect(() => { + // Listen for authentication complete event + const handleAuth = () => { + setAuthenticated(true); + }; + + window.addEventListener('authenticationComplete', handleAuth); + + // Check if already authenticated + if (isAuthenticated()) { + handleAuth(); + } + + return () => { + window.removeEventListener('authenticationComplete', handleAuth); + }; + }, []); + + // The App component will only be rendered when user is authenticated + return authenticated ? ( + + + + ) : null; +}; + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) + diff --git a/chat-interface/tailwind.config.js b/chat-interface/tailwind.config.js new file mode 100644 index 0000000..2955b1a --- /dev/null +++ b/chat-interface/tailwind.config.js @@ -0,0 +1,28 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,jsx,ts,tsx}", + ], + darkMode: 'class', // Enable class-based dark mode + theme: { + extend: { + colors: { + border: "hsl(var(--border))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))" + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + }, + }, + plugins: [], +} + diff --git a/chat-interface/update-backend.sh b/chat-interface/update-backend.sh new file mode 100755 index 0000000..71e7e06 --- /dev/null +++ b/chat-interface/update-backend.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# This script updates the backend URL and rebuilds the application + +# Define the backend URL +BACKEND_URL="https://ai-sandbox.oliver.solutions/netflix_back_v2" + +# Create or update the environment files +echo "# Backend API URL +VITE_BACKEND_URL=${BACKEND_URL} + +# Base URL for the app (changes in production) +VITE_APP_BASE_URL=/" > .env + +echo "# Production backend API URL +VITE_BACKEND_URL=${BACKEND_URL} + +# Base URL for the app in production +VITE_APP_BASE_URL=/netflix_v2/" > .env.production + +# Update components files +for file in src/components/ChatInterface.jsx src/components/ConversationManager.jsx src/App.jsx; do + # Replace hardcoded backend URL references + sed -i '' "s|const BACKEND_URL = ['\"]\/netflix_back_v2['\"]|const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || '${BACKEND_URL}'|g" $file + sed -i '' "s|console.log('.*using backend URL (hardcoded):|console.log('Using backend URL:|g" $file +done + +# Update vite.config.js to use the replacement plugin +sed -i '' 's|plugins: \[react()\],|plugins: [react(), replaceBackendUrl],|g' vite.config.js +sed -i '' 's|code: src.replace(/\['"'\]\/netflix_back\['"'\]/g, '"\/netflix_back_v2"'),|code: src.replace(/\['"'\]\/netflix_back\['"'\]/g, `"${BACKEND_URL}"`),|g' vite.config.js + +# Force a complete rebuild by removing all assets +rm -rf dist/assets/* + +# Rebuild the application +npm run build + +echo "Update complete! The backend URL has been set to ${BACKEND_URL}" \ No newline at end of file diff --git a/chat-interface/vite.config.js b/chat-interface/vite.config.js new file mode 100644 index 0000000..51bb0ed --- /dev/null +++ b/chat-interface/vite.config.js @@ -0,0 +1,124 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' + +export default defineConfig(({ mode }) => { + const baseUrl = mode === 'production' ? '/hp_chatbot/' : '/'; + + // Create a replace plugin to ensure all '/hp_back' gets replaced with the full URL + const replaceBackendUrl = { + name: 'replace-backend-url', + transform(src, id) { + if (id.endsWith('.js') || id.endsWith('.jsx')) { + return { + code: src.replace(/['"]\/hp_back['"]/g, '"https://ai-sandbox.oliver.solutions/hp_back_v2"'), + map: null + }; + } + } + }; + + return { + plugins: [react(), replaceBackendUrl], + base: baseUrl, + server: { + port: 5173, + proxy: { + // New MongoDB conversation endpoints + '/conversations': { + target: 'https://ai-sandbox.oliver.solutions/hp_back_v2', + changeOrigin: true, + secure: true, + timeout: 300000 // 5 minutes timeout + }, + '/conversations/new': { + target: 'https://ai-sandbox.oliver.solutions/hp_back_v2', + changeOrigin: true, + secure: true, + timeout: 300000 // 5 minutes timeout + }, + + // Original endpoints + '/chat': { + target: 'https://ai-sandbox.oliver.solutions/hp_back_v2', + changeOrigin: true, + secure: true, + timeout: 300000 // 5 minutes timeout + }, + '/status': { + target: 'https://ai-sandbox.oliver.solutions/hp_back_v2', + changeOrigin: true, + secure: true, + timeout: 300000 // 5 minutes timeout + }, + '/initialize': { + target: 'https://ai-sandbox.oliver.solutions/hp_back_v2', + changeOrigin: true, + secure: true, + timeout: 300000 // 5 minutes timeout + }, + '/reset': { + target: 'https://ai-sandbox.oliver.solutions/hp_back_v2', + changeOrigin: true, + secure: true, + timeout: 300000 // 5 minutes timeout + }, + '/download-brief': { + target: 'https://ai-sandbox.oliver.solutions/hp_back_v2', + changeOrigin: true, + secure: true, + timeout: 300000 // 5 minutes timeout + }, + '/init-chunked-upload': { + target: 'https://ai-sandbox.oliver.solutions/hp_back_v2', + changeOrigin: true, + secure: true, + timeout: 300000 // 5 minutes timeout + }, + '/upload-chunk': { + target: 'https://ai-sandbox.oliver.solutions/hp_back_v2', + changeOrigin: true, + secure: true, + timeout: 300000 // 5 minutes timeout + }, + '/finalize-upload': { + target: 'https://ai-sandbox.oliver.solutions/hp_back_v2', + changeOrigin: true, + secure: true, + timeout: 300000 // 5 minutes timeout + }, + '/upload-small-file': { + target: 'https://ai-sandbox.oliver.solutions/hp_back_v2', + changeOrigin: true, + secure: true, + timeout: 300000 // 5 minutes timeout + }, + '/initialize-from-uploads': { + target: 'https://ai-sandbox.oliver.solutions/hp_back_v2', + changeOrigin: true, + secure: true, + timeout: 300000 // 5 minutes timeout + }, + '/images': { + target: 'https://ai-sandbox.oliver.solutions/hp_back_v2', + changeOrigin: true, + secure: true, + timeout: 300000 // 5 minutes timeout + }, + '/list-images': { + target: 'https://ai-sandbox.oliver.solutions/hp_back_v2', + changeOrigin: true, + secure: true, + timeout: 300000 // 5 minutes timeout + }, + '/capture-screenshot': { + target: 'https://ai-sandbox.oliver.solutions/hp_back_v2', + changeOrigin: true, + secure: true, + timeout: 300000 // 5 minutes timeout + } + } + } + }; +}) + diff --git a/chat-interface/web.config b/chat-interface/web.config new file mode 100644 index 0000000..75ec618 --- /dev/null +++ b/chat-interface/web.config @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..df7a987 --- /dev/null +++ b/config.py @@ -0,0 +1,103 @@ +# hp_chatbot/config.py +import os +from pathlib import Path +from dotenv import load_dotenv +import sys + +# Load environment variables from .env file +env_path = Path(__file__).resolve().parent / '.env' +if env_path.exists(): + # Force reload to ensure environment variables are set + load_dotenv(dotenv_path=env_path, override=True) + print(f"Loaded environment variables from {env_path}") +else: + print(f"WARNING: .env file not found at {env_path}", file=sys.stderr) + +# --- Directory Paths --- +BASE_DIR = Path(__file__).resolve().parent + +UPLOAD_DIR = BASE_DIR / 'uploads' +CHUNK_FOLDER = UPLOAD_DIR / 'chunks' +UPLOAD_METADATA_FOLDER = UPLOAD_DIR / 'metadata' +IMAGES_DIRECTORY = UPLOAD_DIR / 'images' + +SUPPORTING_FILES_DIR = BASE_DIR / 'supporting_files' +HP_DOCS_FOLDER = SUPPORTING_FILES_DIR / 'files_for_rag_store' + +INDEX_STORAGE_DIR = BASE_DIR / 'index_storage' +INDEX_PERSIST_PATH = INDEX_STORAGE_DIR / "hp_docs_index" + +LOG_FILE_PATH = BASE_DIR / 'app.log' + +# Create necessary directories +os.makedirs(CHUNK_FOLDER, exist_ok=True) +os.makedirs(UPLOAD_METADATA_FOLDER, exist_ok=True) +os.makedirs(INDEX_STORAGE_DIR, exist_ok=True) +os.makedirs(IMAGES_DIRECTORY, exist_ok=True) + +# --- Application Settings --- +ALLOWED_EXTENSIONS = {'pdf', 'doc', 'docx', 'txt', 'xls', 'xlsx', 'ppt', 'pptx', 'eml'} +APPLICATION_ROOT = os.environ.get('APPLICATION_ROOT', '') # For running behind proxy +MAX_CONTENT_LENGTH = 500 * 1024 * 1024 # 500MB limit (adjust as needed) + +# --- API Keys --- +# Load from environment variables or use defaults (replace placeholders or set env vars) +OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "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") +ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "") +LLAMA_CLOUD_API_KEY = os.environ.get("LLAMA_CLOUD_API_KEY", "") + +# Ensure required keys are set +if not OPENAI_API_KEY: + print("ERROR: OPENAI_API_KEY not set in environment or .env file. This is required.", file=sys.stderr) + print("Please add OPENAI_API_KEY=your_key to your .env file.", file=sys.stderr) + print(f"Current environment keys: {list(filter(lambda k: 'key' in k.lower(), os.environ.keys()))}", file=sys.stderr) + +# Always set environment variables, even if empty - the error messages will be handled by the code +os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY +os.environ["ANTHROPIC_API_KEY"] = ANTHROPIC_API_KEY +os.environ["LLAMA_CLOUD_API_KEY"] = LLAMA_CLOUD_API_KEY + +# Print API key status for debugging +print(f"OpenAI API key {'is set' if OPENAI_API_KEY else 'is NOT set'}", file=sys.stderr) + +# --- AI Model Configuration --- +LLM_MODEL = "chatgpt-4o-latest" # Or "gpt-4o" etc. +EMBEDDING_MODEL = "text-embedding-3-small" +LLM_TEMPERATURE = 0.3 +LLM_TIMEOUT = 300.0 # 5 minutes +AGENT_TIMEOUT = 600.0 # 10 minutes for the agent run +TOOL_EXECUTION_TIMEOUT = 300.0 # 5 minutes for individual tool calls + +# --- LlamaParse Configuration --- +LLAMA_PARSE_VENDOR_MODEL = "openai-gpt4o" # Verify model name +LLAMA_PARSE_MAX_TIMEOUT = 3600 # 1 hour + +# --- Indexing Configuration --- +# NODE_PARSER_CHUNK_SIZE = 2048 # Example if using SentenceSplitter +# NODE_PARSER_CHUNK_OVERLAP = 20 +# Use Semantic Splitter by default (see ai_core.py) +SIMILARITY_TOP_K = 10 +SIMILARITY_CUTOFF = 0.0 # Adjust if needed + +# --- CORS Configuration --- +CORS_ALLOWED_ORIGINS = ["http://localhost:5173", "https://ai-sandbox.oliver.solutions"] # HP chatbot CORS origins +CORS_SUPPORTS_CREDENTIALS = True + +# --- Server Configuration --- +SERVER_HOST = "0.0.0.0" if os.environ.get("PRODUCTION", "false").lower() == "true" else "localhost" +SERVER_PORT = int(os.environ.get("PORT", "8746")) # Port for HP chatbot (unique from Netflix) +USE_RELOADER = os.environ.get("PRODUCTION", "false").lower() != "true" +LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO") # Changed default to INFO + +# Hypercorn specific timeouts (in seconds) +KEEP_ALIVE_TIMEOUT = 300 +READ_TIMEOUT = 300 +WRITE_TIMEOUT = 300 + +# --- MongoDB Configuration --- +# Assumes mongodb_utils handles connection details (e.g., via environment variables) + +# --- Neo4j Configuration --- +NEO4J_URL = os.environ.get("NEO4J_URL", "bolt://localhost:7688") # Separate Neo4j instance for HP +NEO4J_USERNAME = os.environ.get("NEO4J_USERNAME", "neo4j") +NEO4J_PASSWORD = os.environ.get("NEO4J_PASSWORD", "hp-graphrag-2024") # HP-specific password \ No newline at end of file diff --git a/docs/graphRAG chatbot technical documentation.pdf b/docs/graphRAG chatbot technical documentation.pdf new file mode 100644 index 0000000..9f361bf Binary files /dev/null and b/docs/graphRAG chatbot technical documentation.pdf differ diff --git a/docs/graphRAG_chatbot_documentation.md b/docs/graphRAG_chatbot_documentation.md new file mode 100644 index 0000000..616967f --- /dev/null +++ b/docs/graphRAG_chatbot_documentation.md @@ -0,0 +1,666 @@ +# HP GraphRAG Chatbot - Technical Documentation + +## Table of Contents + +1. [System Overview](#system-overview) +2. [Architecture](#architecture) +3. [Technology Stack](#technology-stack) +4. [Data Flow](#data-flow) +5. [Database Design](#database-design) +6. [API Reference](#api-reference) +7. [User Flow](#user-flow) +8. [Security](#security) +9. [Deployment](#deployment) +10. [Development](#development) +11. [Troubleshooting](#troubleshooting) + +--- + +## System Overview + +The HP GraphRAG Chatbot is a sophisticated conversational AI system that combines vector search with knowledge graph capabilities to answer questions about HP marketing materials and brand guidelines. It processes multimodal documents (text + images) and uses a hybrid AI agent approach for intelligent information retrieval and response generation. + +### Key Features + +- **Multimodal Document Processing**: Extracts text and images from PDFs, PowerPoint, and other marketing documents +- **GraphRAG Architecture**: Combines vector similarity search with knowledge graph community detection +- **Custom ReAct Agent**: Implements reasoning and action patterns for intelligent query processing +- **Session Management**: Maintains conversation context across multiple interactions +- **Image Display**: Shows relevant document screenshots alongside responses +- **Authentication**: Microsoft Authentication Library (MSAL) integration +- **Conversation History**: Persistent storage and retrieval of chat sessions + +--- + +## Architecture + +```mermaid +graph TB + subgraph "Frontend (React)" + FE[Chat Interface] + AUTH[MSAL Auth] + CONV[Conversation Manager] + UI[UI Components] + end + + subgraph "Backend (Python/Flask)" + API[Flask Routes] + AGENT[ReAct Agent] + GRAPH[GraphRAG Engine] + SESSION[Session Manager] + PARSE[Document Parser] + end + + subgraph "AI/ML Layer" + LLM[OpenAI GPT-4] + EMBED[Text Embeddings] + LLAMAPARSE[LlamaParse] + end + + subgraph "Data Storage" + NEO4J[(Neo4j
Knowledge Graph)] + MONGO[(MongoDB
Conversations)] + VECTOR[(Vector Index
LlamaIndex)] + FILES[File Storage
Images/Documents] + end + + FE --> API + AUTH --> API + CONV --> API + + API --> AGENT + API --> SESSION + API --> PARSE + + AGENT --> GRAPH + GRAPH --> NEO4J + GRAPH --> VECTOR + AGENT --> LLM + + PARSE --> LLAMAPARSE + LLAMAPARSE --> FILES + PARSE --> EMBED + EMBED --> VECTOR + + SESSION --> MONGO + + style FE fill:#e1f5fe + style API fill:#f3e5f5 + style AGENT fill:#e8f5e8 + style NEO4J fill:#fff3e0 + style MONGO fill:#f1f8e9 +``` + +### Component Breakdown + +#### Frontend (React) +- **Chat Interface**: Main conversational UI with message bubbles, image viewing, and input handling +- **Authentication**: MSAL-based Microsoft authentication +- **Conversation Manager**: Handles multiple conversation sessions and history +- **Theme Toggle**: Dark/light mode support + +#### Backend (Python/Flask) +- **Flask Routes**: RESTful API endpoints for chat, authentication, file serving +- **ReAct Agent**: Custom implementation with reasoning, action, and observation cycles +- **GraphRAG Engine**: Hybrid retrieval combining vector search with graph-based community detection +- **Session Manager**: Maps frontend sessions to database conversations +- **Document Parser**: LlamaParse integration for multimodal document processing + +#### Data Layer +- **Neo4j**: Stores knowledge graph with entities, relationships, and communities +- **MongoDB**: Persists user conversations, messages, and session state +- **Vector Index**: LlamaIndex-based semantic search capabilities +- **File Storage**: Local filesystem for processed images and documents + +--- + +## Technology Stack + +### Backend +- **Framework**: Flask + Hypercorn (ASGI) +- **AI/ML**: + - OpenAI GPT-4 (LLM) + - text-embedding-3-small (embeddings) + - LlamaParse (document processing) + - LlamaIndex (vector indexing) +- **Databases**: + - Neo4j (knowledge graph) + - MongoDB (conversations) +- **Languages**: Python 3.9+ + +### Frontend +- **Framework**: React 18 + Vite +- **Styling**: TailwindCSS + Shadcn/ui +- **Authentication**: Microsoft Authentication Library (MSAL) +- **Languages**: JavaScript/JSX + +### Infrastructure +- **Web Server**: Hypercorn (ASGI server) +- **Containerization**: Docker support +- **Deployment**: Azure/Cloud-based + +--- + +## Data Flow + +```mermaid +sequenceDiagram + participant User + participant Frontend + participant API + participant Agent + participant GraphRAG + participant Neo4j + participant Vector + participant OpenAI + participant MongoDB + + User->>Frontend: Send message + Frontend->>API: POST /chat + API->>Agent: Process query + + Agent->>GraphRAG: Retrieve context + GraphRAG->>Vector: Vector similarity search + GraphRAG->>Neo4j: Community detection + GraphRAG->>OpenAI: Generate synthesis + GraphRAG->>Agent: Combined context + + Agent->>OpenAI: Generate response + OpenAI->>Agent: Response + reasoning + + Agent->>API: Structured response + API->>MongoDB: Store conversation + API->>Frontend: Response with sources/images + Frontend->>User: Display response +``` + +### Document Processing Flow + +```mermaid +flowchart TD + UPLOAD[Document Upload] --> PARSE[LlamaParse Processing] + PARSE --> EXTRACT[Extract Text + Images] + EXTRACT --> SPLIT[Semantic Splitting] + SPLIT --> EMBED[Generate Embeddings] + EMBED --> VECTOR[Store in Vector Index] + SPLIT --> GRAPH[Extract Entities/Relations] + GRAPH --> NEO4J[Store in Neo4j] + EXTRACT --> IMAGES[Save Page Images] + IMAGES --> STORAGE[File Storage] + NEO4J --> COMMUNITY[Community Detection] + COMMUNITY --> CACHE[Cache Communities] +``` + +--- + +## Database Design + +### Neo4j Knowledge Graph Schema + +```mermaid +erDiagram + Entity { + string name + string label + string description + dict properties + } + + Relation { + string label + string source_id + string target_id + string description + dict properties + } + + Community { + int community_id + text summary + list entity_ids + } + + Entity ||--o{ Relation : participates_in + Community ||--o{ Entity : contains +``` + +### MongoDB Collections Schema + +```mermaid +erDiagram + Users { + ObjectId _id + string username + string email + datetime created_at + datetime last_login + } + + Conversations { + ObjectId _id + string session_id + ObjectId user_id + string title + datetime created_at + datetime last_updated + boolean is_deleted + } + + Messages { + ObjectId _id + ObjectId conversation_id + string role + text content + array sources + array reasoning + array images + datetime timestamp + } + + Users ||--o{ Conversations : owns + Conversations ||--o{ Messages : contains +``` + +--- + +## API Reference + +### Authentication +All API endpoints require authentication via `X-MS-USERNAME` header (except in development mode). + +### Core Endpoints + +#### POST /chat +Processes chat messages and returns AI responses. + +**Request:** +```json +{ + "message": "string", + "sessionId": "string" +} +``` + +**Response:** +```json +{ + "status": "success", + "data": { + "response": "string", + "sources": [ + { + "content": "string", + "tool_name": "string", + "retrieval_method": "vector_only|graphrag_hybrid" + } + ], + "reasoning": [ + { + "type": "ActionReasoningStep|ObservationReasoningStep", + "action": "string", + "observation": "string" + } + ], + "images": [ + { + "filename": "string", + "document": "string", + "page": "number", + "url_encoded_filename": "string" + } + ] + } +} +``` + +#### GET /status +Returns system initialization status. + +**Response:** +```json +{ + "global_status": "initialized", + "initialized": true, + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +#### GET /conversations +Retrieves user's conversation history. + +**Response:** +```json +{ + "status": "success", + "conversations": [ + { + "id": "string", + "title": "string", + "created_at": "datetime", + "last_updated": "datetime", + "session_id": "string" + } + ] +} +``` + +#### GET /conversations/{id}/messages +Retrieves messages for a specific conversation. + +**Response:** +```json +{ + "status": "success", + "conversation_title": "string", + "messages": [ + { + "id": "string", + "role": "user|assistant", + "content": "string", + "timestamp": "datetime", + "sources": [], + "reasoning": [], + "images": [] + } + ] +} +``` + +#### GET /images/{filename} +Serves processed document images. + +#### POST /reset +Resets the global agent's conversation memory. + +#### DELETE /conversations/{id} +Deletes a conversation (soft delete by default). + +--- + +## User Flow + +```mermaid +flowchart TD + START([User Access]) --> AUTH{Authenticated?} + AUTH -->|No| LOGIN[MSAL Login] + LOGIN --> AUTH + AUTH -->|Yes| LOAD[Load Conversations] + + LOAD --> NEWCHAT[Create New Chat] + NEWCHAT --> INTERFACE[Chat Interface] + + INTERFACE --> INPUT[User Input] + INPUT --> PROCESS[Process with GraphRAG] + PROCESS --> RETRIEVE[Hybrid Retrieval] + RETRIEVE --> GENERATE[Generate Response] + GENERATE --> DISPLAY[Display with Images] + DISPLAY --> INPUT + + DISPLAY --> SAVE[Save to History] + SAVE --> UPDATE[Update Conversation] + + INTERFACE --> HISTORY[View History] + HISTORY --> SELECT[Select Conversation] + SELECT --> LOAD_MSG[Load Messages] + LOAD_MSG --> INTERFACE + + INTERFACE --> EXPORT[Export Brief] + INTERFACE --> DELETE[Delete Conversation] +``` + +### Detailed User Journey + +1. **Authentication**: User logs in via Microsoft MSAL +2. **Conversation Creation**: System creates new conversation or loads existing +3. **Query Processing**: + - User sends message + - GraphRAG performs hybrid retrieval + - Vector similarity search finds relevant chunks + - Knowledge graph identifies related communities + - LLM synthesizes response with reasoning +4. **Response Display**: + - Text response with markdown support + - Source attribution tooltips + - Relevant document images + - Reasoning chain (if available) +5. **History Management**: Conversations persisted and retrievable + +--- + +## Security + +### Authentication +- Microsoft Authentication Library (MSAL) integration +- Azure AD tenant-based access control +- Session-based user identification + +### Data Protection +- No sensitive data logged in plain text +- Conversation data encrypted at rest (MongoDB) +- API key management via environment variables +- CORS configuration for cross-origin requests + +### Access Control +- User-scoped conversation access +- Session-based authorization +- Development vs production mode differentiation + +--- + +## Deployment + +### Environment Configuration + +#### Backend (.env) +```bash +# API Keys +OPENAI_API_KEY=your_openai_key +LLAMA_CLOUD_API_KEY=your_llama_cloud_key +ANTHROPIC_API_KEY=your_anthropic_key + +# Database Configuration +NEO4J_URL=bolt://localhost:7688 +NEO4J_USERNAME=neo4j +NEO4J_PASSWORD=hp-graphrag-2024 + +# Server Configuration +PORT=8746 +PRODUCTION=true +LOG_LEVEL=INFO +``` + +#### Frontend (.env) +```bash +VITE_BACKEND_URL=https://ai-sandbox.oliver.solutions/hp_chatbot_back +VITE_APP_BASE_URL=/hp_chatbot/ +``` + +### Deployment Steps + +1. **Database Setup**: + - Neo4j instance on port 7688 + - MongoDB with authentication + - Initialize collections via `init_mongodb.py` + +2. **Backend Deployment**: + ```bash + pip install -r requirements.txt + python main.py + ``` + +3. **Frontend Build**: + ```bash + cd chat-interface + npm install + npm run build + # Deploy dist/ contents to /hp_chatbot/ path + ``` + +4. **Web Server Configuration**: + - Configure reverse proxy (nginx/Apache) + - Set up SSL certificates + - Configure CORS origins + +--- + +## Development + +### Backend Development + +```bash +# Setup virtual environment +python -m venv env +source env/bin/activate # or env\Scripts\activate on Windows + +# Install dependencies +pip install -r requirements.txt + +# Start development server +python main.py +``` + +### Frontend Development + +```bash +cd chat-interface +npm install +npm run dev +``` + +### Key Development Commands + +| Command | Purpose | +|---------|---------| +| `python main.py` | Start backend server | +| `npm run dev` | Start frontend dev server | +| `npm run build` | Build frontend for production | +| `npm run lint` | Lint frontend code | + +### Code Structure + +``` +hp_graphRAG_bot/ +├── Backend (Python) +│ ├── main.py # Application entry point +│ ├── ai_core.py # Core AI engine & ReAct agent +│ ├── graph_rag_integration.py # GraphRAG system +│ ├── routes.py # Flask API routes +│ ├── session_manager.py # Session management +│ ├── mongodb_utils.py # MongoDB operations +│ ├── config.py # Configuration +│ └── shared_state.py # Global state management +├── Frontend (React) +│ └── chat-interface/ +│ ├── src/ +│ │ ├── App.jsx # Main application component +│ │ ├── components/ # React components +│ │ ├── auth.js # MSAL authentication +│ │ └── lib/ # Utilities +│ └── dist/ # Production build +└── Data Storage + ├── uploads/images/ # Processed document images + ├── index_storage/ # Vector index data + └── supporting_files/ # Source documents +``` + +--- + +## Troubleshooting + +### Common Issues + +#### Backend Issues + +**Problem**: `Global workflow agent not initialized` +**Solution**: Check OpenAI API key and Neo4j connectivity +```bash +# Verify environment variables +echo $OPENAI_API_KEY +# Check Neo4j connection +curl http://localhost:7474 +``` + +**Problem**: `LlamaParse timeout during document processing` +**Solution**: Increase timeout settings in config.py +```python +LLAMA_PARSE_MAX_TIMEOUT = 7200 # 2 hours +``` + +**Problem**: `MongoDB connection failed` +**Solution**: Verify MongoDB service and credentials +```bash +# Check MongoDB status +brew services list | grep mongodb +# Test connection +mongosh mongodb://hp:hp@localhost:27017/hp_chatbot +``` + +#### Frontend Issues + +**Problem**: `CORS policy blocking requests` +**Solution**: Update CORS_ALLOWED_ORIGINS in backend config.py + +**Problem**: `Authentication failures` +**Solution**: Verify MSAL configuration and Azure AD settings + +**Problem**: `Images not loading` +**Solution**: Check image file paths and backend /images/ endpoint + +### Debug Endpoints + +**Development Mode Only:** +- `GET /debug-status` - System state inspection +- `POST /reinitialize` - Force agent reinitialization +- `POST /capture-screenshot` - Manual image extraction testing + +### Logging + +All components use structured logging: +```python +log_structured('info', 'Event description', {'key': 'value'}) +``` + +Log files locations: +- Backend: `app.log` +- MongoDB operations: `mongodb.log` + +--- + +## Performance Considerations + +### Scaling +- **Vector Index**: Consider PostgreSQL pgvector for large deployments +- **Neo4j**: Implement read replicas for query scaling +- **MongoDB**: Use connection pooling and sharding +- **Caching**: Redis for session and community caches + +### Optimization +- **GraphRAG Communities**: Pre-computed and cached +- **Image Processing**: Async processing with queue system +- **Memory Management**: Agent memory reset policies +- **Response Time**: Parallel vector and graph retrieval + +--- + +## Future Enhancements + +### Planned Features +1. **Multi-tenant Architecture**: Support multiple organizations +2. **Advanced Analytics**: Usage metrics and conversation insights +3. **Enhanced Multimodal**: Video and audio processing +4. **Real-time Collaboration**: Multi-user conversations +5. **API Extensions**: Webhook integrations and external tool calling +6. **Advanced Security**: Role-based access control and audit logging + +### Technical Debt +- Implement comprehensive test suite +- Add API rate limiting +- Improve error handling consistency +- Optimize database queries +- Add health check endpoints + +--- + +*Documentation Version: 1.0* +*Last Updated: 2024-01-01* +*System Version: HP GraphRAG Chatbot v1.0* \ No newline at end of file diff --git a/document_generator.py b/document_generator.py new file mode 100644 index 0000000..87a922b --- /dev/null +++ b/document_generator.py @@ -0,0 +1,339 @@ +# hp_chatbot/document_generator.py + +import io +import re +import markdown2 +from bs4 import BeautifulSoup +from docx import Document +from docx.shared import Inches, Pt, RGBColor +from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK +from docx.oxml.shared import OxmlElement, qn +from docx.oxml import parse_xml + +from utils import log_structured + +# --- Helper for Horizontal Line --- +def add_horizontal_line(paragraph): + """Adds a horizontal line after the specified paragraph.""" + p = paragraph._p # the lxml element beneath the paragraph + pPr = p.get_or_add_pPr() # Get or add paragraph properties element + pBdr = OxmlElement('w:pBdr') # Create paragraph border element + # Add a bottom border + bottom_bdr = OxmlElement('w:bottom') + bottom_bdr.set(qn('w:val'), 'single') # Border style + bottom_bdr.set(qn('w:sz'), '6') # Border size (in 1/8 points) + bottom_bdr.set(qn('w:space'), '1') # Space between text and border + bottom_bdr.set(qn('w:color'), 'auto') # Border color + pBdr.append(bottom_bdr) + pPr.append(pBdr) + +# --- Inline Markdown to DOCX Run Formatting --- +def process_inline_formatting(paragraph, text): + """ + Processes simple inline markdown (bold, italic, code) within text + and adds formatted runs to the paragraph. + Handles nested formatting cautiously. + """ + # Regex to find **bold**, *italic*, _italic_, `code` segments + # It captures the marker and the content separately. + pattern = r'(\*\*|`|\*|_)(.*?)(\1)' + last_end = 0 + + for match in re.finditer(pattern, text): + start, end = match.span() + marker = match.group(1) + content = match.group(2) + + # Add preceding text if any + if start > last_end: + paragraph.add_run(text[last_end:start]) + + # Add formatted run + run = paragraph.add_run(content) + if marker == '**': + run.bold = True + elif marker == '*' or marker == '_': + run.italic = True + elif marker == '`': + run.font.name = 'Courier New' + # run.font.size = Pt(10) # Optional: Set size for code + + last_end = end + + # Add any remaining text after the last match + if last_end < len(text): + paragraph.add_run(text[last_end:]) + + +# --- HTML to DOCX Conversion --- +def convert_html_to_docx(doc: Document, html_content: str): + """ + Converts basic HTML content (from markdown conversion) to Word elements. + Handles common tags like paragraphs, headings, lists, bold, italic, code. + """ + # Pre-process HTML slightly for cleaner parsing + html_content = re.sub(r'\s*\n\s*', '\n', html_content).strip() # Normalize whitespace + html_content = f"{html_content}" # Wrap in body for better parsing + soup = BeautifulSoup(html_content, 'html.parser') + + # Recursive function to handle elements + def process_element(element, current_paragraph=None, current_style=None, in_list=False): + # Skip NavigableString if it's just whitespace or newline outside pre + if isinstance(element, str): + text = str(element).strip('\n') # Keep internal spaces, strip leading/trailing newlines + if text: # Only add if there's actual content + if current_paragraph: + run = current_paragraph.add_run(text) + if current_style: + if 'bold' in current_style: run.bold = True + if 'italic' in current_style: run.italic = True + if 'code' in current_style: run.font.name = 'Courier New' + else: + # Text outside paragraph usually means an error or whitespace + # log_structured('debug', f"Orphan text node found: '{text[:50]}...'") + pass # Or create a default paragraph: doc.add_paragraph(text) + return + + # --- Block Level Elements --- + if element.name in ['p', 'div']: + # Avoid creating paragraphs for empty containers unless they contain
+ text_content = element.get_text(strip=True) + has_br = element.find('br') + if text_content or has_br: + para = doc.add_paragraph() + # Apply list indentation if necessary (though lists handle their own paras) + # if in_list: para.paragraph_format.left_indent = Inches(0.5) + new_style = current_style.copy() if current_style else set() + for child in element.children: + process_element(child, para, new_style, in_list) + # else: skip empty p/div + + elif element.name in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']: + try: + level = int(element.name[1]) + heading = doc.add_heading(level=level) + # Process children for inline formatting within heading + new_style = current_style.copy() if current_style else set() + for child in element.children: + process_element(child, heading, new_style, in_list) + # If no children processed (just text), add it directly + if not heading.runs: + heading.add_run(element.get_text(strip=True)) + except ValueError: pass # Should not happen with h1-h6 + + elif element.name == 'ul': + for li in element.find_all('li', recursive=False): + # Each li gets its own paragraph with bullet style + para = doc.add_paragraph(style='List Bullet') + new_style = current_style.copy() if current_style else set() + for child in li.children: + process_element(child, para, new_style, in_list=True) + # If li was empty or only contained whitespace + if not para.text.strip(): + para.text = "" # Ensure empty bullet point exists + + elif element.name == 'ol': + # Numbering is handled by the 'List Number' style + for li in element.find_all('li', recursive=False): + para = doc.add_paragraph(style='List Number') + new_style = current_style.copy() if current_style else set() + for child in li.children: + process_element(child, para, new_style, in_list=True) + if not para.text.strip(): + para.text = "" # Ensure empty numbered item exists + + # Note: 'li' is handled within 'ul'/'ol' processing. + + elif element.name == 'pre': + # Often contains a 'code' element, handle that + code_tag = element.find('code') + content = code_tag.get_text() if code_tag else element.get_text() + if content.strip(): + para = doc.add_paragraph(style='CodeStyle') # Requires 'CodeStyle' to be defined + # Preserve whitespace more carefully for
+                  run = para.add_run(content.strip('\n')) # Strip outer newlines only
+                  run.font.name = 'Courier New'
+                  # run.font.size = Pt(10)
+
+        elif element.name == 'blockquote':
+              para = doc.add_paragraph(style='Quote') # Requires 'Quote' style
+              new_style = current_style.copy() if current_style else set()
+              for child in element.children:
+                  process_element(child, para, new_style, in_list)
+
+        elif element.name == 'hr':
+             para = doc.add_paragraph()
+             add_horizontal_line(para)
+
+        elif element.name == 'br':
+            if current_paragraph:
+                current_paragraph.add_run().add_break() # Add line break within paragraph
+
+
+        # --- Inline Elements ---
+        elif element.name in ['strong', 'b']:
+            new_style = current_style.copy() if current_style else set()
+            new_style.add('bold')
+            for child in element.children:
+                process_element(child, current_paragraph, new_style, in_list)
+
+        elif element.name in ['em', 'i']:
+            new_style = current_style.copy() if current_style else set()
+            new_style.add('italic')
+            for child in element.children:
+                process_element(child, current_paragraph, new_style, in_list)
+
+        elif element.name == 'code':
+            # Handle inline code - assumes it's within a paragraph already
+            if current_paragraph:
+                text = element.get_text()
+                if text:
+                    run = current_paragraph.add_run(text)
+                    run.font.name = 'Courier New'
+                    # Add specific inline code style if desired
+            else:
+                 # Code tag not within a paragraph? Create one.
+                 para = doc.add_paragraph(style='CodeStyle')
+                 run = para.add_run(element.get_text())
+                 run.font.name = 'Courier New'
+
+
+        elif element.name == 'a':
+            # Add hyperlink if possible, otherwise just text
+            text = element.get_text(strip=True)
+            href = element.get('href')
+            if current_paragraph and text:
+                # python-docx doesn't have direct hyperlink support easily added here.
+                # Simplest: add text with underline and blue color.
+                run = current_paragraph.add_run(text)
+                run.underline = True
+                run.font.color.rgb = RGBColor(0x05, 0x63, 0xC1) # Standard link blue
+                # For actual hyperlinks, more complex XML manipulation is needed.
+
+
+        # --- Body/Other Tags: Process children ---
+        elif element.name in ['body', 'span', 'div']: # Treat span/div mostly as containers
+            new_style = current_style.copy() if current_style else set()
+            for child in element.children:
+                 process_element(child, current_paragraph, new_style, in_list)
+
+        # --- Ignored Tags ---
+        elif element.name in ['script', 'style', 'head', 'meta', 'title']:
+             pass # Ignore these tags and their content
+
+        else:
+            # Unknown tag: try to process its children if it's a container,
+            # or add its text content if it's inline-like.
+            log_structured('warning', f"Unhandled HTML tag encountered: <{element.name}>", {'content_preview': element.get_text(strip=True)[:50]})
+            # Default behavior: process children recursively
+            new_style = current_style.copy() if current_style else set()
+            for child in element.children:
+                 process_element(child, current_paragraph, new_style, in_list)
+
+
+    # Start processing from the top-level elements within the parsed body
+    body = soup.find('body')
+    if body:
+        for element in body.children:
+            process_element(element)
+
+
+# --- Main Markdown to DOCX Function ---
+def create_brief_docx(brief_content_markdown: str) -> io.BytesIO:
+    """
+    Creates a Word document (.docx) in memory from markdown content.
+
+    Args:
+        brief_content_markdown: The markdown string content.
+
+    Returns:
+        An io.BytesIO buffer containing the Word document.
+    """
+    doc = Document()
+
+    # --- Define Styles (Optional but recommended) ---
+    styles = doc.styles
+    # Normal style
+    style = styles['Normal']
+    font = style.font
+    font.name = 'Calibri' # Or HP specific font if available
+    font.size = Pt(11)
+
+    # Code style (example)
+    try:
+        code_style = styles.add_style('CodeStyle', 1) # 1 for paragraph style
+        code_style.font.name = 'Courier New'
+        code_style.font.size = Pt(10)
+        # Prevent spell check for code blocks
+        code_style.element.rPr.rFonts.set(qn('w:ascii'), 'Courier New')
+        code_style.element.rPr.rFonts.set(qn('w:hAnsi'), 'Courier New')
+        # code_style.element.xpath('./w:rPr/w:lang')[0].set(qn('w:noProof'), '1') # Requires lxml maybe
+        p_fmt = code_style.paragraph_format
+        p_fmt.space_before = Pt(6)
+        p_fmt.space_after = Pt(6)
+    except ValueError:
+         log_structured('warning', "'CodeStyle' already exists. Using existing.")
+         code_style = styles['CodeStyle'] # Use existing if it fails to add
+
+    # Quote style (example)
+    try:
+         quote_style = styles.add_style('QuoteStyle', 1)
+         quote_style.font.italic = True
+         quote_style.paragraph_format.left_indent = Inches(0.5)
+         quote_style.paragraph_format.space_before = Pt(6)
+         quote_style.paragraph_format.space_after = Pt(6)
+    except ValueError:
+         log_structured('warning', "'QuoteStyle' already exists. Using existing.")
+         quote_style = styles['QuoteStyle']
+
+
+    # --- Document Header ---
+    title = doc.add_heading('Marketing Brief', 0)
+    title.alignment = WD_ALIGN_PARAGRAPH.CENTER
+
+    date_para = doc.add_paragraph()
+    date_para.alignment = WD_ALIGN_PARAGRAPH.RIGHT
+    date_run = date_para.add_run(datetime.now().strftime("%B %d, %Y"))
+    date_run.italic = True
+    # Add some space after date
+    date_para.paragraph_format.space_after = Pt(12)
+
+    # Add a horizontal line separator
+    hr_para = doc.add_paragraph()
+    add_horizontal_line(hr_para)
+    hr_para.paragraph_format.space_after = Pt(18) # Space after the line
+
+
+    # --- Convert Markdown to HTML ---
+    # Using markdown2 with recommended extras for broad compatibility
+    extras = [
+        "tables", "fenced-code-blocks", "header-ids", "footnotes",
+        "task_list", "code-friendly", "cuddled-lists", "markdown-in-html",
+        "strike", "spoiler", "target-blank-links", "smarty-pants" # Added smarty-pants
+    ]
+    html_content = markdown2.markdown(brief_content_markdown, extras=extras)
+
+    log_structured('debug', 'Converted markdown to HTML for DOCX generation', {
+        'md_preview': brief_content_markdown[:200],
+        'html_preview': html_content[:300]
+    })
+
+    # --- Convert HTML to Word Document Elements ---
+    try:
+        convert_html_to_docx(doc, html_content)
+    except Exception as conversion_err:
+         log_structured('error', "Error during HTML to DOCX conversion", {
+             'error': str(conversion_err),
+             'traceback': traceback.format_exc()
+         })
+         # Add error message to the document itself
+         doc.add_paragraph("Error: Could not fully convert content from HTML to DOCX.", style='Emphasis')
+         doc.add_paragraph(str(conversion_err))
+
+
+    # --- Save to Buffer ---
+    doc_buffer = io.BytesIO()
+    doc.save(doc_buffer)
+    doc_buffer.seek(0)
+
+    return doc_buffer
\ No newline at end of file
diff --git a/graphRAG.py b/graphRAG.py
new file mode 100644
index 0000000..c28b533
--- /dev/null
+++ b/graphRAG.py
@@ -0,0 +1,581 @@
+import os
+import json
+import re
+import asyncio
+import networkx as nx
+from collections import defaultdict
+from typing import Any, List, Callable, Optional, Union, Dict
+from dotenv import load_dotenv
+
+# Import LlamaIndex components
+from llama_index.core import Document, Settings
+from llama_index.core.node_parser import SentenceSplitter
+from llama_index.core import PropertyGraphIndex
+from llama_index.core.async_utils import run_jobs
+from llama_index.core.indices.property_graph.utils import default_parse_triplets_fn
+from llama_index.core.graph_stores.types import EntityNode, KG_NODES_KEY, KG_RELATIONS_KEY, Relation
+from llama_index.core.llms.llm import LLM
+from llama_index.core.prompts import PromptTemplate
+from llama_index.core.prompts.default_prompts import DEFAULT_KG_TRIPLET_EXTRACT_PROMPT
+from llama_index.core.schema import TransformComponent, BaseNode
+from llama_index.core.query_engine import CustomQueryEngine
+from llama_index.llms.openai import OpenAI
+from llama_index.core.llms import ChatMessage
+from llama_index.graph_stores.neo4j import Neo4jPropertyGraphStore
+from llama_index.core import SimpleDirectoryReader
+
+# Community detection (using NetworkX instead of graspologic as a fallback)
+try:
+    from community import best_partition  # python-louvain package
+except ImportError:
+    print("Community detection package not found, using NetworkX built-in community detection")
+
+# Load environment variables from .env file
+load_dotenv()
+
+# Use API key from environment as fallback
+if not os.environ.get("OPENAI_API_KEY"):
+    os.environ["OPENAI_API_KEY"] = "sk-proj-wXcoIn81Vwg4Iaw0vhmYT3BlbkFJmt1eOxeEAF1juUfhzMtk"
+
+# Define the GraphRAGExtractor class
+class GraphRAGExtractor(TransformComponent):
+    """Extract triples from a graph.
+
+    Uses an LLM and a simple prompt + output parsing to
+    extract paths (i.e. triples) and entity, relation descriptions
+    from text.
+
+    Args:
+        llm (LLM):
+            The language model to use.
+        extract_prompt (Union[str, PromptTemplate]):
+            The prompt to use for extracting triples.
+        parse_fn (callable):
+            A function to parse the output of the language
+            model.
+        num_workers (int):
+            The number of workers to use for parallel
+            processing.
+        max_paths_per_chunk (int):
+            The maximum number of paths to extract per chunk.
+    """
+
+    llm: LLM
+    extract_prompt: PromptTemplate
+    parse_fn: Callable
+    num_workers: int
+    max_paths_per_chunk: int
+
+    def __init__(
+        self,
+        llm: Optional[LLM] = None,
+        extract_prompt: Optional[Union[str, PromptTemplate]] = None,
+        parse_fn: Callable = default_parse_triplets_fn,
+        max_paths_per_chunk: int = 10,
+        num_workers: int = 4,
+    ) -> None:
+        """Init params."""
+        from llama_index.core import Settings
+
+        if isinstance(extract_prompt, str):
+            extract_prompt = PromptTemplate(extract_prompt)
+
+        super().__init__(
+            llm=llm or Settings.llm,
+            extract_prompt=extract_prompt or DEFAULT_KG_TRIPLET_EXTRACT_PROMPT,
+            parse_fn=parse_fn,
+            num_workers=num_workers,
+            max_paths_per_chunk=max_paths_per_chunk,
+        )
+
+    @classmethod
+    def class_name(cls) -> str:
+        return "GraphExtractor"
+
+    def __call__(
+        self, nodes: List[BaseNode], show_progress: bool = False, **kwargs: Any
+    ) -> List[BaseNode]:
+        """Extract triples from nodes."""
+        return asyncio.run(
+            self.acall(nodes, show_progress=show_progress, **kwargs)
+        )
+
+    async def _aextract(self, node: BaseNode) -> BaseNode:
+        """Extract triples from a node."""
+        assert hasattr(node, "text")
+
+        text = node.get_content(metadata_mode="llm")
+        try:
+            llm_response = await self.llm.apredict(
+                self.extract_prompt,
+                text=text,
+                max_knowledge_triplets=self.max_paths_per_chunk,
+            )
+            entities, entities_relationship = self.parse_fn(llm_response)
+        except ValueError:
+            entities = []
+            entities_relationship = []
+
+        existing_nodes = node.metadata.pop(KG_NODES_KEY, [])
+        existing_relations = node.metadata.pop(KG_RELATIONS_KEY, [])
+        
+        entity_metadata = node.metadata.copy()
+        for entity, entity_type, description in entities:
+            entity_metadata["entity_description"] = description
+            entity_node = EntityNode(
+                name=entity, label=entity_type,
+                properties=entity_metadata
+            )
+            existing_nodes.append(entity_node)
+
+        relation_metadata = node.metadata.copy()
+        for triple in entities_relationship:
+            subj, obj, rel, description = triple
+            relation_metadata["relationship_description"] = description
+            rel_node = Relation(
+                label=rel,
+                source_id=subj,
+                target_id=obj,
+                properties=relation_metadata,
+            )
+            existing_relations.append(rel_node)
+
+        node.metadata[KG_NODES_KEY] = existing_nodes
+        node.metadata[KG_RELATIONS_KEY] = existing_relations
+        return node
+
+    async def acall(
+        self, nodes: List[BaseNode], show_progress: bool = False, **kwargs: Any
+    ) -> List[BaseNode]:
+        """Extract triples from nodes async."""
+        jobs = []
+        for node in nodes:
+            jobs.append(self._aextract(node))
+
+        return await run_jobs(
+            jobs,
+            workers=self.num_workers,
+            show_progress=show_progress,
+            desc="Extracting paths from text",
+        )
+
+# Define the GraphRAGStore class
+class GraphRAGStore(Neo4jPropertyGraphStore):
+    community_summary = {}
+    entity_info = None
+    max_cluster_size = 5
+
+    def generate_community_summary(self, text):
+        """Generate summary for a given text using an LLM."""
+        messages = [
+            ChatMessage(
+                role="system",
+                content=(
+                    "You are provided with a set of "
+                    "relationships from a knowledge graph, each represented as "
+                    "entity1->entity2->relation-"
+                    ">relationship_description. Your task is to create a summary of "
+                    "these "
+                    "relationships. The summary should include "
+                    "the names of the entities involved and a concise synthesis "
+                    "of the relationship descriptions. The "
+                    "goal is to capture the most critical and relevant details that "
+                    "highlight the nature and significance of "
+                    "each relationship. Ensure that the summary is coherent and "
+                    "integrates the information in a way that "
+                    "emphasizes the key aspects of the relationships."
+                ),
+            ),
+            ChatMessage(role="user", content=text),
+        ]
+        response = OpenAI().chat(messages)
+        clean_response = re.sub(r"^assistant:\s*", "", str(response)).strip()
+        return clean_response
+
+    def build_communities(self):
+        """Builds communities from the graph and summarizes them."""
+        nx_graph = self._create_nx_graph()
+        
+        # Use either Leiden algorithm (from graspologic) or an alternative
+        try:
+            from graspologic.partition import hierarchical_leiden
+            community_hierarchical_clusters = hierarchical_leiden(
+                nx_graph, max_cluster_size=self.max_cluster_size
+            )
+            self.entity_info, community_info = self._collect_community_info(
+                nx_graph, community_hierarchical_clusters
+            )
+        except ImportError:
+            # Fallback to community detection using NetworkX or python-louvain
+            try:
+                from community import best_partition
+                partition = best_partition(nx_graph)
+                # Reformat partition data to expected structure
+                clusters = []
+                for node, cluster_id in partition.items():
+                    class Cluster:
+                        def __init__(self, node, cluster):
+                            self.node = node
+                            self.cluster = cluster
+                    clusters.append(Cluster(node, cluster_id))
+                self.entity_info, community_info = self._collect_community_info(
+                    nx_graph, clusters
+                )
+            except ImportError:
+                # Use NetworkX's built-in community detection
+                from networkx.algorithms import community
+                communities = community.greedy_modularity_communities(nx_graph)
+                clusters = []
+                for i, comm in enumerate(communities):
+                    for node in comm:
+                        class Cluster:
+                            def __init__(self, node, cluster):
+                                self.node = node
+                                self.cluster = cluster
+                        clusters.append(Cluster(node, i))
+                self.entity_info, community_info = self._collect_community_info(
+                    nx_graph, clusters
+                )
+                
+        self._summarize_communities(community_info)
+
+    def _create_nx_graph(self):
+        """Converts internal graph representation to NetworkX graph."""
+        nx_graph = nx.Graph()
+        triplets = self.get_triplets()
+        for entity1, relation, entity2 in triplets:
+            nx_graph.add_node(entity1.name)
+            nx_graph.add_node(entity2.name)
+            nx_graph.add_edge(
+                relation.source_id,
+                relation.target_id,
+                relationship=relation.label,
+                description=relation.properties.get("relationship_description", "No description provided"),
+            )
+        return nx_graph
+
+    def _collect_community_info(self, nx_graph, clusters):
+        """
+        Collect information for each node based on their community,
+        allowing entities to belong to multiple clusters.
+        """
+        entity_info = defaultdict(set)
+        community_info = defaultdict(list)
+
+        for item in clusters:
+            node = item.node
+            cluster_id = item.cluster
+
+            # Update entity_info
+            entity_info[node].add(cluster_id)
+
+            for neighbor in nx_graph.neighbors(node):
+                edge_data = nx_graph.get_edge_data(node, neighbor)
+                if edge_data:
+                    detail = f"{node} -> {neighbor} -> {edge_data['relationship']} -> {edge_data['description']}"
+                    community_info[cluster_id].append(detail)
+
+        # Convert sets to lists for easier serialization if needed
+        entity_info = {k: list(v) for k, v in entity_info.items()}
+
+        return dict(entity_info), dict(community_info)
+
+    def _summarize_communities(self, community_info):
+        """Generate and store summaries for each community."""
+        for community_id, details in community_info.items():
+            details_text = "\n".join(details) + "." # Ensure it ends with a period
+            self.community_summary[community_id] = self.generate_community_summary(details_text)
+
+    def get_community_summaries(self):
+        """Returns the community summaries, building them if not already done."""
+        if not self.community_summary:
+            self.build_communities()
+        return self.community_summary
+
+# Define the GraphRAGQueryEngine class
+class GraphRAGQueryEngine(CustomQueryEngine):
+    graph_store: Union[GraphRAGStore, Any]  # Accept any type of graph store
+    index: PropertyGraphIndex
+    llm: LLM
+    similarity_top_k: int = 20
+
+    def custom_query(self, query_str: str) -> str:
+        """Process query using either community-based approach or direct retrieval."""
+        # Check if we're using GraphRAGStore with communities or SimplePropertyGraphStore
+        if hasattr(self.graph_store, 'get_community_summaries'):
+            # GraphRAG approach with communities
+            entities = self.get_entities(query_str, self.similarity_top_k)
+            
+            community_ids = self.retrieve_entity_communities(
+                self.graph_store.entity_info, entities
+            )
+            community_summaries = self.graph_store.get_community_summaries()
+            community_answers = [
+                self.generate_answer_from_summary(community_summary, query_str)
+                for id, community_summary in community_summaries.items()
+                if id in community_ids
+            ]
+
+            final_answer = self.aggregate_answers(community_answers)
+            return final_answer
+        else:
+            # Simple approach for SimplePropertyGraphStore
+            # Just get relevant nodes and generate answer
+            nodes = self.index.as_retriever(
+                similarity_top_k=self.similarity_top_k
+            ).retrieve(query_str)
+            
+            if not nodes:
+                return "I couldn't find any relevant information to answer your question."
+            
+            # Combine text from all retrieved nodes
+            context = "\n\n".join([node.get_content() for node in nodes])
+            
+            # Generate answer using the LLM
+            prompt = f"Based on the following information, please answer this question: {query_str}\n\nInformation:\n{context}"
+            messages = [
+                ChatMessage(role="system", content=prompt),
+                ChatMessage(role="user", content="Please provide a comprehensive answer based on the information provided.")
+            ]
+            response = self.llm.chat(messages)
+            return str(response).strip()
+
+    def get_entities(self, query_str, similarity_top_k):
+        nodes_retrieved = self.index.as_retriever(
+            similarity_top_k=similarity_top_k
+        ).retrieve(query_str)
+
+        entities = set()
+        pattern = r"^(\w+(?:\s+\w+)*)\s*->\s*([a-zA-Z\s]+?)\s*->\s*(\w+(?:\s+\w+)*)$"
+
+        for node in nodes_retrieved:
+            matches = re.findall(
+                pattern, node.text, re.MULTILINE | re.IGNORECASE
+            )
+            for match in matches:
+                subject = match[0]
+                obj = match[2]
+                entities.add(subject)
+                entities.add(obj)
+
+        return list(entities)
+
+    def retrieve_entity_communities(self, entity_info, entities):
+        """
+        Retrieve cluster information for given entities,
+        allowing for multiple clusters per entity.
+
+        Args:
+            entity_info (dict): Dictionary mapping entities to their cluster IDs (list).
+            entities (list): List of entity names to retrieve information for.
+
+        Returns:
+            List of community or cluster IDs to which an entity belongs.
+        """
+        community_ids = []
+
+        for entity in entities:
+            if entity in entity_info:
+                community_ids.extend(entity_info[entity])
+
+        return list(set(community_ids))
+
+    def generate_answer_from_summary(self, community_summary, query):
+        """Generate an answer from a community summary based on a given query using LLM."""
+        prompt = (
+            f"Given the community summary: {community_summary}, "
+            f"how would you answer the following query? Query: {query}"
+        )
+        messages = [
+            ChatMessage(role="system", content=prompt),
+            ChatMessage(
+                role="user",
+                content="I need an answer based on the above information.",
+            ),
+        ]
+        response = self.llm.chat(messages)
+        cleaned_response = re.sub(r"^assistant:\s*", "", str(response)).strip()
+        return cleaned_response
+
+    def aggregate_answers(self, community_answers):
+        """Aggregate individual community answers into a final, coherent response."""
+        prompt = "Combine the following intermediate answers into a final, concise response."
+        messages = [
+            ChatMessage(role="system", content=prompt),
+            ChatMessage(
+                role="user",
+                content=f"Intermediate answers: {community_answers}",
+            ),
+        ]
+        final_response = self.llm.chat(messages)
+        cleaned_final_response = re.sub(
+            r"^assistant:\s*", "", str(final_response)
+        ).strip()
+        return cleaned_final_response
+
+def custom_parse_fn(response_str: str) -> Any:
+    """Custom parser for LLM responses that extract entities and relationships"""
+    json_pattern = r"\{.*\}"
+    match = re.search(json_pattern, response_str, re.DOTALL)
+    entities = []
+    relationships = []
+    
+    if not match:
+        return entities, relationships
+    
+    json_str = match.group(0)
+    try:
+        data = json.loads(json_str)
+        entities = [
+            (
+                entity["entity_name"],
+                entity["entity_type"],
+                entity.get("entity_description", f"Description of {entity['entity_name']}"),
+            )
+            for entity in data.get("entities", [])
+        ]
+        relationships = [
+            (
+                relation["source_entity"],
+                relation["target_entity"],
+                relation["relation"],
+                relation.get("relationship_description", f"Relationship between {relation['source_entity']} and {relation['target_entity']}"),
+            )
+            for relation in data.get("relationships", [])
+        ]
+        return entities, relationships
+    except (json.JSONDecodeError, KeyError) as e:
+        print(f"Error parsing response: {e}")
+        print(f"Problematic JSON: {json_str[:200]}...")
+        return entities, relationships
+
+# Define the prompt template for triple extraction
+KG_TRIPLET_EXTRACT_TMPL = """
+-Goal-
+Given a text document, identify all entities and their entity types from the text and all relationships among the identified entities.
+
+Given the text, extract up to {max_knowledge_triplets} entity-relation triplets.
+
+-Steps-
+1. Identify all entities. For each identified entity, extract the following information:
+- entity_name: Name of the entity, capitalized
+- entity_type: Type of the entity
+- entity_description: Comprehensive description of the entity's attributes and activities
+
+2. From the entities identified in step 1, identify all pairs of (source_entity, target_entity) that are *clearly related* to each other.
+For each pair of related entities, extract the following information:
+- source_entity: name of the source entity, as identified in step 1
+- target_entity: name of the target entity, as identified in step 1
+- relation: relationship between source_entity and target_entity
+- relationship_description: explanation as to why you think the source entity and the target entity are related to each other
+
+3. Output Formatting:
+- Return the result in valid JSON format with two keys: 'entities' (list of entity objects) and 'relationships' (list of relationship objects).
+- Exclude any text outside the JSON structure (e.g., no explanations or comments).
+- If no entities or relationships are identified, return empty lists: { "entities": [], "relationships": [] }.
+
+-Real Data-
+######################
+text: {text}
+######################
+output:
+"""
+
+def main():
+    print("Starting GraphRAG document processing...")
+    
+    # Load documents from specified directory
+    documents = SimpleDirectoryReader(
+        input_dir="supporting_files/files_for_rag_store"
+    ).load_data()
+    
+    print(f"Loaded {len(documents)} documents")
+    
+    # Create nodes using a sentence splitter
+    splitter = SentenceSplitter(chunk_size=1024, chunk_overlap=20)
+    nodes = splitter.get_nodes_from_documents(documents)
+    
+    print(f"Created {len(nodes)} nodes from documents")
+    
+    # Initialize the LLM
+    llm = OpenAI(model="gpt-4")
+    
+    # Create the knowledge graph extractor
+    kg_extractor = GraphRAGExtractor(
+        llm=llm,
+        extract_prompt=KG_TRIPLET_EXTRACT_TMPL,
+        max_paths_per_chunk=2,
+        parse_fn=custom_parse_fn,
+    )
+    
+    # Connect to Neo4j running in Docker
+    neo4j_username = "neo4j"
+    neo4j_password = "tavern-easy-museum-arthur-coconut-3483"
+    neo4j_url = "bolt://localhost:7687"
+    
+    print(f"Connecting to Neo4j at {neo4j_url} with username '{neo4j_username}'")
+    
+    # Create GraphRAGStore (our extended Neo4j store)
+    try:
+        graph_store = GraphRAGStore(
+            username=neo4j_username, 
+            password=neo4j_password,
+            url=neo4j_url
+        )
+        print("Successfully connected to Neo4j database")
+    except Exception as e:
+        print(f"Error connecting to Neo4j: {e}")
+        print("Falling back to in-memory graph store. Some features may be limited.")
+        # Fallback to in-memory graph store if Neo4j connection fails
+        from llama_index.core.graph_stores import SimplePropertyGraphStore
+        graph_store = SimplePropertyGraphStore()
+    
+    # Build the index
+    index = PropertyGraphIndex(
+        nodes=nodes,
+        kg_extractors=[kg_extractor],
+        property_graph_store=graph_store,
+        show_progress=True,
+    )
+    
+    print("Building graph communities...")
+    try:
+        # Build communities for graph-based querying
+        # Only for GraphRAGStore, not for SimplePropertyGraphStore
+        if hasattr(graph_store, 'build_communities'):
+            graph_store.build_communities()
+            print("Communities built successfully")
+        else:
+            print("Skipping community building (not using Neo4j)")
+    except Exception as e:
+        print(f"Error building communities: {e}")
+    
+    # Create the query engine
+    query_engine = GraphRAGQueryEngine(
+        graph_store=graph_store,
+        llm=llm,
+        index=index,
+        similarity_top_k=10,
+    )
+    
+    # Simple interactive query loop
+    print("\n--- GraphRAG Query System Ready ---")
+    print("Type 'exit' to quit")
+    
+    while True:
+        query = input("\nEnter your query: ")
+        
+        if query.lower() in ('exit', 'quit'):
+            break
+            
+        try:
+            response = query_engine.custom_query(query)
+            print("\nResponse:")
+            print(response)
+        except Exception as e:
+            print(f"Error processing query: {e}")
+    
+    print("GraphRAG session ended")
+
+if __name__ == "__main__":
+    main()
\ No newline at end of file
diff --git a/graph_rag_integration.py b/graph_rag_integration.py
new file mode 100644
index 0000000..a0dd2e0
--- /dev/null
+++ b/graph_rag_integration.py
@@ -0,0 +1,882 @@
+"""
+HP GraphRAG Integration
+
+Integrates GraphRAG functionality into the HP RAG pipeline.
+- GraphRAG for knowledge graph construction from semantically split nodes
+- Community detection and summarization for improved context retrieval
+"""
+
+import os
+import json
+import re
+import asyncio
+import networkx as nx
+from collections import defaultdict
+from typing import Any, List, Callable, Optional, Union, Dict
+from pathlib import Path
+
+# Import LlamaIndex components
+from llama_index.core import Document, Settings
+from llama_index.core.node_parser import SentenceSplitter
+from llama_index.core import PropertyGraphIndex
+from llama_index.core.async_utils import run_jobs
+from llama_index.core.indices.property_graph.utils import default_parse_triplets_fn
+from llama_index.core.graph_stores.types import EntityNode, KG_NODES_KEY, KG_RELATIONS_KEY, Relation
+from llama_index.core.llms.llm import LLM
+from llama_index.core.prompts import PromptTemplate
+from llama_index.core.prompts.default_prompts import DEFAULT_KG_TRIPLET_EXTRACT_PROMPT
+from llama_index.core.schema import TransformComponent, BaseNode
+from llama_index.core.query_engine import CustomQueryEngine
+from llama_index.llms.openai import OpenAI
+from llama_index.core.llms import ChatMessage
+from llama_index.graph_stores.neo4j import Neo4jPropertyGraphStore
+from llama_index.core import SimpleDirectoryReader
+from llama_index.core.vector_stores.types import VectorStoreInfo, MetadataInfo
+from llama_index.core.retrievers import VectorIndexRetriever
+
+# Community detection (using NetworkX instead of graspologic as a fallback)
+try:
+    from community import best_partition  # python-louvain package
+except ImportError:
+    print("Community detection package not found, using NetworkX built-in community detection")
+
+# Import from our modules
+from utils import logger, log_structured
+from config import NEO4J_URL, NEO4J_USERNAME, NEO4J_PASSWORD
+import config
+
+# Define the GraphRAGExtractor class
+class GraphRAGExtractor(TransformComponent):
+    """Extract triples from a graph.
+
+    Uses an LLM and a simple prompt + output parsing to
+    extract paths (i.e. triples) and entity, relation descriptions
+    from text.
+
+    Args:
+        llm (LLM):
+            The language model to use.
+        extract_prompt (Union[str, PromptTemplate]):
+            The prompt to use for extracting triples.
+        parse_fn (callable):
+            A function to parse the output of the language
+            model.
+        num_workers (int):
+            The number of workers to use for parallel
+            processing.
+        max_paths_per_chunk (int):
+            The maximum number of paths to extract per chunk.
+    """
+
+    llm: LLM
+    extract_prompt: PromptTemplate
+    parse_fn: Callable
+    num_workers: int
+    max_paths_per_chunk: int
+
+    def __init__(
+        self,
+        llm: Optional[LLM] = None,
+        extract_prompt: Optional[Union[str, PromptTemplate]] = None,
+        parse_fn: Callable = default_parse_triplets_fn,
+        max_paths_per_chunk: int = 10,
+        num_workers: int = 8,
+    ) -> None:
+        """Init params."""
+        from llama_index.core import Settings
+
+        if isinstance(extract_prompt, str):
+            extract_prompt = PromptTemplate(extract_prompt)
+
+        super().__init__(
+            llm=llm or Settings.llm,
+            extract_prompt=extract_prompt or DEFAULT_KG_TRIPLET_EXTRACT_PROMPT,
+            parse_fn=parse_fn,
+            num_workers=num_workers,
+            max_paths_per_chunk=max_paths_per_chunk,
+        )
+
+    @classmethod
+    def class_name(cls) -> str:
+        return "GraphExtractor"
+
+    def __call__(
+        self, nodes: List[BaseNode], show_progress: bool = False, **kwargs: Any
+    ) -> List[BaseNode]:
+        """Extract triples from nodes."""
+        return asyncio.run(
+            self.acall(nodes, show_progress=show_progress, **kwargs)
+        )
+
+    async def _aextract(self, node: BaseNode) -> BaseNode:
+        """Extract triples from a node."""
+        assert hasattr(node, "text")
+
+        text = node.get_content(metadata_mode="llm")
+        try:
+            llm_response = await self.llm.apredict(
+                self.extract_prompt,
+                text=text,
+                max_knowledge_triplets=self.max_paths_per_chunk,
+            )
+            entities, entities_relationship = self.parse_fn(llm_response)
+        except ValueError:
+            entities = []
+            entities_relationship = []
+
+        existing_nodes = node.metadata.pop(KG_NODES_KEY, [])
+        existing_relations = node.metadata.pop(KG_RELATIONS_KEY, [])
+        
+        entity_metadata = node.metadata.copy()
+        for entity, entity_type, description in entities:
+            entity_metadata["entity_description"] = description
+            entity_node = EntityNode(
+                name=entity, label=entity_type,
+                properties=entity_metadata
+            )
+            existing_nodes.append(entity_node)
+
+        relation_metadata = node.metadata.copy()
+        for triple in entities_relationship:
+            subj, obj, rel, description = triple
+            relation_metadata["relationship_description"] = description
+            rel_node = Relation(
+                label=rel,
+                source_id=subj,
+                target_id=obj,
+                properties=relation_metadata,
+            )
+            existing_relations.append(rel_node)
+
+        node.metadata[KG_NODES_KEY] = existing_nodes
+        node.metadata[KG_RELATIONS_KEY] = existing_relations
+        return node
+
+    async def acall(
+        self, nodes: List[BaseNode], show_progress: bool = False, **kwargs: Any
+    ) -> List[BaseNode]:
+        """Extract triples from nodes async."""
+        jobs = []
+        for node in nodes:
+            jobs.append(self._aextract(node))
+
+        return await run_jobs(
+            jobs,
+            workers=self.num_workers,
+            show_progress=show_progress,
+            desc="Extracting paths from text",
+        )
+
+# Define the GraphRAGStore class (integrating with Neo4j)
+import pickle
+import os
+from pathlib import Path
+
+class GraphRAGStore:
+    community_summary = {}
+    entity_info = None
+    max_cluster_size = 5
+    property_graph_store = None
+    communities_built = False  # Track if communities have been built
+    
+    # Path for cached community data
+    CACHE_DIR = Path("index_storage/graphrag_cache")
+    COMMUNITY_CACHE_FILE = CACHE_DIR / "community_summary.pickle"
+    ENTITY_INFO_CACHE_FILE = CACHE_DIR / "entity_info.pickle"
+    
+    def __init__(self, property_graph_store):
+        """Initialize with a property_graph_store (Neo4j or in-memory)."""
+        self.property_graph_store = property_graph_store
+        self.community_summary = {}
+        self.entity_info = None
+        self.communities_built = False
+        
+        # Ensure cache directory exists
+        os.makedirs(self.CACHE_DIR, exist_ok=True)
+        
+    def add_nodes(self, nodes):
+        """Add nodes to the property graph store."""
+        return self.property_graph_store.add_nodes(nodes)
+        
+    def add_relationships(self, relationships):
+        """Add relationships to the property graph store."""
+        return self.property_graph_store.add_relationships(relationships)
+    
+    def get_triplets(self):
+        """Get triplets from the property graph store."""
+        return self.property_graph_store.get_triplets()
+
+    def generate_community_summary(self, text):
+        """Generate summary for a given text using an LLM with handling for large contexts."""
+        
+        # Check if text is too long and chunk if needed
+        if len(text) > 30000:  # Approximate character limit
+            log_structured('info', f'Community text is large ({len(text)} chars). Chunking for summarization.')
+            # Split into smaller chunks (simple approach)
+            chunks = [text[i:i+30000] for i in range(0, len(text), 30000)]
+            summaries = []
+            
+            for i, chunk in enumerate(chunks):
+                try:
+                    # Use GPT-4o-mini model for better cost efficiency
+                    llm = OpenAI(model="gpt-4o-mini")
+                    messages = [
+                        ChatMessage(
+                            role="system",
+                            content="Summarize these knowledge graph relationships concisely."
+                        ),
+                        ChatMessage(role="user", content=chunk),
+                    ]
+                    response = llm.chat(messages)
+                    summaries.append(str(response).strip())
+                    log_structured('info', f'Successfully summarized community chunk {i+1}/{len(chunks)}')
+                except Exception as e:
+                    log_structured('error', f'Error summarizing community chunk {i+1}/{len(chunks)}: {e}')
+                    
+            # Then summarize the summaries
+            if summaries:
+                final_summary_text = "\n\n".join(summaries)
+                try:
+                    llm = OpenAI(model="gpt-4o-mini")
+                    messages = [
+                        ChatMessage(
+                            role="system",
+                            content="Create a coherent summary from these partial summaries."
+                        ),
+                        ChatMessage(role="user", content=final_summary_text),
+                    ]
+                    response = llm.chat(messages)
+                    return str(response).strip()
+                except Exception as e:
+                    log_structured('error', f'Error creating final summary from chunks: {e}')
+                    # Return the concatenated summaries if we can't summarize them
+                    return final_summary_text
+            else:
+                return "Unable to generate community summary due to size limitations."
+        
+        # For normal size text, use the larger model directly
+        try:
+            # Use GPT-4o-mini model for better cost efficiency
+            llm = OpenAI(model="gpt-4o-mini")
+            messages = [
+                ChatMessage(
+                    role="system",
+                    content=(
+                        "You are provided with a set of "
+                        "relationships from a knowledge graph, each represented as "
+                        "entity1->entity2->relation-"
+                        ">relationship_description. Your task is to create a summary of "
+                        "these relationships. The summary should include "
+                        "the names of the entities involved and a concise synthesis "
+                        "of the relationship descriptions. The "
+                        "goal is to capture the most critical and relevant details that "
+                        "highlight the nature and significance of "
+                        "each relationship. Ensure that the summary is coherent and "
+                        "integrates the information in a way that "
+                        "emphasizes the key aspects of the relationships."
+                    ),
+                ),
+                ChatMessage(role="user", content=text),
+            ]
+            response = llm.chat(messages)
+            clean_response = re.sub(r"^assistant:\s*", "", str(response)).strip()
+            return clean_response
+        except Exception as e:
+            log_structured('error', f'Error generating community summary: {e}')
+            return f"Error summarizing community: {str(e)}"
+
+    def build_communities(self):
+        """Builds communities from the graph and summarizes them."""
+        # Skip if communities are already built in this session
+        if self.communities_built:
+            log_structured('info', 'Communities already built in this session, skipping rebuild')
+            return
+            
+        # First check if we can load from cache
+        if self.load_from_cache():
+            log_structured('info', 'Using cached community data instead of rebuilding')
+            self.communities_built = True
+            return
+            
+        log_structured('info', 'Building communities from graph data')
+        nx_graph = self._create_nx_graph()
+        
+        # Use either Leiden algorithm (from graspologic) or an alternative
+        try:
+            from graspologic.partition import hierarchical_leiden
+            community_hierarchical_clusters = hierarchical_leiden(
+                nx_graph, max_cluster_size=self.max_cluster_size
+            )
+            self.entity_info, community_info = self._collect_community_info(
+                nx_graph, community_hierarchical_clusters
+            )
+        except ImportError:
+            # Fallback to community detection using NetworkX or python-louvain
+            try:
+                from community import best_partition
+                partition = best_partition(nx_graph)
+                # Reformat partition data to expected structure
+                clusters = []
+                for node, cluster_id in partition.items():
+                    class Cluster:
+                        def __init__(self, node, cluster):
+                            self.node = node
+                            self.cluster = cluster
+                    clusters.append(Cluster(node, cluster_id))
+                self.entity_info, community_info = self._collect_community_info(
+                    nx_graph, clusters
+                )
+            except ImportError:
+                # Use NetworkX's built-in community detection
+                from networkx.algorithms import community
+                communities = community.greedy_modularity_communities(nx_graph)
+                clusters = []
+                for i, comm in enumerate(communities):
+                    for node in comm:
+                        class Cluster:
+                            def __init__(self, node, cluster):
+                                self.node = node
+                                self.cluster = cluster
+                        clusters.append(Cluster(node, i))
+                self.entity_info, community_info = self._collect_community_info(
+                    nx_graph, clusters
+                )
+                
+        self._summarize_communities(community_info)
+        
+        # Cache the results after building
+        self.save_to_cache()
+        
+        # Mark communities as built for this session
+        self.communities_built = True
+
+    def _create_nx_graph(self):
+        """Converts internal graph representation to NetworkX graph."""
+        nx_graph = nx.Graph()
+        triplets = self.get_triplets()
+        for entity1, relation, entity2 in triplets:
+            nx_graph.add_node(entity1.name)
+            nx_graph.add_node(entity2.name)
+            nx_graph.add_edge(
+                relation.source_id,
+                relation.target_id,
+                relationship=relation.label,
+                description=relation.properties.get("relationship_description", "No description provided"),
+            )
+        return nx_graph
+
+    def _collect_community_info(self, nx_graph, clusters):
+        """
+        Collect information for each node based on their community,
+        allowing entities to belong to multiple clusters.
+        """
+        entity_info = defaultdict(set)
+        community_info = defaultdict(list)
+
+        for item in clusters:
+            node = item.node
+            cluster_id = item.cluster
+
+            # Update entity_info
+            entity_info[node].add(cluster_id)
+
+            for neighbor in nx_graph.neighbors(node):
+                edge_data = nx_graph.get_edge_data(node, neighbor)
+                if edge_data:
+                    detail = f"{node} -> {neighbor} -> {edge_data['relationship']} -> {edge_data['description']}"
+                    community_info[cluster_id].append(detail)
+
+        # Convert sets to lists for easier serialization if needed
+        entity_info = {k: list(v) for k, v in entity_info.items()}
+
+        return dict(entity_info), dict(community_info)
+
+    def _summarize_communities(self, community_info):
+        """Generate and store summaries for each community."""
+        for community_id, details in community_info.items():
+            details_text = "\n".join(details) + "." # Ensure it ends with a period
+            self.community_summary[community_id] = self.generate_community_summary(details_text)
+
+    def save_to_cache(self):
+        """Save community data to disk cache."""
+        try:
+            # Save community summary
+            with open(self.COMMUNITY_CACHE_FILE, 'wb') as f:
+                pickle.dump(self.community_summary, f)
+            
+            # Save entity info
+            with open(self.ENTITY_INFO_CACHE_FILE, 'wb') as f:
+                pickle.dump(self.entity_info, f)
+                
+            log_structured('info', 'Successfully cached GraphRAG community data', 
+                        {'community_count': len(self.community_summary),
+                         'entity_count': len(self.entity_info) if self.entity_info else 0})
+            return True
+        except Exception as e:
+            log_structured('error', f'Error saving GraphRAG cache: {e}')
+            return False
+    
+    def load_from_cache(self):
+        """Load community data from disk cache if available."""
+        if not self.COMMUNITY_CACHE_FILE.exists() or not self.ENTITY_INFO_CACHE_FILE.exists():
+            log_structured('info', 'GraphRAG cache files not found, will build communities from scratch')
+            return False
+        
+        try:
+            # Load community summary
+            with open(self.COMMUNITY_CACHE_FILE, 'rb') as f:
+                self.community_summary = pickle.load(f)
+            
+            # Load entity info
+            with open(self.ENTITY_INFO_CACHE_FILE, 'rb') as f:
+                self.entity_info = pickle.load(f)
+                
+            log_structured('info', 'Successfully loaded GraphRAG community data from cache', 
+                        {'community_count': len(self.community_summary),
+                         'entity_count': len(self.entity_info) if self.entity_info else 0})
+            
+            # Mark communities as built when successfully loaded from cache
+            self.communities_built = True
+            return True
+        except Exception as e:
+            log_structured('error', f'Error loading GraphRAG cache: {e}')
+            # Reset to empty in case of partial load
+            self.community_summary = {}
+            self.entity_info = None
+            self.communities_built = False
+            return False
+            
+    def get_community_summaries(self):
+        """Returns the community summaries, building them if not already done."""
+        if not self.community_summary:
+            # Try to load from cache first
+            if not self.load_from_cache():
+                # If cache load fails, build from scratch
+                self.build_communities()
+                # Cache the results for next time
+                self.save_to_cache()
+        return self.community_summary
+
+# Define the GraphRAGQueryEngine class
+from typing import Dict, Any
+
+class GraphRAGQueryEngine:
+    """Query engine that combines vector retrieval with graph-based community retrieval."""
+    
+    def __init__(
+        self, 
+        vector_retriever: VectorIndexRetriever,
+        graph_store: GraphRAGStore,
+        llm: Optional[LLM] = None,
+        similarity_top_k: int = 20
+    ):
+        """Initialize with both a vector retriever and graph store."""
+        # Initialize all required fields
+        self.vector_retriever = vector_retriever
+        self.graph_store = graph_store
+        self.llm = llm or Settings.llm
+        self.similarity_top_k = similarity_top_k
+        
+        # Check if communities are built, but don't try to build them here
+        # since that might cause errors with large graphs
+        if not hasattr(self.graph_store, 'entity_info') or self.graph_store.entity_info is None:
+            log_structured('warning', 'GraphRAGQueryEngine initialized without community data. Vector retrieval will still work, but community retrieval may be limited.')
+
+    def custom_query(self, query_str: str) -> Dict:
+        """Process query using both vector retrieval and community-based approach."""
+        log_structured('info', 'GraphRAG query engine: Starting dual retrieval', {'query': query_str})
+        
+        # Step 1: Get vector search results
+        vector_nodes = self.vector_retriever.retrieve(query_str)
+        vector_context = "\n\n".join([node.node.get_content() for node in vector_nodes])
+        log_structured('info', 'GraphRAG query engine: Vector retrieval complete', 
+                     {'node_count': len(vector_nodes)})
+        
+        # Step 2: Get GraphRAG community results (if communities exist)
+        graphrag_context = ""
+        community_ids = []
+        
+        if hasattr(self.graph_store, 'entity_info') and self.graph_store.entity_info is not None:
+            try:
+                entities = self.get_entities(query_str, vector_nodes)
+                community_ids = self.retrieve_entity_communities(self.graph_store.entity_info, entities)
+                
+                try:
+                    community_summaries = self.graph_store.get_community_summaries()
+                    
+                    if community_ids:
+                        filtered_summaries = {id: summary for id, summary in community_summaries.items() 
+                                          if id in community_ids}
+                        graphrag_context = "\n\n".join(filtered_summaries.values())
+                        log_structured('info', 'GraphRAG query engine: Community retrieval complete', 
+                                    {'community_count': len(filtered_summaries)})
+                    else:
+                        log_structured('info', 'GraphRAG query engine: No relevant communities found')
+                except Exception as e:
+                    log_structured('error', f'Error getting community summaries: {e}')
+                    # Continue without graph context
+            except Exception as e:
+                log_structured('error', f'Error during community retrieval: {e}')
+                # Continue with just vector context
+        else:
+            log_structured('warning', 'GraphRAG query engine: No community data available. Using only vector retrieval.')
+        
+        # Step 3: Combine contexts and generate answer
+        combined_result = {
+            "vector_context": vector_context,
+            "graphrag_context": graphrag_context,
+            "vector_nodes": vector_nodes,
+            "community_ids": community_ids
+        }
+        
+        return combined_result
+
+    def get_entities(self, query_str, vector_nodes):
+        """Extract entities from vector nodes that match the query."""
+        entities = set()
+        
+        # Extract entities from the retrieved nodes
+        for node_with_score in vector_nodes:
+            node = node_with_score.node
+            if hasattr(node, 'metadata') and KG_NODES_KEY in node.metadata:
+                for entity_node in node.metadata[KG_NODES_KEY]:
+                    if hasattr(entity_node, 'name'):
+                        entities.add(entity_node.name)
+        
+        # If no entities were found in metadata, try extracting them from text
+        if not entities:
+            pattern = r"(?:^|\s)([A-Z][a-zA-Z0-9\s]+)(?:\s|$)"
+            for node_with_score in vector_nodes:
+                matches = re.findall(pattern, node_with_score.node.get_content())
+                entities.update(matches)
+        
+        log_structured('debug', 'GraphRAG query engine: Extracted entities', 
+                     {'entities': list(entities), 'count': len(entities)})
+        return list(entities)
+
+    def retrieve_entity_communities(self, entity_info, entities):
+        """
+        Retrieve cluster information for given entities,
+        allowing for multiple clusters per entity.
+
+        Args:
+            entity_info (dict): Dictionary mapping entities to their cluster IDs (list).
+            entities (list): List of entity names to retrieve information for.
+
+        Returns:
+            List of community or cluster IDs to which an entity belongs.
+        """
+        community_ids = []
+
+        for entity in entities:
+            if entity in entity_info:
+                community_ids.extend(entity_info[entity])
+            else:
+                # Try case-insensitive matching as fallback
+                for stored_entity, clusters in entity_info.items():
+                    if stored_entity.lower() == entity.lower():
+                        community_ids.extend(clusters)
+                        break
+
+        return list(set(community_ids))
+
+def custom_parse_fn(response_str: str) -> Any:
+    """Custom parser for LLM responses that extract entities and relationships"""
+    json_pattern = r"\{.*\}"
+    match = re.search(json_pattern, response_str, re.DOTALL)
+    entities = []
+    relationships = []
+    
+    if not match:
+        return entities, relationships
+    
+    json_str = match.group(0)
+    try:
+        data = json.loads(json_str)
+        entities = [
+            (
+                entity["entity_name"],
+                entity["entity_type"],
+                entity.get("entity_description", f"Description of {entity['entity_name']}"),
+            )
+            for entity in data.get("entities", [])
+        ]
+        relationships = [
+            (
+                relation["source_entity"],
+                relation["target_entity"],
+                relation["relation"],
+                relation.get("relationship_description", f"Relationship between {relation['source_entity']} and {relation['target_entity']}"),
+            )
+            for relation in data.get("relationships", [])
+        ]
+        return entities, relationships
+    except (json.JSONDecodeError, KeyError) as e:
+        log_structured('error', f"Error parsing response: {e}", {'json_str': json_str[:200]})
+        return entities, relationships
+
+# Define the prompt template for triple extraction
+KG_TRIPLET_EXTRACT_TMPL = """
+-Goal-
+Given a text document, identify all entities and their entity types from the text and all relationships among the identified entities.
+
+Given the text, extract up to {max_knowledge_triplets} entity-relation triplets.
+
+-Steps-
+1. Identify all entities. For each identified entity, extract the following information:
+- entity_name: Name of the entity, capitalized
+- entity_type: Type of the entity
+- entity_description: Comprehensive description of the entity's attributes and activities
+
+2. From the entities identified in step 1, identify all pairs of (source_entity, target_entity) that are *clearly related* to each other.
+For each pair of related entities, extract the following information:
+- source_entity: name of the source entity, as identified in step 1
+- target_entity: name of the target entity, as identified in step 1
+- relation: relationship between source_entity and target_entity
+- relationship_description: explanation as to why you think the source entity and the target entity are related to each other
+
+3. Output Formatting:
+- Return the result in valid JSON format with two keys: 'entities' (list of entity objects) and 'relationships' (list of relationship objects).
+- Exclude any text outside the JSON structure (e.g., no explanations or comments).
+- If no entities or relationships are identified, return empty lists: { "entities": [], "relationships": [] }.
+
+-Real Data-
+######################
+text: {text}
+######################
+output:
+"""
+
+def create_graph_components(llm, nodes=None, max_paths_per_chunk=10, force_reindex=False):
+    """
+    Create GraphRAG components for the HP RAG pipeline.
+    
+    Args:
+        llm: The LLM to use for graph extraction and querying
+        nodes: List of nodes to create the graph from (only used if indexing is needed)
+        max_paths_per_chunk: Maximum number of paths to extract per chunk
+        force_reindex: If True, always recreate the index even if content exists
+        
+    Returns:
+        tuple: (graph_store, property_graph_index)
+    """
+    log_structured('info', 'Creating GraphRAG components')
+    
+    # Note: The graph_store object created here will automatically:
+    # 1. Try to load community data from cache files when build_communities() is called
+    # 2. Save to cache after building communities if loading failed
+    
+    # Connect to Neo4j - hard error if not available
+    property_graph_store = None
+    try:
+        log_structured('info', f'Connecting to Neo4j at {NEO4J_URL}')
+        property_graph_store = Neo4jPropertyGraphStore(
+            username=NEO4J_USERNAME, 
+            password=NEO4J_PASSWORD,
+            url=NEO4J_URL
+        )
+        log_structured('info', 'Successfully connected to Neo4j database')
+    except Exception as e:
+        log_structured('critical', f'FATAL ERROR: Cannot connect to Neo4j: {e}')
+        raise RuntimeError(f"Neo4j connection failed. This application requires Neo4j to be running. Error: {e}")
+    
+    # Create GraphRAGStore wrapper
+    graph_store = GraphRAGStore(property_graph_store)
+    
+    # Check if Neo4j already has content
+    triplets = graph_store.get_triplets()
+    has_existing_content = len(triplets) > 0
+    
+    log_structured('info', f'Neo4j check: Found {len(triplets)} triplets')
+    
+    if has_existing_content and not force_reindex:
+        log_structured('info', f'Neo4j already contains {len(triplets)} triplets. Skipping indexing.')
+        
+        # Create a minimal PropertyGraphIndex without indexing
+        property_graph_index = PropertyGraphIndex(
+            nodes=[],  # Empty nodes since we're not indexing
+            property_graph_store=property_graph_store,
+        )
+        
+        # Build communities from existing data (if not already built)
+        if not graph_store.communities_built:
+            log_structured('info', 'Building graph communities from existing Neo4j data')
+            try:
+                graph_store.build_communities()
+                log_structured('info', 'Communities built successfully')
+            except Exception as e:
+                log_structured('error', f'Error building communities: {e}')
+        else:
+            log_structured('info', 'Communities already built, skipping rebuild')
+    else:
+        # Need to perform indexing
+        if not nodes:
+            log_structured('error', 'No nodes provided for indexing and Neo4j is empty or force_reindex=True')
+            raise ValueError("Nodes must be provided for indexing when Neo4j is empty or force_reindex=True")
+        
+        # Create the knowledge graph extractor
+        kg_extractor = GraphRAGExtractor(
+            llm=llm,
+            extract_prompt=KG_TRIPLET_EXTRACT_TMPL,
+            max_paths_per_chunk=max_paths_per_chunk,
+            parse_fn=custom_parse_fn,
+        )
+        
+        if has_existing_content and force_reindex:
+            log_structured('info', 'Force reindexing requested. Clearing existing Neo4j data.')
+            try:
+                # Try to clear the graph using Neo4j's native query
+                # Note: This requires the Neo4j APOC plugin to be installed
+                from neo4j import GraphDatabase
+                driver = GraphDatabase.driver(NEO4J_URL, auth=(NEO4J_USERNAME, NEO4J_PASSWORD))
+                with driver.session() as session:
+                    session.run("MATCH (n) DETACH DELETE n")
+                driver.close()
+                log_structured('info', 'Successfully cleared Neo4j database')
+            except Exception as e:
+                log_structured('warning', f'Error clearing Neo4j database: {e}. Proceeding with indexing anyway.')
+        
+        # Build the property graph index
+        log_structured('info', 'Building PropertyGraphIndex', {'node_count': len(nodes)})
+        property_graph_index = PropertyGraphIndex(
+            nodes=nodes,
+            kg_extractors=[kg_extractor],
+            property_graph_store=property_graph_store,
+            show_progress=True,
+        )
+        
+        # Build communities
+        log_structured('info', 'Building graph communities')
+        try:
+            graph_store.build_communities()
+            log_structured('info', 'Communities built successfully')
+        except Exception as e:
+            log_structured('error', f'Error building communities: {e}')
+    
+    return graph_store, property_graph_index
+
+def create_graphrag_query_engine(vector_retriever, graph_store, llm, similarity_top_k=20):
+    """
+    Create GraphRAG query engine that combines vector and graph-based retrieval.
+    
+    Args:
+        vector_retriever: VectorIndexRetriever for standard retrieval
+        graph_store: GraphRAGStore for community-based retrieval
+        llm: LLM for generating answer
+        similarity_top_k: Number of top results to retrieve
+        
+    Returns:
+        GraphRAGQueryEngine: Query engine for hybrid retrieval
+    """
+    from utils import log_structured
+    
+    try:
+        # Explicitly validate inputs before passing to constructor
+        if vector_retriever is None:
+            raise ValueError("vector_retriever cannot be None")
+        if graph_store is None:
+            raise ValueError("graph_store cannot be None")
+        if llm is None:
+            raise ValueError("llm cannot be None")
+            
+        # Log for debugging
+        log_structured('debug', 'Creating GraphRAGQueryEngine with parameters', {
+            'vector_retriever_type': type(vector_retriever).__name__,
+            'graph_store_type': type(graph_store).__name__,
+            'llm_type': type(llm).__name__,
+            'similarity_top_k': similarity_top_k
+        })
+        
+        # Create the engine
+        return GraphRAGQueryEngine(
+            vector_retriever=vector_retriever,
+            graph_store=graph_store,
+            llm=llm,
+            similarity_top_k=similarity_top_k,
+        )
+    except Exception as e:
+        log_structured('error', f'Error in create_graphrag_query_engine: {e}')
+        raise  # Re-raise the exception for proper handling
+
+def generate_final_answer(query, retrieval_result, llm):
+    """
+    Generate a final answer using both vector and graph-based context.
+    
+    Args:
+        query: The user's query
+        retrieval_result: Result from GraphRAGQueryEngine with vector and graph contexts
+        llm: LLM for generating the final response
+        
+    Returns:
+        str: The final answer
+    """
+    vector_context = retrieval_result.get("vector_context", "")
+    graphrag_context = retrieval_result.get("graphrag_context", "")
+    
+    # Log the contexts for debugging (truncated for brevity)
+    log_structured('debug', 'Generating final answer with dual context', {
+        'query': query,
+        'vector_context_length': len(vector_context),
+        'graphrag_context_length': len(graphrag_context)
+    })
+    
+    if not vector_context and not graphrag_context:
+        return "I couldn't find any relevant information to answer your question."
+    
+    # If no model was provided or we're forcing to use a specific model
+    if llm is None or not hasattr(llm, 'chat'):
+        # Fallback to gpt-4o-mini for better cost efficiency
+        llm = OpenAI(model="gpt-4o-mini")
+        log_structured('info', 'Using gpt-4o-mini model for final answer generation')
+    
+    prompt = f"""
+    Based on the following information from two different sources, please answer this question: {query}
+
+    SOURCE 1 - VECTOR RETRIEVAL:
+    {vector_context}
+
+    SOURCE 2 - KNOWLEDGE GRAPH COMMUNITIES:
+    {graphrag_context}
+    
+    Answer the question based on all the provided information. If there are differences between the sources, 
+    try to reconcile them or note the discrepancy. Please be concise and direct.
+    """
+    
+    messages = [
+        ChatMessage(role="system", content=prompt),
+        ChatMessage(role="user", content="Please provide a comprehensive answer based on all the information provided.")
+    ]
+    
+    response = llm.chat(messages)
+    
+    # Extract just the message content, not the entire response object
+    if hasattr(response, 'message') and hasattr(response.message, 'content'):
+        content = response.message.content
+    elif hasattr(response, 'content'):
+        content = response.content
+    else:
+        # Fallback: convert to string but clean it
+        content = str(response)
+    
+    # Clean any remaining thinking patterns from the response
+    import re
+    thinking_patterns = [
+        r'(?i)Thought:.*?Action:.*?Action Input:.*',  # Remove the specific pattern
+        r'(?i)^Thought:.*',  # Remove any line starting with "Thought:"
+        r'(?i)Action:.*?Action Input:.*',  # Remove Action/Action Input patterns
+        r'(?i)^(Thought|Action|Observation):.*',  # Remove ReAct patterns
+    ]
+    
+    for pattern in thinking_patterns:
+        content = re.sub(pattern, '', content, flags=re.DOTALL | re.MULTILINE)
+    
+    # Clean up extra whitespace
+    content = re.sub(r'\n{3,}', '\n\n', content)
+    content = content.strip()
+    
+    # Final safety check
+    if not content or re.search(r'(?i)^(Thought|Action|Observation):', content):
+        log_structured('warning', 'GraphRAG final answer still contains thinking patterns, using fallback')
+        content = "I found relevant information in the HP marketing materials that can help answer your question. Please let me know if you need more specific details."
+    
+    return content
\ No newline at end of file
diff --git a/init_mongodb.py b/init_mongodb.py
new file mode 100644
index 0000000..6de29cf
--- /dev/null
+++ b/init_mongodb.py
@@ -0,0 +1,127 @@
+"""
+MongoDB Initialization Script for HP Chatbot
+
+This script initializes the MongoDB database with the necessary collections for the HP chatbot.
+It creates collections for users, conversations, and messages.
+
+Usage:
+    python init_mongodb.py
+"""
+
+import pymongo
+import logging
+from datetime import datetime
+import os
+import sys
+
+# Configure logging
+logging.basicConfig(
+    level=logging.INFO,
+    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+    handlers=[
+        logging.StreamHandler(),
+        logging.FileHandler('mongodb_init.log')
+    ]
+)
+logger = logging.getLogger(__name__)
+
+# MongoDB connection information
+MONGO_URI = "mongodb://hp:hp@localhost:27017/?authSource=hp_chatbot"  # HP user with hp_chatbot auth source
+DB_NAME = "hp_chatbot"
+
+# Collection names
+USERS_COLLECTION = "users"
+CONVERSATIONS_COLLECTION = "conversations"
+MESSAGES_COLLECTION = "messages"
+
+def init_mongodb():
+    """Initialize MongoDB database and collections."""
+    try:
+        # Connect to MongoDB
+        logger.info("Connecting to MongoDB...")
+        client = pymongo.MongoClient(MONGO_URI)
+        
+        # Test connection
+        client.admin.command('ping')
+        logger.info("Successfully connected to MongoDB")
+        
+        # Create or access database
+        db = client[DB_NAME]
+        logger.info(f"Using database: {DB_NAME}")
+        
+        # Create collections if they don't exist
+        if USERS_COLLECTION not in db.list_collection_names():
+            db.create_collection(USERS_COLLECTION)
+            logger.info(f"Created collection: {USERS_COLLECTION}")
+            
+            # Create indexes for users collection
+            db[USERS_COLLECTION].create_index("username", unique=True)
+            # Create a unique sparse index for email - only enforces uniqueness when email exists
+            db[USERS_COLLECTION].create_index("email", unique=True, sparse=True)
+            logger.info("Created indexes for users collection")
+        
+        if CONVERSATIONS_COLLECTION not in db.list_collection_names():
+            db.create_collection(CONVERSATIONS_COLLECTION)
+            logger.info(f"Created collection: {CONVERSATIONS_COLLECTION}")
+            
+            # Create indexes for conversations collection
+            db[CONVERSATIONS_COLLECTION].create_index("user_id")
+            db[CONVERSATIONS_COLLECTION].create_index("session_id", unique=True)
+            db[CONVERSATIONS_COLLECTION].create_index("created_at")
+            db[CONVERSATIONS_COLLECTION].create_index("last_updated")
+            logger.info("Created indexes for conversations collection")
+        
+        if MESSAGES_COLLECTION not in db.list_collection_names():
+            db.create_collection(MESSAGES_COLLECTION)
+            logger.info(f"Created collection: {MESSAGES_COLLECTION}")
+            
+            # Create indexes for messages collection
+            db[MESSAGES_COLLECTION].create_index("conversation_id")
+            db[MESSAGES_COLLECTION].create_index("timestamp")
+            logger.info("Created indexes for messages collection")
+        
+        logger.info("MongoDB initialization completed successfully")
+        return True
+        
+    except pymongo.errors.ConnectionFailure as e:
+        logger.error(f"Could not connect to MongoDB: {e}")
+        return False
+    except Exception as e:
+        logger.error(f"An error occurred during MongoDB initialization: {e}")
+        return False
+
+def display_collection_info(client):
+    """Display information about the collections in the database."""
+    db = client[DB_NAME]
+    
+    logger.info("=== Database Structure ===")
+    for collection_name in db.list_collection_names():
+        count = db[collection_name].count_documents({})
+        logger.info(f"Collection: {collection_name}, Documents: {count}")
+        
+        # Display indexes
+        indexes = db[collection_name].index_information()
+        logger.info(f"  Indexes: {list(indexes.keys())}")
+
+if __name__ == "__main__":
+    if init_mongodb():
+        # Display collection information
+        client = pymongo.MongoClient(MONGO_URI)
+        display_collection_info(client)
+        
+        # Add sample user if none exist (optional)
+        db = client[DB_NAME]
+        if db[USERS_COLLECTION].count_documents({}) == 0:
+            sample_user = {
+                "username": "sample_user",
+                "email": "sample@example.com",
+                "created_at": datetime.utcnow(),
+                "last_login": datetime.utcnow()
+            }
+            db[USERS_COLLECTION].insert_one(sample_user)
+            logger.info("Added sample user for testing")
+        
+        logger.info("Initialization complete. The database is ready for use.")
+    else:
+        logger.error("Failed to initialize MongoDB. See logs for details.")
+        sys.exit(1)
\ No newline at end of file
diff --git a/json_utils.py b/json_utils.py
new file mode 100644
index 0000000..2bc19bc
--- /dev/null
+++ b/json_utils.py
@@ -0,0 +1,133 @@
+# hp_chatbot/json_utils.py
+import json
+import llama_index
+from llama_index.core.tools import ToolOutput
+from llama_index.core.agent.react.types import (
+    ActionReasoningStep,
+    ObservationReasoningStep,
+    ResponseReasoningStep,
+    BaseReasoningStep,
+)
+from llama_index.core.llms import ChatMessage, LLM, ChatResponse as LlamaResponse
+from llama_index.core.base.response.schema import Response
+from flask.json.provider import JSONProvider
+from bson import ObjectId # Import ObjectId if used in responses/data
+from datetime import datetime
+
+class CustomJSONEncoder(json.JSONEncoder):
+    """
+    Custom JSON Encoder to handle LlamaIndex objects, BSON ObjectId, and other types.
+    """
+    def default(self, obj):
+        try:
+            # Specific LlamaIndex Types
+            if isinstance(obj, ToolOutput):
+                return {
+                    'content': str(obj.content) if obj.content is not None else "",
+                    'tool_name': getattr(obj, 'tool_name', None),
+                    'raw_output': str(getattr(obj, 'raw_output', None)), # Safely convert raw_output
+                    'type': 'tool_output',
+                    'metadata': getattr(obj, 'metadata', {})
+                }
+            elif isinstance(obj, (llama_index.core.llms.ChatMessage, ChatMessage)):
+                 return {
+                     'role': str(obj.role),
+                     'content': str(obj.content),
+                     'additional_kwargs': obj.additional_kwargs if hasattr(obj, 'additional_kwargs') else {}
+                 }
+            elif isinstance(obj, (LlamaResponse, Response)):
+                 return {
+                    'content': str(getattr(obj, 'response', getattr(obj, 'message', None))),
+                    'metadata': getattr(obj, 'metadata', {}),
+                    'type': 'llm_response'
+                 }
+            elif isinstance(obj, ActionReasoningStep):
+                return {
+                    'type': 'action_step',
+                    'action': obj.action,
+                    'action_input': obj.action_input, # Should be serializable dict
+                    'thought': getattr(obj, 'thought', None)
+                }
+            elif isinstance(obj, ObservationReasoningStep):
+                return {
+                    'type': 'observation_step',
+                    'observation': str(obj.observation), # Ensure observation is string
+                    'thought': getattr(obj, 'thought', None)
+                }
+            elif isinstance(obj, ResponseReasoningStep):
+                return {
+                    'type': 'response_step',
+                    'response': str(obj.response), # Ensure response is string
+                    'is_streaming': getattr(obj, 'is_streaming', False),
+                    'thought': getattr(obj, 'thought', None)
+                }
+            elif isinstance(obj, BaseReasoningStep): # Catch-all for other steps
+                 return {
+                    'type': 'base_reasoning_step',
+                    'thought': getattr(obj, 'thought', None),
+                    'is_done': getattr(obj, 'is_done', False),
+                 }
+            # Handle LlamaIndex Document/Node related objects if needed
+            elif isinstance(obj, llama_index.core.schema.Document):
+                 return {
+                     'doc_id': obj.id_,
+                     'text_preview': obj.text[:100] + "..." if obj.text else "",
+                     'metadata': obj.metadata, # Metadata should be serializable
+                     'type': 'llama_document'
+                 }
+            elif isinstance(obj, llama_index.core.schema.NodeWithScore):
+                 return {
+                     'node': self.default(obj.node), # Recursively serialize the node
+                     'score': obj.score,
+                     'type': 'node_with_score'
+                 }
+            elif isinstance(obj, llama_index.core.schema.TextNode):
+                 return {
+                     'node_id': obj.id_,
+                     'text_preview': obj.text[:100] + "..." if obj.text else "",
+                     'metadata': obj.metadata,
+                     'type': 'text_node'
+                 }
+
+            # Common Python Types
+            elif isinstance(obj, datetime):
+                return obj.isoformat()
+            elif isinstance(obj, ObjectId):
+                return str(obj)
+            elif isinstance(obj, bytes):
+                 return "" # Or encode to base64 if needed
+
+            # General Fallback for objects with __dict__
+            elif hasattr(obj, '__dict__'):
+                # Filter out private/callable attributes, be cautious with recursion
+                try:
+                    d = {k: v for k, v in obj.__dict__.items()
+                         if not k.startswith('_') and not callable(v)}
+                    # Basic check to prevent deep recursion errors
+                    if len(d) > 50: # Arbitrary limit
+                         return f""
+                    return d
+                except Exception:
+                     return f""
+
+            # Final fallback using standard JSON encoding
+            return super().default(obj)
+
+        except Exception as e:
+            # Log the error? Be careful about logging sensitive data
+            # print(f"DEBUG: JSON encoding error for type {type(obj).__name__}: {e}")
+            return f""
+
+
+class CustomJSONProvider(JSONProvider):
+    """
+    Flask JSON Provider using the CustomJSONEncoder.
+    """
+    def dumps(self, obj, **kwargs):
+        kwargs.setdefault('cls', CustomJSONEncoder)
+        kwargs.setdefault('ensure_ascii', False) # Often useful for non-English text
+        kwargs.setdefault('indent', None) # No indent for production APIs
+        return json.dumps(obj, **kwargs)
+
+    def loads(self, s, **kwargs):
+        return json.loads(s, **kwargs)
\ No newline at end of file
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..e87c3d9
--- /dev/null
+++ b/main.py
@@ -0,0 +1,168 @@
+# hp_chatbot/main.py
+
+import asyncio
+import os
+import sys
+from flask import Flask
+from flask_cors import CORS
+
+# Ensure the project directory is in the Python path
+current_dir = os.path.dirname(os.path.abspath(__file__))
+if current_dir not in sys.path:
+    sys.path.insert(0, current_dir)
+
+# Import necessary components from our modules
+from config import (
+    APPLICATION_ROOT, MAX_CONTENT_LENGTH,
+    CORS_ALLOWED_ORIGINS, CORS_SUPPORTS_CREDENTIALS,
+    SERVER_HOST, SERVER_PORT, USE_RELOADER, LOG_LEVEL,
+    KEEP_ALIVE_TIMEOUT, READ_TIMEOUT, WRITE_TIMEOUT
+)
+from utils import logger, log_structured
+from json_utils import CustomJSONProvider
+from ai_core import initialize_global_index # Import the initialization function
+from shared_state import global_workflow_agent, is_agent_available # Import shared state
+from routes import register_routes
+from init_mongodb import init_mongodb # Your MongoDB initialization script
+
+# --- Flask App Initialization ---
+app = Flask(__name__)
+
+# Apply custom JSON provider for handling special types (LlamaIndex objects, etc.)
+app.json_provider_class = CustomJSONProvider
+app.json = CustomJSONProvider(app)
+
+# Configuration
+app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH
+if APPLICATION_ROOT:
+    app.config['APPLICATION_ROOT'] = APPLICATION_ROOT
+    # If using APPLICATION_ROOT, you might need to adjust route prefixes
+    # or use a Blueprint with url_prefix=APPLICATION_ROOT
+    log_structured('info', f"Flask Application Root set to: {APPLICATION_ROOT}")
+
+# CORS Configuration
+CORS(app,
+     resources={r"/*": {"origins": CORS_ALLOWED_ORIGINS}},
+     supports_credentials=CORS_SUPPORTS_CREDENTIALS,
+     # Expose custom headers if needed by the frontend
+     # expose_headers=["Content-Disposition"] # Example for downloads
+)
+log_structured('info', f"CORS configured for origins: {CORS_ALLOWED_ORIGINS}")
+
+
+# --- Register Routes ---
+# Pass the app object to the function in routes.py
+register_routes(app)
+log_structured('info', "Flask routes registered.")
+
+
+# --- Startup Function ---
+async def startup_event() -> bool:
+    """Tasks to run when the application starts.
+    
+    Returns:
+        bool: True if all startup tasks completed successfully, False otherwise
+    """
+    log_structured('info', "Application startup sequence initiated.")
+    all_success = True
+
+    # 1. Initialize MongoDB Connection & Schema (using your script)
+    log_structured('info', "Initializing MongoDB connection...")
+    mongo_success = False
+    try:
+        if init_mongodb():
+            log_structured('info', "MongoDB initialized successfully.")
+            mongo_success = True
+        else:
+            log_structured('warning', "MongoDB initialization script finished, but reported issues.")
+            all_success = False
+    except Exception as db_err:
+        log_structured('critical', "FATAL: MongoDB initialization failed.", {'error': str(db_err)})
+        all_success = False
+        # We'll continue in a degraded state
+
+    # 2. Initialize Global AI Index and Agent
+    log_structured('info', "Initializing global AI index and agent...")
+    index_success = await initialize_global_index()
+    
+    # Explicitly check the status after initialization
+    if not is_agent_available():
+        log_structured('critical', "After initialize_global_index, global_workflow_agent is still unavailable, even though function may have reported success")
+        all_success = False
+    elif not index_success:
+        log_structured('warning', "AI initialization reported failure, but will continue in degraded state")
+        all_success = False
+    else:
+        log_structured('info', "AI initialization successful, global_workflow_agent is available")
+
+    log_structured('info', f"Application startup sequence complete. Overall success: {all_success}")
+    return all_success
+
+
+# --- Shutdown Function (Optional) ---
+async def shutdown_event():
+    """Tasks to run when the application stops."""
+    log_structured('info', "Application shutdown sequence initiated.")
+    # Add any cleanup tasks here (e.g., closing connections if not handled elsewhere)
+    # Note: Hypercorn might not always guarantee graceful shutdown execution.
+    log_structured('info', "Application shutdown sequence complete.")
+
+
+# --- Main Execution Block ---
+if __name__ == '__main__':
+    from hypercorn.config import Config as HypercornConfig
+    from hypercorn.asyncio import serve as hypercorn_serve
+
+    # Create Hypercorn config object
+    config = HypercornConfig()
+
+    # Basic settings
+    config.bind = [f"{SERVER_HOST}:{SERVER_PORT}"]
+    config.use_reloader = USE_RELOADER
+    config.accesslog = '-' # Log to stdout/stderr
+    config.errorlog = '-' # Log to stdout/stderr
+    config.loglevel = LOG_LEVEL.upper()
+    config.worker_class = 'asyncio'
+
+    # Timeouts (ensure these are floats or ints)
+    config.keep_alive_timeout = float(KEEP_ALIVE_TIMEOUT)
+    config.read_timeout = float(READ_TIMEOUT)
+    config.write_timeout = float(WRITE_TIMEOUT)
+
+    # Request size limits (check Hypercorn docs for exact names, might vary slightly)
+    # These might apply to HTTP/1.1 or HTTP/2 differently.
+    # config.h11_max_incomplete_size = MAX_CONTENT_LENGTH # Example for HTTP/1.1
+    # config.h2_max_concurrent_streams = 100 # Example for HTTP/2
+    # config.max_app_buffer_size = MAX_CONTENT_LENGTH # Another potential setting
+    # It's safer to configure these via a reverse proxy (like Nginx) in production.
+    # Hypercorn's defaults are usually reasonable. Let's comment these out for now.
+
+    # Assign startup and shutdown handlers
+    config.startup_hooks = [startup_event]
+    config.shutdown_hooks = [shutdown_event]
+
+    log_structured('info', f"Starting Hypercorn server on {SERVER_HOST}:{SERVER_PORT}")
+    log_structured('info', f"Reload mode: {'Enabled' if USE_RELOADER else 'Disabled'}")
+
+    # Execute startup task before running the server
+    log_structured('info', "Manually executing startup sequence before server start")
+    startup_success = asyncio.run(startup_event())
+    
+    # Double-check that the agent is initialized
+    if not is_agent_available():
+        log_structured('critical', "After startup, global_workflow_agent is still unavailable. Forcing re-initialization...")
+        # Try once more to initialize
+        index_success = asyncio.run(initialize_global_index())
+        if not index_success or not is_agent_available():
+            log_structured('critical', "Emergency initialization also failed. Server will run but chat functionality will be impaired.")
+        else:
+            log_structured('info', "Emergency initialization succeeded.")
+
+    # Run the server
+    try:
+        asyncio.run(hypercorn_serve(app, config))
+    except KeyboardInterrupt:
+         log_structured('info', "Server stopped manually (KeyboardInterrupt).")
+    except Exception as run_err:
+         log_structured('critical', "Hypercorn server failed to run.", {'error': str(run_err)})
+         sys.exit(1)
\ No newline at end of file
diff --git a/mongodb_utils.py b/mongodb_utils.py
new file mode 100644
index 0000000..75086be
--- /dev/null
+++ b/mongodb_utils.py
@@ -0,0 +1,458 @@
+"""
+MongoDB Utilities for HP Chatbot
+
+This module provides utility functions for interacting with MongoDB in the HP chatbot application.
+It includes functions for connecting to MongoDB, and managing users, conversations, and messages.
+"""
+
+import pymongo
+import logging
+from datetime import datetime
+import uuid
+from typing import Dict, List, Optional, Any, Union
+from bson.objectid import ObjectId
+import json
+
+# Configure logging
+logging.basicConfig(
+    level=logging.INFO,
+    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+    handlers=[
+        logging.StreamHandler(),
+        logging.FileHandler('mongodb.log')
+    ]
+)
+logger = logging.getLogger(__name__)
+
+# MongoDB connection information
+MONGO_URI = "mongodb://hp:hp@localhost:27017/?authSource=hp_chatbot"
+DB_NAME = "hp_chatbot"
+
+# Collection names
+USERS_COLLECTION = "users"
+CONVERSATIONS_COLLECTION = "conversations"
+MESSAGES_COLLECTION = "messages"
+
+# Global MongoDB client
+mongo_client = None
+db = None
+
+def get_db():
+    """Get or initialize the MongoDB database connection."""
+    global mongo_client, db
+    
+    if mongo_client is None:
+        try:
+            mongo_client = pymongo.MongoClient(MONGO_URI)
+            mongo_client.admin.command('ping')  # Test connection
+            db = mongo_client[DB_NAME]
+            logger.info("Successfully connected to MongoDB")
+        except Exception as e:
+            logger.error(f"Failed to connect to MongoDB: {e}")
+            raise
+    
+    return db
+
+def close_connection():
+    """Close the MongoDB connection."""
+    global mongo_client
+    
+    if mongo_client:
+        mongo_client.close()
+        mongo_client = None
+        logger.info("MongoDB connection closed")
+
+# User functions
+def get_user_by_username(username: str) -> Optional[Dict]:
+    """Get a user by username."""
+    try:
+        db = get_db()
+        user = db[USERS_COLLECTION].find_one({"username": username})
+        return user
+    except Exception as e:
+        logger.error(f"Error getting user by username: {e}")
+        return None
+
+def create_or_update_user(username: str, email: Optional[str] = None) -> Optional[str]:
+    """Create a new user or update an existing one."""
+    try:
+        db = get_db()
+        
+        # Check if user exists
+        existing_user = db[USERS_COLLECTION].find_one({"username": username})
+        
+        if existing_user:
+            # Update last login
+            db[USERS_COLLECTION].update_one(
+                {"username": username},
+                {"$set": {"last_login": datetime.utcnow()}}
+            )
+            return str(existing_user["_id"])
+        else:
+            # Create new user
+            new_user = {
+                "username": username,
+                "created_at": datetime.utcnow(),
+                "last_login": datetime.utcnow()
+            }
+            
+            # Only include email if it's not None to avoid unique constraint issues
+            if email:
+                new_user["email"] = email
+                
+            result = db[USERS_COLLECTION].insert_one(new_user)
+            return str(result.inserted_id)
+    except Exception as e:
+        logger.error(f"Error creating or updating user: {e}")
+        # If the error is a duplicate key error, try to find the existing user
+        if "duplicate key error" in str(e) and "username" in str(e):
+            try:
+                existing_user = db[USERS_COLLECTION].find_one({"username": username})
+                if existing_user:
+                    return str(existing_user["_id"])
+            except:
+                pass
+        return None
+
+# Conversation functions
+def get_conversation(session_id: str) -> Optional[Dict]:
+    """Get a conversation by session ID."""
+    try:
+        db = get_db()
+        conversation = db[CONVERSATIONS_COLLECTION].find_one({"session_id": session_id})
+        return conversation
+    except Exception as e:
+        logger.error(f"Error getting conversation: {e}")
+        return None
+
+def get_conversation_by_id(conversation_id: str) -> Optional[Dict]:
+    """Get a conversation by its MongoDB ID."""
+    try:
+        db = get_db()
+        # Convert string ID to ObjectId
+        try:
+            obj_id = ObjectId(conversation_id)
+            conversation = db[CONVERSATIONS_COLLECTION].find_one({"_id": obj_id})
+            return conversation
+        except Exception as e:
+            logger.error(f"Error converting conversation ID to ObjectId: {e}")
+            return None
+    except Exception as e:
+        logger.error(f"Error getting conversation by ID: {e}")
+        return None
+
+def get_user_conversations(user_id: str) -> List[Dict]:
+    """Get all conversations for a user that are not marked as deleted."""
+    try:
+        db = get_db()
+        conversations = list(db[CONVERSATIONS_COLLECTION].find(
+            {
+                "user_id": user_id,
+                # Only return conversations that either don't have is_deleted or have it set to False
+                "$or": [
+                    {"is_deleted": {"$exists": False}},
+                    {"is_deleted": False}
+                ]
+            }
+        ).sort("last_updated", pymongo.DESCENDING))
+        return conversations
+    except Exception as e:
+        logger.error(f"Error getting user conversations: {e}")
+        return []
+
+def create_conversation(session_id: str, user_id: str, title: str = "New conversation") -> Optional[str]:
+    """Create a new conversation."""
+    try:
+        db = get_db()
+        
+        # Check if conversation already exists with this session_id
+        existing = db[CONVERSATIONS_COLLECTION].find_one({"session_id": session_id})
+        if existing:
+            return str(existing["_id"])
+        
+        # Create new conversation
+        new_conversation = {
+            "session_id": session_id,
+            "user_id": user_id,
+            "title": title,
+            "created_at": datetime.utcnow(),
+            "last_updated": datetime.utcnow()
+        }
+        result = db[CONVERSATIONS_COLLECTION].insert_one(new_conversation)
+        return str(result.inserted_id)
+    except Exception as e:
+        logger.error(f"Error creating conversation: {e}")
+        return None
+
+def update_conversation_title(conversation_id: str, title: str) -> bool:
+    """Update the title of a conversation."""
+    try:
+        db = get_db()
+        db[CONVERSATIONS_COLLECTION].update_one(
+            {"_id": ObjectId(conversation_id)},
+            {"$set": {"title": title, "last_updated": datetime.utcnow()}}
+        )
+        return True
+    except Exception as e:
+        logger.error(f"Error updating conversation title: {e}")
+        return False
+
+def update_conversation_timestamp(conversation_id: str) -> bool:
+    """Update the last_updated timestamp of a conversation."""
+    try:
+        db = get_db()
+        db[CONVERSATIONS_COLLECTION].update_one(
+            {"_id": ObjectId(conversation_id)},
+            {"$set": {"last_updated": datetime.utcnow()}}
+        )
+        return True
+    except Exception as e:
+        logger.error(f"Error updating conversation timestamp: {e}")
+        return False
+
+# Message functions
+def add_message(conversation_id: str, role: str, content: str, 
+                sources: Optional[List] = None, reasoning: Optional[List] = None, 
+                images: Optional[List] = None) -> Optional[str]:
+    """Add a message to a conversation."""
+    try:
+        db = get_db()
+        
+        # Prepare the message document
+        message = {
+            "conversation_id": conversation_id,
+            "role": role,
+            "content": content,
+            "timestamp": datetime.utcnow()
+        }
+        
+        # Add optional fields with serialization
+        if sources:
+            # Serialize sources
+            serialized_sources = json.loads(json.dumps(sources, default=serialize_custom_objects))
+            message["sources"] = serialized_sources
+            
+        if reasoning:
+            # Serialize reasoning steps
+            serialized_reasoning = json.loads(json.dumps(reasoning, default=serialize_custom_objects))
+            message["reasoning"] = serialized_reasoning
+            
+        if images:
+            # Serialize images
+            serialized_images = json.loads(json.dumps(images, default=serialize_custom_objects))
+            message["images"] = serialized_images
+        
+        # Insert the message
+        result = db[MESSAGES_COLLECTION].insert_one(message)
+        
+        # Update the conversation timestamp
+        update_conversation_timestamp(conversation_id)
+        
+        return str(result.inserted_id)
+    except Exception as e:
+        logger.error(f"Error adding message: {e}")
+        return None
+
+def serialize_custom_objects(obj):
+    """
+    Custom serialization function for MongoDB.
+    Handles special types like ActionReasoningStep and other custom classes.
+    """
+    if hasattr(obj, '__dict__'):
+        # For ActionReasoningStep, ObservationReasoningStep, etc.
+        if obj.__class__.__name__.endswith('ReasoningStep'):
+            result = {
+                'type': obj.__class__.__name__
+            }
+            
+            # Add attributes based on the specific type
+            if hasattr(obj, 'action'):
+                result['action'] = obj.action
+            if hasattr(obj, 'action_input'):
+                result['action_input'] = obj.action_input
+            if hasattr(obj, 'observation'):
+                result['observation'] = obj.observation
+            if hasattr(obj, 'response'):
+                result['response'] = obj.response
+            if hasattr(obj, 'thought'):
+                result['thought'] = obj.thought
+                
+            return result
+        
+        # For other objects with __dict__
+        return {k: v for k, v in obj.__dict__.items() 
+                if not k.startswith('_') and not callable(v)}
+    
+    # For objects with content property
+    if hasattr(obj, 'content'):
+        return str(obj.content)
+        
+    # For objects with string representation
+    try:
+        return str(obj)
+    except:
+        return f""
+
+def get_conversation_messages(conversation_id: str) -> List[Dict]:
+    """Get all messages in a conversation."""
+    try:
+        db = get_db()
+        messages = list(db[MESSAGES_COLLECTION].find(
+            {"conversation_id": conversation_id}
+        ).sort("timestamp", pymongo.ASCENDING))
+        return messages
+    except Exception as e:
+        logger.error(f"Error getting conversation messages: {e}")
+        return []
+
+def generate_conversation_title(conversation_id: str, content: List[Dict]) -> Optional[str]:
+    """
+    Generate a title for a conversation based on its content using AI.
+    
+    Args:
+        conversation_id: The ID of the conversation
+        content: List of messages in the conversation
+    
+    Returns:
+        A generated title, or None if generation failed
+    """
+    try:
+        from llama_index.llms.openai import OpenAI as LlamaOpenAI
+        
+        # Extract text from the conversation (first few messages)
+        conversation_text = "\n".join([
+            f"{msg['role']}: {msg['content']}" 
+            for msg in content[:5]  # Use first 5 messages or fewer
+        ])
+        
+        # Create LLM instance
+        llm = LlamaOpenAI(
+            model="chatgpt-4o-latest",
+            temperature=0.3,
+        )
+        
+        # Generate title
+        prompt = f"""
+        Based on the following conversation, generate a short, descriptive title (max 5 words):
+        
+        {conversation_text}
+        
+        Title:
+        """
+        
+        response = llm.complete(prompt)
+        title = response.text.strip()
+        
+        # Update the conversation with the new title
+        update_conversation_title(conversation_id, title)
+        
+        return title
+    except Exception as e:
+        logger.error(f"Error generating conversation title: {e}")
+        return "New conversation"  # Fallback title
+
+def delete_conversation(conversation_id: str, hard_delete: bool = False) -> bool:
+    """
+    Delete a conversation and its messages.
+    
+    Args:
+        conversation_id: The ID of the conversation to delete
+        hard_delete: If True, physically delete the records; if False, mark as deleted
+        
+    Returns:
+        True if successful, False otherwise
+    """
+    try:
+        db = get_db()
+        
+        if hard_delete:
+            # Permanently delete all messages in the conversation
+            db[MESSAGES_COLLECTION].delete_many({"conversation_id": conversation_id})
+            
+            # Permanently delete the conversation
+            db[CONVERSATIONS_COLLECTION].delete_one({"_id": ObjectId(conversation_id)})
+        else:
+            # Mark the conversation as deleted
+            db[CONVERSATIONS_COLLECTION].update_one(
+                {"_id": ObjectId(conversation_id)},
+                {"$set": {"is_deleted": True}}
+            )
+        
+        return True
+    except Exception as e:
+        logger.error(f"Error deleting conversation: {e}")
+        return False
+
+# Session state management
+def get_session_state(session_id: str) -> Optional[Dict]:
+    """
+    Get the session state from MongoDB.
+    
+    Args:
+        session_id: The session ID
+        
+    Returns:
+        The session state or None if not found
+    """
+    try:
+        conversation = get_conversation(session_id)
+        if conversation:
+            # Return a minimal session state
+            return {
+                "initialized": True,
+                "conversation_id": str(conversation["_id"]),
+                "user_id": conversation["user_id"]
+            }
+        return None
+    except Exception as e:
+        logger.error(f"Error getting session state: {e}")
+        return None
+
+def create_session_state(session_id: str, user_id: str, conversation_id: Optional[str] = None) -> Optional[Dict]:
+    """
+    Create a new session state in MongoDB.
+    
+    Args:
+        session_id: The session ID
+        user_id: The user ID
+        conversation_id: Optional conversation ID. If not provided, a new conversation will be created.
+        
+    Returns:
+        The created session state or None if creation failed
+    """
+    try:
+        if not conversation_id:
+            conversation_id = create_conversation(session_id, user_id)
+            
+        if conversation_id:
+            return {
+                "initialized": True,
+                "conversation_id": conversation_id,
+                "user_id": user_id
+            }
+        return None
+    except Exception as e:
+        logger.error(f"Error creating session state: {e}")
+        return None
+
+def update_session_state(session_id: str, state: Dict) -> bool:
+    """
+    Update the session state in MongoDB.
+    
+    Args:
+        session_id: The session ID
+        state: The new state to save
+        
+    Returns:
+        True if the update was successful, False otherwise
+    """
+    try:
+        conversation = get_conversation(session_id)
+        if conversation:
+            # If we need to store additional session state beyond the conversation
+            # we could add a separate collection for that
+            return True
+        return False
+    except Exception as e:
+        logger.error(f"Error updating session state: {e}")
+        return False
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..baafad6
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,106 @@
+aiohappyeyeballs==2.6.1
+aiohttp==3.11.16
+aiosignal==1.3.2
+annotated-types==0.7.0
+anyio==4.9.0
+asgiref==3.8.1
+asyncio==3.4.3
+attrs==25.3.0
+banks==2.1.1
+beautifulsoup4==4.13.3
+blinker==1.9.0
+certifi==2025.1.31
+charset-normalizer==3.4.1
+click==8.1.8
+colorama==0.4.6
+dataclasses-json==0.6.7
+Deprecated==1.2.18
+dirtyjson==1.0.8
+distro==1.9.0
+dnspython==2.7.0
+filetype==1.2.0
+Flask==3.1.0
+flask-cors==5.0.1
+frozenlist==1.5.0
+fsspec==2025.3.2
+future==1.0.0
+greenlet==3.1.1
+griffe==1.7.2
+h11==0.14.0
+h2==4.2.0
+hpack==4.1.0
+httpcore==1.0.7
+httpx==0.28.1
+Hypercorn==0.17.3
+hyperframe==6.1.0
+idna==3.10
+itsdangerous==2.2.0
+Jinja2==3.1.6
+jiter==0.9.0
+joblib==1.4.2
+llama-cloud==0.1.17
+llama-cloud-services==0.6.9
+llama-index==0.12.33
+llama-index-agent-openai==0.4.6
+llama-index-cli==0.4.1
+llama-index-core==0.12.33.post1
+llama-index-embeddings-openai==0.3.1
+llama-index-graph-stores-neo4j==0.4.6
+llama-index-indices-managed-llama-cloud==0.6.11
+llama-index-llms-openai==0.3.30
+llama-index-multi-modal-llms-openai==0.4.3
+llama-index-program-openai==0.3.1
+llama-index-question-gen-openai==0.3.0
+llama-index-readers-file==0.4.7
+llama-index-readers-llama-parse==0.4.0
+llama-parse==0.6.4.post1
+lxml==5.3.2
+markdown2==2.5.3
+MarkupSafe==3.0.2
+marshmallow==3.26.1
+multidict==6.3.2
+mypy-extensions==1.0.0
+neo4j==5.28.1
+nest-asyncio==1.6.0
+networkx==3.4.2
+nltk==3.9.1
+numpy==2.2.4
+openai==1.71.0
+packaging==24.2
+pandas==2.2.3
+pillow==11.1.0
+platformdirs==4.3.7
+priority==2.0.0
+propcache==0.3.1
+pydantic==2.11.2
+pydantic_core==2.33.1
+pymongo==4.7.0
+pypdf==5.4.0
+python-dateutil==2.9.0.post0
+python-docx==1.1.2
+python-dotenv==1.1.0
+python-louvain==0.16
+pytz==2025.2
+PyYAML==6.0.2
+regex==2024.11.6
+requests==2.32.3
+setuptools==79.0.0
+six==1.17.0
+sniffio==1.3.1
+soupsieve==2.6
+SQLAlchemy==2.0.40
+striprtf==0.0.26
+tenacity==9.1.2
+tiktoken==0.9.0
+tqdm==4.67.1
+typing-inspect==0.9.0
+typing-inspection==0.4.0
+typing_extensions==4.13.1
+tzdata==2025.2
+urllib3==2.3.0
+uuid==1.30
+Werkzeug==3.1.3
+wheel==0.45.1
+wrapt==1.17.2
+wsproto==1.2.0
+yarl==1.19.0
diff --git a/routes.py b/routes.py
new file mode 100644
index 0000000..18ad928
--- /dev/null
+++ b/routes.py
@@ -0,0 +1,1032 @@
+# hp_chatbot/routes.py
+
+import os
+import asyncio
+import traceback
+import uuid
+import shutil
+import urllib.parse
+from datetime import datetime
+from pathlib import Path
+
+from flask import (
+    Flask, request, jsonify, make_response, send_from_directory, send_file,
+    Blueprint # Consider using Blueprint for larger apps
+)
+from werkzeug.utils import secure_filename # Keep if file uploads are reintroduced
+
+# Import from our modules
+from utils import logger, log_structured, allowed_file # Keep allowed_file if uploads return
+from json_utils import CustomJSONEncoder # Used indirectly by make_response/jsonify with custom provider
+from session_manager import get_or_create_session_state, clear_chat_state_cache
+from ai_core import initialize_global_index # Import initialization function
+from shared_state import global_workflow_agent, global_index, is_agent_available # Import shared state variables
+from document_generator import create_brief_docx
+from config import IMAGES_DIRECTORY, HP_DOCS_FOLDER, BASE_DIR, SUPPORTING_FILES_DIR # Import necessary configs
+from llama_index.core.tools import ToolOutput  # Import ToolOutput for type checking
+from llama_index.core.llms import ChatMessage  # Import ChatMessage for memory management
+
+# Import MongoDB utilities
+from mongodb_utils import (
+    add_message, get_conversation_messages, get_user_conversations,
+    get_conversation_by_id, create_conversation, create_or_update_user,
+    delete_conversation as db_delete_conversation, # Alias to avoid conflict
+    generate_conversation_title, update_session_state
+)
+
+# Create a Blueprint or define routes on the app passed from main.py
+# Using direct app registration for simplicity here.
+# If using Blueprint:
+# api_bp = Blueprint('api', __name__)
+# @api_bp.route('/chat', methods=['POST', 'OPTIONS'])
+# async def chat(): ...
+# In main.py: app.register_blueprint(api_bp)
+
+def register_routes(app: Flask):
+
+    @app.route('/chat', methods=['POST', 'OPTIONS'])
+    async def chat():
+        """Handle chat messages, interact with the AI agent, include image handling."""
+        # Declare global variables we'll need to access
+        global global_workflow_agent
+        
+        if request.method == 'OPTIONS':
+            # Pre-flight request. Reply successfully:
+            resp = make_response(jsonify({'status': 'ok'}))
+            # Add necessary CORS headers if not handled globally by Flask-CORS middleware
+            # resp.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin')
+            # resp.headers['Access-Control-Allow-Credentials'] = 'true'
+            # resp.headers['Access-Control-Allow-Methods'] = 'POST, OPTIONS'
+            # resp.headers['Access-Control-Allow-Headers'] = 'Content-Type, X-MS-USERNAME, Authorization' # Add any custom headers
+            return resp
+
+        session_id = None # Initialize for error logging
+        try:
+            data = request.get_json()
+            if not data or 'message' not in data:
+                log_structured('warning', 'Chat request missing message payload')
+                return jsonify({'error': 'No message provided'}), 400
+
+            session_id = data.get('sessionId')
+            if not session_id:
+                log_structured('warning', 'Chat request missing sessionId')
+                return jsonify({'error': 'Session ID is required'}), 400
+
+            # Use X-MS-USERNAME or a similar header for authenticated username
+            username = request.headers.get('X-MS-USERNAME') # Adjust header name if different
+            # Check if we're in development mode
+            if not username and os.environ.get("PRODUCTION", "false").lower() == "false":
+                # For local development, create a default username
+                username = f"dev_user@local"
+                log_structured('info', f'Development mode: Using default username: {username}')
+            elif not username:
+                 # Allow anonymous access based on session or return error?
+                 # For now, allow anonymous tied to session_id
+                 log_structured('info', f'Chat request from anonymous user (session: {session_id})')
+                 # username = f"anonymous_{session_id[:8]}" # Or handle as error if auth is mandatory
+                 # return jsonify({'error': 'Authentication required (X-MS-USERNAME header missing)'}), 401
+
+            log_structured('info', f'Chat request received', {'session_id': session_id, 'username': username or 'anonymous'})
+
+            # Get or create session state (maps session_id to user_id, conversation_id)
+            session_state = get_or_create_session_state(session_id, username)
+
+            # Ensure session is initialized in both development and production modes
+            if not session_state.get('initialized'):
+                log_structured('info', 'Session not yet initialized. Initializing now.', {'session_id': session_id})
+                # Initialize session properly
+                session_state['initialized'] = True
+                # Update the session state in the database to mark as initialized
+                update_session_state(session_id, {'initialized': True})
+
+            conversation_id = session_state.get('conversation_id')
+            user_id = session_state.get('user_id') # Get user_id for context if needed
+
+            if not conversation_id or not user_id:
+                 log_structured('error', 'Session state missing conversation_id or user_id', {'session_id': session_id, 'state': session_state})
+                 return jsonify({'error': 'Session setup incomplete. Please refresh or start a new chat.'}), 500
+
+            user_message = data['message']
+
+            # Store user message
+            add_message(conversation_id, 'user', user_message)
+
+            log_structured('debug', 'Starting agent workflow run', {
+                'session_id': session_id,
+                'conversation_id': conversation_id,
+                'user_id': user_id,
+                'message_preview': user_message[:100]
+            })
+
+            # Check if global agent is available using the shared state helper
+            if not is_agent_available():
+                log_structured('error', 'Global workflow agent is not initialized')
+                
+                # Try to initialize it on-demand if not available
+                try:
+                    log_structured('info', 'Attempting to initialize global index and agent on-demand')
+                    index_success = await initialize_global_index()
+                    
+                    # Let initialization complete, then check the global variable
+                    if index_success:
+                        log_structured('info', 'Index initialization reported success')
+                        
+                        # Force reload the global module variable
+                        import importlib
+                        import ai_core
+                        importlib.reload(ai_core)
+                        from ai_core import global_workflow_agent
+                        
+                        # Check if global_workflow_agent is now available
+                        if global_workflow_agent:
+                            # Test the agent with a simple function call
+                            try:
+                                # Just check if the run method exists and is callable
+                                if hasattr(global_workflow_agent, 'run') and callable(global_workflow_agent.run):
+                                    log_structured('info', 'Successfully initialized global agent on-demand')
+                                else:
+                                    log_structured('error', 'Agent initialized but run method is not callable')
+                            except Exception as agent_test_err:
+                                log_structured('error', f'Agent functional test failed: {str(agent_test_err)}')
+                        else:
+                            log_structured('error', 'Agent still not available after successful index initialization')
+                    else:
+                        log_structured('error', 'Index initialization reported failure')
+                except Exception as init_err:
+                    log_structured('error', f'Error initializing agent on-demand: {str(init_err)}')
+                
+                # Final check
+                import ai_core
+                # We're already using the global variable from the function declaration
+                # Just reassign it from the imported module
+                global_workflow_agent = ai_core.global_workflow_agent
+                if not global_workflow_agent:
+                    log_structured('critical', 'Failed to get or initialize global workflow agent')
+                    
+                    # Provide a more detailed error message for debugging
+                    error_details = {
+                        'error': 'Chat agent is not available. Please try again later.',
+                        'details': 'The AI agent failed to initialize properly. This might be due to API key issues or other backend problems.',
+                        'code': 'AGENT_UNAVAILABLE'
+                    }
+                    return jsonify(error_details), 503
+            
+            # Use the agent to process the query (with one final check to be sure)
+            if not is_agent_available():
+                log_structured('critical', 'Global agent is unavailable after initialization checks')
+                error_details = {
+                    'error': 'Chat agent became unavailable. Please try again.',
+                    'code': 'AGENT_DISAPPEARED'
+                }
+                return jsonify(error_details), 500
+            
+            # Import fresh reference to agent
+            from shared_state import global_workflow_agent
+            if global_workflow_agent is None:
+                log_structured('critical', 'Global agent is None after direct import from shared_state')
+                error_details = {
+                    'error': 'Chat agent became unavailable. Please try again.',
+                    'code': 'AGENT_DISAPPEARED'
+                }
+                return jsonify(error_details), 500
+                
+            log_structured('info', 'Running agent to process query')
+            
+            # Get a fresh reference to the agent from shared state
+            from shared_state import global_workflow_agent as current_agent
+            
+            # Double-check that the agent is available
+            if current_agent is None or not hasattr(current_agent, 'run'):
+                log_structured('critical', 'Agent disappeared at runtime: null check before .run() call')
+                return jsonify({'error': 'Chat agent became unavailable just before processing. Please try again.', 'code': 'AGENT_GONE'}), 500
+            
+            # Load conversation history into the agent's memory
+            if conversation_id:
+                try:
+                    conv_messages = get_conversation_messages(conversation_id)
+                    # Skip if we already have messages in memory (avoid duplication)
+                    if current_agent.memory and len(current_agent.memory.get()) <= 1:
+                        log_structured('info', f'Loading conversation history: {len(conv_messages)} messages', 
+                                     {'conversation_id': conversation_id})
+                        
+                        # Clear memory first to avoid duplicate history
+                        current_agent.memory.reset()
+                        
+                        # Add messages to memory in the correct order
+                        for msg in conv_messages:
+                            # Skip the current message which will be added by the agent
+                            if msg.get('content') == user_message and msg.get('role') == 'user':
+                                continue
+                            role = msg.get('role')
+                            content = msg.get('content', '')
+                            if role and content:
+                                current_agent.memory.put(ChatMessage(role=role, content=content))
+                except Exception as hist_err:
+                    log_structured('error', f'Error loading conversation history: {str(hist_err)}', 
+                                 {'conversation_id': conversation_id})
+                    # Continue without history rather than failing the request
+            
+            # Call the run method on the current agent
+            raw_response = await current_agent.run(user_message)
+
+            log_structured('debug', 'Raw response received from workflow agent', {
+                'session_id': session_id, 'conversation_id': conversation_id,
+                'response_type': type(raw_response).__name__,
+                'response_keys': list(raw_response.keys()) if isinstance(raw_response, dict) else None,
+            })
+
+            # --- Process Response and Extract Images ---
+            final_response_text = "Sorry, I encountered an issue generating a response."
+            final_sources = []
+            final_reasoning = []
+            images = []
+            seen_images = set() # Track unique images added
+
+            if isinstance(raw_response, dict):
+                final_response_text = str(raw_response.get('response', ''))
+                response_sources = raw_response.get('sources', []) # Should be list of ToolOutput
+                final_reasoning = raw_response.get('reasoning', []) # Should be list of ReasoningSteps
+
+                log_structured('debug', f'Processing {len(response_sources)} sources for metadata and images', {'session_id': session_id})
+
+                # Extract source details and potential images
+                if response_sources and isinstance(response_sources, list):
+                    for source_idx, tool_output_source in enumerate(response_sources):
+                        if tool_output_source is None:
+                            continue
+                            
+                        source_info = {'content': "Source content unavailable", 'metadata': {}} # Default
+                        if isinstance(tool_output_source, ToolOutput):
+                            source_info['content'] = str(tool_output_source.content) if tool_output_source.content else "No content"
+                            source_info['tool_name'] = getattr(tool_output_source, 'tool_name', 'unknown_tool')
+                            source_info['raw_output_preview'] = str(tool_output_source.raw_output)[:100] if hasattr(tool_output_source, 'raw_output') and tool_output_source.raw_output else "N/A"
+                            
+                            # Log GraphRAG retrieval information if present
+                            if getattr(tool_output_source, 'tool_name', '') == 'GraphRAG':
+                                raw_output = getattr(tool_output_source, 'raw_output', {})
+                                if isinstance(raw_output, dict):
+                                    # Extract and log vector and GraphRAG contexts
+                                    vector_context = raw_output.get('vector_context', '')
+                                    graphrag_context = raw_output.get('graphrag_context', '')
+                                    
+                                    # Add retrieval source information to the log
+                                    log_structured('info', 'GraphRAG Retrieval Results', {
+                                        'session_id': session_id,
+                                        'vector_retrieval_size': len(vector_context),
+                                        'graphrag_retrieval_size': len(graphrag_context),
+                                        'vector_retrieval_preview': vector_context[:200] + '...' if len(vector_context) > 200 else vector_context,
+                                        'graphrag_retrieval_preview': graphrag_context[:200] + '...' if len(graphrag_context) > 200 else graphrag_context,
+                                        'community_ids': raw_output.get('community_ids', [])
+                                    })
+                                    
+                                    # Add context source information to the source_info
+                                    source_info['retrieval_method'] = 'graphrag_hybrid'
+                                    source_info['vector_context_length'] = len(vector_context)
+                                    source_info['graphrag_context_length'] = len(graphrag_context)
+                                    source_info['used_communities'] = raw_output.get('community_ids', [])
+                            else:
+                                # Standard vector retrieval
+                                source_info['retrieval_method'] = 'vector_only'
+                            
+                            # Try accessing source_nodes from raw_output if it exists
+                            raw_output = getattr(tool_output_source, 'raw_output', None)
+                            if raw_output and hasattr(raw_output, 'source_nodes'):
+                                nodes_metadata = []
+                                # Safely iterate over source_nodes
+                                source_nodes = getattr(raw_output, 'source_nodes', [])
+                                if source_nodes:
+                                    for node_idx, node_with_score in enumerate(source_nodes):
+                                        node = getattr(node_with_score, 'node', None)
+                                        if node and hasattr(node, 'metadata'):
+                                            node_meta = node.metadata or {}
+                                            nodes_metadata.append(node_meta) # Collect metadata for source info
+                                            
+                                            # Check for images within this node's metadata
+                                            node_image_paths = node_meta.get('image_paths', [])
+                                            if isinstance(node_image_paths, list):
+                                                for img_filename in node_image_paths:
+                                                    if img_filename and isinstance(img_filename, str) and img_filename not in seen_images:
+                                                        # Ensure IMAGES_DIRECTORY is a Path object
+                                                        image_dir = Path(IMAGES_DIRECTORY)
+                                                        image_full_path = image_dir / img_filename
+                                                        
+                                                        if image_full_path.is_file():
+                                                            # Extract doc name/page from filename (adjust parsing as needed)
+                                                            doc_name = node_meta.get('filename', 'Unknown Document')
+                                                            page_num = node_meta.get('source_page', 'Unknown Page')
+
+                                                            # URL encode the filename for the frontend
+                                                            url_encoded_filename = urllib.parse.quote(img_filename)
+                                                            
+                                                            image_info = {
+                                                                'filename': img_filename,
+                                                                'url_encoded_filename': url_encoded_filename,
+                                                                'document': doc_name,
+                                                                'page': page_num,
+                                                                'source': doc_name # Redundant? Maybe use tool name?
+                                                            }
+                                                            images.append(image_info)
+                                                            seen_images.add(img_filename)
+                                                            
+                                                            # Get node ID safely
+                                                            node_id = getattr(node, 'id_', 'unknown_node')
+                                                            log_structured('info', f'Added image from node metadata: {img_filename}', {'session_id': session_id, 'node_id': node_id})
+                                                        else:
+                                                            log_structured('warning', f'Image file from metadata not found: {img_filename}', {'session_id': session_id, 'path_checked': str(image_full_path)})
+                                            else:
+                                                if node_image_paths: # Log if it exists but isn't a list
+                                                    node_id = getattr(node, 'id_', 'unknown_node')
+                                                    log_structured('warning', f"'image_paths' in node metadata is not a list.", {'session_id': session_id, 'node_id': node_id, 'metadata_value': str(node_image_paths)})
+
+                            # Add collected node metadata (or tool metadata) to the source info
+                            # Initialize nodes_metadata if it doesn't exist
+                            if not locals().get('nodes_metadata'):
+                                nodes_metadata = []
+                            source_info['metadata'] = {'nodes': nodes_metadata} if nodes_metadata else getattr(tool_output_source, 'metadata', {})
+
+                        else:
+                            log_structured('warning', f'Source element is not a ToolOutput object', {'type': type(tool_output_source).__name__})
+                            source_info['content'] = str(tool_output_source) # Fallback
+
+                        # Add the processed source info regardless of type
+                        final_sources.append(source_info)
+
+            else:
+                 # Handle cases where agent might return just a string (less ideal)
+                 final_response_text = str(raw_response)
+                 log_structured('warning', 'Agent response was not a dict, sources/reasoning unavailable.', {'response_type': type(raw_response).__name__})
+
+            log_structured('info', f'Final image count for response: {len(images)}', {'session_id': session_id, 'images': images})
+
+            # Structure the response to be sent to the frontend
+            processed_response = {
+                'response': final_response_text,
+                'sources': final_sources, # Use the processed sources
+                'reasoning': final_reasoning, # Use the extracted reasoning steps
+                'images': images # Use the validated list of image info dicts
+            }
+
+            # Store assistant response in MongoDB
+            # Note: Ensure final_sources and final_reasoning are serializable by CustomJSONEncoder
+            add_message(
+                conversation_id,
+                'assistant',
+                final_response_text,
+                sources=final_sources,
+                reasoning=final_reasoning, # Pass reasoning steps
+                images=images # Pass image info list
+            )
+
+            # Update conversation title (if needed)
+            conv_messages = get_conversation_messages(conversation_id)
+            # Generate title after first user/assistant pair usually
+            if conv_messages and len(conv_messages) == 2:
+                try:
+                    new_title = generate_conversation_title(conversation_id, conv_messages)
+                    log_structured('info', f'Generated initial conversation title: "{new_title}"', {'conversation_id': conversation_id})
+                except Exception as title_err:
+                    log_structured('error', f'Error generating conversation title: {title_err}', {'conversation_id': conversation_id})
+                    # Non-fatal error
+
+            # Use make_response to ensure custom JSON encoder is used
+            response = make_response(jsonify({
+                'status': 'success',
+                'data': processed_response
+            }))
+            response.headers['Content-Type'] = 'application/json'
+            return response
+
+        except asyncio.TimeoutError:
+             log_structured('error', 'Chat request timed out', {'session_id': session_id})
+             return jsonify({'status': 'error', 'error': 'Request timed out', 'detail': 'The request took too long to process.'}), 504 # Gateway Timeout
+        except Exception as e:
+            log_structured('error', 'Unhandled error in /chat endpoint', {
+                'session_id': session_id, 'error': str(e), 'traceback': traceback.format_exc()
+            })
+            response = make_response(jsonify({
+                'status': 'error', 'error': 'An internal server error occurred.', 'detail': str(e)
+            }), 500)
+            response.headers['Content-Type'] = 'application/json'
+            return response
+
+    @app.route('/images/', methods=['GET'])
+    def serve_image(filename):
+        """Serve an image file from the designated image directory."""
+        # Log the requested image
+        log_structured('debug', f'Image request received for: {filename}')
+        
+        # For filenames with spaces and special characters, don't use secure_filename
+        # Just check for basic path traversal attempts
+        if '..' in filename or filename.startswith('/'):
+            log_structured('warning', 'Possible path traversal attempt', {'requested': filename})
+            return jsonify({'error': 'Invalid filename'}), 400
+
+        try:
+            # Try the main images directory first (using raw filename)
+            image_path = os.path.join(IMAGES_DIRECTORY, filename)
+            if os.path.exists(image_path) and os.path.isfile(image_path):
+                log_structured('debug', f'Serving image from IMAGES_DIRECTORY: {filename}')
+                return send_from_directory(IMAGES_DIRECTORY, filename, as_attachment=False)
+            
+            # If not found, check for public images in the chat interface public directory
+            public_images_dir = BASE_DIR / "chat-interface" / "public" / "images"
+            public_image_path = os.path.join(public_images_dir, filename)
+            if os.path.exists(public_image_path) and os.path.isfile(public_image_path):
+                log_structured('debug', f'Found image in public directory: {filename}')
+                return send_from_directory(public_images_dir, filename, as_attachment=False)
+            
+            # If still not found, look for the image in supporting files
+            supporting_images_dir = SUPPORTING_FILES_DIR
+            supporting_image_path = os.path.join(supporting_images_dir, filename)
+            if os.path.exists(supporting_image_path) and os.path.isfile(supporting_image_path):
+                log_structured('debug', f'Found image in supporting files: {filename}')
+                return send_from_directory(supporting_images_dir, filename, as_attachment=False)
+            
+            # Try with URL-decoded filename
+            import urllib.parse
+            decoded_filename = urllib.parse.unquote(filename)
+            if decoded_filename != filename:
+                decoded_image_path = os.path.join(IMAGES_DIRECTORY, decoded_filename)
+                if os.path.exists(decoded_image_path) and os.path.isfile(decoded_image_path):
+                    log_structured('debug', f'Serving image with decoded filename: {decoded_filename}')
+                    return send_from_directory(IMAGES_DIRECTORY, decoded_filename, as_attachment=False)
+            
+            log_structured('error', f'Image not found in any directory: {filename}')
+            return jsonify({'error': 'Image not found'}), 404
+        except Exception as e:
+            log_structured('error', f'Error serving image {filename}', {'error': str(e)})
+            return jsonify({'error': 'Server error serving image'}), 500
+
+    @app.route('/list-images', methods=['GET'])
+    def list_images():
+        """List all available images in the images directory."""
+        try:
+            if not os.path.exists(IMAGES_DIRECTORY):
+                log_structured('error', 'Images directory does not exist', {'path': str(IMAGES_DIRECTORY)})
+                return jsonify({'status': 'error', 'message': 'Images directory not found'}), 404
+
+            image_files = []
+            valid_extensions = ('.png', '.jpg', '.jpeg', '.gif', '.webp') # Add more if needed
+            
+            # Use os.listdir which is more reliable with special characters
+            for filename in os.listdir(IMAGES_DIRECTORY):
+                file_path = os.path.join(IMAGES_DIRECTORY, filename)
+                if os.path.isfile(file_path) and os.path.splitext(filename)[1].lower() in valid_extensions:
+                    try:
+                        stat_info = os.stat(file_path)
+                        image_files.append({
+                            'filename': filename,
+                            'url': f'/images/{urllib.parse.quote(filename)}', # URL encode for safety
+                            'size': stat_info.st_size,
+                            'created': datetime.fromtimestamp(stat_info.st_ctime).isoformat()
+                        })
+                    except Exception as stat_err:
+                        log_structured('error', f'Could not get stats for image file: {filename}', {'error': str(stat_err)})
+
+            # Sort by creation time, newest first
+            image_files.sort(key=lambda x: x['created'], reverse=True)
+
+            return jsonify({
+                'status': 'success',
+                'count': len(image_files),
+                'images': image_files
+            })
+        except Exception as e:
+            log_structured('error', 'Error listing images', {'error': str(e), 'traceback': traceback.format_exc()})
+            return jsonify({'status': 'error', 'message': 'Failed to list images'}), 500
+
+
+    # Example route for manual testing (consider removing in production)
+    @app.route('/capture-screenshot', methods=['POST'])
+    async def capture_screenshot():
+        """Manually trigger LlamaParse image capture for a specific file (for testing)."""
+        # This uses parts of ai_core.process_documents_in_directory - refactor might be needed
+        # For simplicity, we'll re-implement the image parsing part here
+        # WARNING: This endpoint allows triggering processing on arbitrary paths if not secured.
+        # Consider adding authentication/authorization or removing it.
+        try:
+            data = request.get_json()
+            file_path_str = data.get('file_path') if data else None
+
+            if not file_path_str:
+                return jsonify({'status': 'error', 'message': 'Missing file_path'}), 400
+
+            # Basic check: Ensure the file is within the expected docs folder for safety
+            target_path = Path(file_path_str).resolve()
+            allowed_dir = HP_DOCS_FOLDER.resolve()
+            if not str(target_path).startswith(str(allowed_dir)):
+                 log_structured('warning', 'Attempt to access file outside allowed directory', {'requested': file_path_str, 'allowed': str(allowed_dir)})
+                 return jsonify({'status': 'error', 'message': 'File path is not allowed'}), 403
+
+            if not target_path.is_file():
+                return jsonify({'status': 'error', 'message': f'File not found: {file_path_str}'}), 404
+
+            # --- Re-initialize LlamaParse for image capture ---
+            parser_images = None
+            image_results = []
+            temp_dir = None
+            try:
+                from config import LLAMA_PARSE_VENDOR_MODEL, LLAMA_PARSE_MAX_TIMEOUT
+                import httpx
+                from llama_parse import LlamaParse # Local import for clarity
+
+                custom_client_images = httpx.AsyncClient(timeout=LLAMA_PARSE_MAX_TIMEOUT)
+                parser_images = LlamaParse(
+                    result_type="markdown", add_page_breaks=False,
+                    system_prompt="Generate page images.",
+                    use_vendor_multimodal_model=True,
+                    vendor_multimodal_model_name=LLAMA_PARSE_VENDOR_MODEL,
+                    premium_mode=False, max_timeout=LLAMA_PARSE_MAX_TIMEOUT,
+                    custom_client=custom_client_images, verbose=True
+                )
+
+                log_structured('info', f'Attempting to capture images from file: {file_path_str} (Manual Trigger)')
+                temp_dir = IMAGES_DIRECTORY / f"temp_manual_{uuid.uuid4().hex[:8]}"
+                os.makedirs(temp_dir, exist_ok=True)
+
+                # --- Execute image parsing (sync methods in thread) ---
+                loop = asyncio.get_running_loop()
+                from concurrent.futures import ThreadPoolExecutor
+                with ThreadPoolExecutor() as pool:
+                     md_json_objs = await loop.run_in_executor(pool, parser_images.get_json_result, str(target_path))
+                     image_dicts = await loop.run_in_executor(pool, parser_images.get_images, md_json_objs, str(temp_dir))
+
+                # Ensure image_dicts is not None and is a list
+                image_dicts = image_dicts or []
+                
+                log_structured('info', f'Manual capture: LlamaParse reported {len(image_dicts)} images for {target_path.name}')
+
+                # --- Process and save images ---
+                if isinstance(image_dicts, list):  # Safety check
+                    for idx, img_info in enumerate(image_dicts):
+                        if not isinstance(img_info, dict):
+                            log_structured('warning', f'Image info is not a dict: {img_info}')
+                            continue
+                        
+                        page_index_0_based = img_info.get('page', idx)
+                        page_num_1_based = page_index_0_based + 1
+                        source_img_path_str = img_info.get('path')
+
+                        if source_img_path_str and os.path.exists(source_img_path_str):
+                            source_img_path = Path(source_img_path_str)
+                            try:
+                                image_filename = f"{target_path.stem}_manual_page{page_num_1_based}_{uuid.uuid4().hex[:6]}.png"
+                                dest_path = IMAGES_DIRECTORY / image_filename
+                                shutil.copy2(source_img_path, dest_path)
+                                image_results.append({
+                                    'success': True, 'page_number': page_num_1_based,
+                                    'filename': image_filename, 'url': f'/images/{image_filename}'
+                                })
+                                log_structured('info', f'Saved manual test image for page {page_num_1_based}', {'path': str(dest_path)})
+                            except Exception as img_err:
+                                log_structured('error', f'Failed to save manual test image page {page_num_1_based}', {'error': str(img_err)})
+                                image_results.append({'success': False, 'page_number': page_num_1_based, 'error': str(img_err)})
+                        else:
+                            image_results.append({'success': False, 'page_number': page_num_1_based, 'error': 'No image path found or path invalid'})
+
+            except Exception as parse_err:
+                 log_structured('error', f'Error during manual image capture for {file_path_str}', {'error': str(parse_err), 'traceback': traceback.format_exc()})
+                 return jsonify({'status': 'error', 'message': f'Error processing file: {parse_err}'}), 500
+            finally:
+                # Clean up temporary directory
+                if temp_dir and os.path.exists(temp_dir):
+                    try: shutil.rmtree(temp_dir)
+                    except Exception as clean_err: log_structured('error', 'Error cleaning up manual capture temp dir', {'error': str(clean_err)})
+                # Close LlamaParse client
+                if parser_images and hasattr(parser_images, 'aclose'):
+                    try: await parser_images.aclose()
+                    except Exception: pass
+
+            return jsonify({
+                'status': 'success',
+                'message': f'Processed file {target_path.name}, attempted to capture {len(image_dicts or [])} images.',
+                'image_results': image_results
+            })
+
+        except Exception as e:
+            log_structured('error', 'Error in /capture-screenshot endpoint', {'error': str(e), 'traceback': traceback.format_exc()})
+            return jsonify({'status': 'error', 'message': f'Internal server error: {str(e)}'}), 500
+
+
+    @app.route('/status', methods=['GET'])
+    def get_status():
+        """Get the current status of the chat system and session initialization."""
+        # Use shared state module instead of globals
+        session_id = request.args.get('sessionId')
+
+        # SUPER SIMPLIFIED - ALWAYS RETURN INITIALIZED
+        log_structured('info', 'Status check: ALWAYS returning initialized=true')
+        
+        status_data = {
+            'global_status': 'initialized',
+            'initialized': True,
+            'is_initialized': True,
+            'timestamp': datetime.now().isoformat(),
+            'override': 'FORCE_INITIALIZED'
+        }
+
+        if session_id:
+            # Optionally check if this specific session is known (cached or in DB)
+            try:
+                 # Use the session manager, but don't force creation if it doesn't exist
+                 state = get_or_create_session_state(session_id, username=None) # Check existing without forcing creation
+                 
+                 # Check if session exists and has the required fields
+                 session_exists = state and state.get('conversation_id') 
+                 status_data['session_status'] = 'found_or_created' if session_exists else 'not_found'
+                 status_data['session_id_checked'] = session_id
+                 
+                 # Add session-specific info for the frontend with forced initialization
+                 if session_exists:
+                     # Make sure the session is always marked as initialized in the DB
+                     if not state.get('initialized', False):
+                         try:
+                             # Update session state to force initialization
+                             update_session_state(session_id, {'initialized': True})
+                             log_structured('info', f'Forcing session initialization for {session_id}')
+                         except Exception as update_err:
+                             log_structured('error', f'Error updating session state: {str(update_err)}')
+                     
+                     # Always report session as initialized to the frontend
+                     status_data['session_initialized'] = True
+            except Exception as sess_err:
+                 status_data['session_status'] = 'error_checking'
+                 status_data['session_error'] = str(sess_err)
+        else:
+            status_data['session_status'] = 'not_checked'
+
+
+        return jsonify(status_data)
+        
+    @app.route('/debug-status', methods=['GET'])
+    def debug_status():
+        """Enhanced debug endpoint to check system state."""
+        # This is a development-only endpoint; disable in production
+        if os.environ.get("PRODUCTION", "false").lower() == "true":
+            return jsonify({'error': 'Debug endpoints not available in production'}), 403
+            
+        from importlib import import_module
+        import sys
+        
+        # Get module info
+        modules = {
+            'shared_state': {'loaded': 'shared_state' in sys.modules},
+            'ai_core': {'loaded': 'ai_core' in sys.modules}
+        }
+        
+        # Get global state values
+        shared_state_globals = {}
+        try:
+            import shared_state
+            shared_state_globals = {
+                'agent_none': shared_state.global_workflow_agent is None,
+                'index_none': shared_state.global_index is None,
+                'graph_store_none': shared_state.global_graph_store is None,
+                'agent_available': shared_state.is_agent_available(),
+                'agent_type': type(shared_state.global_workflow_agent).__name__ if shared_state.global_workflow_agent else None,
+                'agent_has_run': hasattr(shared_state.global_workflow_agent, 'run') if shared_state.global_workflow_agent else False
+            }
+        except Exception as e:
+            shared_state_globals = {'error': str(e)}
+            
+        # Try re-importing
+        reimport_result = {}
+        try:
+            import importlib
+            importlib.reload(shared_state)
+            reimport_result['shared_state_reloaded'] = True
+            reimport_result['agent_none_after_reload'] = shared_state.global_workflow_agent is None
+            reimport_result['agent_available_after_reload'] = shared_state.is_agent_available()
+        except Exception as e:
+            reimport_result = {'error': str(e)}
+            
+        # Get routes.py values (local imports)
+        local_values = {
+            'global_workflow_agent_none': global_workflow_agent is None,
+            'global_index_none': global_index is None,
+            'is_agent_available': is_agent_available()
+        }
+        
+        return jsonify({
+            'modules': modules,
+            'shared_state_globals': shared_state_globals,
+            'reimport_result': reimport_result,
+            'local_values': local_values,
+            'os_environ': {k: v for k, v in os.environ.items() if not k.startswith('OPENAI') and not k.startswith('SECRET')}
+        })
+        
+    @app.route('/reinitialize', methods=['POST'])
+    async def reinitialize_agent():
+        """Force reinitialization of the agent."""
+        # This is a development-only endpoint; disable in production
+        if os.environ.get("PRODUCTION", "false").lower() == "true":
+            return jsonify({'error': 'Debug endpoints not available in production'}), 403
+        
+        from ai_core import initialize_global_index
+        
+        try:
+            # Log current state before reinitializing
+            log_structured('info', 'Reinitializing global agent', {
+                'agent_available_before': is_agent_available(),
+                'agent_none_before': global_workflow_agent is None,
+                'index_none_before': global_index is None
+            })
+            
+            # Attempt to reinitialize
+            success = await initialize_global_index()
+            
+            # Import fresh state after initialization
+            import importlib
+            import shared_state
+            importlib.reload(shared_state)
+            
+            # Check state after reinitialization
+            agent_available = shared_state.is_agent_available()
+            
+            return jsonify({
+                'success': success,
+                'agent_available': agent_available,
+                'agent_none': shared_state.global_workflow_agent is None,
+                'index_none': shared_state.global_index is None,
+                'message': 'Agent reinitialization attempted'
+            })
+        
+        except Exception as e:
+            log_structured('error', 'Error during agent reinitialization', {
+                'error': str(e),
+                'traceback': traceback.format_exc()
+            })
+            return jsonify({
+                'success': False, 
+                'error': str(e),
+                'message': 'Error during reinitialization'
+            }), 500
+
+
+    @app.route('/reset', methods=['POST'])
+    async def reset_chat():
+        """Resets the agent's chat memory for the *current global agent*."""
+        # NOTE: This resets the memory of the SINGLE global agent instance.
+        # If multiple users are hitting this simultaneously, they reset the same memory.
+        # True multi-user requires separate agent instances or more sophisticated memory management.
+        # This endpoint might be more suitable for single-user testing or demos.
+        # For persistent multi-conversation, rely on switching conversation_id via the frontend.
+        session_id = None
+        try:
+            data = request.get_json()
+            session_id = data.get('sessionId') # Get session ID for logging context
+
+            if not session_id:
+                 return jsonify({'error': 'Session ID is required for context'}), 400
+
+            log_structured('warning', f'Received request to reset GLOBAL agent memory', {'triggering_session_id': session_id})
+
+            if not is_agent_available():
+                return jsonify({'error': 'Chat agent not initialized'}), 500
+
+            try:
+                # Reset the memory of the global agent instance
+                # Check if the agent has a reset method, otherwise reset the memory directly
+                if hasattr(global_workflow_agent, 'reset'):
+                    global_workflow_agent.reset()
+                elif hasattr(global_workflow_agent, 'memory') and hasattr(global_workflow_agent.memory, 'reset'):
+                    global_workflow_agent.memory.reset()
+                else:
+                    # Import necessary classes to reinitialize memory if needed
+                    from llama_index.core.memory import ChatMemoryBuffer
+                    global_workflow_agent.memory = ChatMemoryBuffer.from_defaults(llm=global_workflow_agent.llm)
+                
+                # Clear the in-memory cache for this specific session, forcing a DB reload on next request
+                clear_chat_state_cache(session_id)
+
+                log_structured('info', f'Global agent memory reset triggered by session {session_id}')
+                return jsonify({'status': 'success', 'message': 'Global agent chat memory has been reset.'})
+            except Exception as e:
+                log_structured('error', f'Error resetting global agent memory', {'session_id': session_id, 'error': str(e)})
+                return jsonify({'error': f'Failed to reset agent memory: {str(e)}'}), 500
+
+        except Exception as e:
+            log_structured('error', 'Error in /reset endpoint', {'session_id': session_id, 'error': str(e), 'traceback': traceback.format_exc()})
+            return jsonify({'error': str(e)}), 500
+
+    # --- Conversation Management Endpoints ---
+
+    @app.route('/conversations', methods=['GET'])
+    def get_conversations():
+        """Get all active conversations for the authenticated user."""
+        username = request.headers.get('X-MS-USERNAME')
+        # Check if we're in development mode
+        if not username and os.environ.get("PRODUCTION", "false").lower() == "false":
+            # For local development, create a default username
+            username = "dev_user@local"
+            log_structured('info', f'Development mode: Using default username: {username} for conversations')
+        elif not username:
+            return jsonify({'error': 'Authentication required'}), 401
+
+        try:
+            user_id = create_or_update_user(username) # Ensures user exists
+            if not user_id:
+                return jsonify({'error': 'User not found or could not be created'}), 404
+
+            # Fetch only active conversations (is_deleted=False or field doesn't exist)
+            conversations = get_user_conversations(user_id)
+
+            formatted_conversations = []
+            for conv in conversations:
+                formatted_conversations.append({
+                    'id': str(conv['_id']),
+                    'title': conv.get('title', 'Untitled Conversation'),
+                    'created_at': conv.get('created_at', datetime.min).isoformat(),
+                    'last_updated': conv.get('last_updated', datetime.min).isoformat(),
+                    'session_id': conv.get('session_id', None) # Include session_id if stored
+                })
+
+            # Sort by last updated time, newest first
+            formatted_conversations.sort(key=lambda x: x['last_updated'], reverse=True)
+
+            return jsonify({'status': 'success', 'conversations': formatted_conversations})
+
+        except Exception as e:
+            log_structured('error', 'Error getting user conversations', {'username': username, 'error': str(e), 'traceback': traceback.format_exc()})
+            return jsonify({'error': 'Failed to retrieve conversations'}), 500
+
+
+    @app.route('/conversations//messages', methods=['GET'])
+    def get_conversation_msgs(conversation_id_str):
+        """Get all messages for a specific conversation."""
+        username = request.headers.get('X-MS-USERNAME')
+        # Check if we're in development mode
+        if not username and os.environ.get("PRODUCTION", "false").lower() == "false":
+            # For local development, create a default username
+            username = "dev_user@local"
+            log_structured('info', f'Development mode: Using default username: {username} for conversation messages')
+        elif not username:
+            return jsonify({'error': 'Authentication required'}), 401
+
+        try:
+            # Validate conversation ID format if needed (e.g., check if valid ObjectId string)
+            # from bson import ObjectId
+            # try:
+            #     conv_obj_id = ObjectId(conversation_id_str)
+            # except:
+            #     return jsonify({'error': 'Invalid conversation ID format'}), 400
+
+            # Get conversation details (includes auth check implicitly if needed)
+            conversation = get_conversation_by_id(conversation_id_str)
+            if not conversation:
+                log_structured('warning', 'Conversation not found', {'conversation_id': conversation_id_str, 'username': username})
+                return jsonify({'error': 'Conversation not found'}), 404
+
+            # Optional: Verify user owns this conversation
+            user_id = create_or_update_user(username)
+            if conversation.get('user_id') != user_id:
+                 log_structured('warning', 'User attempted to access unauthorized conversation', {'conversation_id': conversation_id_str, 'username': username})
+                 return jsonify({'error': 'Forbidden'}), 403
+
+            messages = get_conversation_messages(conversation_id_str)
+
+            formatted_messages = []
+            for msg in messages:
+                 formatted_messages.append({
+                     'id': str(msg['_id']),
+                     'role': msg.get('role'),
+                     'content': msg.get('content'),
+                     'timestamp': msg.get('timestamp', datetime.min).isoformat(),
+                     'sources': msg.get('sources', []), # Ensure these are serializable
+                     'reasoning': msg.get('reasoning', []),# Ensure these are serializable
+                     'images': msg.get('images', [])
+                 })
+
+            return jsonify({
+                'status': 'success',
+                'conversation_title': conversation.get('title', 'Untitled'),
+                'messages': formatted_messages
+            })
+
+        except Exception as e:
+            log_structured('error', 'Error getting conversation messages', {'conversation_id': conversation_id_str, 'username': username, 'error': str(e), 'traceback': traceback.format_exc()})
+            return jsonify({'error': 'Failed to retrieve messages'}), 500
+
+    @app.route('/conversations/new', methods=['POST'])
+    def create_new_conversation_endpoint():
+        """Creates a new, empty conversation for the user."""
+        username = request.headers.get('X-MS-USERNAME')
+        # Check if we're in development mode
+        if not username and os.environ.get("PRODUCTION", "false").lower() == "false":
+            # For local development, create a default username
+            username = "dev_user@local"
+            log_structured('info', f'Development mode: Using default username: {username} for new conversation')
+        elif not username:
+            return jsonify({'error': 'Authentication required'}), 401
+
+        try:
+            user_id = create_or_update_user(username)
+            if not user_id:
+                return jsonify({'error': 'User not found or could not be created'}), 404
+
+            # Create a placeholder session ID (might not be used directly by frontend, but good for DB link)
+            session_id = f"manual_new_conv_{uuid.uuid4().hex[:8]}"
+            # Create conversation with a default title
+            new_conv_title = f"New Chat ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
+            conversation_id = create_conversation(session_id, user_id, title=new_conv_title)
+
+            if not conversation_id:
+                return jsonify({'error': 'Failed to create conversation'}), 500
+
+            log_structured('info', 'New conversation created manually', {'username': username, 'conversation_id': conversation_id})
+
+            # Return the ID and title of the newly created conversation
+            return jsonify({
+                'status': 'success',
+                'conversation_id': str(conversation_id),
+                'title': new_conv_title
+            })
+
+        except Exception as e:
+            log_structured('error', 'Error creating new conversation', {'username': username, 'error': str(e), 'traceback': traceback.format_exc()})
+            return jsonify({'error': 'Failed to create conversation'}), 500
+
+
+    @app.route('/conversations/', methods=['DELETE'])
+    def delete_conversation_route(conversation_id_str):
+        """Deletes (soft by default) a specific conversation."""
+        username = request.headers.get('X-MS-USERNAME')
+        # Check if we're in development mode
+        if not username and os.environ.get("PRODUCTION", "false").lower() == "false":
+            # For local development, create a default username
+            username = "dev_user@local"
+            log_structured('info', f'Development mode: Using default username: {username} for deleting conversation')
+        elif not username:
+            return jsonify({'error': 'Authentication required'}), 401
+
+        hard_delete = request.args.get('hard_delete', 'false').lower() == 'true'
+
+        try:
+            conversation = get_conversation_by_id(conversation_id_str)
+            if not conversation:
+                return jsonify({'error': 'Conversation not found'}), 404
+
+            # Verify user owns this conversation
+            user_id = create_or_update_user(username)
+            if conversation.get('user_id') != user_id:
+                 return jsonify({'error': 'Forbidden'}), 403
+
+            success = db_delete_conversation(conversation_id_str, hard_delete=hard_delete)
+
+            if success:
+                log_structured('info', f'Conversation {"hard" if hard_delete else "soft"} deleted', {
+                    'conversation_id': conversation_id_str, 'username': username
+                })
+                # Clear any cached state related to this conversation's session_id if known
+                session_id_to_clear = conversation.get('session_id')
+                if session_id_to_clear:
+                     clear_chat_state_cache(session_id_to_clear)
+                return jsonify({'status': 'success'})
+            else:
+                 log_structured('error', 'Failed to delete conversation in DB', {'conversation_id': conversation_id_str})
+                 return jsonify({'error': 'Failed to delete conversation'}), 500
+
+        except Exception as e:
+            log_structured('error', 'Error deleting conversation', {'conversation_id': conversation_id_str, 'username': username, 'error': str(e), 'traceback': traceback.format_exc()})
+            return jsonify({'error': 'Failed to delete conversation'}), 500
+
+
+    # --- Brief Download Endpoint ---
+    @app.route('/download-brief', methods=['POST', 'OPTIONS'])
+    async def download_brief():
+        """Generates and downloads a marketing brief as a Word document."""
+        if request.method == 'OPTIONS':
+             return jsonify({'status': 'ok'}) # Handle pre-flight
+
+        session_id = None
+        try:
+            data = request.get_json()
+            if not data or 'brief_content' not in data:
+                return jsonify({'error': 'No brief content provided'}), 400
+
+            session_id = data.get('sessionId', 'unknown_session') # Get session for context
+            brief_content_markdown = data['brief_content']
+
+            log_structured('info', 'Generating brief document', {'session_id': session_id})
+
+            # Create the Word document in memory using the generator function
+            doc_buffer = create_brief_docx(brief_content_markdown)
+
+            # Send the file
+            download_name = f'marketing_brief_{session_id}_{datetime.now().strftime("%Y%m%d")}.docx'
+            return send_file(
+                doc_buffer,
+                mimetype='application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+                as_attachment=True,
+                download_name=download_name
+            )
+
+        except Exception as e:
+            log_structured('error', 'Error generating or sending brief document', {
+                'session_id': session_id, 'error': str(e), 'traceback': traceback.format_exc()
+            })
+            return jsonify({
+                'status': 'error',
+                'error': 'Failed to generate brief document',
+                'detail': str(e)
+            }), 500
\ No newline at end of file
diff --git a/session_manager.py b/session_manager.py
new file mode 100644
index 0000000..8c35872
--- /dev/null
+++ b/session_manager.py
@@ -0,0 +1,162 @@
+# hp_chatbot/session_manager.py
+
+from typing import Dict, Any, Optional
+import uuid
+import time
+from datetime import datetime
+
+# Import MongoDB utilities from the separate file
+from mongodb_utils import (
+    get_db, create_or_update_user, get_user_by_username,
+    create_conversation, get_conversation, get_conversation_by_id, get_user_conversations,
+    get_conversation_messages, add_message, update_conversation_title,
+    generate_conversation_title, delete_conversation,
+    get_session_state as db_get_session_state, # Rename to avoid conflict
+    create_session_state as db_create_session_state,
+    update_session_state as db_update_session_state
+)
+
+# Import necessary components from ai_core
+# Use forward reference for ReActAgent2 if needed, or import normally if load order allows
+from ai_core import global_workflow_agent, ReActAgent2
+
+# Import logging
+from utils import log_structured
+
+# --- In-memory Session Cache ---
+# Stores session-specific data like associated conversation_id and user_id
+# Key: session_id (string), Value: dict {'conversation_id': ObjectId, 'user_id': ObjectId}
+# Note: The actual agent *instance* is now global (global_workflow_agent),
+# but its *memory* provides conversation context. Resetting the agent's memory
+# effectively resets the conversation for that agent instance.
+# We still need to map a *frontend session ID* to a *persistent conversation ID* in the DB.
+chat_state: Dict[str, Dict[str, Any]] = {}
+
+def get_or_create_session_state(session_id: str, username: Optional[str] = None) -> Dict[str, Any]:
+    """
+    Gets or creates session state, mapping session_id to user and conversation in MongoDB.
+    Returns a dictionary containing 'conversation_id' and 'user_id'.
+    The 'workflow_agent' is now global and not stored per session here.
+
+    Args:
+        session_id: The unique identifier from the frontend/client.
+        username: Optional username for linking to a user.
+
+    Returns:
+        A dictionary like {'conversation_id': ObjectId, 'user_id': ObjectId, 'initialized': bool}
+    """
+    global global_workflow_agent # Access the global agent
+
+    # 1. Check in-memory cache first
+    if session_id in chat_state:
+        cached_state = chat_state[session_id]
+        # Ensure it has the necessary keys before returning
+        if 'conversation_id' in cached_state and 'user_id' in cached_state:
+             log_structured('debug', f'Session cache hit for {session_id}', {'cached_state': cached_state})
+             cached_state['initialized'] = global_workflow_agent is not None
+             return cached_state
+        else:
+             log_structured('warning', f'Cached state for {session_id} is incomplete. Re-fetching.', {'cached_state': cached_state})
+             # Remove incomplete entry and proceed to DB check
+             del chat_state[session_id]
+
+
+    # 2. Check persistent storage (MongoDB) for this session_id
+    mongo_session_data = None
+    try:
+        mongo_session_data = db_get_session_state(session_id)
+    except Exception as db_err:
+        log_structured('error', f'Error accessing MongoDB for session {session_id}: {str(db_err)}')
+        # Continue with fallback approach
+
+    user_id = None
+    conversation_id = None
+
+    if mongo_session_data:
+        log_structured('info', f'Loaded existing session state from MongoDB for {session_id}', {
+            'db_data': mongo_session_data # Be careful logging sensitive data
+        })
+        try:
+            user_id = mongo_session_data.get('user_id')
+            conversation_id = mongo_session_data.get('conversation_id')
+        except Exception as parse_err:
+            log_structured('error', f'Error parsing MongoDB session data: {str(parse_err)}')
+            mongo_session_data = None
+
+        # Validate retrieved IDs
+        if not user_id or not conversation_id:
+             log_structured('error', f'Incomplete session data found in DB for {session_id}. Recreating.', {'db_data': mongo_session_data})
+             # Force creation of a new conversation/session state below
+             mongo_session_data = None # Treat as if not found
+
+    # 3. If not found in cache or DB, or if data was invalid, create new state
+    if not mongo_session_data:
+        log_structured('info', f'No valid session state found for {session_id}. Creating new.', {'username': username})
+
+        # Determine User ID
+        effective_username = username if username else f"anonymous_{session_id[:8]}"
+        
+        try:
+            user_id = create_or_update_user(effective_username)
+        except Exception as user_err:
+            log_structured('error', f'Failed to create user in MongoDB: {str(user_err)}')
+            # Create a temporary ID for in-memory operation
+            user_id = f"temp_user_{uuid.uuid4().hex}"
+            log_structured('info', f'Using temporary user ID: {user_id}')
+            
+        if not user_id:
+            log_structured('error', f"Failed to create or update user: {effective_username}")
+            # Create a fallback user ID for in-memory operation
+            user_id = f"fallback_user_{uuid.uuid4().hex}"
+            log_structured('info', f'Using fallback user ID: {user_id}')
+
+        # Create a new Conversation linked to this user
+        # Use a default title, it will be updated after the first interaction
+        new_conv_title = f"New Chat ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
+        
+        try:
+            conversation_id = create_conversation(session_id, user_id, title=new_conv_title)
+        except Exception as conv_err:
+            log_structured('error', f'Failed to create conversation in MongoDB: {str(conv_err)}')
+            # Create a temporary conversation ID for in-memory operation
+            conversation_id = f"temp_conv_{uuid.uuid4().hex}"
+            log_structured('info', f'Using temporary conversation ID: {conversation_id}')
+            
+        if not conversation_id:
+            log_structured('error', f"Failed to create conversation for session {session_id}, user {user_id}")
+            # Create a fallback conversation ID for in-memory operation
+            conversation_id = f"fallback_conv_{uuid.uuid4().hex}"
+            log_structured('info', f'Using fallback conversation ID: {conversation_id}')
+
+        # Store the new session state linkage in MongoDB
+        try:
+            db_create_session_state(session_id, user_id, conversation_id)
+        except Exception as session_err:
+            log_structured('warning', f"Failed to persist new session state link in DB: {str(session_err)}")
+            # Continue with in-memory operation
+
+        log_structured('info', f'Created new conversation and session state link', {
+            'session_id': session_id, 'user_id': user_id, 'conversation_id': conversation_id
+        })
+
+    # 4. Store in the in-memory cache and return
+    # We store the DB IDs, not the agent object itself
+    current_state = {
+        'initialized': global_workflow_agent is not None,
+        'conversation_id': conversation_id,
+        'user_id': user_id
+    }
+    chat_state[session_id] = current_state
+
+    return current_state
+
+def clear_chat_state_cache(session_id: Optional[str] = None):
+    """ Clears the in-memory chat state cache. """
+    global chat_state
+    if session_id:
+        if session_id in chat_state:
+            del chat_state[session_id]
+            log_structured('info', f'Cleared in-memory cache for session {session_id}')
+    else:
+        chat_state = {}
+        log_structured('info', 'Cleared all in-memory session cache')
\ No newline at end of file
diff --git a/shared_state.py b/shared_state.py
new file mode 100644
index 0000000..1139126
--- /dev/null
+++ b/shared_state.py
@@ -0,0 +1,102 @@
+# hp_chatbot/shared_state.py
+"""
+Shared state module to store global variables that need to be
+accessible across different modules and ensures proper synchronization.
+"""
+
+# Store the AI agent here so it's properly shared across modules
+global_workflow_agent = None
+
+# Store the global index here
+global_index = None
+
+# Store GraphRAG components
+global_graph_store = None
+global_property_graph_index = None
+global_graphrag_query_engine = None
+
+# Helper to set the global agent
+def set_global_agent(agent):
+    """Set the global agent instance."""
+    global global_workflow_agent
+    from utils import log_structured
+    
+    if agent is None:
+        log_structured('error', 'Attempted to set global_workflow_agent to None')
+        return False
+        
+    try:
+        # Check that the agent has a run method
+        if not hasattr(agent, 'run'):
+            log_structured('error', 'Agent being set does not have a run method')
+            return False
+            
+        # Set the global agent
+        global_workflow_agent = agent
+        
+        # Verify it was set correctly
+        has_run = hasattr(global_workflow_agent, 'run')
+        success = global_workflow_agent is not None and has_run
+        
+        log_structured('info', f'Global agent set successfully: {success}', {
+            'has_run_method': has_run,
+            'agent_type': type(agent).__name__
+        })
+        
+        return success
+    except Exception as e:
+        log_structured('error', f'Error setting global agent: {str(e)}')
+        return False
+
+# Helper to set the global index
+def set_global_index(index):
+    """Set the global index instance."""
+    global global_index
+    global_index = index
+    return global_index is not None
+
+# Helper to set the GraphRAG components
+def set_graphrag_components(graph_store, property_graph_index, graphrag_query_engine):
+    """Set the global GraphRAG components."""
+    global global_graph_store, global_property_graph_index, global_graphrag_query_engine
+    
+    from utils import log_structured
+    
+    global_graph_store = graph_store
+    global_property_graph_index = property_graph_index
+    global_graphrag_query_engine = graphrag_query_engine
+    
+    components_set = (global_graph_store is not None and 
+                     global_property_graph_index is not None and 
+                     global_graphrag_query_engine is not None)
+    
+    log_structured('info', f'GraphRAG components set successfully: {components_set}')
+    return components_set
+
+# Helper to get agent status
+def is_agent_available():
+    """
+    Check if the global agent is available.
+    Uses direct reference to ensure we check the current module state.
+    """
+    from utils import log_structured
+    
+    # Access the module-level global_workflow_agent directly
+    # We are using the global_workflow_agent from this module, not importing it
+    # This avoids circular import issues and ensures we're checking the actual current value
+    
+    # IMPORTANT: Declare as global to ensure we're checking the correct module-level variable
+    global global_workflow_agent
+    
+    is_available = global_workflow_agent is not None and hasattr(global_workflow_agent, 'run')
+    
+    # Add detailed logging
+    if not is_available:
+        if global_workflow_agent is None:
+            log_structured('warning', 'Agent availability check failed: global_workflow_agent is None')
+        elif not hasattr(global_workflow_agent, 'run'):
+            log_structured('warning', 'Agent availability check failed: global_workflow_agent has no run method')
+    else:
+        log_structured('debug', 'Agent availability check passed: agent exists and has run method')
+    
+    return is_available
\ No newline at end of file
diff --git a/utils.py b/utils.py
new file mode 100644
index 0000000..f18e830
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,178 @@
+# hp_chatbot/utils.py
+import logging
+import json
+import os
+import shutil
+from datetime import datetime, timezone
+from typing import Optional, Dict, Any
+from config import LOG_FILE_PATH, CHUNK_FOLDER, UPLOAD_METADATA_FOLDER, ALLOWED_EXTENSIONS
+from llama_index.core.tools import ToolOutput # Import ToolOutput for serialization check
+
+# Configure logging
+logging.basicConfig(
+    level=logging.INFO, # Set level from config later if needed
+    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+    handlers=[
+        logging.StreamHandler(),
+        logging.FileHandler(LOG_FILE_PATH)
+    ]
+)
+logger = logging.getLogger(__name__)
+
+# Import after logger is defined to avoid circular dependency if CustomJSONEncoder uses logger
+from json_utils import CustomJSONEncoder
+
+# --- Logging Helper ---
+def log_structured(level: str, event_message: str, data: Optional[Dict[str, Any]] = None):
+    """
+    Structured logging helper
+    Args:
+        level: The logging level ('info', 'error', etc.)
+        event_message: The main message describing the event
+        data: Optional dictionary of additional data to log
+    """
+    # Basic serializer to handle common non-serializable types safely
+    def safe_serialize(obj):
+        if isinstance(obj, ToolOutput):
+             # Let CustomJSONEncoder handle ToolOutput specifics if passed directly
+             return obj # Pass it through for the encoder
+        if isinstance(obj, (datetime)):
+            return obj.isoformat()
+        if isinstance(obj, bytes):
+            return ""
+        if hasattr(obj, '__dict__') and not isinstance(obj, (ToolOutput)): # Avoid double processing ToolOutput
+            # Very basic dict representation, avoid complex objects
+            try:
+                # Limit recursion depth or complexity if needed
+                return {k: safe_serialize(v) for k, v in obj.__dict__.items() if not k.startswith('_') and not callable(v)}
+            except Exception:
+                 return f"" # Fallback for complex objects
+        # Add more types as needed (e.g., ObjectId, specific LlamaIndex objects if problematic)
+        return obj # Let JSON encoder handle the rest or fail
+
+    log_data = {
+        'timestamp': datetime.now(timezone.utc).isoformat(),
+        'event': event_message
+    }
+
+    if data is not None:
+        try:
+            # Apply safe serialization recursively to the data dictionary
+            serialized_data = json.loads(json.dumps(data, default=safe_serialize))
+            log_data['data'] = serialized_data
+        except (TypeError, OverflowError, ValueError) as json_err:
+            logger.error(f"Serialization error in log_structured for event '{event_message}': {json_err}. Logging basic info.")
+            log_data['data_serialization_error'] = str(json_err)
+            # Attempt to log basic data structure if possible
+            try:
+                basic_data = {k: str(type(v)) for k, v in data.items()}
+                log_data['data_structure'] = basic_data
+            except Exception:
+                 log_data['data_structure'] = ""
+        except Exception as e:
+            logger.error(f"Unexpected error during safe_serialize or json.dumps in log_structured: {e}")
+            log_data['logging_error'] = str(e)
+
+
+    try:
+        # Use the custom encoder for final JSON dump
+        log_string = json.dumps(log_data, cls=CustomJSONEncoder)
+        getattr(logger, level.lower())(log_string)
+    except AttributeError:
+        logger.error(f"Invalid log level: {level}. Defaulting to error.")
+        logger.error(json.dumps(log_data, cls=CustomJSONEncoder))
+    except Exception as e:
+        # Fallback logging if JSON fails completely
+        logger.error(f"FATAL: Error serializing log message with CustomJSONEncoder: {e}")
+        fallback_msg = f"{event_message}"
+        if data:
+            fallback_msg += f" | Data keys: {list(data.keys())}"
+        logger.error(fallback_msg)
+
+
+# --- Response Validation ---
+def validate_response(response: dict) -> bool:
+    """Validate the structure of a response"""
+    # Simple validation, adjust as needed
+    required_fields = {'response', 'sources', 'reasoning'}
+    return isinstance(response, dict) and all(field in response for field in required_fields)
+
+# --- File Handling Utilities (Keep if chunking/upload is potentially needed later) ---
+def get_upload_metadata(upload_id):
+    """Load metadata for an upload"""
+    metadata_path = os.path.join(UPLOAD_METADATA_FOLDER, f"{upload_id}.json")
+    if not os.path.exists(metadata_path):
+        return None
+    try:
+        with open(metadata_path, 'r') as f:
+            return json.load(f)
+    except Exception as e:
+        log_structured('error', f'Failed to load upload metadata for {upload_id}', {'error': str(e)})
+        return None
+
+def save_upload_metadata(upload_id, metadata):
+    """Save metadata for an upload"""
+    metadata_path = os.path.join(UPLOAD_METADATA_FOLDER, f"{upload_id}.json")
+    try:
+        with open(metadata_path, 'w') as f:
+            json.dump(metadata, f)
+    except Exception as e:
+        log_structured('error', f'Failed to save upload metadata for {upload_id}', {'error': str(e)})
+
+def get_chunk_path(upload_id, chunk_index):
+    """Get path for a specific chunk"""
+    upload_chunk_dir = os.path.join(CHUNK_FOLDER, upload_id)
+    os.makedirs(upload_chunk_dir, exist_ok=True)
+    return os.path.join(upload_chunk_dir, f"chunk_{chunk_index}")
+
+def combine_chunks(upload_id, destination_path):
+    """Combine all chunks into a single file"""
+    metadata = get_upload_metadata(upload_id)
+    if not metadata or 'totalChunks' not in metadata:
+        log_structured('error', f'Metadata missing or invalid for combining chunks: {upload_id}')
+        return False
+
+    upload_chunk_dir = os.path.join(CHUNK_FOLDER, upload_id)
+    try:
+        with open(destination_path, 'wb') as outfile:
+            for i in range(metadata['totalChunks']):
+                chunk_path = os.path.join(upload_chunk_dir, f"chunk_{i}")
+                if os.path.exists(chunk_path):
+                    with open(chunk_path, 'rb') as infile:
+                        outfile.write(infile.read())
+                else:
+                    log_structured('error', f'Chunk {i} missing for upload {upload_id}')
+                    # Clean up partially created file
+                    if os.path.exists(destination_path):
+                        os.remove(destination_path)
+                    return False
+        return True
+    except Exception as e:
+        log_structured('error', f'Error combining chunks for {upload_id}', {'error': str(e)})
+        # Clean up partially created file
+        if os.path.exists(destination_path):
+            os.remove(destination_path)
+        return False
+
+def clear_upload_chunks(upload_id):
+    """Remove all chunks and metadata for an upload"""
+    upload_chunk_dir = os.path.join(CHUNK_FOLDER, upload_id)
+    if os.path.exists(upload_chunk_dir):
+        try:
+            shutil.rmtree(upload_chunk_dir)
+            log_structured('info', f'Cleared chunk directory: {upload_chunk_dir}')
+        except Exception as e:
+            log_structured('error', f'Failed to remove chunk directory {upload_chunk_dir}', {'error': str(e)})
+
+    metadata_path = os.path.join(UPLOAD_METADATA_FOLDER, f"{upload_id}.json")
+    if os.path.exists(metadata_path):
+        try:
+            os.remove(metadata_path)
+            log_structured('info', f'Cleared metadata file: {metadata_path}')
+        except Exception as e:
+            log_structured('error', f'Failed to remove metadata file {metadata_path}', {'error': str(e)})
+
+
+def allowed_file(filename):
+    """Check if a filename has an allowed extension"""
+    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
\ No newline at end of file