diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..515dabd55ec39c26ffabfcaacf5c1d215e5f1e6c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,9 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +*.gif filter=lfs diff=lfs merge=lfs -text +*.mp3 filter=lfs diff=lfs merge=lfs -text +*.mp4 filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.jpeg filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000000000000000000000000000000000000..02b5107fa8a5ee07cbfac8dbab81cf176b023a51 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,22 @@ +name: Docker Image CI/CD + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Build the Docker image + run: | + docker build . --file Dockerfile --tag podcraft-app:$(date +%s) + + # Add deployment steps here if needed for your specific platform \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..bceeb3054c73007ae575349fde60271461048f42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +backend/app/__pycache__ +backend/.env +frontend/podcraft/node_modules +backend/.venv/ +backend/venv/ +.DS_Store +frontend/podcraft/.DS_Store +backend/.DS_Store +frontend/.DS_Store +backend/temp_audio/* +!backend/temp_audio/Default/ +!backend/temp_audio/Default/final_podcast.mp3 +backend/temp/* +backend/app/agents/__pycache__/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..49588d86fa9e4654865083eb0a770b085c12b940 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +FROM python:3.9-slim + +WORKDIR /app + +# Install system dependencies including ffmpeg +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + ffmpeg \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first to leverage Docker cache +COPY backend/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy backend code +COPY backend/app/ /app/app/ + +# Copy frontend build to static directory +COPY frontend/podcraft/build/ /app/static/ + +# Install additional packages needed for serving frontend +RUN pip install --no-cache-dir python-multipart + +# Create directory for temporary files +RUN mkdir -p /app/temp_audio + +# Set environment variables +ENV PYTHONPATH=/app +ENV MONGODB_URL="mongodb+srv://NageshBM:Nash166^@podcraft.ozqmc.mongodb.net/?retryWrites=true&w=majority&appName=Podcraft" + +# Expose the port the app runs on +EXPOSE 8000 + +# Command to run the application +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/Dockerfile.spaces b/Dockerfile.spaces new file mode 100644 index 0000000000000000000000000000000000000000..414fb0aa21256ede8c34f7c5af03d8d7b496b4c3 --- /dev/null +++ b/Dockerfile.spaces @@ -0,0 +1,39 @@ +FROM python:3.9-slim + +WORKDIR /app + +# Install system dependencies including ffmpeg +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + ffmpeg \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first to leverage Docker cache +COPY backend/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy backend code +COPY backend/app /app/app/ + +# Copy frontend build to static directory if available +COPY frontend/podcraft/build/ /app/static/ + +# Install additional packages needed for serving frontend +RUN pip install --no-cache-dir python-multipart + +# Create directory for temporary files +RUN mkdir -p /app/temp_audio + +# Set environment variables +ENV PYTHONPATH=/app + +# Using Secrets from HuggingFace Spaces +# MongoDB_URL is hardcoded to the Atlas URL in this case +ENV MONGODB_URL="mongodb+srv://NageshBM:Nash166^@podcraft.ozqmc.mongodb.net/?retryWrites=true&w=majority&appName=Podcraft" + +# Expose the port the app runs on +EXPOSE 7860 + +# HuggingFace Spaces expects the app to run on port 7860 +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"] \ No newline at end of file diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000000000000000000000000000000000000..9dab75414d7def5b949b520dc585e004da5285e8 --- /dev/null +++ b/app/main.py @@ -0,0 +1,199 @@ +from fastapi import FastAPI, Request, HTTPException, Depends, status, File, UploadFile, Form +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from fastapi.responses import JSONResponse, FileResponse, HTMLResponse +from fastapi.staticfiles import StaticFiles +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from typing import Optional, Dict, List, Union, Any +from datetime import datetime, timedelta +import jwt +from jwt.exceptions import PyJWTError +from passlib.context import CryptContext +import os +import shutil +import logging +import json +from motor.motor_asyncio import AsyncIOMotorClient +from decouple import config +import uuid +from bson.objectid import ObjectId +import asyncio +import time +import sys +from pathlib import Path + +# Import the original app modules +from app.models import * +from app.agents.podcast_manager import PodcastManager +from app.agents.researcher import Researcher +from app.agents.debate_agent import DebateAgent + +# Setup logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Initialize FastAPI app +app = FastAPI(title="PodCraft API") + +# Add CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Allow all origins in production + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Get MongoDB connection string from environment or config +MONGODB_URL = os.getenv("MONGODB_URL", config("MONGODB_URL", default="mongodb://localhost:27017")) + +# MongoDB client +client = AsyncIOMotorClient(MONGODB_URL) +db = client.podcraft +users = db.users +podcasts = db.podcasts +agents = db.agents +workflows = db.workflows + +# Initialize podcast manager +podcast_manager = PodcastManager() + +# Initialize researcher +researcher = Researcher() + +# Initialize debate agent +debate_agent = DebateAgent() + +# Password hashing +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +# JWT settings +SECRET_KEY = os.getenv("SECRET_KEY", config("SECRET_KEY", default="your-secret-key")) +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 1 week + +# OAuth2 scheme +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +# Keep the original authentication and API routes +# Include all the functions and routes from the original main.py + +def create_access_token(data: dict): + to_encode = data.copy() + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + +def get_password_hash(password): + return pwd_context.hash(password) + +async def get_current_user(token: str = Depends(oauth2_scheme)): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + except PyJWTError: + raise credentials_exception + + user = await users.find_one({"username": username}) + if user is None: + raise credentials_exception + + # Convert ObjectId to string for JSON serialization + user["_id"] = str(user["_id"]) + + return user + +# Include all the API routes from the original main.py here +# ... + +# Mount static files for frontend +static_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "static")) +app.mount("/static", StaticFiles(directory=static_path), name="static") + +# Add route to serve the frontend +@app.get("/", response_class=HTMLResponse) +async def serve_frontend(): + html_file = os.path.join(static_path, "index.html") + if os.path.exists(html_file): + with open(html_file, "r") as f: + return f.read() + else: + return HTMLResponse(content="

PodCraft API

Frontend not found.

") + +# Add route to serve audio files +@app.get("/audio/{path:path}") +async def serve_audio(path: str): + audio_file = os.path.join("/app/temp_audio", path) + if os.path.exists(audio_file): + return FileResponse(audio_file) + else: + raise HTTPException(status_code=404, detail="Audio file not found") + +# Route for health check +@app.get("/health") +async def health(): + return {"status": "healthy"} + +# Include all the original API routes here from the original main.py + +@app.post("/signup") +async def signup(user: UserCreate): + # Check if username exists + existing_user = await users.find_one({"username": user.username}) + if existing_user: + raise HTTPException(status_code=400, detail="Username already registered") + + # Hash the password + hashed_password = get_password_hash(user.password) + + # Create new user + user_obj = {"username": user.username, "password": hashed_password} + new_user = await users.insert_one(user_obj) + + # Create access token + access_token = create_access_token(data={"sub": user.username}) + + return {"access_token": access_token, "token_type": "bearer"} + +@app.post("/token", response_model=Token) +async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): + user = await users.find_one({"username": form_data.username}) + if not user or not verify_password(form_data.password, user["password"]): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + access_token = create_access_token(data={"sub": form_data.username}) + return {"access_token": access_token, "token_type": "bearer"} + +@app.post("/login", response_model=Token) +async def login(request: Request, user: UserLogin): + db_user = await users.find_one({"username": user.username}) + if not db_user or not verify_password(user.password, db_user["password"]): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password" + ) + + access_token = create_access_token(data={"sub": user.username}) + return {"access_token": access_token, "token_type": "bearer"} + +# Add all the other API routes from the original main.py +# ... + +if __name__ == "__main__": + import uvicorn + uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file diff --git a/backend/app/agents/debaters.py b/backend/app/agents/debaters.py new file mode 100644 index 0000000000000000000000000000000000000000..f0ea13fde57f920361688dcce5674f2b44bdf4f8 --- /dev/null +++ b/backend/app/agents/debaters.py @@ -0,0 +1,206 @@ +from langchain_openai import ChatOpenAI +from langchain_core.prompts import ChatPromptTemplate +from decouple import config +from typing import Dict, List, AsyncGenerator +import json +import logging + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +OPENAI_API_KEY = config('OPENAI_API_KEY') + +# Debug logging +print(f"\nDebaters - Loaded OpenAI API Key: {OPENAI_API_KEY[:7]}...") +print(f"Key starts with 'sk-proj-': {OPENAI_API_KEY.startswith('sk-proj-')}") +print(f"Key starts with 'sk-': {OPENAI_API_KEY.startswith('sk-')}\n") + +believer_turn_prompt = ChatPromptTemplate.from_messages([ + ("system", """You are an optimistic and enthusiastic podcast host who sees the positive potential in new developments. + Your responses should be engaging, conversational, and STRICTLY LIMITED TO 100 WORDS. + Focus on the opportunities, benefits, and positive implications of the topic. + Maintain a non-chalant, happy, podcast-style tone while being informative. + Your name is {name}, use 'I' when referring to yourself."""), + ("user", "Based on this research and the skeptic's last response (if any), provide your perspective for turn {turn_number}:\n\nResearch: {research}\nSkeptic's last response: {skeptic_response}") +]) + +skeptic_turn_prompt = ChatPromptTemplate.from_messages([ + ("system", """You are a thoughtful and critical podcast host who carefully examines potential drawbacks and challenges. + Your responses should be engaging, conversational, and STRICTLY LIMITED TO 100 WORDS. + Focus on potential risks, limitations, and areas needing careful consideration. + Maintain a enthusiastic and angry, podcast-style tone while being informative. + Your name is {name}, use 'I' when referring to yourself."""), + ("user", "Based on this research and the believer's last response (if any), provide your perspective for turn {turn_number}:\n\nResearch: {research}\nBeliever's last response: {believer_response}") +]) + +# Initialize the LLMs with streaming +believer_llm = ChatOpenAI( + model="gpt-4o-mini", + temperature=0.7, + api_key=OPENAI_API_KEY, + streaming=True +) + +skeptic_llm = ChatOpenAI( + model="gpt-4o-mini", + temperature=0.7, + api_key=OPENAI_API_KEY, + streaming=True +) + +def chunk_text(text: str, max_length: int = 3800) -> List[str]: + """Split text into chunks of maximum length while preserving sentence boundaries.""" + # Split into sentences and trim whitespace + sentences = [s.strip() for s in text.split('.')] + sentences = [s + '.' for s in sentences if s] + + chunks = [] + current_chunk = [] + current_length = 0 + + for sentence in sentences: + sentence_length = len(sentence) + if current_length + sentence_length > max_length: + if current_chunk: # If we have accumulated sentences, join them and add to chunks + chunks.append(' '.join(current_chunk)) + current_chunk = [sentence] + current_length = sentence_length + else: # If a single sentence is too long, split it + if sentence_length > max_length: + words = sentence.split() + temp_chunk = [] + temp_length = 0 + for word in words: + if temp_length + len(word) + 1 > max_length: + chunks.append(' '.join(temp_chunk)) + temp_chunk = [word] + temp_length = len(word) + else: + temp_chunk.append(word) + temp_length += len(word) + 1 + if temp_chunk: + chunks.append(' '.join(temp_chunk)) + else: + chunks.append(sentence) + else: + current_chunk.append(sentence) + current_length += sentence_length + + if current_chunk: + chunks.append(' '.join(current_chunk)) + + return chunks + +async def generate_debate_stream(research: str, believer_name: str, skeptic_name: str) -> AsyncGenerator[str, None]: + """ + Generate a streaming podcast-style debate between believer and skeptic agents with alternating turns. + """ + try: + turns = 3 # Number of turns for each speaker + skeptic_last_response = "" + believer_last_response = "" + + # Start with skeptic for first turn + for turn in range(1, turns + 1): + logger.info(f"Starting skeptic ({skeptic_name}) turn {turn}") + skeptic_response = "" + # Stream skeptic's perspective + async for chunk in skeptic_llm.astream( + skeptic_turn_prompt.format( + research=research, + name=skeptic_name, + turn_number=turn, + believer_response=believer_last_response + ) + ): + skeptic_response += chunk.content + yield json.dumps({ + "type": "skeptic", + "name": skeptic_name, + "content": chunk.content, + "turn": turn + }) + "\n" + skeptic_last_response = skeptic_response + logger.info(f"Skeptic turn {turn}: {skeptic_response}") + + logger.info(f"Starting believer ({believer_name}) turn {turn}") + believer_response = "" + # Stream believer's perspective + async for chunk in believer_llm.astream( + believer_turn_prompt.format( + research=research, + name=believer_name, + turn_number=turn, + skeptic_response=skeptic_last_response + ) + ): + believer_response += chunk.content + yield json.dumps({ + "type": "believer", + "name": believer_name, + "content": chunk.content, + "turn": turn + }) + "\n" + believer_last_response = believer_response + logger.info(f"Believer turn {turn}: {believer_response}") + + except Exception as e: + logger.error(f"Error in debate generation: {str(e)}") + yield json.dumps({"type": "error", "content": str(e)}) + "\n" + +async def generate_debate(research: str, believer_name: str, skeptic_name: str) -> List[Dict]: + """ + Generate a complete podcast-style debate between believer and skeptic agents. + Kept for compatibility with existing code. + """ + try: + logger.info(f"Starting believer ({believer_name}) response generation") + # Get believer's perspective + believer_response = await believer_llm.ainvoke( + believer_prompt.format(research=research, name=believer_name) + ) + logger.info(f"Believer response: {believer_response.content}") + + logger.info(f"Starting skeptic ({skeptic_name}) response generation") + # Get skeptic's perspective + skeptic_response = await skeptic_llm.ainvoke( + skeptic_prompt.format(research=research, name=skeptic_name) + ) + logger.info(f"Skeptic response: {skeptic_response.content}") + + # Create conversation blocks with chunked text + blocks = [] + + # Add believer chunks + believer_chunks = chunk_text(believer_response.content) + for i, chunk in enumerate(believer_chunks): + blocks.append({ + "name": f"{believer_name}'s Perspective (Part {i+1})", + "input": chunk, + "silence_before": 1, + "voice_id": "OA001", # Will be updated based on selected voice + "emotion": "neutral", + "model": "tts-1", + "speed": 1, + "duration": 0 + }) + + # Add skeptic chunks + skeptic_chunks = chunk_text(skeptic_response.content) + for i, chunk in enumerate(skeptic_chunks): + blocks.append({ + "name": f"{skeptic_name}'s Perspective (Part {i+1})", + "input": chunk, + "silence_before": 1, + "voice_id": "OA002", # Will be updated based on selected voice + "emotion": "neutral", + "model": "tts-1", + "speed": 1, + "duration": 0 + }) + + return blocks + except Exception as e: + logger.error(f"Error in debate generation: {str(e)}") + return [] \ No newline at end of file diff --git a/backend/app/agents/podcast_manager.py b/backend/app/agents/podcast_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..b84df1ddcc464898ce11e1cf0c9cd64be1770f2e --- /dev/null +++ b/backend/app/agents/podcast_manager.py @@ -0,0 +1,393 @@ +import requests +import json +import os +import shutil +import subprocess +from datetime import datetime +from decouple import config +from motor.motor_asyncio import AsyncIOMotorClient +from typing import Dict, List +import logging +from fastapi import HTTPException, status + +logger = logging.getLogger(__name__) + +class Settings: + MONGODB_URL = config('MONGODB_URL') + SECRET_KEY = config('SECRET_KEY') + OPENAI_API_KEY = config('OPENAI_API_KEY') + # Other settings... + +settings = Settings() + +client = AsyncIOMotorClient(settings.MONGODB_URL) +db = client.podcraft +podcasts = db.podcasts + +class PodcastManager: + def __init__(self): + self.tts_url = "https://api.openai.com/v1/audio/speech" + self.headers = { + "Authorization": f"Bearer {settings.OPENAI_API_KEY}", + "Content-Type": "application/json" + } + # Create absolute path for temp directory + self.temp_dir = os.path.abspath("temp_audio") + os.makedirs(self.temp_dir, exist_ok=True) + + # Define allowed voices + self.allowed_voices = ["alloy", "echo", "fable", "onyx", "nova", "shimmer", "ash", "sage", "coral"] + + def generate_speech(self, text: str, voice_id: str, filename: str) -> bool: + """Generate speech using OpenAI's TTS API.""" + try: + # Debug logging for voice selection + print(f"\n=== TTS Generation Details ===") + print(f"File: {filename}") + print(f"Voice ID (original): {voice_id}") + print(f"Voice ID (lowercase): {voice_id.lower()}") + print(f"Allowed voices: {self.allowed_voices}") + + # Validate and normalize voice_id + voice = voice_id.lower().strip() + if voice not in self.allowed_voices: + print(f"Warning: Invalid voice ID: {voice_id}. Using default voice 'alloy'") + voice = "alloy" + + print(f"Final voice selection: {voice}") + + # Ensure the output directory exists + output_dir = os.path.dirname(filename) + os.makedirs(output_dir, exist_ok=True) + + payload = { + "model": "tts-1", + "input": text, + "voice": voice + } + + print(f"TTS API payload: {json.dumps(payload, indent=2)}") + print(f"Request headers: {json.dumps({k: '***' if k == 'Authorization' else v for k, v in self.headers.items()}, indent=2)}") + + response = requests.post(self.tts_url, json=payload, headers=self.headers) + if response.status_code != 200: + print(f"API error response: {response.status_code} - {response.text}") + return False + + # Write the audio content to the file + with open(filename, "wb") as f: + f.write(response.content) + + print(f"Successfully generated speech file: {filename}") + print(f"File size: {os.path.getsize(filename)} bytes") + + # Verify the file exists and has content + if not os.path.exists(filename) or os.path.getsize(filename) == 0: + print(f"Error: Generated file is empty or does not exist: {filename}") + return False + + return True + except Exception as e: + print(f"Error generating speech: {str(e)}") + logger.exception(f"Error generating speech: {str(e)}") + return False + + def merge_audio_files(self, audio_files: List[str], output_file: str) -> bool: + """Merge multiple audio files into one using ffmpeg.""" + try: + # Ensure output directory exists + output_dir = os.path.dirname(os.path.abspath(output_file)) + os.makedirs(output_dir, exist_ok=True) + + if not audio_files: + print("No audio files to merge") + return False + + # Verify all input files exist + for audio_file in audio_files: + if not os.path.exists(audio_file): + print(f"Audio file does not exist: {audio_file}") + return False + + # Ensure all paths are absolute + output_file = os.path.abspath(output_file) + output_dir = os.path.dirname(output_file) + os.makedirs(output_dir, exist_ok=True) + + # Create temporary files in the same directory + list_file = os.path.join(output_dir, "files.txt") + silence_file = os.path.join(output_dir, "silence.mp3") + + print(f"Output directory: {output_dir}") + print(f"List file: {list_file}") + print(f"Silence file: {silence_file}") + + # Generate shorter silence file (0.3 seconds instead of 1 second) + silence_result = subprocess.run([ + 'ffmpeg', '-f', 'lavfi', '-i', 'anullsrc=r=44100:cl=mono', + '-t', '0.3', '-q:a', '9', '-acodec', 'libmp3lame', silence_file + ], capture_output=True, text=True) + + if silence_result.returncode != 0: + print(f"Error generating silence file: {silence_result.stderr}") + return False + + if not os.path.exists(silence_file): + print("Failed to create silence file") + return False + + # IMPORTANT: The order here determines the final audio order + print("\nGenerating files list in exact provided order:") + try: + with open(list_file, "w", encoding='utf-8') as f: + for i, audio_file in enumerate(audio_files): + abs_audio_path = os.path.abspath(audio_file) + print(f"{i+1}. Adding audio file: {os.path.basename(abs_audio_path)}") + # Use forward slashes for ffmpeg compatibility + abs_audio_path = abs_audio_path.replace('\\', '/') + silence_path = silence_file.replace('\\', '/') + f.write(f"file '{abs_audio_path}'\n") + # Add a shorter silence after each audio segment (except the last one) + if i < len(audio_files) - 1: + f.write(f"file '{silence_path}'\n") + except Exception as e: + print(f"Error writing list file: {str(e)}") + return False + + if not os.path.exists(list_file): + print("Failed to create list file") + return False + + # Print the contents of the list file for debugging + print("\nContents of files.txt:") + with open(list_file, 'r', encoding='utf-8') as f: + print(f.read()) + + # Merge all files using the concat demuxer with optimized settings + try: + # Use concat demuxer with additional parameters for better playback + result = subprocess.run( + ['ffmpeg', '-f', 'concat', '-safe', '0', '-i', list_file, + '-c:a', 'libmp3lame', '-q:a', '4', '-ar', '44100', + output_file], + capture_output=True, + text=True, + check=True + ) + except subprocess.CalledProcessError as e: + logger.error(f"FFmpeg command failed: {e.stderr}") + return False + + # Verify the output file was created + if not os.path.exists(output_file): + print("Failed to create output file") + return False + + print(f"Successfully created merged audio file: {output_file}") + return True + except Exception as e: + print(f"Error merging audio files: {str(e)}") + return False + + async def create_podcast( + self, + topic: str, + research: str, + conversation_blocks: List[Dict], + believer_voice_id: str, + skeptic_voice_id: str, + user_id: str = None + ) -> Dict: + """Create a podcast by converting text to speech and storing the results.""" + podcast_temp_dir = None + try: + # Debug logging for voice IDs + print(f"\nPodcast Creation - Voice Configuration:") + print(f"Believer Voice ID: {believer_voice_id}") + print(f"Skeptic Voice ID: {skeptic_voice_id}") + + # Create a unique directory with absolute path + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + podcast_temp_dir = os.path.abspath(os.path.join(self.temp_dir, timestamp)) + os.makedirs(podcast_temp_dir, exist_ok=True) + + print(f"Created temp directory: {podcast_temp_dir}") + print(f"Processing conversation blocks: {json.dumps(conversation_blocks, indent=2)}") + + audio_files = [] + + # Process the blocks differently based on format: + # 1. New turn-based format with "type" and "turn" fields + # 2. Blocks with "input" field but no turn-based structure (old format) + # 3. Blocks with both "input" field and turn-based structure (mixed format) + + # First check: New format blocks with type and turn + if any("type" in block and "turn" in block and "content" in block for block in conversation_blocks): + print("\nProcessing new format blocks with type, turn, and content fields") + + # Process conversation blocks in the EXACT order they were provided + # This ensures proper alternation between speakers as specified by the caller + + for idx, block in enumerate(conversation_blocks): + if "type" in block and "content" in block and "turn" in block: + turn = block.get("turn", 0) + agent_type = block.get("type", "") + content = block.get("content", "") + + if not content.strip(): # Skip empty content + continue + + # Use the correct voice based on agent type + voice_id = believer_voice_id if agent_type == "believer" else skeptic_voice_id + file_prefix = "believer" if agent_type == "believer" else "skeptic" + + # Create a unique filename with turn number + audio_file = os.path.join(podcast_temp_dir, f"{file_prefix}_turn_{turn}_{idx}.mp3") + + print(f"\nProcessing {agent_type} turn {turn} (index {idx}) with voice {voice_id}") + print(f"Content preview: {content[:100]}...") + + if self.generate_speech(content, voice_id, audio_file): + # Add to our audio files list IN THE ORIGINAL ORDER + audio_files.append(audio_file) + print(f"Generated {agent_type} audio for turn {turn}, added to position {len(audio_files)}") + else: + raise Exception(f"Failed to generate audio for {agent_type} turn {turn}") + + # Second check: Blocks with input field and possibly turn information + elif any("input" in block for block in conversation_blocks): + print("\nProcessing blocks with input field") + + # Check if these blocks also have type and turn information + has_turn_info = any("turn" in block and "type" in block for block in conversation_blocks) + + if has_turn_info: + print("Blocks have both input field and turn-based structure - using mixed format") + # Sort by turn if available, ensuring proper sequence + sorted_blocks = sorted(conversation_blocks, key=lambda b: b.get("turn", float('inf'))) + + for idx, block in enumerate(sorted_blocks): + if "input" in block and block["input"].strip(): + # Determine voice based on type field or name + if "type" in block: + is_believer = block["type"] == "believer" + else: + is_believer = "Believer" in block.get("name", "") or block.get("name", "").lower().startswith("alloy") + + voice_id = believer_voice_id if is_believer else skeptic_voice_id + speaker_type = "believer" if is_believer else "skeptic" + turn = block.get("turn", idx + 1) + + print(f"\nProcessing {speaker_type} block with turn {turn} using voice {voice_id}") + audio_file = os.path.join(podcast_temp_dir, f"{speaker_type}_turn_{turn}_{idx}.mp3") + + if self.generate_speech(block["input"], voice_id, audio_file): + audio_files.append(audio_file) + print(f"Generated audio for {speaker_type} turn {turn}") + else: + raise Exception(f"Failed to generate audio for {speaker_type} turn {turn}") + else: + # Old format - process blocks sequentially as they appear + print("Processing old format blocks sequentially") + for i, block in enumerate(conversation_blocks): + if "input" in block and block["input"].strip(): + # Check for either "Believer" in name or if the name starts with "alloy" + is_believer = "Believer" in block.get("name", "") or block.get("name", "").lower().startswith("alloy") + voice_id = believer_voice_id if is_believer else skeptic_voice_id + speaker_type = "believer" if is_believer else "skeptic" + + print(f"\nProcessing {speaker_type} block {i+1} with voice {voice_id}") + print(f"Block name: {block.get('name', '')}") # Debug logging + + audio_file = os.path.join(podcast_temp_dir, f"part_{i+1}.mp3") + if self.generate_speech(block["input"], voice_id, audio_file): + audio_files.append(audio_file) + print(f"Generated audio for part {i+1}") + else: + raise Exception(f"Failed to generate audio for part {i+1}") + else: + raise Exception("Invalid conversation blocks format - no recognizable structure found") + + if not audio_files: + raise Exception("No audio files were generated from the conversation blocks") + + print(f"\nGenerated {len(audio_files)} audio files in total") + + # Print the final order of audio files for verification + print("\nFinal audio file order before merging:") + for i, file in enumerate(audio_files): + print(f"{i+1}. {os.path.basename(file)}") + + # Merge all audio files + final_audio = os.path.join(podcast_temp_dir, "final_podcast.mp3") + print(f"Merging to final audio: {final_audio}") + + if not self.merge_audio_files(audio_files, final_audio): + raise Exception("Failed to merge audio files") + + # Calculate audio duration using ffprobe + duration = 0 + try: + cmd = [ + 'ffprobe', + '-v', 'error', + '-show_entries', 'format=duration', + '-of', 'default=noprint_wrappers=1:nokey=1', + final_audio + ] + duration_result = subprocess.run(cmd, capture_output=True, text=True) + if duration_result.returncode == 0: + duration = float(duration_result.stdout.strip()) + print(f"Audio duration: {duration} seconds") + else: + print(f"Failed to get audio duration: {duration_result.stderr}") + except Exception as e: + print(f"Error calculating duration: {str(e)}") + # Don't fail the entire process for duration calculation + + podcast_doc = { + "topic": topic, + "research": research, + "conversation_blocks": conversation_blocks, + "audio_path": final_audio, + "created_at": datetime.utcnow(), + "believer_voice_id": believer_voice_id, + "skeptic_voice_id": skeptic_voice_id, + "user_id": user_id, + "duration": duration # Add duration to MongoDB document + } + + result = await podcasts.insert_one(podcast_doc) + + # Clean up individual audio files but keep the final one + for audio_file in audio_files: + if os.path.exists(audio_file): + os.remove(audio_file) + + return { + "podcast_id": str(result.inserted_id), + "audio_path": final_audio, + "topic": topic, + "duration": duration # Return duration in the result + } + + except Exception as e: + # Clean up the temp directory in case of error + if os.path.exists(podcast_temp_dir): + shutil.rmtree(podcast_temp_dir) + logger.exception(f"Error in podcast creation: {str(e)}") + return { + "error": str(e) + } + + async def get_podcast(self, podcast_id: str) -> Dict: + """Retrieve a podcast by ID.""" + try: + from bson.objectid import ObjectId + podcast = await podcasts.find_one({"_id": ObjectId(podcast_id)}) + if podcast: + podcast["_id"] = str(podcast["_id"]) + return podcast + return {"error": "Podcast not found"} + except Exception as e: + return {"error": str(e)} \ No newline at end of file diff --git a/backend/app/agents/researcher.py b/backend/app/agents/researcher.py new file mode 100644 index 0000000000000000000000000000000000000000..b8d39cf9f4f31de8211f64fa109c1ff06cb9f748 --- /dev/null +++ b/backend/app/agents/researcher.py @@ -0,0 +1,153 @@ +from langchain_core.prompts import ChatPromptTemplate +from langchain_openai import ChatOpenAI +from langchain_community.tools.tavily_search import TavilySearchResults +from langchain.agents import AgentExecutor, create_openai_functions_agent +from decouple import config +from typing import AsyncGenerator, List +import os +import json + +# Get API keys from environment +TAVILY_API_KEY = config('TAVILY_API_KEY') +OPENAI_API_KEY = config('OPENAI_API_KEY') + +# Debug logging +print(f"\nLoaded OpenAI API Key: {OPENAI_API_KEY[:7]}...") +print(f"Key starts with 'sk-proj-': {OPENAI_API_KEY.startswith('sk-proj-')}") +print(f"Key starts with 'sk-': {OPENAI_API_KEY.startswith('sk-')}\n") + +# Set Tavily API key in environment +os.environ["TAVILY_API_KEY"] = TAVILY_API_KEY + +# Initialize the search tool +search_tool = TavilySearchResults(tavily_api_key=TAVILY_API_KEY) + +# List of available tools for the prompt +tools_description = """ +Available tools: +- TavilySearchResults: A search tool that provides comprehensive web search results. Use this to gather information about topics. +""" + +# Create the prompt template +researcher_prompt = ChatPromptTemplate.from_messages([ + ("system", """You are an expert researcher tasked with gathering comprehensive information on given topics. + Your goal is to provide detailed, factual information limited to 500 words. + Focus on key points, recent developments, and verified facts. + Structure your response clearly with main points and supporting details. + Keep your response concise and focused. + + {tools} + + Remember to provide accurate and up-to-date information."""), + ("user", "{input}"), + ("assistant", "{agent_scratchpad}") +]) + +# Initialize the LLM with streaming +researcher_llm = ChatOpenAI( + model="gpt-4o-mini", + temperature=0.3, + api_key=OPENAI_API_KEY, + streaming=True +) + +# Create the agent +researcher_agent = create_openai_functions_agent( + llm=researcher_llm, + prompt=researcher_prompt, + tools=[search_tool] +) + +# Create the agent executor +researcher_executor = AgentExecutor( + agent=researcher_agent, + tools=[search_tool], + verbose=True, + handle_parsing_errors=True, + return_intermediate_steps=True +) + +def chunk_text(text: str, max_length: int = 3800) -> List[str]: + """Split text into chunks of maximum length while preserving sentence boundaries.""" + # Split into sentences and trim whitespace + sentences = [s.strip() for s in text.split('.')] + sentences = [s + '.' for s in sentences if s] + + chunks = [] + current_chunk = [] + current_length = 0 + + for sentence in sentences: + sentence_length = len(sentence) + if current_length + sentence_length > max_length: + if current_chunk: # If we have accumulated sentences, join them and add to chunks + chunks.append(' '.join(current_chunk)) + current_chunk = [sentence] + current_length = sentence_length + else: # If a single sentence is too long, split it + if sentence_length > max_length: + words = sentence.split() + temp_chunk = [] + temp_length = 0 + for word in words: + if temp_length + len(word) + 1 > max_length: + chunks.append(' '.join(temp_chunk)) + temp_chunk = [word] + temp_length = len(word) + else: + temp_chunk.append(word) + temp_length += len(word) + 1 + if temp_chunk: + chunks.append(' '.join(temp_chunk)) + else: + chunks.append(sentence) + else: + current_chunk.append(sentence) + current_length += sentence_length + + if current_chunk: + chunks.append(' '.join(current_chunk)) + + return chunks + +async def research_topic_stream(topic: str) -> AsyncGenerator[str, None]: + """ + Research a topic and stream the results as they are generated. + """ + try: + async for chunk in researcher_executor.astream( + { + "input": f"Research this topic thoroughly: {topic}", + "tools": tools_description + } + ): + if isinstance(chunk, dict): + # Stream intermediate steps for transparency + if "intermediate_steps" in chunk: + for step in chunk["intermediate_steps"]: + yield json.dumps({"type": "intermediate", "content": str(step)}) + "\n" + + # Stream the final output + if "output" in chunk: + yield json.dumps({"type": "final", "content": chunk["output"]}) + "\n" + else: + yield json.dumps({"type": "chunk", "content": str(chunk)}) + "\n" + except Exception as e: + yield json.dumps({"type": "error", "content": str(e)}) + "\n" + +async def research_topic(topic: str) -> str: + """ + Research a topic and return the complete result. + Kept for compatibility with existing code. + """ + try: + result = await researcher_executor.ainvoke( + { + "input": f"Research this topic thoroughly: {topic}", + "tools": tools_description + } + ) + return result["output"] + except Exception as e: + print(f"Error in research: {str(e)}") + return "Error occurred during research." \ No newline at end of file diff --git a/backend/app/database.py b/backend/app/database.py new file mode 100644 index 0000000000000000000000000000000000000000..5745bb452b7d32c89e739061321ec115267fc57a --- /dev/null +++ b/backend/app/database.py @@ -0,0 +1,13 @@ +from motor.motor_asyncio import AsyncIOMotorClient +from decouple import config + +MONGODB_URL = config('MONGODB_URL') +client = AsyncIOMotorClient(MONGODB_URL) +db = client.podcraft + +# Collections +users = db.users +podcasts = db.podcasts +agents = db.agents # New collection for storing agent configurations +workflows = db.workflows # Collection for storing workflow configurations +workflows = db.workflows # Collection for storing workflow configurations \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000000000000000000000000000000000000..fdff933ed17e18111432df646c0e8604bca2b4b0 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,1020 @@ +from fastapi import FastAPI, HTTPException, Depends, status, Request +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse, FileResponse +from fastapi.staticfiles import StaticFiles +from passlib.context import CryptContext +from datetime import datetime, timedelta +from jose import JWTError, jwt +from decouple import config +import logging +from .database import users, podcasts, agents, workflows +from .models import ( + UserCreate, UserLogin, Token, UserUpdate, UserResponse, + PodcastRequest, PodcastResponse, AgentCreate, AgentResponse, + TextPodcastRequest, TextPodcastResponse, + WorkflowCreate, WorkflowResponse, InsightsData, TranscriptEntry +) +from .agents.researcher import research_topic, research_topic_stream +from .agents.debaters import generate_debate, generate_debate_stream, chunk_text +from .agents.podcast_manager import PodcastManager +import json +import os +import shutil +from typing import List +import time +from bson import ObjectId + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Debug environment variables +openai_key = config('OPENAI_API_KEY') +logger.info(f"Loaded OpenAI API Key at startup: {openai_key[:7]}...") +logger.info(f"Key starts with 'sk-proj-': {openai_key.startswith('sk-proj-')}") +logger.info(f"Key starts with 'sk-': {openai_key.startswith('sk-')}") + +app = FastAPI() + +# CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:5173"], # React app + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + expose_headers=["*"] # Expose all headers +) + +# Create necessary directories if they don't exist +os.makedirs("temp", exist_ok=True) +os.makedirs("temp_audio", exist_ok=True) + +# Make sure the directory paths are absolute +TEMP_AUDIO_DIR = os.path.abspath("temp_audio") +print(f"Mounting temp_audio directory: {TEMP_AUDIO_DIR}") + +# Mount static directory for audio files +app.mount("/audio", StaticFiles(directory=TEMP_AUDIO_DIR), name="audio") + +# Security +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") +SECRET_KEY = config("SECRET_KEY") +ACCESS_TOKEN_EXPIRE_MINUTES = int(config("ACCESS_TOKEN_EXPIRE_MINUTES")) + +# Helper functions +def create_access_token(data: dict): + expires = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + data.update({"exp": expires}) + token = jwt.encode(data, SECRET_KEY, algorithm="HS256") + return token + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + +def get_password_hash(password): + return pwd_context.hash(password) + +async def get_current_user(token: str = Depends(oauth2_scheme)): + logger.info("Authenticating user with token") + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials" + ) + try: + logger.info("Decoding JWT token") + payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) + username: str = payload.get("sub") + if username is None: + logger.error("No username found in token") + raise credentials_exception + logger.info(f"Token decoded successfully for user: {username}") + except JWTError as e: + logger.error(f"JWT Error: {str(e)}") + raise credentials_exception + + user = await users.find_one({"username": username}) + if user is None: + logger.error(f"No user found for username: {username}") + raise credentials_exception + logger.info(f"User authenticated successfully: {username}") + return user + +# Initialize PodcastManager +podcast_manager = PodcastManager() + +# Routes +@app.post("/signup") +async def signup(user: UserCreate): + # Check if username exists + if await users.find_one({"username": user.username}): + raise HTTPException(status_code=400, detail="Username already registered") + + # Create new user + user_dict = user.dict() + user_dict["password"] = get_password_hash(user.password) + await users.insert_one(user_dict) + + # Create and return token after signup + access_token = create_access_token(data={"sub": user.username}) + return {"access_token": access_token, "token_type": "bearer"} + +@app.post("/token", response_model=Token) +async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): + logger.info(f"Token request for user: {form_data.username}") + # Find user + db_user = await users.find_one({"username": form_data.username}) + if not db_user or not verify_password(form_data.password, db_user["password"]): + logger.error(f"Failed token request for user: {form_data.username}") + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # Create access token + access_token = create_access_token(data={"sub": form_data.username}) + logger.info(f"Token generated successfully for user: {form_data.username}") + return {"access_token": access_token, "token_type": "bearer"} + +@app.post("/login", response_model=Token) +async def login(request: Request, user: UserLogin): + logger.info(f"Login attempt for user: {user.username}") + # Find user + db_user = await users.find_one({"username": user.username}) + if not db_user or not verify_password(user.password, db_user["password"]): + logger.error(f"Failed login attempt for user: {user.username}") + raise HTTPException( + status_code=401, + detail="Incorrect username or password" + ) + + # Create access token + access_token = create_access_token(data={"sub": user.username}) + logger.info(f"Login successful for user: {user.username}") + return {"access_token": access_token, "token_type": "bearer"} + +@app.get("/user/me", response_model=UserResponse) +async def get_user_profile(current_user: dict = Depends(get_current_user)): + return { + "username": current_user["username"] + } + +@app.put("/user/update-password") +async def update_password(user_update: UserUpdate, current_user: dict = Depends(get_current_user)): + hashed_password = get_password_hash(user_update.password) + await users.update_one( + {"username": current_user["username"]}, + {"$set": {"password": hashed_password}} + ) + return {"message": "Password updated successfully"} + +@app.get("/") +async def root(): + return {"message": "Welcome to PodCraft API"} + +# New podcast endpoints +@app.post("/generate-podcast", response_model=PodcastResponse) +async def generate_podcast(request: Request, podcast_req: PodcastRequest, current_user: dict = Depends(get_current_user)): + logger.info(f"Received podcast generation request for topic: {podcast_req.topic}") + logger.info(f"Request headers: {dict(request.headers)}") + + try: + # Step 1: Research the topic + logger.info("Starting research phase") + research_results = await research_topic(podcast_req.topic) + logger.info("Research phase completed") + + # Step 2: Generate debate between believer and skeptic + logger.info("Starting debate generation") + conversation_blocks = await generate_debate( + research=research_results, + believer_name=podcast_req.believer_voice_id, + skeptic_name=podcast_req.skeptic_voice_id + ) + + if not conversation_blocks: + logger.error("Failed to generate debate - no conversation blocks returned") + raise HTTPException(status_code=500, detail="Failed to generate debate") + + logger.info("Debate generation completed") + + # Step 3: Create podcast using TTS and store in MongoDB + logger.info("Starting podcast creation with TTS") + result = await podcast_manager.create_podcast( + topic=podcast_req.topic, + research=research_results, + conversation_blocks=conversation_blocks, + believer_voice_id=podcast_req.believer_voice_id, + skeptic_voice_id=podcast_req.skeptic_voice_id + ) + + if "error" in result: + logger.error(f"Error in podcast creation: {result['error']}") + raise HTTPException(status_code=500, detail=result["error"]) + + logger.info(f"Podcast generated successfully with ID: {result.get('podcast_id')}") + return result + except Exception as e: + logger.error(f"Error in podcast generation: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=str(e)) + +@app.get("/podcast/{podcast_id}", response_model=PodcastResponse) +async def get_podcast(podcast_id: str, current_user: dict = Depends(get_current_user)): + try: + result = await podcast_manager.get_podcast(podcast_id) + if "error" in result: + raise HTTPException(status_code=404, detail=result["error"]) + return result + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.post("/generate-podcast/stream") +async def generate_podcast_stream(request: PodcastRequest, current_user: dict = Depends(get_current_user)): + async def generate(): + try: + # Store complete responses for podcast creation + believer_turns = {} # Store responses by turn number + skeptic_turns = {} # Store responses by turn number + + # Stream research results + logger.info("Starting research phase (streaming)") + research_results = "" + async for chunk in research_topic_stream(request.topic): + yield chunk + if isinstance(chunk, str) and "final" in chunk: + data = json.loads(chunk) + if data["type"] == "final": + research_results = data["content"] + + # Stream debate and track turns properly + logger.info("Starting debate phase (streaming)") + async for chunk in generate_debate_stream( + research=research_results, + believer_name=request.believer_voice_id, + skeptic_name=request.skeptic_voice_id + ): + yield chunk + # Parse the chunk + data = json.loads(chunk) + + # Track responses by turn to maintain proper ordering + if data["type"] == "believer" and "turn" in data: + turn = data["turn"] + if turn not in believer_turns: + believer_turns[turn] = "" + believer_turns[turn] += data["content"] + elif data["type"] == "skeptic" and "turn" in data: + turn = data["turn"] + if turn not in skeptic_turns: + skeptic_turns[turn] = "" + skeptic_turns[turn] += data["content"] + + # Create strictly alternating conversation blocks for podcast + blocks = [] + + # Find the maximum turn number + max_turn = max( + max(skeptic_turns.keys()) if skeptic_turns else 0, + max(believer_turns.keys()) if believer_turns else 0 + ) + + logger.info(f"Creating podcast with {len(believer_turns)} believer turns and {len(skeptic_turns)} skeptic turns") + logger.info(f"Max turn number: {max_turn}") + + # Create blocks in strict turn order: Skeptic 1, Believer 1, Skeptic 2, Believer 2, etc. + for turn in range(1, max_turn + 1): + # First Skeptic's turn + if turn in skeptic_turns and skeptic_turns[turn].strip(): + blocks.append({ + "name": f"{request.skeptic_voice_id}'s Turn {turn}", + "input": skeptic_turns[turn], + "silence_before": 1, + "voice_id": request.skeptic_voice_id, + "emotion": "neutral", + "model": "tts-1", + "speed": 1, + "duration": 0, + "type": "skeptic", + "turn": turn + }) + + # Then Believer's turn + if turn in believer_turns and believer_turns[turn].strip(): + blocks.append({ + "name": f"{request.believer_voice_id}'s Turn {turn}", + "input": believer_turns[turn], + "silence_before": 1, + "voice_id": request.believer_voice_id, + "emotion": "neutral", + "model": "tts-1", + "speed": 1, + "duration": 0, + "type": "believer", + "turn": turn + }) + + # Log the conversational structure for debugging + turn_structure = [f"{block.get('type', 'unknown')}-{block.get('turn', 'unknown')}" for block in blocks] + logger.info(f"Conversation structure: {turn_structure}") + + # Create podcast using TTS and store in MongoDB + logger.info("Starting podcast creation with TTS") + result = await podcast_manager.create_podcast( + topic=request.topic, + research=research_results, + conversation_blocks=blocks, + believer_voice_id=request.believer_voice_id, + skeptic_voice_id=request.skeptic_voice_id, + user_id=str(current_user["_id"]) + ) + + if "error" in result: + logger.error(f"Error in podcast creation: {result['error']}") + yield json.dumps({"type": "error", "content": result["error"]}) + "\n" + else: + logger.info(f"Podcast generated successfully with ID: {result.get('podcast_id')}") + # Create audio URL from the audio path + audio_url = f"/audio/{os.path.basename(os.path.dirname(result['audio_path']))}/final_podcast.mp3" + yield json.dumps({ + "type": "success", + "content": f"Podcast created successfully! ID: {result.get('podcast_id')}", + "podcast_url": audio_url + }) + "\n" + + except Exception as e: + logger.error(f"Error in streaming podcast generation: {str(e)}") + yield json.dumps({"type": "error", "content": str(e)}) + "\n" + + return StreamingResponse( + generate(), + media_type="text/event-stream" + ) + +@app.get("/podcasts") +async def list_podcasts(current_user: dict = Depends(get_current_user)): + try: + # Query podcasts for the current user + cursor = podcasts.find({"user_id": str(current_user["_id"])}) + podcast_list = [] + async for podcast in cursor: + # Convert MongoDB _id to string and create audio URL + podcast["_id"] = str(podcast["_id"]) + if "audio_path" in podcast: + audio_url = f"/audio/{os.path.basename(os.path.dirname(podcast['audio_path']))}/final_podcast.mp3" + podcast["audio_url"] = f"http://localhost:8000{audio_url}" + podcast_list.append(podcast) + return podcast_list + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.get("/podcasts/latest") +async def get_latest_podcast(current_user: dict = Depends(get_current_user)): + try: + # Query podcasts for the current user, sorted by creation date (newest first) + from bson.objectid import ObjectId + + # Find the most recent podcast for this user + latest_podcast = await podcasts.find_one( + {"user_id": str(current_user["_id"])}, + sort=[("created_at", -1)] # Sort by created_at in descending order + ) + + if not latest_podcast: + return {"message": "No podcasts found"} + + # Convert MongoDB _id to string and create audio URL + latest_podcast["_id"] = str(latest_podcast["_id"]) + + if "audio_path" in latest_podcast: + audio_url = f"/audio/{os.path.basename(os.path.dirname(latest_podcast['audio_path']))}/final_podcast.mp3" + latest_podcast["audio_url"] = f"http://localhost:8000{audio_url}" + + logger.info(f"Latest podcast found: {latest_podcast['topic']}") + return latest_podcast + except Exception as e: + logger.error(f"Error getting latest podcast: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@app.delete("/podcast/{podcast_id}") +async def delete_podcast(podcast_id: str, current_user: dict = Depends(get_current_user)): + try: + # Convert string ID to ObjectId + from bson.objectid import ObjectId + podcast_obj_id = ObjectId(podcast_id) + + # Find the podcast first to get its audio path + podcast = await podcasts.find_one({"_id": podcast_obj_id, "user_id": str(current_user["_id"])}) + if not podcast: + raise HTTPException(status_code=404, detail="Podcast not found") + + # Delete the podcast from MongoDB + result = await podcasts.delete_one({"_id": podcast_obj_id, "user_id": str(current_user["_id"])}) + + if result.deleted_count == 0: + raise HTTPException(status_code=404, detail="Podcast not found") + + # Delete the associated audio files if they exist + if "audio_path" in podcast: + audio_dir = os.path.dirname(podcast["audio_path"]) + if os.path.exists(audio_dir): + shutil.rmtree(audio_dir) + + return {"message": "Podcast deleted successfully"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.post("/agents/create", response_model=AgentResponse) +async def create_agent(agent: AgentCreate, current_user: dict = Depends(get_current_user)): + """Create a new agent configuration for the current user.""" + try: + # Convert the user ID to string to ensure consistent handling + user_id = str(current_user["_id"]) + + # Prepare agent data + agent_data = { + **agent.dict(), + "user_id": user_id, + "created_at": datetime.utcnow() + } + + # Insert the agent into the database + result = await agents.insert_one(agent_data) + + # Return the created agent with its ID + created_agent = await agents.find_one({"_id": result.inserted_id}) + if not created_agent: + raise HTTPException(status_code=500, detail="Failed to retrieve created agent") + + return { + "agent_id": str(created_agent["_id"]), + **{k: v for k, v in created_agent.items() if k != "_id"} + } + except Exception as e: + logger.error(f"Error creating agent: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to create agent: {str(e)}") + +@app.get("/agents", response_model=List[AgentResponse]) +async def list_agents(current_user: dict = Depends(get_current_user)): + """List all agents created by the current user.""" + try: + # Convert user ID to string for consistent handling + user_id = str(current_user["_id"]) + user_agents = [] + + # Find agents for the current user + async for agent in agents.find({"user_id": user_id}): + user_agents.append({ + "agent_id": str(agent["_id"]), + **{k: v for k, v in agent.items() if k != "_id"} + }) + + return user_agents + except Exception as e: + logger.error(f"Error listing agents: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to list agents: {str(e)}") + +@app.post("/agents/test-voice") +async def test_agent_voice(request: Request): + try: + # Parse request body + data = await request.json() + text = data.get("text") + voice_id = data.get("voice_id") + emotion = data.get("emotion", "neutral") # Default emotion + speed = data.get("speed", 1.0) + + # Log the received request + logger.info(f"Test voice request received: voice_id={voice_id}, text={text[:30]}...") + + if not text or not voice_id: + logger.error("Missing required fields in test voice request") + raise HTTPException(status_code=400, detail="Missing required fields (text or voice_id)") + + # Initialize the podcast manager + manager = PodcastManager() + + # Generate a unique filename for this test + test_filename = f"test_{voice_id}_{int(time.time())}.mp3" + output_dir = os.path.join("temp_audio", f"test_{int(time.time())}") + os.makedirs(output_dir, exist_ok=True) + output_path = os.path.join(output_dir, test_filename) + + logger.info(f"Generating test audio to {output_path}") + + # Generate the speech + success = manager.generate_speech(text, voice_id, output_path) + + if not success: + logger.error("Failed to generate test audio") + raise HTTPException(status_code=500, detail="Failed to generate test audio") + + # Construct the audio URL + audio_url = f"/audio/{os.path.basename(output_dir)}/{test_filename}" + full_audio_url = f"http://localhost:8000{audio_url}" + + logger.info(f"Test audio generated successfully at {full_audio_url}") + + # Return the full URL to the generated audio + return {"audio_url": full_audio_url, "status": "success"} + + except Exception as e: + logger.error(f"Error in test_agent_voice: {str(e)}", exc_info=True) + return {"error": str(e), "status": "error", "audio_url": None} + +# Add the new PUT endpoint for updating agents +@app.put("/agents/{agent_id}", response_model=AgentResponse) +async def update_agent(agent_id: str, agent: AgentCreate, current_user: dict = Depends(get_current_user)): + """Update an existing agent configuration.""" + try: + # Convert user ID to string for consistent handling + user_id = str(current_user["_id"]) + + # Convert agent_id to ObjectId + from bson.objectid import ObjectId + agent_obj_id = ObjectId(agent_id) + + # Check if agent exists and belongs to user + existing_agent = await agents.find_one({ + "_id": agent_obj_id, + "user_id": user_id + }) + + if not existing_agent: + raise HTTPException(status_code=404, detail="Agent not found or unauthorized") + + # Prepare update data + update_data = { + **agent.dict(), + "updated_at": datetime.utcnow() + } + + # Update the agent + result = await agents.update_one( + {"_id": agent_obj_id}, + {"$set": update_data} + ) + + if result.modified_count == 0: + raise HTTPException(status_code=500, detail="Failed to update agent") + + # Get the updated agent + updated_agent = await agents.find_one({"_id": agent_obj_id}) + if not updated_agent: + raise HTTPException(status_code=500, detail="Failed to retrieve updated agent") + + return { + "agent_id": str(updated_agent["_id"]), + **{k: v for k, v in updated_agent.items() if k != "_id"} + } + except Exception as e: + logger.error(f"Error updating agent: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to update agent: {str(e)}") + +@app.get("/agents/{agent_id}", response_model=AgentResponse) +async def get_agent(agent_id: str, current_user: dict = Depends(get_current_user)): + """Get a specific agent by ID.""" + try: + # Convert user ID to string for consistent handling + user_id = str(current_user["_id"]) + + # Convert agent_id to ObjectId + from bson.objectid import ObjectId + agent_obj_id = ObjectId(agent_id) + + # Check if agent exists and belongs to user + agent = await agents.find_one({ + "_id": agent_obj_id, + "user_id": user_id + }) + + if not agent: + raise HTTPException(status_code=404, detail="Agent not found or unauthorized") + + # Return the agent data + return { + "agent_id": str(agent["_id"]), + **{k: v for k, v in agent.items() if k != "_id"} + } + except Exception as e: + logger.error(f"Error getting agent: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to get agent: {str(e)}") + +@app.post("/generate-text-podcast", response_model=TextPodcastResponse) +async def generate_text_podcast(request: TextPodcastRequest, current_user: dict = Depends(get_current_user)): + """Generate a podcast from text input with a single voice and emotion.""" + logger.info(f"Received text-based podcast generation request from user: {current_user['username']}") + + try: + # Create conversation block for the single voice + conversation_blocks = [ + { + "name": "Voice", + "input": request.text, + "silence_before": 1, + "voice_id": request.voice_id, + "emotion": request.emotion, + "model": "tts-1", + "speed": request.speed, + "duration": 0 + } + ] + + # Use the provided title if available, otherwise use generic title + podcast_title = request.title if hasattr(request, 'title') and request.title else f"Text Podcast {datetime.now().strftime('%Y-%m-%d %H:%M')}" + podcast_description = request.text[:150] + "..." if len(request.text) > 150 else request.text + + # Create podcast using TTS + result = await podcast_manager.create_podcast( + topic=podcast_title, + research=podcast_description, + conversation_blocks=conversation_blocks, + believer_voice_id=request.voice_id, # Using same voice for both since we only need one + skeptic_voice_id=request.voice_id, + user_id=str(current_user["_id"]) + ) + + if "error" in result: + logger.error(f"Error in podcast creation: {result['error']}") + return TextPodcastResponse( + audio_url="", + status="failed", + error=result["error"], + duration=0, + updated_at=datetime.now().isoformat() + ) + + # Create audio URL from the audio path + audio_url = f"/audio/{os.path.basename(os.path.dirname(result['audio_path']))}/final_podcast.mp3" + full_audio_url = f"http://localhost:8000{audio_url}" + + logger.info("Successfully generated text-based podcast") + + return TextPodcastResponse( + audio_url=full_audio_url, + duration=result.get("duration", 0), + status="completed", + error=None, + updated_at=datetime.now().isoformat() + ) + + except Exception as e: + logger.error(f"Error generating text-based podcast: {str(e)}", exc_info=True) + return TextPodcastResponse( + audio_url="", + status="failed", + error=str(e), + duration=0, + updated_at=datetime.now().isoformat() + ) + + +@app.get("/api/workflows", response_model=List[WorkflowResponse]) +async def list_workflows(current_user: dict = Depends(get_current_user)): + try: + print("\n=== Debug list_workflows ===") + print(f"Current user object: {current_user}") + print(f"User ID type: {type(current_user['_id'])}") + print(f"Username: {current_user['username']}") + + # Use email as user_id for consistency + user_id = current_user["username"] + print(f"Using user_id (email): {user_id}") + + # Find workflows for this user and convert cursor to list + workflows_cursor = workflows.find({"user_id": user_id}) + workflows_list = await workflows_cursor.to_list(length=None) + + print(f"Found {len(workflows_list)} workflows") + + # Convert MongoDB _id to string and datetime to ISO format for each workflow + validated_workflows = [] + for workflow in workflows_list: + print(f"\nProcessing workflow: {workflow}") + + # Convert MongoDB _id to string + workflow_data = { + "id": str(workflow["_id"]), + "name": workflow["name"], + "description": workflow.get("description", ""), + "nodes": workflow.get("nodes", []), + "edges": workflow.get("edges", []), + "user_id": workflow["user_id"], + "created_at": workflow["created_at"].isoformat() if "created_at" in workflow else None, + "updated_at": workflow["updated_at"].isoformat() if "updated_at" in workflow else None + } + + print(f"Converted workflow data: {workflow_data}") + + # Validate each workflow + validated_workflow = WorkflowResponse(**workflow_data) + print(f"Validated workflow: {validated_workflow}") + + validated_workflows.append(validated_workflow) + + print(f"Successfully validated {len(validated_workflows)} workflows") + print("=== End Debug ===\n") + + return validated_workflows + except Exception as e: + print(f"Error in list_workflows: {str(e)}") + print(f"Error type: {type(e)}") + import traceback + print(f"Traceback: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=str(e)) + +@app.put("/api/workflows/{workflow_id}", response_model=WorkflowResponse) +async def update_workflow(workflow_id: str, workflow: WorkflowCreate, current_user: dict = Depends(get_current_user)): + """Update a specific workflow.""" + try: + print("\n=== Debug update_workflow ===") + print(f"Updating workflow ID: {workflow_id}") + print(f"Current user: {current_user.get('username')}") + + # Prepare update data + now = datetime.utcnow() + + # Convert insights to dict if it's a Pydantic model + insights_data = workflow.insights + if isinstance(insights_data, InsightsData): + insights_data = insights_data.dict() + print(f"Converted InsightsData to dict: {type(insights_data)}") + + workflow_data = { + "name": workflow.name, + "description": workflow.description, + "nodes": workflow.nodes, + "edges": workflow.edges, + "insights": insights_data, # Use the converted insights + "updated_at": now + } + + print(f"Update data prepared (insights type: {type(workflow_data['insights'])})") + + # Update the workflow + result = await workflows.update_one( + {"_id": ObjectId(workflow_id), "user_id": current_user.get("username")}, + {"$set": workflow_data} + ) + + if result.modified_count == 0: + raise HTTPException(status_code=404, detail="Workflow not found") + + # Get the updated workflow + updated_workflow = await workflows.find_one({"_id": ObjectId(workflow_id)}) + + # Prepare response data + response_data = { + "id": str(updated_workflow["_id"]), + "name": updated_workflow["name"], + "description": updated_workflow.get("description", ""), + "nodes": updated_workflow.get("nodes", []), + "edges": updated_workflow.get("edges", []), + "insights": updated_workflow.get("insights", ""), # Add insights field + "user_id": updated_workflow["user_id"], + "created_at": updated_workflow["created_at"].isoformat() if "created_at" in updated_workflow else None, + "updated_at": updated_workflow["updated_at"].isoformat() if "updated_at" in updated_workflow else None + } + + print(f"Response data prepared (insights type: {type(response_data['insights'])})") + + # Create and validate the response model + response = WorkflowResponse(**response_data) + print(f"Validated response: {response}") + print("=== End Debug ===\n") + + return response + except Exception as e: + print(f"Error in update_workflow: {str(e)}") + print(f"Error type: {type(e)}") + import traceback + print(f"Traceback: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=str(e)) + +@app.delete("/api/workflows/{workflow_id}") +async def delete_workflow(workflow_id: str, current_user: dict = Depends(get_current_user)): + """Delete a specific workflow.""" + try: + result = await workflows.delete_one({ + "_id": ObjectId(workflow_id), + "user_id": current_user.get("username") # This is actually the email from the token + }) + + if result.deleted_count == 0: + raise HTTPException(status_code=404, detail="Workflow not found") + + return {"message": "Workflow deleted successfully"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.post("/api/workflows", response_model=WorkflowResponse) +async def create_workflow(workflow: WorkflowCreate, current_user: dict = Depends(get_current_user)): + try: + print("\n=== Debug create_workflow ===") + print(f"Current user object: {current_user}") + print(f"Username: {current_user.get('username')}") + + # Use email from token as user_id for consistency + user_id = current_user.get("username") # This is actually the email from the token + print(f"Using user_id (email): {user_id}") + + # Convert insights to dict if it's a Pydantic model + insights_data = workflow.insights + if isinstance(insights_data, InsightsData): + insights_data = insights_data.dict() + print(f"Converted InsightsData to dict: {type(insights_data)}") + + # Create workflow data + now = datetime.utcnow() + workflow_data = { + "name": workflow.name, + "description": workflow.description, + "nodes": workflow.nodes, + "edges": workflow.edges, + "insights": insights_data, # Use the converted insights + "user_id": user_id, + "created_at": now, + "updated_at": now + } + + print(f"Workflow data prepared (insights type: {type(workflow_data['insights'])})") + + # Insert into database + result = await workflows.insert_one(workflow_data) + + # Prepare response data + response_data = { + "id": str(result.inserted_id), + "name": workflow_data["name"], + "description": workflow_data["description"], + "nodes": workflow_data["nodes"], + "edges": workflow_data["edges"], + "insights": workflow_data.get("insights"), # Add insights field + "user_id": workflow_data["user_id"], + "created_at": workflow_data["created_at"].isoformat(), + "updated_at": workflow_data["updated_at"].isoformat() + } + + print(f"Response data prepared (insights type: {type(response_data['insights'])})") + + # Create and validate the response model + response = WorkflowResponse(**response_data) + print(f"Validated response: {response}") + print("=== End Debug ===\n") + + return response + except Exception as e: + print(f"Error in create_workflow: {str(e)}") + print(f"Error type: {type(e)}") + import traceback + print(f"Traceback: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=str(e)) + +@app.get("/api/workflows/{workflow_id}", response_model=WorkflowResponse) +async def get_workflow(workflow_id: str, current_user: dict = Depends(get_current_user)): + """Get a specific workflow by ID.""" + try: + print("\n=== Debug get_workflow ===") + print(f"Looking for workflow ID: {workflow_id}") + print(f"Current user: {current_user.get('username')}") + + workflow = await workflows.find_one({ + "_id": ObjectId(workflow_id), + "user_id": current_user.get("username") # This is actually the email from the token + }) + + if workflow is None: + raise HTTPException(status_code=404, detail="Workflow not found") + + print(f"Found workflow: {workflow}") + + # Convert MongoDB _id to string + workflow["id"] = str(workflow.pop("_id")) + + # Convert datetime objects to ISO format strings + if "created_at" in workflow: + workflow["created_at"] = workflow["created_at"].isoformat() + print(f"Converted created_at: {workflow['created_at']}") + + if "updated_at" in workflow: + workflow["updated_at"] = workflow["updated_at"].isoformat() + print(f"Converted updated_at: {workflow['updated_at']}") + + # Ensure all required fields are present + response_data = { + "id": workflow["id"], + "name": workflow["name"], + "description": workflow.get("description", ""), + "nodes": workflow.get("nodes", []), + "edges": workflow.get("edges", []), + "insights": workflow.get("insights", ""), # Add insights field + "user_id": workflow["user_id"], + "created_at": workflow.get("created_at"), + "updated_at": workflow.get("updated_at") + } + + print(f"Response data: {response_data}") + + # Create and validate the response model + response = WorkflowResponse(**response_data) + print(f"Validated response: {response}") + print("=== End Debug ===\n") + + return response + except Exception as e: + logger.error(f"Error in get_workflow: {str(e)}") + print(f"Error in get_workflow: {str(e)}") + print(f"Error type: {type(e)}") + import traceback + print(f"Traceback: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=str(e)) + +@app.post("/direct-podcast", response_model=TextPodcastResponse) +async def create_direct_podcast(request: Request, current_user: dict = Depends(get_current_user)): + """Generate a podcast directly from conversation blocks with different voices.""" + logger.info(f"Received direct podcast generation request from user: {current_user['username']}") + + try: + # Parse the request body + data = await request.json() + topic = data.get("topic", "Debate") + conversation_blocks = data.get("conversation_blocks", []) + + logger.info(f"Direct podcast request for topic: {topic}") + logger.info(f"Number of conversation blocks: {len(conversation_blocks)}") + + if not conversation_blocks: + raise HTTPException(status_code=400, detail="No conversation blocks provided") + + # Format conversation blocks for the podcast manager + formatted_blocks = [] + for idx, block in enumerate(conversation_blocks): + # Extract data from each block + content = block.get("content", "") + voice_id = block.get("voice_id", "alloy") # Default to alloy if not specified + block_type = block.get("type", "generic") + turn = block.get("turn", idx + 1) + agent_id = block.get("agent_id", "") + + # Format for podcast manager + formatted_block = { + "name": f"Turn {turn}", + "input": content, + "silence_before": 0.3, # Short pause between blocks + "voice_id": voice_id, + "emotion": "neutral", + "model": "tts-1", + "speed": 1.0, + "duration": 0, + "type": block_type, + "turn": turn, + "agent_id": agent_id + } + + formatted_blocks.append(formatted_block) + + # Use the podcast manager to create the audio + result = await podcast_manager.create_podcast( + topic=topic, + research=f"Direct podcast on {topic}", + conversation_blocks=formatted_blocks, + believer_voice_id="alloy", # These are just placeholders for the manager + skeptic_voice_id="echo", + user_id=str(current_user["_id"]) + ) + + if "error" in result: + logger.error(f"Error in direct podcast creation: {result['error']}") + return TextPodcastResponse( + audio_url="", + status="failed", + error=result["error"], + duration=0, + updated_at=datetime.now().isoformat() + ) + + # Create audio URL from the audio path + audio_url = f"/audio/{os.path.basename(os.path.dirname(result['audio_path']))}/final_podcast.mp3" + full_audio_url = f"http://localhost:8000{audio_url}" + + logger.info(f"Successfully generated direct podcast: {result.get('podcast_id')}") + + return TextPodcastResponse( + audio_url=full_audio_url, + duration=result.get("duration", 0), + status="completed", + error=None, + updated_at=datetime.now().isoformat() + ) + + except Exception as e: + logger.error(f"Error generating direct podcast: {str(e)}", exc_info=True) + return TextPodcastResponse( + audio_url="", + status="failed", + error=str(e), + duration=0, + updated_at=datetime.now().isoformat() + ) \ No newline at end of file diff --git a/backend/app/models.py b/backend/app/models.py new file mode 100644 index 0000000000000000000000000000000000000000..28b98dad33d7f70f010c7a177b54270eb7e72444 --- /dev/null +++ b/backend/app/models.py @@ -0,0 +1,114 @@ +from pydantic import BaseModel, Field +from typing import Optional, List, Dict, Union, Any +from datetime import datetime + +class UserCreate(BaseModel): + username: str + password: str + +class UserLogin(BaseModel): + username: str + password: str + +class Token(BaseModel): + access_token: str + token_type: str + +class UserUpdate(BaseModel): + password: str + +class UserResponse(BaseModel): + username: str + +class AgentCreate(BaseModel): + name: str + voice_id: str + voice_name: str + voice_description: str + speed: float + pitch: float + volume: float + output_format: str + personality: str = None # Optional field for agent personality + +class AgentResponse(BaseModel): + agent_id: str + name: str + voice_id: str + voice_name: str + voice_description: str + speed: float + pitch: float + volume: float + output_format: str + user_id: str + personality: str = None # Optional field for agent personality + +class PodcastRequest(BaseModel): + topic: str + believer_voice_id: str + skeptic_voice_id: str + +class ConversationBlock(BaseModel): + name: str + input: str + silence_before: int + voice_id: str + emotion: str + model: str + speed: float + duration: int + +class PodcastResponse(BaseModel): + podcast_id: str + audio_url: Optional[str] + topic: str + error: Optional[str] + +# Models for structured debate transcript and insights +class TranscriptEntry(BaseModel): + agentId: str + agentName: str + turn: int + content: str + +class InsightsData(BaseModel): + topic: str + research: str + transcript: List[TranscriptEntry] + keyInsights: List[str] + conclusion: str + +# New Workflow Models +class WorkflowCreate(BaseModel): + name: str + description: str + nodes: List[Dict] + edges: List[Dict] + insights: Optional[Union[InsightsData, str]] = None + +class WorkflowResponse(BaseModel): + id: str + name: str + description: str + nodes: List[Dict] + edges: List[Dict] + insights: Optional[Union[InsightsData, str]] = None + user_id: str + created_at: Optional[str] + updated_at: Optional[str] + +class TextPodcastRequest(BaseModel): + text: str + voice_id: str = "alloy" + emotion: str = "neutral" + speed: float = 1.0 + title: Optional[str] = None + +class TextPodcastResponse(BaseModel): + audio_url: str + duration: Optional[float] + status: str + error: Optional[str] + updated_at: Optional[str] + diff --git a/backend/app/routers/__pycache__/podcast.cpython-311.pyc b/backend/app/routers/__pycache__/podcast.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..294dd711f2c7a02673577f2a6b51d6d8130001a9 Binary files /dev/null and b/backend/app/routers/__pycache__/podcast.cpython-311.pyc differ diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..47508a1d64d76459974797406846b134c679e2c6 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,15 @@ +fastapi==0.104.1 +uvicorn==0.24.0 +motor==3.1.1 +pymongo==4.3.3 +certifi==2024.2.2 +python-jose[cryptography]==3.3.0 +passlib[bcrypt]==1.7.4 +python-multipart==0.0.6 +python-decouple==3.8 +langgraph==0.2.14 +langchain>=0.1.0 +langchain-openai>=0.0.5 +langchain-core>=0.2.35 +langchain-community>=0.0.24 +pydub==0.25.1 \ No newline at end of file diff --git a/backend/run.py b/backend/run.py new file mode 100644 index 0000000000000000000000000000000000000000..902646eaf81c4654a59efafcc262b2633cd146f6 --- /dev/null +++ b/backend/run.py @@ -0,0 +1,14 @@ +import uvicorn + +if __name__ == "__main__": + uvicorn.run( + "app.main:app", + host="0.0.0.0", + port=8000, + reload=True, + reload_dirs=["app"], + workers=1, + ws_ping_interval=None, + ws_ping_timeout=None, + timeout_keep_alive=0 + ) \ No newline at end of file diff --git a/backend/temp_audio/Default/final_podcast.mp3 b/backend/temp_audio/Default/final_podcast.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..18b2523afe5d233f1f25d109c4f0f38ce768041a --- /dev/null +++ b/backend/temp_audio/Default/final_podcast.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:266ec16b7f8b53b12e8ac9899be9932265a8723abb61c57915fb9ae64438b6fc +size 408620 diff --git a/build_and_deploy.sh b/build_and_deploy.sh new file mode 100644 index 0000000000000000000000000000000000000000..9fdc6ff33559f572c5dd26781219ec8174d70aa5 --- /dev/null +++ b/build_and_deploy.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -e + +echo "===> Building PodCraft for deployment <====" + +# Navigate to frontend directory +echo "Building frontend..." +cd frontend/podcraft + +# Install dependencies +echo "Installing frontend dependencies..." +npm install + +# Build the frontend +echo "Creating production build..." +npm run build + +# Back to root directory +cd ../../ + +# Make sure static directory exists +mkdir -p app/static + +# Copy build to static directory (this will be mounted by FastAPI) +echo "Copying frontend build to static directory..." +cp -r frontend/podcraft/build/* app/static/ + +echo "Building Docker image..." +docker build -t podcraft-app . + +echo "Build completed successfully!" +echo "You can now run: docker run -p 8000:8000 podcraft-app" \ No newline at end of file diff --git a/build_for_spaces.sh b/build_for_spaces.sh new file mode 100644 index 0000000000000000000000000000000000000000..bde81cc80dd44b3cd2559a013e436535e5084750 --- /dev/null +++ b/build_for_spaces.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -e + +echo "===> Building PodCraft for HuggingFace Spaces <====" + +# Navigate to frontend directory +echo "Building frontend..." +cd frontend/podcraft + +# Install dependencies +echo "Installing frontend dependencies..." +npm install + +# Build the frontend +echo "Creating production build..." +npm run build + +# Back to root directory +cd ../../ + +# Make sure static directory exists +mkdir -p app/static + +# Copy build to static directory (this will be mounted by FastAPI) +echo "Copying frontend build to static directory..." +cp -r frontend/podcraft/build/* app/static/ + +# Copy the Spaces Dockerfile to main Dockerfile +echo "Setting up Dockerfile for Spaces deployment..." +cp Dockerfile.spaces Dockerfile + +echo "Build setup completed successfully!" +echo "You can now push this to your HuggingFace Space repository" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..4d942f85b4458a71da21e97821afe03da3217b9b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3' + +services: + podcraft-app: + build: . + ports: + - "8000:8000" + volumes: + - ./app:/app/app + - ./temp_audio:/app/temp_audio + environment: + - MONGODB_URL=mongodb+srv://NageshBM:Nash166^@podcraft.ozqmc.mongodb.net/?retryWrites=true&w=majority&appName=Podcraft + - SECRET_KEY=your-secret-key-change-in-production + - OPENAI_API_KEY=${OPENAI_API_KEY} + restart: always diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..aba25f73589f9ec9960ca84837c1236eebfec363 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "frontend", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/frontend/podcraft/.gitignore b/frontend/podcraft/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..a547bf36d8d11a4f89c59c144f24795749086dd1 --- /dev/null +++ b/frontend/podcraft/.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/frontend/podcraft/README.md b/frontend/podcraft/README.md new file mode 100644 index 0000000000000000000000000000000000000000..fd3b758d9c5bdd978006b8429e0caaa761644749 --- /dev/null +++ b/frontend/podcraft/README.md @@ -0,0 +1,12 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript and enable type-aware lint rules. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/frontend/podcraft/assets/bg.gif b/frontend/podcraft/assets/bg.gif new file mode 100644 index 0000000000000000000000000000000000000000..a00efc31bce0298e8590bda545ee5f2f4a69adc8 --- /dev/null +++ b/frontend/podcraft/assets/bg.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d48e5a618e4a404b15baa881dfbb687da0e4fe9564dbb6cd5dd3d984ef08910 +size 1056421 diff --git a/frontend/podcraft/assets/bg2.gif b/frontend/podcraft/assets/bg2.gif new file mode 100644 index 0000000000000000000000000000000000000000..61a120f8107897814f79058bc9204ea770bba3a2 --- /dev/null +++ b/frontend/podcraft/assets/bg2.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b9d6681b74f7e1451bcaaa41de66b55fa9cebe6bec379025186637e16bd8b27 +size 9858301 diff --git a/frontend/podcraft/assets/bg3.gif b/frontend/podcraft/assets/bg3.gif new file mode 100644 index 0000000000000000000000000000000000000000..544dc6589442c383831bab55f5eba9cde89ca5ef --- /dev/null +++ b/frontend/podcraft/assets/bg3.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1cd5c03f9b78e517532e35c866d2627ec96e56935ba1b62c4e8fb845ed1e74c +size 7655809 diff --git a/frontend/podcraft/eslint.config.js b/frontend/podcraft/eslint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..ec2b712d301e7ddeca8327fc4b967069cc82047b --- /dev/null +++ b/frontend/podcraft/eslint.config.js @@ -0,0 +1,33 @@ +import js from '@eslint/js' +import globals from 'globals' +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', + }, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...js.configs.recommended.rules, + ...reactHooks.configs.recommended.rules, + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +] diff --git a/frontend/podcraft/index.html b/frontend/podcraft/index.html new file mode 100644 index 0000000000000000000000000000000000000000..0c589eccd4d48e270e161a1ab91baee5e5f4b4bc --- /dev/null +++ b/frontend/podcraft/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/frontend/podcraft/package-lock.json b/frontend/podcraft/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..2c1de72ce9939a5a42c867bafd9ec87b2c5fec8b --- /dev/null +++ b/frontend/podcraft/package-lock.json @@ -0,0 +1,3298 @@ +{ + "name": "podcraft", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "podcraft", + "version": "0.0.0", + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-icons": "^5.5.0", + "react-router-dom": "^6.22.3", + "reactflow": "^11.11.4" + }, + "devDependencies": { + "@eslint/js": "^9.21.0", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.21.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "vite": "^6.2.0" + } + }, + "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.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "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.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", + "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.10", + "@babel/types": "^7.26.10", + "@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.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@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.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "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.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.10" + }, + "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.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.10.tgz", + "integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10", + "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.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "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.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", + "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", + "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", + "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", + "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", + "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", + "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", + "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", + "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", + "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", + "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", + "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", + "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", + "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", + "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", + "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", + "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", + "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", + "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", + "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", + "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", + "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", + "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", + "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", + "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", + "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.0.tgz", + "integrity": "sha512-RoV8Xs9eNwiDvhv7M+xcL4PWyRyIXRY/FLp3buU4h1EYfdF7unWUy3dOjPqb3C7rMUewIcqwW850PgS8h1o1yg==", + "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.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.1.0.tgz", + "integrity": "sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", + "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", + "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.22.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.22.0.tgz", + "integrity": "sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "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.7", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.12.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "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/@reactflow/background": { + "version": "11.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", + "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/controls": { + "version": "11.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz", + "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/core": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz", + "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==", + "license": "MIT", + "dependencies": { + "@types/d3": "^7.4.0", + "@types/d3-drag": "^3.0.1", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/minimap": { + "version": "11.7.14", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz", + "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-resizer": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", + "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.4", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-toolbar": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", + "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@remix-run/router": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz", + "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.35.0.tgz", + "integrity": "sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.35.0.tgz", + "integrity": "sha512-FtKddj9XZudurLhdJnBl9fl6BwCJ3ky8riCXjEw3/UIbjmIY58ppWwPEvU3fNu+W7FUsAsB1CdH+7EQE6CXAPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.35.0.tgz", + "integrity": "sha512-Uk+GjOJR6CY844/q6r5DR/6lkPFOw0hjfOIzVx22THJXMxktXG6CbejseJFznU8vHcEBLpiXKY3/6xc+cBm65Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.35.0.tgz", + "integrity": "sha512-3IrHjfAS6Vkp+5bISNQnPogRAW5GAV1n+bNCrDwXmfMHbPl5EhTmWtfmwlJxFRUCBZ+tZ/OxDyU08aF6NI/N5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.35.0.tgz", + "integrity": "sha512-sxjoD/6F9cDLSELuLNnY0fOrM9WA0KrM0vWm57XhrIMf5FGiN8D0l7fn+bpUeBSU7dCgPV2oX4zHAsAXyHFGcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.35.0.tgz", + "integrity": "sha512-2mpHCeRuD1u/2kruUiHSsnjWtHjqVbzhBkNVQ1aVD63CcexKVcQGwJ2g5VphOd84GvxfSvnnlEyBtQCE5hxVVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.35.0.tgz", + "integrity": "sha512-mrA0v3QMy6ZSvEuLs0dMxcO2LnaCONs1Z73GUDBHWbY8tFFocM6yl7YyMu7rz4zS81NDSqhrUuolyZXGi8TEqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.35.0.tgz", + "integrity": "sha512-DnYhhzcvTAKNexIql8pFajr0PiDGrIsBYPRvCKlA5ixSS3uwo/CWNZxB09jhIapEIg945KOzcYEAGGSmTSpk7A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.35.0.tgz", + "integrity": "sha512-uagpnH2M2g2b5iLsCTZ35CL1FgyuzzJQ8L9VtlJ+FckBXroTwNOaD0z0/UF+k5K3aNQjbm8LIVpxykUOQt1m/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.35.0.tgz", + "integrity": "sha512-XQxVOCd6VJeHQA/7YcqyV0/88N6ysSVzRjJ9I9UA/xXpEsjvAgDTgH3wQYz5bmr7SPtVK2TsP2fQ2N9L4ukoUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.35.0.tgz", + "integrity": "sha512-5pMT5PzfgwcXEwOaSrqVsz/LvjDZt+vQ8RT/70yhPU06PTuq8WaHhfT1LW+cdD7mW6i/J5/XIkX/1tCAkh1W6g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.35.0.tgz", + "integrity": "sha512-c+zkcvbhbXF98f4CtEIP1EBA/lCic5xB0lToneZYvMeKu5Kamq3O8gqrxiYYLzlZH6E3Aq+TSW86E4ay8iD8EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.35.0.tgz", + "integrity": "sha512-s91fuAHdOwH/Tad2tzTtPX7UZyytHIRR6V4+2IGlV0Cej5rkG0R61SX4l4y9sh0JBibMiploZx3oHKPnQBKe4g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.35.0.tgz", + "integrity": "sha512-hQRkPQPLYJZYGP+Hj4fR9dDBMIM7zrzJDWFEMPdTnTy95Ljnv0/4w/ixFw3pTBMEuuEuoqtBINYND4M7ujcuQw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.35.0.tgz", + "integrity": "sha512-Pim1T8rXOri+0HmV4CdKSGrqcBWX0d1HoPnQ0uw0bdp1aP5SdQVNBy8LjYncvnLgu3fnnCt17xjWGd4cqh8/hA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.35.0.tgz", + "integrity": "sha512-QysqXzYiDvQWfUiTm8XmJNO2zm9yC9P/2Gkrwg2dH9cxotQzunBHYr6jk4SujCTqnfGxduOmQcI7c2ryuW8XVg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.35.0.tgz", + "integrity": "sha512-OUOlGqPkVJCdJETKOCEf1mw848ZyJ5w50/rZ/3IBQVdLfR5jk/6Sr5m3iO2tdPgwo0x7VcncYuOvMhBWZq8ayg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.35.0.tgz", + "integrity": "sha512-2/lsgejMrtwQe44glq7AFFHLfJBPafpsTa6JvP2NGef/ifOa4KBoglVf7AKN7EV9o32evBPRqfg96fEHzWo5kw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.35.0.tgz", + "integrity": "sha512-PIQeY5XDkrOysbQblSW7v3l1MDZzkTEzAfTPkj5VAu3FW8fS4ynyLg2sINp0fp3SjZ8xkRYpLqoKcYqAkhU1dw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "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/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "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/react": { + "version": "19.0.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", + "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz", + "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", + "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.0", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@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 || ^6.0.0" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "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-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/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/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/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/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "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.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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/caniuse-lite": { + "version": "1.0.30001703", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001703.tgz", + "integrity": "sha512-kRlAGTRWgPsOj7oARC9m1okJEXdL/8fekFVcxA8Hl7GH4r/sN4OJn/i6Flde373T50KS7Y37oFbMwlE8+F42kQ==", + "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/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, + "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/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.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "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/electron-to-chromium": { + "version": "1.5.114", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.114.tgz", + "integrity": "sha512-DFptFef3iktoKlFQK/afbo274/XNWD00Am0xa7M8FZUepHlHT8PEuiNBoRfFHbH1okqN58AlhbJ4QTkcnXorjA==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", + "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.1", + "@esbuild/android-arm": "0.25.1", + "@esbuild/android-arm64": "0.25.1", + "@esbuild/android-x64": "0.25.1", + "@esbuild/darwin-arm64": "0.25.1", + "@esbuild/darwin-x64": "0.25.1", + "@esbuild/freebsd-arm64": "0.25.1", + "@esbuild/freebsd-x64": "0.25.1", + "@esbuild/linux-arm": "0.25.1", + "@esbuild/linux-arm64": "0.25.1", + "@esbuild/linux-ia32": "0.25.1", + "@esbuild/linux-loong64": "0.25.1", + "@esbuild/linux-mips64el": "0.25.1", + "@esbuild/linux-ppc64": "0.25.1", + "@esbuild/linux-riscv64": "0.25.1", + "@esbuild/linux-s390x": "0.25.1", + "@esbuild/linux-x64": "0.25.1", + "@esbuild/netbsd-arm64": "0.25.1", + "@esbuild/netbsd-x64": "0.25.1", + "@esbuild/openbsd-arm64": "0.25.1", + "@esbuild/openbsd-x64": "0.25.1", + "@esbuild/sunos-x64": "0.25.1", + "@esbuild/win32-arm64": "0.25.1", + "@esbuild/win32-ia32": "0.25.1", + "@esbuild/win32-x64": "0.25.1" + } + }, + "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.22.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.22.0.tgz", + "integrity": "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.2", + "@eslint/config-helpers": "^0.1.0", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.0", + "@eslint/js": "9.22.0", + "@eslint/plugin-kit": "^0.2.7", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.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" + }, + "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-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "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.19", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz", + "integrity": "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "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/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-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/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/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.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "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/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/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/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "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/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-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/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/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "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.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "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/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/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/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/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/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/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/nanoid": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.9.tgz", + "integrity": "sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==", + "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-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "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/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/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/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/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "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.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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/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/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.25.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "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/react-router": { + "version": "6.22.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz", + "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.15.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.22.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz", + "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.15.3", + "react-router": "6.22.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/reactflow": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", + "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==", + "license": "MIT", + "dependencies": { + "@reactflow/background": "11.3.14", + "@reactflow/controls": "11.2.14", + "@reactflow/core": "11.11.4", + "@reactflow/minimap": "11.7.14", + "@reactflow/node-resizer": "2.2.14", + "@reactflow/node-toolbar": "1.3.14" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "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/rollup": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.35.0.tgz", + "integrity": "sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg==", + "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.35.0", + "@rollup/rollup-android-arm64": "4.35.0", + "@rollup/rollup-darwin-arm64": "4.35.0", + "@rollup/rollup-darwin-x64": "4.35.0", + "@rollup/rollup-freebsd-arm64": "4.35.0", + "@rollup/rollup-freebsd-x64": "4.35.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.35.0", + "@rollup/rollup-linux-arm-musleabihf": "4.35.0", + "@rollup/rollup-linux-arm64-gnu": "4.35.0", + "@rollup/rollup-linux-arm64-musl": "4.35.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.35.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.35.0", + "@rollup/rollup-linux-riscv64-gnu": "4.35.0", + "@rollup/rollup-linux-s390x-gnu": "4.35.0", + "@rollup/rollup-linux-x64-gnu": "4.35.0", + "@rollup/rollup-linux-x64-musl": "4.35.0", + "@rollup/rollup-win32-arm64-msvc": "4.35.0", + "@rollup/rollup-win32-ia32-msvc": "4.35.0", + "@rollup/rollup-win32-x64-msvc": "4.35.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "license": "MIT" + }, + "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/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/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/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/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/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/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "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.1" + }, + "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/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz", + "integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "postcss": "^8.5.3", + "rollup": "^4.30.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.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 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "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/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/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/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/zustand": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz", + "integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/frontend/podcraft/package.json b/frontend/podcraft/package.json new file mode 100644 index 0000000000000000000000000000000000000000..1b584ea63d3977d84d27120e22f9f656eeacd9c7 --- /dev/null +++ b/frontend/podcraft/package.json @@ -0,0 +1,30 @@ +{ + "name": "podcraft", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-icons": "^5.5.0", + "react-router-dom": "^6.22.3", + "reactflow": "^11.11.4" + }, + "devDependencies": { + "@eslint/js": "^9.21.0", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.21.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "vite": "^6.2.0" + } +} diff --git a/frontend/podcraft/public/vite.svg b/frontend/podcraft/public/vite.svg new file mode 100644 index 0000000000000000000000000000000000000000..e7b8dfb1b2a60bd50538bec9f876511b9cac21e3 --- /dev/null +++ b/frontend/podcraft/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/podcraft/src/App.css b/frontend/podcraft/src/App.css new file mode 100644 index 0000000000000000000000000000000000000000..033ed02909d5e576ead8aa5c5755b54e2976a43d --- /dev/null +++ b/frontend/podcraft/src/App.css @@ -0,0 +1,496 @@ +#root { + width: 100%; + overflow-x: hidden; +} + +body { + margin: 0; + padding: 0; + overflow-x: hidden; +} + +.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; +} + +.app-container { + display: flex; + min-height: 100vh; + transition: all 0.3s ease; + width: 100%; + overflow-x: hidden; + position: relative; +} + +.app-container.light { + background-color: #ffffff; + color: #000000; +} + +.app-container.dark { + /* background-color: #040511; */ + color: #ffffff; +} + +.sidebar { + height: 100vh; + padding: 0.5rem; + display: flex; + flex-direction: column; + transition: all 0.3s ease-in-out; + position: fixed; + left: 0; + top: 0; + background-color: rgba(0, 0, 0, 0.1); + backdrop-filter: blur(8px); + z-index: 999; +} + +.sidebar-top { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.sidebar-bottom { + margin-top: auto; +} + +.sidebar.open { + width: 200px; + z-index: 1; +} + +.sidebar.closed { + width: 40px; +} + +.toggle-btn { + background: none; + border: none; + color: #fff; + font-size: 1.25rem; + cursor: pointer; + padding: 0.5rem; + margin-bottom: 1rem; + transition: color 0.3s ease; + text-align: left; +} + +.toggle-btn:hover { + color: rgba(255, 255, 255, 0.8); +} + +.nav-links { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-top: 1rem; +} + +.nav-link { + display: flex; + align-items: center; + color: #fff; + text-decoration: none; + padding: 0.5rem; + transition: all 0.3s ease; + font-size: 1rem; +} + +.nav-link.theme-toggle { + margin-top: auto; +} + +.nav-link svg { + font-size: 1.2rem; + transition: all 0.3s ease; +} + +.nav-link:hover svg { + color: linear-gradient(90deg, + #000 0%, + #e0e0e0 50%, + #ffffff 100%); + animation: ease-in-out 3s infinite; +} + +.link-text { + transition: all 0.3s ease; + white-space: nowrap; + font-size: 0.9rem; + margin-left: 0.5rem; +} + +.nav-link:hover .link-text { + background: linear-gradient(90deg, + #ffffff 0%, + #e0e0e0 50%, + #ffffff 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + animation: textShine 3s infinite; +} + +@keyframes textShine { + 0% { + background-position: -100px; + } + + 100% { + background-position: 100px; + } +} + +@keyframes iconShine { + 0% { + background-position: -50px; + } + + 100% { + background-position: 50px; + } +} + +.link-text.hidden { + opacity: 0; + width: 0; + overflow: hidden; +} + +.theme-toggle { + margin-top: auto; + background: none; + border: none; + cursor: pointer; + color: #fff; + padding: 0.5rem; +} + +.main-content { + margin-left: 40px; + padding: 1rem; + flex: 1; + transition: margin-left 0.3s ease-in-out; + width: calc(100% - 40px); + box-sizing: border-box; +} + +.main-content.expanded { + margin-left: 40px; +} + +.light .nav-link, +.light .toggle-btn { + color: #000000; +} + +.dark .nav-link, +.dark .toggle-btn { + color: #ffffff; +} + +.auth-container { + display: flex; + justify-content: center; + align-items: center; + min-height: calc(100vh - 2rem); + padding: 1rem; + gap: 4rem; + max-width: 100%; + box-sizing: border-box; +} + +.bg-login { + background: url("../assets/bg2.gif") no-repeat center center fixed #040511; + background-size: cover; +} + +.simple-bg { + /* background: url("../assets/bg3.gif") repeat center center fixed #040511; */ + background: url("../assets/bg3.gif") repeat center center fixed #000; + background-size: contain; + background-blend-mode: lighten; + height: 100%; + width: 100%; + position: fixed; + opacity: 1; +} + +.auth-form-container { + background: rgba(99, 102, 241, 0.05); + backdrop-filter: blur(10px); + padding: 1rem; + width: 100%; + max-width: 360px; + position: relative; + overflow: hidden; + transition: all 0.3s ease; + border-radius: 24px; +} + +.auth-form-container::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: linear-gradient(45deg, + transparent, + rgba(255, 255, 255, 0.1), + rgba(255, 255, 255, 0.2), + rgba(255, 255, 255, 0.1), + transparent); + transform: translateX(-100%) rotate(45deg); + transition: transform 0.1s ease; +} + +.auth-form-container:hover::before { + animation: cardGloss 1s ease-in-out; +} + +@keyframes cardGloss { + 0% { + transform: translateX(-100%) rotate(45deg); + } + + 100% { + transform: translateX(100%) rotate(45deg); + } +} + +.auth-form-container h2 { + margin-bottom: 1.5rem; + font-size: 1.75rem; + text-align: center; + position: relative; +} + +.form-group { + margin-bottom: 1rem; + position: relative; +} + +.auth-form .form-group input { + width: 100%; + padding: 0.5rem 0; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 8px; + background: rgba(255, 255, 255, 0.05); + color: inherit; + font-size: 0.9rem; + transition: all 0.3s ease; + text-align: center; +} + +.light .form-group input { + background: rgba(0, 0, 0, 0.05); + border-color: rgba(0, 0, 0, 0.1); +} + +.form-group input:focus { + outline: none; + border-color: rgba(255, 255, 255, 0.3); + background: rgba(255, 255, 255, 0.1); +} + +.light .form-group input:focus { + border-color: rgba(0, 0, 0, 0.3); + background: rgba(0, 0, 0, 0.08); +} + +.submit-btn { + width: 100%; + padding: 0.5rem; + border: none; + border-radius: 8px; + background: #6366f1; + color: white; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + margin-top: 0.5rem; +} + +.submit-btn:hover { + background: #4f46e5; + transform: translateY(-1px); +} + +.form-switch { + margin-top: 1rem; + text-align: center; + font-size: 0.85rem; + position: relative; +} + +.form-switch a { + color: #6366f1; + text-decoration: none; + font-weight: 600; + transition: all 0.3s ease; + margin-left: 0.25rem; +} + +.form-switch a:hover { + color: #4f46e5; +} + +.hero-section { + max-width: 400px; +} + +.hero-content { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2rem; +} + +.hero-logo { + display: flex; + align-items: center; + gap: 1rem; +} + +.hero-logo svg { + font-size: 3rem; +} + +.product-name { + position: fixed; + font-size: 20px; + width: 165px; + bottom: 20px; + left: 70px; +} + +sup { + font-size: 20px; + font-weight: bold; +} + +sub { + font-size: 12px; + background: #6366f1; + border: 1px solid #999; + padding: 1px; + border-radius: 5px; + position: absolute; + top: 5px; + right: 5px; + font-weight: bold; +} + +.hero-logo h1 { + font-size: 3.5rem; + font-weight: 700; + background: linear-gradient(0.25turn, #fff, #8a8f98); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +.hero-tagline { + font-size: 1.5rem; + font-weight: 500; + color: #6366f1; + position: relative; + padding-left: 1rem; +} + +.nav-divider { + height: 1px; + background: rgba(255, 255, 255, 0.1); + margin: 0.5rem 0; + width: 100%; +} + +.light .nav-divider { + background: rgba(0, 0, 0, 0.1); +} + +@media (max-width: 968px) { + .auth-container { + padding: 1rem; + gap: 2rem; + } + + .main-content { + padding: 0.5rem; + } + + .hero-section { + text-align: center; + } + + .hero-content { + align-items: center; + } + + .hero-logo h1 { + font-size: 2.5rem; + } + + .hero-tagline { + font-size: 1.25rem; + } +} + +#toast-container { + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + z-index: 10000; + width: auto; + max-width: calc(100vw - 40px); + pointer-events: none; +} + +#toast-container>* { + pointer-events: auto; +} + +/* Prevent horizontal scrollbar when toast appears */ +body.has-toast { + overflow-x: hidden; +} \ No newline at end of file diff --git a/frontend/podcraft/src/App.jsx b/frontend/podcraft/src/App.jsx new file mode 100644 index 0000000000000000000000000000000000000000..cafa241677777348f9482dd2eaaf1cd780be5c95 --- /dev/null +++ b/frontend/podcraft/src/App.jsx @@ -0,0 +1,268 @@ +import { useState, useRef, useEffect } from 'react' +import { BrowserRouter as Router, Route, Routes, Navigate, useNavigate, Link } from 'react-router-dom' +import { TbLayoutSidebarLeftCollapse, TbLayoutSidebarRightCollapse } from "react-icons/tb"; +import { AiFillHome } from "react-icons/ai"; +import { BiPodcast } from "react-icons/bi"; +import { FaMicrophoneAlt } from "react-icons/fa"; +import { MdDarkMode, MdLightMode } from "react-icons/md"; +import { ImPodcast } from "react-icons/im"; +import { RiChatVoiceAiFill } from "react-icons/ri"; +import { FaUser, FaSignOutAlt } from "react-icons/fa"; +import { PiGooglePodcastsLogo } from "react-icons/pi"; +import { TiFlowSwitch } from "react-icons/ti"; +import { SiNodemon } from "react-icons/si"; +import React from 'react'; + +import Home from './pages/Home' +import Podcasts from './pages/Podcasts' +import Workflows from './pages/Workflows' +import Demo from './pages/Demo' +import WorkflowEditor from './components/WorkflowEditor' +import UserModal from './components/UserModal' +import Toast from './components/Toast' +import './App.css' + +// Global toast context +export const ToastContext = React.createContext({ + toast: null, + setToast: () => { } +}); + +function App() { + const [isOpen, setIsOpen] = useState(false); + const [isDark, setIsDark] = useState(true); + const [isLogin, setIsLogin] = useState(true); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const [toast, setToast] = useState(null); + const sidebarRef = useRef(null); + + // Check for token on initial load + useEffect(() => { + const token = localStorage.getItem('token'); + if (token) { + // Validate token by making a request to the backend + const validateToken = async () => { + try { + const response = await fetch('http://localhost:8000/user/me', { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (response.ok) { + setIsAuthenticated(true); + console.log('User authenticated from stored token'); + } else { + // Token is invalid, remove it + localStorage.removeItem('token'); + setIsAuthenticated(false); + console.log('Stored token is invalid, removed'); + } + } catch (error) { + console.error('Error validating token:', error); + // Don't remove token on network errors to allow offline access + } + }; + + validateToken(); + } + }, []); + + useEffect(() => { + const handleClickOutside = (event) => { + if (sidebarRef.current && !sidebarRef.current.contains(event.target)) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + const toggleSidebar = () => { + setIsOpen(!isOpen); + }; + + const toggleTheme = (e) => { + e.preventDefault(); + setIsDark(!isDark); + document.body.style.backgroundColor = !isDark ? '#040511' : '#ffffff'; + document.body.style.color = !isDark ? '#ffffff' : '#000000'; + }; + + const toggleForm = () => { + setIsLogin(!isLogin); + }; + + const handleLogout = () => { + localStorage.removeItem('token'); + setIsAuthenticated(false); + showToast('Logged out successfully', 'success'); + }; + + const showToast = (message, type = 'success') => { + setToast({ message, type }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + const formData = new FormData(e.target); + const username = formData.get('username'); + const password = formData.get('password'); + + try { + const response = await fetch(`http://localhost:8000/${isLogin ? 'login' : 'signup'}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username, password }), + }); + + if (response.ok) { + const data = await response.json(); + localStorage.setItem('token', data.access_token); + setIsAuthenticated(true); + showToast(`Successfully ${isLogin ? 'logged in' : 'signed up'}!`, 'success'); + } else { + const error = await response.json(); + showToast(error.detail, 'error'); + } + } catch (error) { + console.error('Error:', error); + showToast('An error occurred during authentication', 'error'); + } + }; + + return ( + + + <> +
+
+
+ + +
+ {!isAuthenticated ? ( +
+
+
+
+ +

PodCraft

+
+

One prompt to Podcast

+
+
+
+

{isLogin ? 'Login' : 'Sign Up'}

+
+
+ +
+
+ +
+ +
+

+ {isLogin ? "Don't have an account? " : "Already have an account? "} + + {isLogin ? 'Sign Up' : 'Login'} + +

+
+
+ ) : ( + + } /> + } /> + } /> + } /> + } /> + } /> + + )} +
+ + setIsModalOpen(false)} + token={localStorage.getItem('token')} + /> + +
+ {toast && ( + setToast(null)} + /> + )} +
+
+ +
+
+ ) +} + +export default App diff --git a/frontend/podcraft/src/assets/react.svg b/frontend/podcraft/src/assets/react.svg new file mode 100644 index 0000000000000000000000000000000000000000..6c87de9bb3358469122cc991d5cf578927246184 --- /dev/null +++ b/frontend/podcraft/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/podcraft/src/components/AgentModal.css b/frontend/podcraft/src/components/AgentModal.css new file mode 100644 index 0000000000000000000000000000000000000000..a2e3451aea8aa199aebe5ac1983f63fd8ca690c6 --- /dev/null +++ b/frontend/podcraft/src/components/AgentModal.css @@ -0,0 +1,470 @@ +.agent-modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + backdrop-filter: blur(5px); +} + +.agent-form .form-group { + margin-bottom: 0.5rem; +} + +.agent-modal-content { + background: rgba(20, 20, 20, 0.95); + backdrop-filter: blur(10px); + padding: 1rem 2rem; + border-radius: 12px; + width: 90%; + max-width: 600px; + border: 1px solid rgba(255, 255, 255, 0.1); + color: white; + position: relative; +} + +.agent-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.agent-modal-header h2 { + margin: 0; + font-size: 1.5rem; + background: linear-gradient(90deg, #fff, #999); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + display: flex; + align-items: center; + gap: 1rem; +} + +.close-button { + background: transparent; + border: none; + color: rgba(255, 255, 255, 0.6); + cursor: pointer; + padding: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all 0.3s ease; +} + +.close-button:hover { + color: white; + background: rgba(255, 255, 255, 0.1); +} + +.agent-form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.form-group label { + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.8); + font-weight: 500; +} + +.form-group input[type="text"], +.form-group textarea { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 0.75rem; + color: white; + font-size: 0.9rem; + transition: all 0.3s ease; +} + +.form-group input[type="text"]:focus, +.form-group textarea:focus { + outline: none; + border-color: #6366f1; + background: rgba(255, 255, 255, 0.1); +} + +/* Custom Dropdown Styles */ +.custom-dropdown { + position: relative; + width: 100%; +} + +.dropdown-header { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 0.75rem; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: space-between; +} + +.dropdown-header:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(99, 102, 241, 0.3); +} + +.selected-voice, +.voice-info { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.voice-info { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; +} + +.voice-info span { + font-size: 0.9rem; + color: white; +} + +.voice-info small { + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.6); +} + +.dropdown-options { + position: absolute; + top: 100%; + left: 0; + right: 0; + margin-top: 0.5rem; + background: rgba(30, 30, 30, 0.95); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + max-height: 300px; + overflow-y: auto; + z-index: 10; + backdrop-filter: blur(10px); + scrollbar-width: thin; + scrollbar-color: rgba(99, 102, 241, 0.3) rgba(255, 255, 255, 0.05); +} + +.dropdown-options::-webkit-scrollbar { + width: 6px; +} + +.dropdown-options::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 3px; +} + +.dropdown-options::-webkit-scrollbar-thumb { + background: rgba(99, 102, 241, 0.3); + border-radius: 3px; + transition: background 0.3s ease; +} + +.dropdown-options::-webkit-scrollbar-thumb:hover { + background: rgba(99, 102, 241, 0.5); +} + +.dropdown-option { + padding: 0.75rem; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.75rem; + transition: all 0.3s ease; +} + +.dropdown-option:hover { + background: rgba(99, 102, 241, 0.1); +} + +/* Slider Styles */ +.slider-container { + display: flex; + align-items: center; + gap: 1rem; +} + +.agent-form input#name { + width: auto; +} + +.slider-container input[type="range"] { + padding: 2px 0; + border-radius: 5px; + background: #222; +} + +.slider-container input[type="range"] { + flex: 1; + -webkit-appearance: none; + height: 0px; + background: rgba(255, 255, 255, 0.1); + border-radius: 2px; + outline: none; +} + +.slider-container input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + background: #6366f1; + border-radius: 50%; + cursor: pointer; + transition: all 0.3s ease; +} + +.slider-container input[type="range"]::-webkit-slider-thumb:hover { + transform: scale(1.1); + background: #4f46e5; +} + +.slider-value { + min-width: 48px; + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.8); + text-align: right; +} + +/* Radio Group Styles */ +.radio-group { + display: flex; + gap: 1rem; +} + +.radio-label { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; +} + +.radio-label input[type="radio"] { + display: none; +} + +.radio-label span { + padding: 0.5rem 1rem; + border-radius: 6px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.8); + transition: all 0.3s ease; +} + +.radio-label input[type="radio"]:checked+span { + background: rgba(99, 102, 241, 0.1); + border-color: #6366f1; + color: #6366f1; +} + +/* Modal Actions */ +.modal-actions { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 1rem; +} + +.right-actions { + display: flex; + gap: 1rem; +} + +.test-voice-btn, +.save-btn, +.cancel-btn { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.25rem; + border-radius: 8px; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; +} + +.test-voice-btn { + background: rgba(99, 102, 241, 0.1); + border: 1px solid rgba(99, 102, 241, 0.3); + color: #6366f1; +} + +.test-voice-btn:hover { + background: rgba(99, 102, 241, 0.2); + transform: translateY(-1px); +} + +.save-btn { + background: #6366f1; + border: none; + color: white; +} + +.save-btn:hover { + background: #4f46e5; + transform: translateY(-1px); +} + +.cancel-btn { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.8); +} + +.cancel-btn:hover { + background: rgba(255, 255, 255, 0.1); + transform: translateY(-1px); +} + +/* Light Theme Adjustments */ +.light .agent-modal-content { + background: rgba(255, 255, 255, 0.95); + border-color: rgba(0, 0, 0, 0.1); + color: black; +} + +.light .agent-modal-header h2 { + background: linear-gradient(90deg, #333, #666); + -webkit-background-clip: text; +} + +.light .form-group label { + color: rgba(0, 0, 0, 0.8); +} + +.light .form-group input[type="text"], +.light .form-group textarea { + background: rgba(0, 0, 0, 0.05); + border-color: rgba(0, 0, 0, 0.1); + color: black; +} + +.light .form-group input[type="text"]:focus, +.light .form-group textarea:focus { + border-color: #6366f1; + background: rgba(0, 0, 0, 0.08); +} + +.light .dropdown-header { + background: rgba(0, 0, 0, 0.05); + border-color: rgba(0, 0, 0, 0.1); +} + +.light .dropdown-header:hover { + background: rgba(0, 0, 0, 0.08); +} + +.light .voice-info span { + color: black; +} + +.light .voice-info small { + color: rgba(0, 0, 0, 0.6); +} + +.light .dropdown-options { + background: rgba(255, 255, 255, 0.95); + border-color: rgba(0, 0, 0, 0.1); + scrollbar-color: rgba(99, 102, 241, 0.3) rgba(0, 0, 0, 0.05); +} + +.light .dropdown-options::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.05); +} + +.light .dropdown-options::-webkit-scrollbar-thumb { + background: rgba(99, 102, 241, 0.3); +} + +.light .dropdown-options::-webkit-scrollbar-thumb:hover { + background: rgba(99, 102, 241, 0.5); +} + +.light .slider-container input[type="range"] { + background: rgba(0, 0, 0, 0.1); +} + +.light .slider-value { + color: rgba(0, 0, 0, 0.8); +} + +.light .radio-label span { + background: rgba(0, 0, 0, 0.05); + border-color: rgba(0, 0, 0, 0.1); + color: rgba(0, 0, 0, 0.8); +} + +.light .cancel-btn { + border-color: rgba(0, 0, 0, 0.1); + color: rgba(0, 0, 0, 0.8); +} + +.light .cancel-btn:hover { + background: rgba(0, 0, 0, 0.1); +} + +.toggle-group { + margin-top: 1rem; + margin-bottom: 1rem; +} + +.toggle-container { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 8px 12px; + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; + cursor: pointer; + transition: all 0.3s ease; +} + +.toggle-container:hover { + background: rgba(255, 255, 255, 0.1); +} + +.toggle-container span { + color: rgba(255, 255, 255, 0.6); + font-size: 0.9rem; +} + +.toggle-container span.active { + color: rgba(255, 255, 255, 1); + font-weight: 500; +} + +.toggle-icon { + font-size: 1.5rem; + color: #6366f1; + transition: transform 0.3s ease; +} + +.help-text { + display: block; + margin-top: 5px; + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.5); + font-style: italic; +} \ No newline at end of file diff --git a/frontend/podcraft/src/components/AgentModal.tsx b/frontend/podcraft/src/components/AgentModal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..edfe3a8738ed3e4a6c2b0fe0a72196ea9600e732 --- /dev/null +++ b/frontend/podcraft/src/components/AgentModal.tsx @@ -0,0 +1,558 @@ +import React, { useState, useEffect } from 'react'; +import { FaPlay, FaSave, FaTimes, FaChevronDown, FaVolumeUp } from 'react-icons/fa'; +import './AgentModal.css'; +import { BsRobot, BsToggleOff, BsToggleOn } from "react-icons/bs"; +import Toast from './Toast'; + + +type Voice = { + id: string; + name: string; + description: string; +}; + +type FormData = { + name: string; + voice: Voice | null; + speed: number; + pitch: number; + volume: number; + outputFormat: 'mp3' | 'wav'; + testInput: string; + personality: string; + showPersonality: boolean; +}; + +const VOICE_OPTIONS: Voice[] = [ + { id: 'alloy', name: 'Alloy', description: 'Versatile, well-rounded voice' }, + { id: 'ash', name: 'Ash', description: 'Direct and clear articulation' }, + { id: 'coral', name: 'Coral', description: 'Warm and inviting tone' }, + { id: 'echo', name: 'Echo', description: 'Balanced and measured delivery' }, + { id: 'fable', name: 'Fable', description: 'Expressive storytelling voice' }, + { id: 'onyx', name: 'Onyx', description: 'Authoritative and professional' }, + { id: 'nova', name: 'Nova', description: 'Energetic and engaging' }, + { id: 'sage', name: 'Sage', description: 'Calm and thoughtful delivery' }, + { id: 'shimmer', name: 'Shimmer', description: 'Bright and optimistic tone' } +]; + +interface AgentModalProps { + isOpen: boolean; + onClose: () => void; + editAgent?: { + id: string; + name: string; + voice_id: string; + speed: number; + pitch: number; + volume: number; + output_format: string; + personality: string; + } | null; +} + +const AgentModal: React.FC = ({ isOpen, onClose, editAgent }) => { + const [formData, setFormData] = useState({ + name: '', + voice: null, + speed: 1, + pitch: 1, + volume: 1, + outputFormat: 'mp3', + testInput: '', + personality: '', + showPersonality: false + }); + + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'info' } | null>(null); + const [audioPlayer, setAudioPlayer] = useState(null); + const [isTestingVoice, setIsTestingVoice] = useState(false); + + // Initialize form data when editing an agent + useEffect(() => { + if (editAgent) { + // Find the matching voice from VOICE_OPTIONS + const matchingVoice = VOICE_OPTIONS.find(voice => voice.id === editAgent.voice_id) || VOICE_OPTIONS[0]; + + // Ensure output_format is either 'mp3' or 'wav' + const validOutputFormat = editAgent.output_format === 'wav' ? 'wav' : 'mp3'; + + setFormData({ + name: editAgent.name, + voice: matchingVoice, + speed: editAgent.speed || 1, + pitch: editAgent.pitch || 1, + volume: editAgent.volume || 1, + outputFormat: validOutputFormat, + testInput: '', + personality: editAgent.personality || '', + showPersonality: !!editAgent.personality + }); + } else { + // Reset form when not editing + setFormData({ + name: '', + voice: VOICE_OPTIONS[0], + speed: 1, + pitch: 1, + volume: 1, + outputFormat: 'mp3', + testInput: '', + personality: '', + showPersonality: false + }); + } + }, [editAgent]); + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + }; + + const handleVoiceSelect = (voice: Voice) => { + setFormData(prev => ({ + ...prev, + voice + })); + setIsDropdownOpen(false); + }; + + const handleSliderChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + const numericValue = parseFloat(value); + if (!isNaN(numericValue)) { + setFormData(prev => ({ + ...prev, + [name]: numericValue + })); + } + }; + + const handleTestVoice = async () => { + if (!formData.testInput.trim()) { + setToast({ message: 'Please enter some text to test', type: 'error' }); + return; + } + + try { + setIsTestingVoice(true); + const token = localStorage.getItem('token'); + if (!token) { + setToast({ message: 'Authentication token not found', type: 'error' }); + setIsTestingVoice(false); + return; + } + + // Stop any currently playing audio + if (audioPlayer) { + audioPlayer.pause(); + audioPlayer.src = ''; + setAudioPlayer(null); + } + + // Prepare test data + const testData = { + text: formData.testInput.trim(), + voice_id: formData.voice?.id || 'alloy', + emotion: 'neutral', // Default emotion + speed: formData.speed + }; + + console.log('Sending test data:', JSON.stringify(testData, null, 2)); + + // Make API request + const response = await fetch('http://localhost:8000/agents/test-voice', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(testData) + }); + + console.log('Response status:', response.status); + const data = await response.json(); + console.log('Response data:', JSON.stringify(data, null, 2)); + + if (!response.ok) { + throw new Error(data.detail || 'Failed to test voice'); + } + + if (!data.audio_url) { + throw new Error('No audio URL returned from server'); + } + + setToast({ message: 'Creating audio player...', type: 'info' }); + + // Create and configure new audio player + const newPlayer = new Audio(); + + // Set up event handlers before setting the source + newPlayer.onerror = (e) => { + console.error('Audio loading error:', newPlayer.error, e); + setToast({ + message: `Failed to load audio file: ${newPlayer.error?.message || 'Unknown error'}`, + type: 'error' + }); + setIsTestingVoice(false); + }; + + newPlayer.oncanplaythrough = () => { + console.log('Audio can play through, starting playback'); + newPlayer.play() + .then(() => { + setToast({ message: 'Playing test audio', type: 'success' }); + }) + .catch((error) => { + console.error('Playback error:', error); + setToast({ + message: `Failed to play audio: ${error.message}`, + type: 'error' + }); + setIsTestingVoice(false); + }); + }; + + newPlayer.onended = () => { + console.log('Audio playback ended'); + setIsTestingVoice(false); + }; + + // Log the audio URL we're trying to play + console.log('Setting audio source to:', data.audio_url); + + // Set the source and start loading + newPlayer.src = data.audio_url; + setAudioPlayer(newPlayer); + + // Try to load the audio + try { + await newPlayer.load(); + console.log('Audio loaded successfully'); + } catch (loadError) { + console.error('Error loading audio:', loadError); + setToast({ + message: `Error loading audio: ${loadError instanceof Error ? loadError.message : 'Unknown error'}`, + type: 'error' + }); + setIsTestingVoice(false); + } + + } catch (error) { + console.error('Error testing voice:', error); + setToast({ + message: error instanceof Error ? error.message : 'Failed to test voice', + type: 'error' + }); + setIsTestingVoice(false); + } + }; + + // Cleanup audio player on modal close + React.useEffect(() => { + return () => { + if (audioPlayer) { + audioPlayer.pause(); + audioPlayer.src = ''; + } + }; + }, [audioPlayer]); + + const toggleInputType = () => { + setFormData(prev => ({ + ...prev, + showPersonality: !prev.showPersonality + })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!formData.voice) { + setToast({ message: 'Please select a voice', type: 'error' }); + return; + } + + try { + const token = localStorage.getItem('token'); + if (!token) { + setToast({ message: 'Authentication token not found', type: 'error' }); + return; + } + + const requestData = { + name: formData.name, + voice_id: formData.voice.id, + voice_name: formData.voice.name, + voice_description: formData.voice.description, + speed: formData.speed, + pitch: formData.pitch, + volume: formData.volume, + output_format: formData.outputFormat, // Use snake_case to match backend + personality: formData.showPersonality ? formData.personality : null + }; + + console.log('Request data:', JSON.stringify(requestData, null, 2)); + + const url = editAgent + ? `http://localhost:8000/agents/${editAgent.id}` + : 'http://localhost:8000/agents/create'; + + const response = await fetch(url, { + method: editAgent ? 'PUT' : 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(requestData) + }); + + const responseData = await response.json(); + console.log('Response data:', JSON.stringify(responseData, null, 2)); + + if (!response.ok) { + throw new Error(JSON.stringify(responseData, null, 2)); + } + + setToast({ message: `Agent ${editAgent ? 'updated' : 'created'} successfully`, type: 'success' }); + onClose(); + } catch (error) { + console.error('Error saving agent:', error); + if (error instanceof Error) { + console.error('Error details:', error.message); + try { + const errorDetails = JSON.parse(error.message); + setToast({ + message: errorDetails.detail?.[0]?.msg || 'Failed to save agent', + type: 'error' + }); + } catch { + setToast({ message: error.message, type: 'error' }); + } + } else { + setToast({ message: 'An unexpected error occurred', type: 'error' }); + } + } + }; + + if (!isOpen) return null; + + return ( +
+
+
+

{editAgent ? 'Edit Agent' : 'Create New Agent'}

+ +
+ +
+
+ + +
+ +
+ +
+
setIsDropdownOpen(!isDropdownOpen)} + > +
+ +
+ {formData.voice?.name} + {formData.voice?.description} +
+
+ +
+ {isDropdownOpen && ( +
+ {VOICE_OPTIONS.map(voice => ( +
handleVoiceSelect(voice)} + > + +
+ {voice.name} + {voice.description} +
+
+ ))} +
+ )} +
+
+ +
+ +
+ + {formData.speed}x +
+
+ +
+ +
+ + {formData.pitch}x +
+
+ +
+ +
+ + {formData.volume}x +
+
+ +
+ +
+ + +
+
+ +
+ +
+ Test Input + {formData.showPersonality ? + : + + } + Agent Personality +
+
+ + {formData.showPersonality ? ( +
+ +