Richard Lai commited on
Commit
4024eae
·
1 Parent(s): 9010fb4

first commit

Browse files
.dockerignore ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Git
2
+ .git
3
+ .gitignore
4
+
5
+ # Documentation
6
+ README.md
7
+ *.md
8
+
9
+ # Environment files
10
+ .env
11
+ .env.local
12
+ .env.example
13
+
14
+ # Python
15
+ __pycache__/
16
+ *.py[cod]
17
+ *$py.class
18
+ *.so
19
+ .Python
20
+ env/
21
+ venv/
22
+ .venv/
23
+ .coverage
24
+ .pytest_cache/
25
+
26
+ # Node.js
27
+ node_modules/
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .npm
32
+ .yarn/
33
+ .pnpm-store/
34
+
35
+ # Frontend build artifacts (will be copied from build stage)
36
+ frontend/dist/
37
+ frontend/.vite/
38
+ frontend/coverage/
39
+
40
+ # IDE and editor files
41
+ .vscode/
42
+ .idea/
43
+ *.swp
44
+ *.swo
45
+ *~
46
+
47
+ # OS generated files
48
+ .DS_Store
49
+ .DS_Store?
50
+ ._*
51
+ .Spotlight-V100
52
+ .Trashes
53
+ ehthumbs.db
54
+ Thumbs.db
55
+
56
+ # Logs
57
+ logs/
58
+ *.log
59
+
60
+ # Temporary files
61
+ tmp/
62
+ temp/
63
+
64
+ # Old project files
65
+ AI-NewsLetter-old/
66
+
67
+ # Deployment files
68
+ .vercel/
69
+ .railway/
70
+
71
+ # Package manager lock files (except the ones we need)
72
+ package-lock.json
73
+ yarn.lock
74
+
75
+ # Test files
76
+ *.test.js
77
+ *.test.ts
78
+ *.test.tsx
.gitignore ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.js
7
+
8
+ # testing
9
+ /coverage
10
+
11
+ # next.js
12
+ /.next/
13
+ /out/
14
+
15
+ # production
16
+ /build
17
+
18
+ # misc
19
+ .DS_Store
20
+ *.pem
21
+
22
+ # debug
23
+ npm-debug.log*
24
+ yarn-debug.log*
25
+ yarn-error.log*
26
+
27
+ # local env files
28
+ .env*.local
29
+ .env
30
+
31
+ # vercel
32
+ .vercel
33
+
34
+ # typescript
35
+ *.tsbuildinfo
36
+ next-env.d.ts
37
+
38
+ # Python
39
+ __pycache__/
40
+ *.py[cod]
41
+ *$py.class
42
+ *.pyo
43
+ *.pyd
44
+ *.pyc
45
+
46
+ # Distribution / packaging
47
+ .Python
48
+ build/
49
+ develop-eggs/
50
+ dist/
51
+ downloads/
52
+ eggs/
53
+ .eggs/
54
+ lib/
55
+ lib64/
56
+ parts/
57
+ sdist/
58
+ var/
59
+ wheels/
60
+ pip-wheel-metadata/
61
+ share/python-wheels/
62
+ *.egg-info/
63
+ .installed.cfg
64
+ *.egg
65
+ MANIFEST
66
+
67
+ # PyInstaller
68
+ # Usually these files are written by a python script from a template
69
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
70
+ *.manifest
71
+ *.spec
72
+
73
+ # Installer logs
74
+ pip-log.txt
75
+ pip-delete-this-directory.txt
76
+
77
+ # Unit test / coverage reports
78
+ htmlcov/
79
+ .tox/
80
+ .nox/
81
+ .coverage
82
+ .coverage.*
83
+ .cache
84
+ nosetests.xml
85
+ coverage.xml
86
+ *.cover
87
+ .hypothesis/
88
+ .pytest_cache/
89
+
90
+ # Jupyter Notebook
91
+ .ipynb_checkpoints
92
+
93
+ # pyenv
94
+ .python-version
95
+
96
+ # mypy
97
+ .mypy_cache/
98
+ .dmypy.json
99
+ dmypy.json
100
+
101
+ # Pyre type checker
102
+ .pyre/
103
+
104
+ # pytype
105
+ .pytype/
106
+
107
+ # Cython debug symbols
108
+ cython_debug/
109
+
110
+ # Virtual environment
111
+ .venv/*
112
+
113
+ # vscode
114
+ .vscode/*
115
+
116
+ # cursor
117
+ .cursor/*
118
+
Dockerfile ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-stage build for AI Newsletter Generator
2
+ # Optimized for Hugging Face Spaces deployment
3
+
4
+ # Stage 1: Build frontend
5
+ FROM node:20-slim as frontend-builder
6
+
7
+ # Install pnpm
8
+ RUN npm install -g pnpm
9
+
10
+ # Set working directory
11
+ WORKDIR /app/frontend
12
+
13
+ # Copy frontend package files
14
+ COPY frontend/package.json frontend/pnpm-lock.yaml ./
15
+
16
+ # Install frontend dependencies
17
+ RUN pnpm install --frozen-lockfile
18
+
19
+ # Copy frontend source
20
+ COPY frontend/ .
21
+
22
+ # Build frontend for production
23
+ RUN pnpm build
24
+
25
+ # Stage 2: Python backend with built frontend
26
+ FROM python:3.12-slim
27
+
28
+ # Set environment variables
29
+ ENV PYTHONUNBUFFERED=1
30
+ ENV PYTHONDONTWRITEBYTECODE=1
31
+ ENV PORT=7860
32
+
33
+ # Install system dependencies
34
+ RUN apt-get update && apt-get install -y \
35
+ curl \
36
+ && rm -rf /var/lib/apt/lists/*
37
+
38
+ # Install uv for fast Python package management
39
+ RUN pip install uv
40
+
41
+ # Set working directory
42
+ WORKDIR /app
43
+
44
+ # Copy Python configuration
45
+ COPY pyproject.toml uv.lock ./
46
+
47
+ # Install Python dependencies
48
+ RUN uv sync --no-dev
49
+
50
+ # Copy backend source
51
+ COPY backend/ ./backend/
52
+
53
+ # Copy built frontend from previous stage
54
+ COPY --from=frontend-builder /app/frontend/dist ./frontend/dist
55
+
56
+ # Create non-root user for security
57
+ RUN useradd --create-home --shell /bin/bash app
58
+ RUN chown -R app:app /app
59
+ USER app
60
+
61
+ # Expose port (Hugging Face Spaces uses 7860)
62
+ EXPOSE 7860
63
+
64
+ # Health check
65
+ HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
66
+ CMD curl -f http://localhost:7860/api/health || exit 1
67
+
68
+ # Start command for Hugging Face Spaces
69
+ CMD ["uv", "run", "uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,11 +1,236 @@
1
- ---
2
- title: AI Newsletter
3
- emoji: 🏢
4
- colorFrom: green
5
- colorTo: green
6
- sdk: docker
7
- pinned: false
8
- short_description: Create Tweets and Newsletters
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ # AI Newsletter Generator
2
+
3
+ A full-stack AI-powered newsletter generator that creates engaging newsletters from RSS feeds with intelligent article summarization, tweet generation, and AI-assisted editing.
4
+
5
+ ## ✨ Features
6
+
7
+ - 🤖 **AI-Enhanced Article Summaries**: LLM-generated engaging abstracts for better readability
8
+ - 📰 **RSS Feed Aggregation**: Curate content from multiple AI/tech news sources
9
+ - 🔍 **Smart Article Selection**: Interactive interface to choose articles for processing
10
+ - 📝 **Deep Article Summarization**: AI-powered detailed summaries of selected articles
11
+ - 🐦 **Social Media Content**: Generate Twitter/X posts with AI editing capabilities
12
+ - 📧 **Professional Newsletters**: Create polished HTML newsletters
13
+ - ✨ **Interactive AI Editing**: Real-time AI assistance for content refinement
14
+ - 🎨 **Modern UI**: Beautiful React interface with gradient backgrounds and smooth interactions
15
+ - ⚡ **Fast Performance**: Vite-powered frontend with hot reload
16
+ - 🔒 **Environment Security**: Secure API key management
17
+
18
+ ## 🏗️ Architecture
19
+
20
+ ### Project Structure
21
+ ```
22
+ AI-NewsLetter/
23
+ ├── backend/ # FastAPI backend
24
+ │ └── main.py # API endpoints + static file serving
25
+ ├── frontend/ # React + Vite + Tailwind frontend
26
+ │ ├── src/
27
+ │ │ ├── components/ # React components
28
+ │ │ │ ├── FeedPicker.tsx
29
+ │ │ │ ├── TweetCards.tsx
30
+ │ │ │ └── EditorModal.tsx
31
+ │ │ ├── App.tsx # Main application
32
+ │ │ └── index.css # Tailwind styles
33
+ │ └── dist/ # Built frontend (served by backend)
34
+ ├── pyproject.toml # Python dependencies (managed by uv)
35
+ ├── .env # Environment variables
36
+ └── README.md
37
+ ```
38
+
39
+ ### Technology Stack
40
+
41
+ **Backend:**
42
+ - **FastAPI** - Modern Python web framework with automatic API docs
43
+ - **OpenAI API** - GPT-4o-mini for content generation and enhancement
44
+ - **httpx** - Async HTTP client for web scraping
45
+ - **feedparser** - RSS/Atom feed parsing
46
+ - **uvicorn** - High-performance ASGI server
47
+
48
+ **Frontend:**
49
+ - **React 19** - Latest React with modern hooks
50
+ - **TypeScript** - Type safety and better developer experience
51
+ - **Vite** - Lightning-fast build tool and dev server
52
+ - **Tailwind CSS v3** - Utility-first styling with custom components
53
+ - **pnpm** - Fast, disk-efficient package manager
54
+
55
+ **Development Tools:**
56
+ - **uv** - Ultra-fast Python package manager
57
+ - **ESLint** - Code linting and formatting
58
+ - **PostCSS** - CSS processing with Tailwind
59
+
60
+ ## 🚀 Quick Start
61
+
62
+ ### Prerequisites
63
+
64
+ - **Python 3.12+** with [uv](https://docs.astral.sh/uv/) installed
65
+ - **Node.js 18+**
66
+ - **pnpm** (recommended) or npm
67
+ - **OpenAI API Key** - Get one from [OpenAI Platform](https://platform.openai.com/account/api-keys)
68
+
69
+ ### 1. Environment Setup
70
+
71
+ Create a `.env` file in the project root:
72
+ ```bash
73
+ OPENAI_API_KEY=sk-your-actual-openai-api-key-here
74
+ ```
75
+
76
+ ### 2. Backend Setup
77
+
78
+ ```bash
79
+ # Install Python dependencies
80
+ uv sync
81
+
82
+ # Start the FastAPI server (serves both API and frontend)
83
+ uv run uvicorn backend.main:app --host 127.0.0.1 --port 8000 --reload
84
+ ```
85
+
86
+ ### 3. Frontend Setup
87
+
88
+ ```bash
89
+ cd frontend
90
+
91
+ # Install dependencies
92
+ pnpm install
93
+
94
+ # Build for production
95
+ pnpm build
96
+ ```
97
+
98
+ ### 4. Access the Application
99
+
100
+ Open your browser to **http://127.0.0.1:8000**
101
+
102
+ The backend serves both the API endpoints and the built React frontend from a single port.
103
+
104
+ ## 📖 User Guide
105
+
106
+ ### Workflow
107
+
108
+ 1. **Select Sources**: Choose from curated AI/tech RSS feeds
109
+ 2. **Get Highlights**: Fetch articles and generate initial AI summary
110
+ 3. **Select Articles**: Review articles with AI-enhanced abstracts
111
+ 4. **Get Summaries**: Generate detailed summaries for selected articles (max 5)
112
+ 5. **Generate Tweets**: Create social media content with AI editing
113
+ 6. **Create Newsletter**: Build professional HTML newsletter
114
+ 7. **Download**: Export your newsletter
115
+
116
+ ### Key Features Explained
117
+
118
+ **AI-Enhanced Abstracts**: When you click "Get Highlights", the system not only fetches articles but uses GPT to create engaging 2-3 sentence summaries for each article, making them much more readable and compelling than raw RSS descriptions.
119
+
120
+ **Smart Article Selection**: The interface shows checkboxes for each article with enhanced summaries, publication dates, and sources. You can easily select which articles to dive deeper into.
121
+
122
+ **Detailed Summarization**: The "Get Summaries" feature scrapes full article content and creates comprehensive summaries using AI, perfect for busy readers who want key insights.
123
+
124
+ **Interactive AI Editing**: Both tweets and newsletter content can be edited with AI assistance through natural language commands.
125
+
126
+ ## 🔧 Development
127
+
128
+ ### Full-Stack Development (Recommended)
129
+ ```bash
130
+ # Terminal 1: Start backend
131
+ uv run uvicorn backend.main:app --host 127.0.0.1 --port 8000 --reload
132
+
133
+ # Terminal 2: Build frontend after changes
134
+ cd frontend && pnpm build
135
+ ```
136
+
137
+ ### Frontend-Only Development
138
+ For rapid UI development with hot reload:
139
+ ```bash
140
+ # Terminal 1: Backend
141
+ uv run uvicorn backend.main:app --host 127.0.0.1 --port 8000 --reload
142
+
143
+ # Terminal 2: Frontend dev server
144
+ cd frontend && pnpm dev --port 3002
145
+ ```
146
+
147
+ Then open http://127.0.0.1:3002 for development or http://127.0.0.1:8000 for production.
148
+
149
+ ### Available Scripts
150
+
151
+ **Backend:**
152
+ ```bash
153
+ uv sync # Install dependencies
154
+ uv run uvicorn backend.main:app --reload # Start server
155
+ ```
156
+
157
+ **Frontend:**
158
+ ```bash
159
+ pnpm install # Install dependencies
160
+ pnpm build # Build for production
161
+ pnpm dev # Development server
162
+ pnpm type-check # TypeScript checking
163
+ pnpm clean # Clean build artifacts
164
+ ```
165
+
166
+ ## 🌐 API Reference
167
+
168
+ ### Core Endpoints
169
+
170
+ - `GET /` - Serves React frontend application
171
+ - `GET /api/health` - API health check
172
+ - `GET /api/defaults` - Get default RSS feed sources
173
+
174
+ ### Content Generation
175
+
176
+ - `POST /api/aggregate` - Fetch articles from RSS feeds with AI-enhanced summaries
177
+ - `POST /api/highlights` - Generate weekly highlights summary
178
+ - `POST /api/summaries_selected` - Create detailed summaries for selected articles
179
+ - `POST /api/tweets` - Generate social media posts from summaries
180
+ - `POST /api/newsletter` - Create HTML newsletter
181
+ - `POST /api/edit_tweet` - AI-powered tweet editing
182
+
183
+ ### Example API Usage
184
+
185
+ ```bash
186
+ # Get enhanced articles with AI summaries
187
+ curl -X POST "http://127.0.0.1:8000/api/aggregate" \
188
+ -H "Content-Type: application/json" \
189
+ -d '{"sources": ["https://huggingface.co/blog/feed.xml"]}'
190
+
191
+ # Generate detailed summaries
192
+ curl -X POST "http://127.0.0.1:8000/api/summaries_selected" \
193
+ -H "Content-Type: application/json" \
194
+ -d '{"articles": [...]}'
195
+ ```
196
+
197
+ ## 🐳 Deployment
198
+
199
+ ### Hugging Face Spaces
200
+
201
+ This project includes a Dockerfile optimized for Hugging Face Spaces deployment:
202
+
203
+ 1. Push your code to a Hugging Face repository
204
+ 2. Set your `OPENAI_API_KEY` in the Space settings
205
+ 3. The Dockerfile will handle the rest!
206
+
207
+ ### Other Platforms
208
+
209
+ The application can be deployed on any platform that supports Docker containers:
210
+ - Railway
211
+ - Render
212
+ - DigitalOcean App Platform
213
+ - AWS ECS
214
+ - Google Cloud Run
215
+
216
+ ## 🤝 Contributing
217
+
218
+ Contributions are welcome! This project uses:
219
+ - **Python**: Black formatting, type hints encouraged
220
+ - **TypeScript**: Strict mode, ESLint configuration
221
+ - **Git**: Conventional commit messages preferred
222
+
223
+ ## 📄 License
224
+
225
+ This project is open source and available under the MIT License.
226
+
227
+ ## 🙋‍♂️ Support
228
+
229
+ Having issues?
230
+ 1. Check that your OpenAI API key is correctly set in `.env`
231
+ 2. Ensure all dependencies are installed (`uv sync` and `pnpm install`)
232
+ 3. Verify the frontend is built (`pnpm build`) before accessing the full-stack app
233
+
234
  ---
235
 
236
+ Built with ❤️ using modern web technologies and AI.
README_HF.md ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AI Newsletter Generator - Hugging Face Space
2
+
3
+ This Space runs an AI-powered newsletter generator that creates engaging newsletters from RSS feeds.
4
+
5
+ ## Features
6
+
7
+ - 🤖 AI-enhanced article summaries using GPT-4o-mini
8
+ - 📰 RSS feed aggregation from top AI/tech sources
9
+ - 🐦 Social media content generation
10
+ - 📧 Professional HTML newsletter creation
11
+ - ✨ Interactive AI-powered editing
12
+
13
+ ## Usage
14
+
15
+ 1. **Select Sources**: Choose from curated RSS feeds
16
+ 2. **Get Highlights**: Fetch articles with AI-enhanced summaries
17
+ 3. **Select Articles**: Pick articles for detailed processing
18
+ 4. **Generate Content**: Create summaries, tweets, and newsletters
19
+ 5. **Download**: Export your newsletter
20
+
21
+ ## Requirements
22
+
23
+ This Space requires an OpenAI API key to function. Set your `OPENAI_API_KEY` in the Space settings.
24
+
25
+ ## Tech Stack
26
+
27
+ - **Backend**: FastAPI + OpenAI API
28
+ - **Frontend**: React + Vite + Tailwind CSS
29
+ - **Deployment**: Docker multi-stage build
30
+
31
+ Built with modern web technologies and AI for content creators and tech enthusiasts.
app.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ AI Newsletter Generator - Hugging Face Spaces Entry Point
4
+
5
+ This file serves as an alternative entry point for Hugging Face Spaces.
6
+ The main application is in backend/main.py and runs via uvicorn.
7
+ """
8
+
9
+ import subprocess
10
+ import sys
11
+ import os
12
+
13
+ def main():
14
+ """Start the FastAPI application for Hugging Face Spaces"""
15
+
16
+ # Set default port for Hugging Face Spaces
17
+ port = os.getenv("PORT", "7860")
18
+
19
+ # Command to start the FastAPI app
20
+ cmd = [
21
+ sys.executable, "-m", "uvicorn",
22
+ "backend.main:app",
23
+ "--host", "0.0.0.0",
24
+ "--port", port
25
+ ]
26
+
27
+ print(f"Starting AI Newsletter Generator on port {port}...")
28
+ print(f"Command: {' '.join(cmd)}")
29
+
30
+ try:
31
+ subprocess.run(cmd, check=True)
32
+ except KeyboardInterrupt:
33
+ print("\nShutting down...")
34
+ except subprocess.CalledProcessError as e:
35
+ print(f"Error starting application: {e}")
36
+ sys.exit(1)
37
+
38
+ if __name__ == "__main__":
39
+ main()
backend/main.py ADDED
@@ -0,0 +1,1173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import json
4
+ from datetime import datetime, timedelta, timezone
5
+ import re
6
+ from html import unescape
7
+ import httpx
8
+ from typing import List, Optional, Dict, Any
9
+
10
+ import feedparser
11
+ from fastapi import FastAPI, HTTPException, Response, Request
12
+ from fastapi.middleware.cors import CORSMiddleware
13
+ from fastapi.staticfiles import StaticFiles
14
+ from fastapi.responses import FileResponse
15
+ from pydantic import BaseModel, Field, AnyHttpUrl
16
+ from dateutil import parser as dateparser
17
+ from openai import OpenAI
18
+ from dotenv import load_dotenv
19
+
20
+ # Load environment variables from .env file (if it exists)
21
+ try:
22
+ load_dotenv()
23
+ except Exception:
24
+ pass # Ignore if .env file doesn't exist (like in Railway)
25
+
26
+
27
+ # ASGI app for Vercel Python function: export `app`
28
+ app = FastAPI(title="AI Newsletter Generator API", version="1.0.0")
29
+
30
+ # CORS (same-origin on Vercel, but allow localhost for dev)
31
+ allowed_origins = [
32
+ os.getenv("ALLOWED_ORIGIN", "*"),
33
+ "http://localhost:3000",
34
+ "https://localhost:3000",
35
+ "http://localhost:3001",
36
+ "https://localhost:3001",
37
+ "http://localhost:3010",
38
+ "https://localhost:3010",
39
+ "http://localhost:3002",
40
+ "https://localhost:3002",
41
+ ]
42
+ app.add_middleware(
43
+ CORSMiddleware,
44
+ allow_origins=allowed_origins,
45
+ allow_credentials=True,
46
+ allow_methods=["*"]
47
+ ,
48
+ allow_headers=["*"]
49
+ )
50
+
51
+
52
+ # ----- Memory Store (ephemeral in serverless) -----
53
+ class ConversationTurn(BaseModel):
54
+ role: str
55
+ content: str
56
+
57
+
58
+ class SessionMemory(BaseModel):
59
+ session_id: str
60
+ history: List[ConversationTurn] = Field(default_factory=list)
61
+ last_newsletter_html: Optional[str] = None
62
+ last_summary: Optional[str] = None
63
+ last_tweets: Optional[List[str]] = None
64
+
65
+
66
+ memory_store: Dict[str, SessionMemory] = {}
67
+
68
+
69
+ def get_memory(session_id: str) -> SessionMemory:
70
+ if session_id not in memory_store:
71
+ memory_store[session_id] = SessionMemory(session_id=session_id)
72
+ return memory_store[session_id]
73
+
74
+
75
+ # ----- Default RSS Feeds (AI-focused) -----
76
+ DEFAULT_FEEDS: Dict[str, str] = {
77
+ # Working RSS feeds verified as of 2025
78
+ "Hugging Face Blog": "https://huggingface.co/blog/feed.xml",
79
+ "The Gradient": "https://thegradient.pub/rss/",
80
+ "MIT Technology Review AI": "https://www.technologyreview.com/tag/artificial-intelligence/feed/",
81
+ "VentureBeat AI": "https://venturebeat.com/ai/feed/",
82
+ "AI News": "https://artificialintelligence-news.com/feed/",
83
+ }
84
+
85
+
86
+ # ----- Models -----
87
+ class AggregateRequest(BaseModel):
88
+ sources: Optional[List[AnyHttpUrl]] = None
89
+ since_days: int = Field(default=7, ge=1, le=31)
90
+
91
+
92
+ class Article(BaseModel):
93
+ title: str
94
+ link: AnyHttpUrl
95
+ summary: Optional[str] = None
96
+ published: Optional[str] = None
97
+ source: Optional[str] = None
98
+
99
+
100
+ class AggregateResponse(BaseModel):
101
+ articles: List[Article]
102
+
103
+
104
+ class SummarizeRequest(BaseModel):
105
+ session_id: str
106
+ articles: List[Article]
107
+ instructions: Optional[str] = Field(
108
+ default=(
109
+ "Summarize the week's most important AI developments for a technical but busy audience. "
110
+ "Be concise, structured with headings and bullet points, and include source attributions."
111
+ )
112
+ )
113
+ prior_history: Optional[List[ConversationTurn]] = None
114
+
115
+
116
+ class SummarizeResponse(BaseModel):
117
+ summary_markdown: str
118
+
119
+
120
+ # ----- Per-Article Summaries (Highlights) -----
121
+ class HighlightItem(BaseModel):
122
+ title: str
123
+ link: AnyHttpUrl
124
+ source: Optional[str] = None
125
+ summary: str
126
+
127
+
128
+ class TweetsRequest(BaseModel):
129
+ session_id: str
130
+ summaries: List[HighlightItem] # Changed to use individual summaries
131
+ prior_history: Optional[List[ConversationTurn]] = None
132
+
133
+
134
+ class Tweet(BaseModel):
135
+ id: str
136
+ content: str
137
+ summary_title: str
138
+ summary_link: str
139
+ summary_source: str
140
+
141
+
142
+ class TweetsResponse(BaseModel):
143
+ tweets: List[Tweet]
144
+
145
+
146
+ class NewsletterRequest(BaseModel):
147
+ session_id: str
148
+ summary_markdown: str
149
+ articles: List[Article]
150
+ prior_history: Optional[List[ConversationTurn]] = None
151
+
152
+
153
+ class NewsletterResponse(BaseModel):
154
+ html: str
155
+
156
+
157
+ class EditRequest(BaseModel):
158
+ session_id: str
159
+ text: str
160
+ instruction: str
161
+ prior_history: Optional[List[ConversationTurn]] = None
162
+
163
+
164
+ class SummariesSelectedRequest(BaseModel):
165
+ articles: List[Article]
166
+
167
+
168
+ class EditResponse(BaseModel):
169
+ edited_text: str
170
+ history: List[ConversationTurn]
171
+
172
+
173
+ class TweetEditRequest(BaseModel):
174
+ session_id: str
175
+ tweet_id: str
176
+ current_tweet: str
177
+ original_summary: str
178
+ user_message: str
179
+ conversation_history: Optional[List[ConversationTurn]] = None
180
+
181
+
182
+ class TweetEditResponse(BaseModel):
183
+ new_tweet: str
184
+ ai_response: str
185
+ conversation_history: List[ConversationTurn]
186
+
187
+
188
+ # Initialize OpenAI client with error handling
189
+ try:
190
+ api_key = os.getenv("OPENAI_API_KEY")
191
+ if not api_key:
192
+ print("WARNING: OPENAI_API_KEY not found in environment variables")
193
+ openai_client = None
194
+ else:
195
+ openai_client = OpenAI(api_key=api_key)
196
+ MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
197
+ except Exception as e:
198
+ print(f"ERROR initializing OpenAI client: {e}")
199
+ openai_client = None
200
+
201
+
202
+ def _parse_date(dt_str: Optional[str]) -> Optional[datetime]:
203
+ if not dt_str:
204
+ return None
205
+ try:
206
+ return dateparser.parse(dt_str)
207
+ except Exception:
208
+ return None
209
+
210
+
211
+ # Mount static files for serving React build
212
+ static_dir = os.path.join(os.path.dirname(__file__), "..", "frontend", "dist")
213
+ if os.path.exists(static_dir):
214
+ app.mount("/static", StaticFiles(directory=static_dir), name="static")
215
+
216
+ @app.get("/api/health")
217
+ def api_health():
218
+ """API health check endpoint"""
219
+ return {"status": "ok", "message": "AI Newsletter API is running"}
220
+
221
+ @app.get("/")
222
+ def serve_frontend():
223
+ """Serve React frontend from dist folder"""
224
+ static_dir = os.path.join(os.path.dirname(__file__), "..", "frontend", "dist")
225
+ index_file = os.path.join(static_dir, "index.html")
226
+
227
+ # If frontend build exists, serve it
228
+ if os.path.exists(index_file):
229
+ return FileResponse(index_file)
230
+
231
+ # Fallback to API health check if frontend not built
232
+ return {"status": "ok", "message": "AI Newsletter API is running", "note": "Frontend not built yet"}
233
+
234
+
235
+
236
+
237
+ @app.get("/api/defaults", response_model=Dict[str, str])
238
+ def get_defaults() -> Dict[str, str]:
239
+ """Get default RSS feed sources"""
240
+ try:
241
+ return DEFAULT_FEEDS
242
+ except Exception as e:
243
+ print(f"Error in get_defaults: {e}")
244
+ raise HTTPException(status_code=500, detail=f"Server error: {str(e)}")
245
+
246
+
247
+ def _generate_engaging_summaries(articles: List[Article]) -> List[Article]:
248
+ """Generate engaging, short summaries for articles using LLM"""
249
+ if not openai_client:
250
+ return articles # Return unchanged if no OpenAI client
251
+
252
+ enhanced_articles = []
253
+
254
+ for article in articles:
255
+ try:
256
+ # Create a prompt to generate an engaging summary
257
+ if article.summary:
258
+ # Improve existing summary
259
+ prompt = f"""
260
+ Create an engaging, concise summary (2-3 sentences, ~50-80 words) for this article:
261
+
262
+ Title: {article.title}
263
+ Source: {article.source}
264
+ Current Summary: {article.summary}
265
+
266
+ Make it more engaging and accessible while keeping the key information. Focus on why readers should care.
267
+ """
268
+ else:
269
+ # Generate summary from title only
270
+ prompt = f"""
271
+ Create an engaging, concise summary (2-3 sentences, ~50-80 words) for this article based on its title:
272
+
273
+ Title: {article.title}
274
+ Source: {article.source}
275
+
276
+ Make it intriguing and accessible while staying true to what the title suggests. Focus on why readers should care about this topic.
277
+ """
278
+
279
+ enhanced_summary = _chat([
280
+ {"role": "system", "content": "You are an expert content writer who creates engaging, accessible summaries for busy readers interested in AI and technology."},
281
+ {"role": "user", "content": prompt}
282
+ ], temperature=0.7)
283
+
284
+ # Create new article with enhanced summary
285
+ enhanced_articles.append(Article(
286
+ title=article.title,
287
+ link=article.link,
288
+ summary=enhanced_summary.strip(),
289
+ published=article.published,
290
+ source=article.source
291
+ ))
292
+
293
+ except Exception as e:
294
+ # If LLM fails, keep original article
295
+ print(f"Failed to enhance summary for {article.title}: {e}")
296
+ enhanced_articles.append(article)
297
+
298
+ return enhanced_articles
299
+
300
+
301
+ @app.post("/api/aggregate", response_model=AggregateResponse)
302
+ def aggregate(req: AggregateRequest) -> AggregateResponse:
303
+ # Only retrieve from explicitly selected sources. If none provided, return empty.
304
+ sources = req.sources or []
305
+ cutoff = datetime.now(timezone.utc) - timedelta(days=req.since_days)
306
+
307
+ collected: List[Article] = []
308
+ for src in sources:
309
+ feed = feedparser.parse(str(src))
310
+ source_title = getattr(feed.feed, "title", None) or "Unknown Source"
311
+ for entry in feed.entries[:50]:
312
+ published = None
313
+ published_dt: Optional[datetime] = None
314
+ if hasattr(entry, "published"):
315
+ published = entry.published
316
+ published_dt = _parse_date(published)
317
+ elif hasattr(entry, "updated"):
318
+ published = entry.updated
319
+ published_dt = _parse_date(published)
320
+
321
+ # Filter by recency if date available
322
+ if published_dt and published_dt.tzinfo is None:
323
+ published_dt = published_dt.replace(tzinfo=timezone.utc)
324
+ if published_dt and published_dt < cutoff:
325
+ continue
326
+
327
+ summary = getattr(entry, "summary", None)
328
+ if summary:
329
+ # Decode HTML entities and clean up
330
+ summary = unescape(summary.strip())
331
+ link = getattr(entry, "link", None)
332
+ title = getattr(entry, "title", None)
333
+ if not (title and link):
334
+ continue
335
+
336
+ collected.append(
337
+ Article(
338
+ title=title,
339
+ link=link,
340
+ summary=summary,
341
+ published=published,
342
+ source=source_title,
343
+ )
344
+ )
345
+
346
+ # Generate engaging summaries for articles that don't have them or improve existing ones
347
+ if openai_client and collected:
348
+ enhanced_articles = _generate_engaging_summaries(collected[:10]) # Limit to first 10 for performance
349
+ # Update the collected articles with enhanced summaries
350
+ for i, enhanced in enumerate(enhanced_articles):
351
+ if i < len(collected):
352
+ collected[i] = enhanced
353
+
354
+ return AggregateResponse(articles=collected)
355
+
356
+
357
+ # ----- Simple Web Scraper (no external heavy deps) -----
358
+ class ScrapeRequest(BaseModel):
359
+ url: AnyHttpUrl
360
+
361
+
362
+ class ScrapeResponse(BaseModel):
363
+ content_text: str
364
+
365
+
366
+ def _extract_main_text(html: str) -> str:
367
+ # Try to focus on <article> or <main> blocks first
368
+ try:
369
+ article_match = re.search(r"<article[\s\S]*?</article>", html, flags=re.IGNORECASE)
370
+ main_match = re.search(r"<main[\s\S]*?</main>", html, flags=re.IGNORECASE)
371
+ snippet = None
372
+ if article_match:
373
+ snippet = article_match.group(0)
374
+ elif main_match:
375
+ snippet = main_match.group(0)
376
+ else:
377
+ snippet = html
378
+ # Remove scripts/styles
379
+ snippet = re.sub(r"<script[\s\S]*?</script>", " ", snippet, flags=re.IGNORECASE)
380
+ snippet = re.sub(r"<style[\s\S]*?</style>", " ", snippet, flags=re.IGNORECASE)
381
+ # Strip tags
382
+ text = re.sub(r"<[^>]+>", " ", snippet)
383
+ text = unescape(text)
384
+ # Collapse whitespace
385
+ text = re.sub(r"\s+", " ", text).strip()
386
+ return text
387
+ except Exception:
388
+ return ""
389
+
390
+
391
+ @app.post("/api/scrape", response_model=ScrapeResponse)
392
+ def scrape(req: ScrapeRequest) -> ScrapeResponse:
393
+ try:
394
+ with httpx.Client(timeout=10.0, follow_redirects=True, headers={"User-Agent": "Mozilla/5.0 (compatible; AI-Newsletter/1.0)"}) as client:
395
+ resp = client.get(str(req.url))
396
+ resp.raise_for_status()
397
+ text = _extract_main_text(resp.text)
398
+ # Limit to a safe size for LLM context
399
+ if len(text) > 8000:
400
+ text = text[:8000]
401
+ return ScrapeResponse(content_text=text)
402
+ except Exception:
403
+ return ScrapeResponse(content_text="")
404
+
405
+
406
+ class HighlightsRequest(BaseModel):
407
+ sources: List[AnyHttpUrl]
408
+ since_days: int = Field(default=7, ge=1, le=31)
409
+ max_articles: int = Field(default=8, ge=1, le=20)
410
+
411
+
412
+ class HighlightsResponse(BaseModel):
413
+ items: List[HighlightItem]
414
+
415
+
416
+ @app.post("/api/summaries", response_model=HighlightsResponse)
417
+ def summaries(req: HighlightsRequest) -> HighlightsResponse:
418
+ # Enforce selection: if no sources, return empty list
419
+ if not req.sources:
420
+ return HighlightsResponse(items=[])
421
+
422
+ articles_resp = aggregate(AggregateRequest(sources=req.sources, since_days=req.since_days))
423
+ items: List[HighlightItem] = []
424
+
425
+ # Use configurable limit (default 8, max 20)
426
+ limited_articles = articles_resp.articles[:req.max_articles]
427
+
428
+ for a in limited_articles:
429
+ # Scrape content with shorter timeout
430
+ content_text = ""
431
+ try:
432
+ with httpx.Client(timeout=5.0, follow_redirects=True, headers={"User-Agent": "Mozilla/5.0 (compatible; AI-Newsletter/1.0)"}) as client:
433
+ resp = client.get(str(a.link))
434
+ resp.raise_for_status()
435
+ raw_html = resp.text
436
+ content_text = _extract_main_text(raw_html)
437
+ except Exception:
438
+ # Fallback to RSS summary if scraping fails
439
+ content_text = a.summary or ""
440
+
441
+ if len(content_text) > 4000: # Reduced from 8000 for faster processing
442
+ content_text = content_text[:4000]
443
+
444
+ # If no content available, use title and RSS summary
445
+ if not content_text.strip():
446
+ content_text = f"Title: {a.title}\nRSS Summary: {a.summary or 'No summary available'}"
447
+
448
+ # Summarize the single article's content
449
+ system = (
450
+ "You are an expert AI news editor. Summarize the article content for a busy technical audience. "
451
+ "Be concise (3-5 bullet points), capture key findings. If content is limited, work with what's available."
452
+ )
453
+ user = (
454
+ f"Title: {a.title}\nSource: {a.source or ''}\nURL: {a.link}\n\n"
455
+ f"Content:\n{content_text}\n\n"
456
+ "Write a clear, concise summary."
457
+ )
458
+
459
+ try:
460
+ summary_text = _chat([
461
+ {"role": "system", "content": system},
462
+ {"role": "user", "content": user},
463
+ ], temperature=0.3)
464
+ except Exception:
465
+ # Fallback if OpenAI fails
466
+ summary_text = a.summary or f"Unable to generate summary for: {a.title}"
467
+
468
+ items.append(HighlightItem(title=a.title, link=a.link, source=a.source, summary=summary_text.strip()))
469
+
470
+ return HighlightsResponse(items=items)
471
+
472
+
473
+ @app.post("/api/summaries_selected", response_model=HighlightsResponse)
474
+ def summaries_selected(req: SummariesSelectedRequest) -> HighlightsResponse:
475
+ """Process summaries for only selected articles (no RSS aggregation needed)"""
476
+ items: List[HighlightItem] = []
477
+
478
+ for a in req.articles[:5]: # Limit to 5 articles max for performance
479
+ # Scrape content with shorter timeout
480
+ content_text = ""
481
+ try:
482
+ with httpx.Client(timeout=5.0, follow_redirects=True, headers={"User-Agent": "Mozilla/5.0 (compatible; AI-Newsletter/1.0)"}) as client:
483
+ resp = client.get(str(a.link))
484
+ resp.raise_for_status()
485
+ raw_html = resp.text
486
+ content_text = _extract_main_text(raw_html)
487
+ except Exception:
488
+ # Fallback to RSS summary if scraping fails
489
+ content_text = a.summary or ""
490
+
491
+ if len(content_text) > 4000: # Reduced for faster processing
492
+ content_text = content_text[:4000]
493
+
494
+ # If no content available, use title and RSS summary
495
+ if not content_text.strip():
496
+ content_text = f"Title: {a.title}\nRSS Summary: {a.summary or 'No summary available'}"
497
+
498
+ # Summarize the single article's content
499
+ system = (
500
+ "You are an expert AI news editor. Summarize the article content for a busy technical audience. "
501
+ "Be concise (3-5 bullet points), capture key findings. If content is limited, work with what's available."
502
+ )
503
+ user = (
504
+ f"Title: {a.title}\nSource: {a.source or ''}\nURL: {a.link}\n\n"
505
+ f"Content:\n{content_text}\n\n"
506
+ "Write a clear, concise summary."
507
+ )
508
+
509
+ try:
510
+ summary_text = _chat([
511
+ {"role": "system", "content": system},
512
+ {"role": "user", "content": user},
513
+ ], temperature=0.3)
514
+ except Exception:
515
+ # Fallback if OpenAI fails
516
+ summary_text = a.summary or f"Unable to generate summary for: {a.title}"
517
+
518
+ items.append(HighlightItem(title=a.title, link=a.link, source=a.source, summary=summary_text.strip()))
519
+
520
+ return HighlightsResponse(items=items)
521
+
522
+
523
+ def _chat(messages: List[Dict[str, str]], temperature: float = 0.4) -> str:
524
+ if not openai_client:
525
+ raise Exception("OpenAI client not initialized - API key missing")
526
+
527
+ completion = openai_client.chat.completions.create(
528
+ model=MODEL,
529
+ messages=messages,
530
+ temperature=temperature,
531
+ )
532
+ return completion.choices[0].message.content or ""
533
+
534
+
535
+ @app.post("/api/highlights", response_model=SummarizeResponse)
536
+ def highlights_endpoint(req: SummarizeRequest) -> SummarizeResponse:
537
+ if not os.getenv("OPENAI_API_KEY"):
538
+ raise HTTPException(status_code=500, detail="OPENAI_API_KEY not set")
539
+
540
+ memory = get_memory(req.session_id)
541
+ if req.prior_history:
542
+ memory.history.extend(req.prior_history[-8:])
543
+ # Build context from articles
544
+ articles_text = "\n".join(
545
+ [
546
+ f"- {a.title} ({a.source}) — {a.link}\n{a.summary or ''}"
547
+ for a in req.articles[:20]
548
+ ]
549
+ )
550
+
551
+ # Anchor summary to the current week to avoid stale dates from the model
552
+ now_local = datetime.now()
553
+ week_start = now_local - timedelta(days=now_local.weekday()) # Monday
554
+ week_of = week_start.strftime("%b %d, %Y")
555
+
556
+ system = (
557
+ "You are an expert AI news editor. Create a crisp weekly summary for a technical audience. "
558
+ "Use clear section headings, bullet points, and callouts. Include hyperlinks when relevant. "
559
+ f"Always label the summary with a top heading 'Week of {week_of}'."
560
+ )
561
+ user = (
562
+ f"Write a weekly highlights summary based on these items:\n\n{articles_text}\n\n"
563
+ f"Instructions: {req.instructions}"
564
+ )
565
+
566
+ messages: List[Dict[str, str]] = (
567
+ [
568
+ {"role": "system", "content": system},
569
+ ]
570
+ + [{"role": t.role, "content": t.content} for t in memory.history[-6:]]
571
+ + [
572
+ {"role": "user", "content": user},
573
+ ]
574
+ )
575
+
576
+ content = _chat(messages, temperature=0.3)
577
+ # Ensure the summary includes the correct 'Week of' label without duplication
578
+ content_clean = content.strip()
579
+ if not content_clean.lower().startswith(("week of", "# week of", "## week of")):
580
+ content = f"## Week of {week_of}\n\n" + content_clean
581
+ else:
582
+ content = content_clean
583
+ memory.last_summary = content
584
+ memory.history.append(ConversationTurn(role="user", content=user))
585
+ memory.history.append(ConversationTurn(role="assistant", content=content))
586
+ return SummarizeResponse(summary_markdown=content)
587
+
588
+
589
+ @app.post("/api/tweets", response_model=TweetsResponse)
590
+ def generate_tweets(req: TweetsRequest) -> TweetsResponse:
591
+ memory = get_memory(req.session_id)
592
+ if req.prior_history:
593
+ memory.history.extend(req.prior_history[-8:])
594
+
595
+ tweets: List[Tweet] = []
596
+
597
+ for i, summary in enumerate(req.summaries):
598
+ system = (
599
+ "You write engaging, factual, and concise Twitter posts (X). "
600
+ "Create ONE tweet about this specific AI news article."
601
+ )
602
+ user = (
603
+ f"Create a single engaging tweet about this AI news article:\n\n"
604
+ f"Title: {summary.title}\n"
605
+ f"Source: {summary.source}\n"
606
+ f"Summary: {summary.summary}\n\n"
607
+ "Include 1-2 relevant emojis and 1-2 hashtags. Keep under 280 characters. "
608
+ "Return only the tweet text, no JSON formatting."
609
+ )
610
+
611
+ messages = (
612
+ [{"role": "system", "content": system}]
613
+ + [{"role": t.role, "content": t.content} for t in memory.history[-4:]]
614
+ + [{"role": "user", "content": user}]
615
+ )
616
+
617
+ try:
618
+ tweet_content = _chat(messages, temperature=0.7)
619
+ # Clean up the response
620
+ tweet_content = tweet_content.strip().strip('"').strip("'")
621
+
622
+ tweet = Tweet(
623
+ id=f"tweet_{i}_{summary.title[:20].replace(' ', '_')}",
624
+ content=tweet_content,
625
+ summary_title=summary.title,
626
+ summary_link=str(summary.link),
627
+ summary_source=summary.source or "Unknown"
628
+ )
629
+ tweets.append(tweet)
630
+
631
+ except Exception:
632
+ # Fallback tweet if AI generation fails
633
+ fallback_content = f"🤖 {summary.title[:200]}... #AI #Tech"
634
+ tweet = Tweet(
635
+ id=f"tweet_{i}_{summary.title[:20].replace(' ', '_')}",
636
+ content=fallback_content,
637
+ summary_title=summary.title,
638
+ summary_link=str(summary.link),
639
+ summary_source=summary.source or "Unknown"
640
+ )
641
+ tweets.append(tweet)
642
+
643
+ # Store conversation context
644
+ turn_user = ConversationTurn(role="user", content=f"Generated {len(tweets)} tweets from summaries")
645
+ turn_assistant = ConversationTurn(role="assistant", content="Tweets generated successfully")
646
+ memory.history.append(turn_user)
647
+ memory.history.append(turn_assistant)
648
+
649
+ memory.last_tweets = [t.content for t in tweets] # Store for backward compatibility
650
+ return TweetsResponse(tweets=tweets)
651
+
652
+
653
+ def _build_newsletter_html(summary_md: str, articles: List[Article]) -> str:
654
+ # Select featured article (first article with good content)
655
+ featured_article = None
656
+ remaining_articles = []
657
+
658
+ for article in articles[:8]: # Use first 8 articles
659
+ if not featured_article and article.summary and len(article.summary) > 100:
660
+ featured_article = article
661
+ else:
662
+ remaining_articles.append(article)
663
+
664
+ # If no good featured article found, use the first one
665
+ if not featured_article and articles:
666
+ featured_article = articles[0]
667
+ remaining_articles = articles[1:8]
668
+
669
+ # Build news grid items (max 6 items, 2x3 grid)
670
+ news_items = ""
671
+ for i, article in enumerate(remaining_articles[:6]):
672
+ news_items += f"""
673
+ <div class="news-item">
674
+ <h4>{article.title}</h4>
675
+ <p>{(article.summary or 'Click to read more about this story.')[:150]}{'...' if len(article.summary or '') > 150 else ''}</p>
676
+ <a href="{article.link}" class="read-more">Read more →</a>
677
+ </div>
678
+ """
679
+
680
+ now = datetime.now().strftime("%B %d, %Y")
681
+
682
+ # Format featured article
683
+ featured_title = featured_article.title if featured_article else "AI Weekly Highlights"
684
+ featured_summary = (featured_article.summary or "This week brings exciting developments in AI and technology.")[:200] + "..." if featured_article and len(featured_article.summary or "") > 200 else (featured_article.summary if featured_article else "This week brings exciting developments in AI and technology.")
685
+ featured_link = featured_article.link if featured_article else "#"
686
+
687
+ return f"""<!DOCTYPE html>
688
+ <html lang="en">
689
+ <head>
690
+ <meta charset="UTF-8">
691
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
692
+ <title>AI Weekly - Newsletter</title>
693
+ <style>
694
+ * {{
695
+ margin: 0;
696
+ padding: 0;
697
+ box-sizing: border-box;
698
+ }}
699
+
700
+ body {{
701
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
702
+ line-height: 1.6;
703
+ background-color: #f4f4f4;
704
+ color: #333;
705
+ }}
706
+
707
+ .container {{
708
+ max-width: 600px;
709
+ margin: 20px auto;
710
+ background-color: white;
711
+ box-shadow: 0 0 20px rgba(0,0,0,0.1);
712
+ }}
713
+
714
+ .header {{
715
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
716
+ color: white;
717
+ padding: 30px 20px;
718
+ text-align: center;
719
+ }}
720
+
721
+ .logo {{
722
+ font-size: 28px;
723
+ font-weight: bold;
724
+ margin-bottom: 10px;
725
+ }}
726
+
727
+ .tagline {{
728
+ font-size: 14px;
729
+ opacity: 0.9;
730
+ }}
731
+
732
+ .content {{
733
+ padding: 30px 20px;
734
+ }}
735
+
736
+ .section {{
737
+ margin-bottom: 30px;
738
+ border-bottom: 1px solid #eee;
739
+ padding-bottom: 30px;
740
+ }}
741
+
742
+ .section:last-child {{
743
+ border-bottom: none;
744
+ margin-bottom: 0;
745
+ padding-bottom: 0;
746
+ }}
747
+
748
+ .section h2 {{
749
+ color: #667eea;
750
+ font-size: 22px;
751
+ margin-bottom: 15px;
752
+ border-left: 4px solid #667eea;
753
+ padding-left: 15px;
754
+ }}
755
+
756
+ .featured-article {{
757
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
758
+ color: white;
759
+ padding: 25px;
760
+ border-radius: 10px;
761
+ margin-bottom: 20px;
762
+ }}
763
+
764
+ .featured-article h3 {{
765
+ font-size: 20px;
766
+ margin-bottom: 10px;
767
+ }}
768
+
769
+ .featured-article p {{
770
+ margin-bottom: 15px;
771
+ opacity: 0.95;
772
+ }}
773
+
774
+ .btn {{
775
+ display: inline-block;
776
+ background-color: white;
777
+ color: #f5576c;
778
+ padding: 12px 25px;
779
+ text-decoration: none;
780
+ border-radius: 25px;
781
+ font-weight: bold;
782
+ transition: transform 0.3s ease;
783
+ }}
784
+
785
+ .btn:hover {{
786
+ transform: translateY(-2px);
787
+ }}
788
+
789
+ .news-grid {{
790
+ display: grid;
791
+ grid-template-columns: 1fr 1fr;
792
+ gap: 20px;
793
+ margin-top: 20px;
794
+ }}
795
+
796
+ .news-item {{
797
+ border: 1px solid #eee;
798
+ border-radius: 8px;
799
+ padding: 20px;
800
+ transition: box-shadow 0.3s ease;
801
+ }}
802
+
803
+ .news-item:hover {{
804
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
805
+ }}
806
+
807
+ .news-item h4 {{
808
+ color: #333;
809
+ margin-bottom: 10px;
810
+ font-size: 16px;
811
+ }}
812
+
813
+ .news-item p {{
814
+ font-size: 14px;
815
+ color: #666;
816
+ margin-bottom: 10px;
817
+ }}
818
+
819
+ .read-more {{
820
+ color: #667eea;
821
+ text-decoration: none;
822
+ font-size: 14px;
823
+ font-weight: bold;
824
+ }}
825
+
826
+ .cta-section {{
827
+ background-color: #f8f9fa;
828
+ padding: 30px;
829
+ text-align: center;
830
+ border-radius: 10px;
831
+ }}
832
+
833
+ .cta-section h3 {{
834
+ color: #333;
835
+ margin-bottom: 15px;
836
+ }}
837
+
838
+ .cta-btn {{
839
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
840
+ color: white;
841
+ padding: 15px 30px;
842
+ text-decoration: none;
843
+ border-radius: 30px;
844
+ font-weight: bold;
845
+ display: inline-block;
846
+ margin-top: 10px;
847
+ }}
848
+
849
+ .social-links {{
850
+ text-align: center;
851
+ margin-top: 30px;
852
+ }}
853
+
854
+ .social-links a {{
855
+ display: inline-block;
856
+ margin: 0 10px;
857
+ width: 40px;
858
+ height: 40px;
859
+ background-color: #667eea;
860
+ color: white;
861
+ text-decoration: none;
862
+ border-radius: 50%;
863
+ line-height: 40px;
864
+ transition: background-color 0.3s ease;
865
+ }}
866
+
867
+ .social-links a:hover {{
868
+ background-color: #764ba2;
869
+ }}
870
+
871
+ .footer {{
872
+ background-color: #333;
873
+ color: white;
874
+ padding: 30px 20px;
875
+ text-align: center;
876
+ }}
877
+
878
+ .footer p {{
879
+ margin-bottom: 10px;
880
+ font-size: 14px;
881
+ }}
882
+
883
+ .footer a {{
884
+ color: #667eea;
885
+ text-decoration: none;
886
+ }}
887
+
888
+ @media (max-width: 600px) {{
889
+ .news-grid {{
890
+ grid-template-columns: 1fr;
891
+ }}
892
+
893
+ .container {{
894
+ margin: 10px;
895
+ }}
896
+
897
+ .content {{
898
+ padding: 20px 15px;
899
+ }}
900
+ }}
901
+ </style>
902
+ </head>
903
+ <body>
904
+ <div class="container">
905
+ <!-- Header -->
906
+ <div class="header">
907
+ <div class="logo" style="display:flex; align-items:center; justify-content:center; gap:10px;">
908
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZGVmcz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0ibG9nb0dyYWQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjNjY3ZWVhIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iNTAlIiBzdG9wLWNvbG9yPSIjNzY0YmEyIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2YwOTNmYiIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICA8L2RlZnM+CiAgCiAgPCEtLSBNYWluIGNvbnRhaW5lciBjaXJjbGUgLS0+CiAgPGNpcmNsZSBjeD0iMzIiIGN5PSIzMiIgcj0iMjgiIGZpbGw9InVybCgjbG9nb0dyYWQpIi8+CiAgCiAgPCEtLSBOZXVyYWwgbmV0d29yayBub2RlcyAtLT4KICA8ZyBmaWxsPSIjZmZmZmZmIiBvcGFjaXR5PSIwLjkiPgogICAgPCEtLSBJbnB1dCBsYXllciAtLT4KICAgIDxjaXJjbGUgY3g9IjE4IiBjeT0iMjQiIHI9IjIuNSIvPgogICAgPGNpcmNsZSBjeD0iMTgiIGN5PSIzMiIgcj0iMi41Ii8+CiAgICA8Y2lyY2xlIGN4PSIxOCIgY3k9IjQwIiByPSIyLjUiLz4KICAgIAogICAgPCEtLSBIaWRkZW4gbGF5ZXIgLS0+CiAgICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjIwIiByPSIyIi8+CiAgICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjI4IiByPSIyIi8+CiAgICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjM2IiByPSIyIi8+CiAgICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjQ0IiByPSIyIi8+CiAgICAKICAgIDwhLS0gT3V0cHV0IGxheWVyIC0tPgogICAgPGNpcmNsZSBjeD0iNDYiIGN5PSIyOCIgcj0iMi41Ii8+CiAgICA8Y2lyY2xlIGN4PSI0NiIgY3k9IjM2IiByPSIyLjUiLz4KICA8L2c+CiAgCiAgPCEtLSBOZXVyYWwgbmV0d29yayBjb25uZWN0aW9ucyAtLT4KICA8ZyBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMSIgb3BhY2l0eT0iMC42IiBmaWxsPSJub25lIj4KICAgIDwhLS0gSW5wdXQgdG8gaGlkZGVuIGNvbm5lY3Rpb25zIC0tPgogICAgPGxpbmUgeDE9IjIwLjUiIHkxPSIyNCIgeDI9IjMwIiB5Mj0iMjAiLz4KICAgIDxsaW5lIHgxPSIyMC41IiB5MT0iMjQiIHgyPSIzMCIgeTI9IjI4Ii8+CiAgICA8bGluZSB4MT0iMjAuNSIgeTE9IjMyIiB4Mj0iMzAiIHkyPSIyOCIvPgogICAgPGxpbmUgeDE9IjIwLjUiIHkxPSIzMiIgeDI9IjMwIiB5Mj0iMzYiLz4KICAgIDxsaW5lIHgxPSIyMC41IiB5MT0iNDAiIHgyPSIzMCIgeTI9IjM2Ii8+CiAgICA8bGluZSB4MT0iMjAuNSIgeTE9IjQwIiB4Mj0iMzAiIHkyPSI0NCIvPgogICAgCiAgICA8IS0tIEhpZGRlbiB0byBvdXRwdXQgY29ubmVjdGlvbnMgLS0+CiAgICA8bGluZSB4MT0iMzQiIHkxPSIyMCIgeDI9IjQzLjUiIHkyPSIyOCIvPgogICAgPGxpbmUgeDE9IjM0IiB5MT0iMjgiIHgyPSI0My41IiB5Mj0iMjgiLz4KICAgIDxsaW5lIHgxPSIzNCIgeTE9IjM2IiB4Mj0iNDMuNSIgeTI9IjM2Ii8+CiAgICA8bGluZSB4MT0iMzQiIHkxPSI0NCIgeDI9IjQzLjUiIHkyPSIzNiIvPgogIDwvZz4KICAKICA8IS0tIEFJIHN5bWJvbCBpbiBjZW50ZXIgLS0+CiAgPGcgZmlsbD0iI2ZmZmZmZiIgb3BhY2l0eT0iMC44Ij4KICAgIDx0ZXh0IHg9IjMyIiB5PSIxNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjgiIGZvbnQtd2VpZ2h0PSJib2xkIj5BSTwvdGV4dD4KICA8L2c+CiAgCiAgPCEtLSBOZXdzbGV0dGVyL2RvY3VtZW50IGljb24gLS0+CiAgPGcgZmlsbD0iI2ZmZmZmZiIgb3BhY2l0eT0iMC43Ij4KICAgIDxyZWN0IHg9IjI2IiB5PSI0OCIgd2lkdGg9IjEyIiBoZWlnaHQ9IjgiIHJ4PSIxIi8+CiAgICA8bGluZSB4MT0iMjgiIHkxPSI1MCIgeDI9IjM2IiB5Mj0iNTAiIHN0cm9rZT0iIzY2N2VlYSIgc3Ryb2tlLXdpZHRoPSIwLjgiLz4KICAgIDxsaW5lIHgxPSIyOCIgeTE9IjUyIiB4Mj0iMzQiIHkyPSI1MiIgc3Ryb2tlPSIjNjY3ZWVhIiBzdHJva2Utd2lkdGg9IjAuOCIvPgogICAgPGxpbmUgeDE9IjI4IiB5MT0iNTQiIHgyPSIzNiIgeTI9IjU0IiBzdHJva2U9IiM2NjdlZWEiIHN0cm9rZS13aWR0aD0iMC44Ii8+CiAgPC9nPgo8L3N2Zz4K" alt="AI Weekly" width="28" height="28" style="display:inline-block; vertical-align:middle;" />
909
+ <span>AI Weekly</span>
910
+ </div>
911
+ <div class="tagline">Your weekly dose of AI insights • {now}</div>
912
+ </div>
913
+
914
+ <!-- Main Content -->
915
+ <div class="content">
916
+ <!-- Welcome Section -->
917
+ <div class="section">
918
+ <h2>📧 This Week's Highlights</h2>
919
+ <p>Hello AI Tech Enthusiasts! Welcome to another edition of AI Weekly. This week, we're diving deep into the latest AI developments, breakthrough innovations, and emerging technologies that are shaping our digital future.</p>
920
+ </div>
921
+
922
+ <!-- Featured Article -->
923
+ <div class="section">
924
+ <h2>🌟 Featured Story</h2>
925
+ <div class="featured-article">
926
+ <h3>{featured_title}</h3>
927
+ <p>{featured_summary}</p>
928
+ <a href="{featured_link}" class="btn">Read Full Article</a>
929
+ </div>
930
+ </div>
931
+
932
+ <!-- News Section -->
933
+ <div class="section">
934
+ <h2>📰 Latest AI News</h2>
935
+ <div class="news-grid">
936
+ {news_items}
937
+ </div>
938
+ </div>
939
+
940
+ <!-- CTA Section -->
941
+ <div class="section">
942
+ <div class="cta-section">
943
+ <h3>🤖 Stay Connected</h3>
944
+ <p>Join thousands of AI enthusiasts getting the latest insights delivered weekly.</p>
945
+ <a href="#" class="cta-btn">Subscribe for Updates</a>
946
+ </div>
947
+ </div>
948
+
949
+ <!-- Social Links -->
950
+ <div class="social-links">
951
+ <a href="#">🐦</a>
952
+ <a href="#">📘</a>
953
+ <a href="#">💼</a>
954
+ <a href="#">📧</a>
955
+ </div>
956
+ </div>
957
+
958
+ <!-- Footer -->
959
+ <div class="footer">
960
+ <p><strong>AI Weekly</strong></p>
961
+ <p>Curated with ❤️ by AI Newsletter</p>
962
+ <p>© 2025 AI Weekly. All rights reserved.</p>
963
+ <p style="margin-top: 20px;">
964
+ <a href="#">Unsubscribe</a> |
965
+ <a href="#">Update Preferences</a> |
966
+ <a href="#">Privacy Policy</a>
967
+ </p>
968
+ </div>
969
+ </div>
970
+ </body>
971
+ </html>"""
972
+
973
+
974
+ @app.post("/api/newsletter", response_model=NewsletterResponse)
975
+ def newsletter(req: NewsletterRequest) -> NewsletterResponse:
976
+ memory = get_memory(req.session_id)
977
+ if req.prior_history:
978
+ memory.history.extend(req.prior_history[-8:])
979
+ html = _build_newsletter_html(req.summary_markdown, req.articles)
980
+ memory.last_newsletter_html = html
981
+ return NewsletterResponse(html=html)
982
+
983
+
984
+ @app.post("/api/edit", response_model=EditResponse)
985
+ def edit(req: EditRequest) -> EditResponse:
986
+ memory = get_memory(req.session_id)
987
+ if req.prior_history:
988
+ # Allow client to supply recent context from local storage when serverless memory resets
989
+ memory.history.extend(req.prior_history[-8:])
990
+
991
+ system = (
992
+ "You are a helpful writing assistant. Edit the provided text according to the instruction, "
993
+ "preserving facts and links. Return only the edited text."
994
+ )
995
+ user = f"Instruction: {req.instruction}\n\nText to edit:\n{req.text}"
996
+ messages = (
997
+ [{"role": "system", "content": system}]
998
+ + [{"role": t.role, "content": t.content} for t in memory.history[-8:]]
999
+ + [{"role": "user", "content": user}]
1000
+ )
1001
+ content = _chat(messages, temperature=0.4)
1002
+ turn_user = ConversationTurn(role="user", content=user)
1003
+ turn_assistant = ConversationTurn(role="assistant", content=content)
1004
+ memory.history.append(turn_user)
1005
+ memory.history.append(turn_assistant)
1006
+ return EditResponse(edited_text=content, history=memory.history[-10:])
1007
+
1008
+
1009
+ @app.post("/api/edit_tweet", response_model=TweetEditResponse)
1010
+ def edit_tweet(req: TweetEditRequest) -> TweetEditResponse:
1011
+ # Get or create conversation history for this specific tweet
1012
+ conversation_key = f"{req.session_id}_tweet_{req.tweet_id}"
1013
+ if conversation_key not in memory_store:
1014
+ memory_store[conversation_key] = SessionMemory(session_id=conversation_key)
1015
+
1016
+ tweet_memory = memory_store[conversation_key]
1017
+
1018
+ # Add any provided conversation history
1019
+ if req.conversation_history:
1020
+ tweet_memory.history.extend(req.conversation_history)
1021
+
1022
+ system = (
1023
+ "You are an AI assistant helping to edit and improve Twitter/X posts. "
1024
+ "You have context about the original article summary and the current tweet. "
1025
+ "Help the user modify the tweet based on their requests while keeping it STRICTLY under 280 characters. "
1026
+ "CRITICAL: Count characters carefully - if adding hashtags would exceed 280 chars, shorten the main text to make room. "
1027
+ "IMPORTANT: Always structure your response as follows:\n"
1028
+ "1. A brief conversational response to the user\n"
1029
+ "2. Then on a new line, write 'UPDATED TWEET:' followed by the new tweet content\n"
1030
+ "Example format:\n"
1031
+ "Sure! I'll add more hashtags and shorten the text to fit.\n\n"
1032
+ "UPDATED TWEET: Your concise tweet content with #hashtags #AI #Tech"
1033
+ )
1034
+
1035
+ context = (
1036
+ f"Original Article Summary: {req.original_summary}\n"
1037
+ f"Current Tweet: {req.current_tweet}\n"
1038
+ f"User Request: {req.user_message}"
1039
+ )
1040
+
1041
+ messages = (
1042
+ [{"role": "system", "content": system}]
1043
+ + [{"role": t.role, "content": t.content} for t in tweet_memory.history[-6:]]
1044
+ + [{"role": "user", "content": context}]
1045
+ )
1046
+
1047
+ ai_response = _chat(messages, temperature=0.7)
1048
+
1049
+ # Extract the new tweet and AI message using the structured format
1050
+ new_tweet = req.current_tweet # Fallback to current tweet
1051
+ ai_message = ai_response
1052
+
1053
+ # Look for "UPDATED TWEET:" pattern
1054
+ if "UPDATED TWEET:" in ai_response:
1055
+ parts = ai_response.split("UPDATED TWEET:", 1)
1056
+ if len(parts) == 2:
1057
+ ai_message = parts[0].strip()
1058
+ new_tweet = parts[1].strip()
1059
+
1060
+ # Clean up the new tweet (remove any quotes or extra formatting)
1061
+ new_tweet = new_tweet.strip('"').strip("'").strip()
1062
+
1063
+ # Validate tweet length and truncate smartly
1064
+ if len(new_tweet) > 280:
1065
+ # Try to truncate at word boundaries to avoid cutting hashtags
1066
+ words = new_tweet.split(' ')
1067
+ truncated = ""
1068
+ for word in words:
1069
+ if len(truncated + " " + word) <= 280:
1070
+ if truncated:
1071
+ truncated += " " + word
1072
+ else:
1073
+ truncated = word
1074
+ else:
1075
+ break
1076
+ new_tweet = truncated if truncated else new_tweet[:280]
1077
+
1078
+ if not ai_message:
1079
+ ai_message = "I've updated your tweet based on your request!"
1080
+ else:
1081
+ # Fallback: if the structured format wasn't followed, try to extract tweet-like content
1082
+ lines = ai_response.split('\n')
1083
+ for line in lines:
1084
+ line = line.strip()
1085
+ if len(line) > 20 and len(line) <= 280 and ('#' in line or '@' in line or any(emoji in line for emoji in ['🔥', '🚀', '💡', '🤖', '⚡'])):
1086
+ new_tweet = line
1087
+ ai_message = ai_response.replace(new_tweet, "").strip()
1088
+ if not ai_message:
1089
+ ai_message = "I've updated your tweet based on your request!"
1090
+ break
1091
+
1092
+ # Store conversation
1093
+ turn_user = ConversationTurn(role="user", content=req.user_message)
1094
+ turn_assistant = ConversationTurn(role="assistant", content=ai_response)
1095
+ tweet_memory.history.append(turn_user)
1096
+ tweet_memory.history.append(turn_assistant)
1097
+
1098
+ return TweetEditResponse(
1099
+ new_tweet=new_tweet,
1100
+ ai_response=ai_message,
1101
+ conversation_history=tweet_memory.history[-10:]
1102
+ )
1103
+
1104
+
1105
+ # Provide a synchronous alternative endpoint with explicit model
1106
+ class DownloadRequest(BaseModel):
1107
+ session_id: Optional[str] = None
1108
+ html: Optional[str] = None
1109
+
1110
+
1111
+ @app.post("/api/download_html")
1112
+ def download_html(req: DownloadRequest):
1113
+ html = req.html
1114
+ if not html and req.session_id:
1115
+ mem = get_memory(req.session_id)
1116
+ html = mem.last_newsletter_html
1117
+ if not html:
1118
+ raise HTTPException(status_code=400, detail="No HTML provided or found for session")
1119
+ buffer = io.BytesIO(html.encode("utf-8"))
1120
+ headers = {
1121
+ "Content-Disposition": "attachment; filename=ai_weekly.html"
1122
+ }
1123
+ return Response(content=buffer.getvalue(), headers=headers, media_type="text/html")
1124
+
1125
+
1126
+ # Catch-all route for SPA routing - MUST be at the very end
1127
+ @app.get("/{path:path}")
1128
+ def catch_all(path: str):
1129
+ """Catch-all route to serve React app for client-side routing"""
1130
+ # Don't intercept API routes
1131
+ if path.startswith("api/"):
1132
+ raise HTTPException(status_code=404, detail="API endpoint not found")
1133
+
1134
+ static_dir = os.path.join(os.path.dirname(__file__), "..", "frontend", "dist")
1135
+ index_file = os.path.join(static_dir, "index.html")
1136
+
1137
+ # Serve static files if they exist
1138
+ file_path = os.path.join(static_dir, path)
1139
+ if os.path.isfile(file_path):
1140
+ return FileResponse(file_path)
1141
+
1142
+ # Otherwise serve index.html for SPA routing
1143
+ if os.path.exists(index_file):
1144
+ return FileResponse(index_file)
1145
+
1146
+ # Fallback if no frontend built
1147
+ return {"status": "error", "message": "Frontend not available"}
1148
+
1149
+
1150
+ # Lambda handler for AWS
1151
+ def handler(event, context):
1152
+ """AWS Lambda handler for FastAPI - Version 2.0"""
1153
+ print(f"[DEBUG v2.0] Lambda handler called with event: {event.get('httpMethod', 'unknown')}")
1154
+ print(f"[DEBUG v2.0] Event keys: {list(event.keys())}")
1155
+ try:
1156
+ from mangum import Mangum
1157
+ print("[DEBUG v2.0] Mangum imported successfully")
1158
+ asgi_handler = Mangum(app)
1159
+ print("[DEBUG v2.0] Mangum handler created")
1160
+ result = asgi_handler(event, context)
1161
+ print(f"[DEBUG v2.0] Handler result type: {type(result)}")
1162
+ return result
1163
+ except Exception as e:
1164
+ print(f"[ERROR v2.0] Handler failed: {str(e)}")
1165
+ import traceback
1166
+ traceback.print_exc()
1167
+ raise
1168
+
1169
+
1170
+ # Export for Vercel - app is automatically detected
1171
+ if __name__ == "__main__":
1172
+ import uvicorn
1173
+ uvicorn.run(app, host="0.0.0.0", port=8000)
frontend/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/.nvmrc ADDED
@@ -0,0 +1 @@
 
 
1
+ 20.11.1
frontend/.pnpmfile.cjs ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // pnpm configuration for the AI Newsletter frontend
2
+ module.exports = {
3
+ hooks: {
4
+ readPackage(pkg) {
5
+ // Ensure React types compatibility
6
+ if (pkg.name === '@types/react') {
7
+ pkg.peerDependencies = {
8
+ ...pkg.peerDependencies,
9
+ 'react': '*'
10
+ }
11
+ }
12
+ return pkg
13
+ }
14
+ }
15
+ }
frontend/README.md ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + TypeScript + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
+
10
+ ## Expanding the ESLint configuration
11
+
12
+ If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
13
+
14
+ ```js
15
+ export default tseslint.config([
16
+ globalIgnores(['dist']),
17
+ {
18
+ files: ['**/*.{ts,tsx}'],
19
+ extends: [
20
+ // Other configs...
21
+
22
+ // Remove tseslint.configs.recommended and replace with this
23
+ ...tseslint.configs.recommendedTypeChecked,
24
+ // Alternatively, use this for stricter rules
25
+ ...tseslint.configs.strictTypeChecked,
26
+ // Optionally, add this for stylistic rules
27
+ ...tseslint.configs.stylisticTypeChecked,
28
+
29
+ // Other configs...
30
+ ],
31
+ languageOptions: {
32
+ parserOptions: {
33
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
34
+ tsconfigRootDir: import.meta.dirname,
35
+ },
36
+ // other options...
37
+ },
38
+ },
39
+ ])
40
+ ```
41
+
42
+ You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
43
+
44
+ ```js
45
+ // eslint.config.js
46
+ import reactX from 'eslint-plugin-react-x'
47
+ import reactDom from 'eslint-plugin-react-dom'
48
+
49
+ export default tseslint.config([
50
+ globalIgnores(['dist']),
51
+ {
52
+ files: ['**/*.{ts,tsx}'],
53
+ extends: [
54
+ // Other configs...
55
+ // Enable lint rules for React
56
+ reactX.configs['recommended-typescript'],
57
+ // Enable lint rules for React DOM
58
+ reactDom.configs.recommended,
59
+ ],
60
+ languageOptions: {
61
+ parserOptions: {
62
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
63
+ tsconfigRootDir: import.meta.dirname,
64
+ },
65
+ // other options...
66
+ },
67
+ },
68
+ ])
69
+ ```
frontend/eslint.config.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+ import { globalIgnores } from 'eslint/config'
7
+
8
+ export default tseslint.config([
9
+ globalIgnores(['dist']),
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs['recommended-latest'],
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ ecmaVersion: 2020,
20
+ globals: globals.browser,
21
+ },
22
+ },
23
+ ])
frontend/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Vite + React + TS</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
frontend/package.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite --port 3002",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview",
11
+ "clean": "rm -rf dist node_modules/.vite",
12
+ "type-check": "tsc --noEmit"
13
+ },
14
+ "dependencies": {
15
+ "react": "^19.1.1",
16
+ "react-dom": "^19.1.1"
17
+ },
18
+ "devDependencies": {
19
+ "@eslint/js": "^9.32.0",
20
+ "@types/react": "^19.1.9",
21
+ "@types/react-dom": "^19.1.7",
22
+ "@vitejs/plugin-react": "^4.7.0",
23
+ "autoprefixer": "^10.4.21",
24
+ "classnames": "^2.5.1",
25
+ "eslint": "^9.32.0",
26
+ "eslint-plugin-react-hooks": "^5.2.0",
27
+ "eslint-plugin-react-refresh": "^0.4.20",
28
+ "globals": "^16.3.0",
29
+ "postcss": "^8.5.6",
30
+ "tailwindcss": "^3.4.17",
31
+ "typescript": "~5.8.3",
32
+ "typescript-eslint": "^8.39.0",
33
+ "vite": "^7.1.0"
34
+ },
35
+ "packageManager": "pnpm@10.12.3",
36
+ "engines": {
37
+ "node": ">=18.0.0",
38
+ "pnpm": ">=8.0.0"
39
+ }
40
+ }
frontend/pnpm-lock.yaml ADDED
@@ -0,0 +1,2780 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ lockfileVersion: '9.0'
2
+
3
+ settings:
4
+ autoInstallPeers: true
5
+ excludeLinksFromLockfile: false
6
+
7
+ pnpmfileChecksum: sha256-gvDTDqUZJL4m3Sr1Vuxqtbv0pSJ33/voxpVpvmgK1ic=
8
+
9
+ importers:
10
+
11
+ .:
12
+ dependencies:
13
+ react:
14
+ specifier: ^19.1.1
15
+ version: 19.1.1
16
+ react-dom:
17
+ specifier: ^19.1.1
18
+ version: 19.1.1(react@19.1.1)
19
+ devDependencies:
20
+ '@eslint/js':
21
+ specifier: ^9.32.0
22
+ version: 9.33.0
23
+ '@types/react':
24
+ specifier: ^19.1.9
25
+ version: 19.1.9(react@19.1.1)
26
+ '@types/react-dom':
27
+ specifier: ^19.1.7
28
+ version: 19.1.7(@types/react@19.1.9(react@19.1.1))
29
+ '@vitejs/plugin-react':
30
+ specifier: ^4.7.0
31
+ version: 4.7.0(vite@7.1.1(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1))
32
+ autoprefixer:
33
+ specifier: ^10.4.21
34
+ version: 10.4.21(postcss@8.5.6)
35
+ classnames:
36
+ specifier: ^2.5.1
37
+ version: 2.5.1
38
+ eslint:
39
+ specifier: ^9.32.0
40
+ version: 9.33.0(jiti@2.5.1)
41
+ eslint-plugin-react-hooks:
42
+ specifier: ^5.2.0
43
+ version: 5.2.0(eslint@9.33.0(jiti@2.5.1))
44
+ eslint-plugin-react-refresh:
45
+ specifier: ^0.4.20
46
+ version: 0.4.20(eslint@9.33.0(jiti@2.5.1))
47
+ globals:
48
+ specifier: ^16.3.0
49
+ version: 16.3.0
50
+ postcss:
51
+ specifier: ^8.5.6
52
+ version: 8.5.6
53
+ tailwindcss:
54
+ specifier: ^3.4.17
55
+ version: 3.4.17
56
+ typescript:
57
+ specifier: ~5.8.3
58
+ version: 5.8.3
59
+ typescript-eslint:
60
+ specifier: ^8.39.0
61
+ version: 8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)
62
+ vite:
63
+ specifier: ^7.1.0
64
+ version: 7.1.1(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1)
65
+
66
+ packages:
67
+
68
+ '@alloc/quick-lru@5.2.0':
69
+ resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
70
+ engines: {node: '>=10'}
71
+
72
+ '@ampproject/remapping@2.3.0':
73
+ resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
74
+ engines: {node: '>=6.0.0'}
75
+
76
+ '@babel/code-frame@7.27.1':
77
+ resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
78
+ engines: {node: '>=6.9.0'}
79
+
80
+ '@babel/compat-data@7.28.0':
81
+ resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==}
82
+ engines: {node: '>=6.9.0'}
83
+
84
+ '@babel/core@7.28.0':
85
+ resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==}
86
+ engines: {node: '>=6.9.0'}
87
+
88
+ '@babel/generator@7.28.0':
89
+ resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==}
90
+ engines: {node: '>=6.9.0'}
91
+
92
+ '@babel/helper-compilation-targets@7.27.2':
93
+ resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
94
+ engines: {node: '>=6.9.0'}
95
+
96
+ '@babel/helper-globals@7.28.0':
97
+ resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
98
+ engines: {node: '>=6.9.0'}
99
+
100
+ '@babel/helper-module-imports@7.27.1':
101
+ resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
102
+ engines: {node: '>=6.9.0'}
103
+
104
+ '@babel/helper-module-transforms@7.27.3':
105
+ resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==}
106
+ engines: {node: '>=6.9.0'}
107
+ peerDependencies:
108
+ '@babel/core': ^7.0.0
109
+
110
+ '@babel/helper-plugin-utils@7.27.1':
111
+ resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
112
+ engines: {node: '>=6.9.0'}
113
+
114
+ '@babel/helper-string-parser@7.27.1':
115
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
116
+ engines: {node: '>=6.9.0'}
117
+
118
+ '@babel/helper-validator-identifier@7.27.1':
119
+ resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
120
+ engines: {node: '>=6.9.0'}
121
+
122
+ '@babel/helper-validator-option@7.27.1':
123
+ resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
124
+ engines: {node: '>=6.9.0'}
125
+
126
+ '@babel/helpers@7.28.2':
127
+ resolution: {integrity: sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==}
128
+ engines: {node: '>=6.9.0'}
129
+
130
+ '@babel/parser@7.28.0':
131
+ resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==}
132
+ engines: {node: '>=6.0.0'}
133
+ hasBin: true
134
+
135
+ '@babel/plugin-transform-react-jsx-self@7.27.1':
136
+ resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
137
+ engines: {node: '>=6.9.0'}
138
+ peerDependencies:
139
+ '@babel/core': ^7.0.0-0
140
+
141
+ '@babel/plugin-transform-react-jsx-source@7.27.1':
142
+ resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
143
+ engines: {node: '>=6.9.0'}
144
+ peerDependencies:
145
+ '@babel/core': ^7.0.0-0
146
+
147
+ '@babel/template@7.27.2':
148
+ resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
149
+ engines: {node: '>=6.9.0'}
150
+
151
+ '@babel/traverse@7.28.0':
152
+ resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==}
153
+ engines: {node: '>=6.9.0'}
154
+
155
+ '@babel/types@7.28.2':
156
+ resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
157
+ engines: {node: '>=6.9.0'}
158
+
159
+ '@esbuild/aix-ppc64@0.25.8':
160
+ resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==}
161
+ engines: {node: '>=18'}
162
+ cpu: [ppc64]
163
+ os: [aix]
164
+
165
+ '@esbuild/android-arm64@0.25.8':
166
+ resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==}
167
+ engines: {node: '>=18'}
168
+ cpu: [arm64]
169
+ os: [android]
170
+
171
+ '@esbuild/android-arm@0.25.8':
172
+ resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==}
173
+ engines: {node: '>=18'}
174
+ cpu: [arm]
175
+ os: [android]
176
+
177
+ '@esbuild/android-x64@0.25.8':
178
+ resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==}
179
+ engines: {node: '>=18'}
180
+ cpu: [x64]
181
+ os: [android]
182
+
183
+ '@esbuild/darwin-arm64@0.25.8':
184
+ resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==}
185
+ engines: {node: '>=18'}
186
+ cpu: [arm64]
187
+ os: [darwin]
188
+
189
+ '@esbuild/darwin-x64@0.25.8':
190
+ resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==}
191
+ engines: {node: '>=18'}
192
+ cpu: [x64]
193
+ os: [darwin]
194
+
195
+ '@esbuild/freebsd-arm64@0.25.8':
196
+ resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==}
197
+ engines: {node: '>=18'}
198
+ cpu: [arm64]
199
+ os: [freebsd]
200
+
201
+ '@esbuild/freebsd-x64@0.25.8':
202
+ resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==}
203
+ engines: {node: '>=18'}
204
+ cpu: [x64]
205
+ os: [freebsd]
206
+
207
+ '@esbuild/linux-arm64@0.25.8':
208
+ resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==}
209
+ engines: {node: '>=18'}
210
+ cpu: [arm64]
211
+ os: [linux]
212
+
213
+ '@esbuild/linux-arm@0.25.8':
214
+ resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==}
215
+ engines: {node: '>=18'}
216
+ cpu: [arm]
217
+ os: [linux]
218
+
219
+ '@esbuild/linux-ia32@0.25.8':
220
+ resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==}
221
+ engines: {node: '>=18'}
222
+ cpu: [ia32]
223
+ os: [linux]
224
+
225
+ '@esbuild/linux-loong64@0.25.8':
226
+ resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==}
227
+ engines: {node: '>=18'}
228
+ cpu: [loong64]
229
+ os: [linux]
230
+
231
+ '@esbuild/linux-mips64el@0.25.8':
232
+ resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==}
233
+ engines: {node: '>=18'}
234
+ cpu: [mips64el]
235
+ os: [linux]
236
+
237
+ '@esbuild/linux-ppc64@0.25.8':
238
+ resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==}
239
+ engines: {node: '>=18'}
240
+ cpu: [ppc64]
241
+ os: [linux]
242
+
243
+ '@esbuild/linux-riscv64@0.25.8':
244
+ resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==}
245
+ engines: {node: '>=18'}
246
+ cpu: [riscv64]
247
+ os: [linux]
248
+
249
+ '@esbuild/linux-s390x@0.25.8':
250
+ resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==}
251
+ engines: {node: '>=18'}
252
+ cpu: [s390x]
253
+ os: [linux]
254
+
255
+ '@esbuild/linux-x64@0.25.8':
256
+ resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==}
257
+ engines: {node: '>=18'}
258
+ cpu: [x64]
259
+ os: [linux]
260
+
261
+ '@esbuild/netbsd-arm64@0.25.8':
262
+ resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==}
263
+ engines: {node: '>=18'}
264
+ cpu: [arm64]
265
+ os: [netbsd]
266
+
267
+ '@esbuild/netbsd-x64@0.25.8':
268
+ resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==}
269
+ engines: {node: '>=18'}
270
+ cpu: [x64]
271
+ os: [netbsd]
272
+
273
+ '@esbuild/openbsd-arm64@0.25.8':
274
+ resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==}
275
+ engines: {node: '>=18'}
276
+ cpu: [arm64]
277
+ os: [openbsd]
278
+
279
+ '@esbuild/openbsd-x64@0.25.8':
280
+ resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==}
281
+ engines: {node: '>=18'}
282
+ cpu: [x64]
283
+ os: [openbsd]
284
+
285
+ '@esbuild/openharmony-arm64@0.25.8':
286
+ resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==}
287
+ engines: {node: '>=18'}
288
+ cpu: [arm64]
289
+ os: [openharmony]
290
+
291
+ '@esbuild/sunos-x64@0.25.8':
292
+ resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==}
293
+ engines: {node: '>=18'}
294
+ cpu: [x64]
295
+ os: [sunos]
296
+
297
+ '@esbuild/win32-arm64@0.25.8':
298
+ resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==}
299
+ engines: {node: '>=18'}
300
+ cpu: [arm64]
301
+ os: [win32]
302
+
303
+ '@esbuild/win32-ia32@0.25.8':
304
+ resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==}
305
+ engines: {node: '>=18'}
306
+ cpu: [ia32]
307
+ os: [win32]
308
+
309
+ '@esbuild/win32-x64@0.25.8':
310
+ resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==}
311
+ engines: {node: '>=18'}
312
+ cpu: [x64]
313
+ os: [win32]
314
+
315
+ '@eslint-community/eslint-utils@4.7.0':
316
+ resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
317
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
318
+ peerDependencies:
319
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
320
+
321
+ '@eslint-community/regexpp@4.12.1':
322
+ resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
323
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
324
+
325
+ '@eslint/config-array@0.21.0':
326
+ resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==}
327
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
328
+
329
+ '@eslint/config-helpers@0.3.1':
330
+ resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==}
331
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
332
+
333
+ '@eslint/core@0.15.2':
334
+ resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==}
335
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
336
+
337
+ '@eslint/eslintrc@3.3.1':
338
+ resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
339
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
340
+
341
+ '@eslint/js@9.33.0':
342
+ resolution: {integrity: sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==}
343
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
344
+
345
+ '@eslint/object-schema@2.1.6':
346
+ resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
347
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
348
+
349
+ '@eslint/plugin-kit@0.3.5':
350
+ resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==}
351
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
352
+
353
+ '@humanfs/core@0.19.1':
354
+ resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
355
+ engines: {node: '>=18.18.0'}
356
+
357
+ '@humanfs/node@0.16.6':
358
+ resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==}
359
+ engines: {node: '>=18.18.0'}
360
+
361
+ '@humanwhocodes/module-importer@1.0.1':
362
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
363
+ engines: {node: '>=12.22'}
364
+
365
+ '@humanwhocodes/retry@0.3.1':
366
+ resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
367
+ engines: {node: '>=18.18'}
368
+
369
+ '@humanwhocodes/retry@0.4.3':
370
+ resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
371
+ engines: {node: '>=18.18'}
372
+
373
+ '@isaacs/cliui@8.0.2':
374
+ resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
375
+ engines: {node: '>=12'}
376
+
377
+ '@jridgewell/gen-mapping@0.3.12':
378
+ resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
379
+
380
+ '@jridgewell/resolve-uri@3.1.2':
381
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
382
+ engines: {node: '>=6.0.0'}
383
+
384
+ '@jridgewell/sourcemap-codec@1.5.4':
385
+ resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
386
+
387
+ '@jridgewell/trace-mapping@0.3.29':
388
+ resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
389
+
390
+ '@nodelib/fs.scandir@2.1.5':
391
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
392
+ engines: {node: '>= 8'}
393
+
394
+ '@nodelib/fs.stat@2.0.5':
395
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
396
+ engines: {node: '>= 8'}
397
+
398
+ '@nodelib/fs.walk@1.2.8':
399
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
400
+ engines: {node: '>= 8'}
401
+
402
+ '@pkgjs/parseargs@0.11.0':
403
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
404
+ engines: {node: '>=14'}
405
+
406
+ '@rolldown/pluginutils@1.0.0-beta.27':
407
+ resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
408
+
409
+ '@rollup/rollup-android-arm-eabi@4.46.2':
410
+ resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==}
411
+ cpu: [arm]
412
+ os: [android]
413
+
414
+ '@rollup/rollup-android-arm64@4.46.2':
415
+ resolution: {integrity: sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==}
416
+ cpu: [arm64]
417
+ os: [android]
418
+
419
+ '@rollup/rollup-darwin-arm64@4.46.2':
420
+ resolution: {integrity: sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==}
421
+ cpu: [arm64]
422
+ os: [darwin]
423
+
424
+ '@rollup/rollup-darwin-x64@4.46.2':
425
+ resolution: {integrity: sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==}
426
+ cpu: [x64]
427
+ os: [darwin]
428
+
429
+ '@rollup/rollup-freebsd-arm64@4.46.2':
430
+ resolution: {integrity: sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==}
431
+ cpu: [arm64]
432
+ os: [freebsd]
433
+
434
+ '@rollup/rollup-freebsd-x64@4.46.2':
435
+ resolution: {integrity: sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==}
436
+ cpu: [x64]
437
+ os: [freebsd]
438
+
439
+ '@rollup/rollup-linux-arm-gnueabihf@4.46.2':
440
+ resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==}
441
+ cpu: [arm]
442
+ os: [linux]
443
+
444
+ '@rollup/rollup-linux-arm-musleabihf@4.46.2':
445
+ resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==}
446
+ cpu: [arm]
447
+ os: [linux]
448
+
449
+ '@rollup/rollup-linux-arm64-gnu@4.46.2':
450
+ resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==}
451
+ cpu: [arm64]
452
+ os: [linux]
453
+
454
+ '@rollup/rollup-linux-arm64-musl@4.46.2':
455
+ resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==}
456
+ cpu: [arm64]
457
+ os: [linux]
458
+
459
+ '@rollup/rollup-linux-loongarch64-gnu@4.46.2':
460
+ resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==}
461
+ cpu: [loong64]
462
+ os: [linux]
463
+
464
+ '@rollup/rollup-linux-ppc64-gnu@4.46.2':
465
+ resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==}
466
+ cpu: [ppc64]
467
+ os: [linux]
468
+
469
+ '@rollup/rollup-linux-riscv64-gnu@4.46.2':
470
+ resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==}
471
+ cpu: [riscv64]
472
+ os: [linux]
473
+
474
+ '@rollup/rollup-linux-riscv64-musl@4.46.2':
475
+ resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==}
476
+ cpu: [riscv64]
477
+ os: [linux]
478
+
479
+ '@rollup/rollup-linux-s390x-gnu@4.46.2':
480
+ resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==}
481
+ cpu: [s390x]
482
+ os: [linux]
483
+
484
+ '@rollup/rollup-linux-x64-gnu@4.46.2':
485
+ resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==}
486
+ cpu: [x64]
487
+ os: [linux]
488
+
489
+ '@rollup/rollup-linux-x64-musl@4.46.2':
490
+ resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==}
491
+ cpu: [x64]
492
+ os: [linux]
493
+
494
+ '@rollup/rollup-win32-arm64-msvc@4.46.2':
495
+ resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==}
496
+ cpu: [arm64]
497
+ os: [win32]
498
+
499
+ '@rollup/rollup-win32-ia32-msvc@4.46.2':
500
+ resolution: {integrity: sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==}
501
+ cpu: [ia32]
502
+ os: [win32]
503
+
504
+ '@rollup/rollup-win32-x64-msvc@4.46.2':
505
+ resolution: {integrity: sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==}
506
+ cpu: [x64]
507
+ os: [win32]
508
+
509
+ '@types/babel__core@7.20.5':
510
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
511
+
512
+ '@types/babel__generator@7.27.0':
513
+ resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
514
+
515
+ '@types/babel__template@7.4.4':
516
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
517
+
518
+ '@types/babel__traverse@7.28.0':
519
+ resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
520
+
521
+ '@types/estree@1.0.8':
522
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
523
+
524
+ '@types/json-schema@7.0.15':
525
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
526
+
527
+ '@types/react-dom@19.1.7':
528
+ resolution: {integrity: sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==}
529
+ peerDependencies:
530
+ '@types/react': ^19.0.0
531
+
532
+ '@types/react@19.1.9':
533
+ resolution: {integrity: sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==}
534
+ peerDependencies:
535
+ react: '*'
536
+
537
+ '@typescript-eslint/eslint-plugin@8.39.0':
538
+ resolution: {integrity: sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==}
539
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
540
+ peerDependencies:
541
+ '@typescript-eslint/parser': ^8.39.0
542
+ eslint: ^8.57.0 || ^9.0.0
543
+ typescript: '>=4.8.4 <6.0.0'
544
+
545
+ '@typescript-eslint/parser@8.39.0':
546
+ resolution: {integrity: sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==}
547
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
548
+ peerDependencies:
549
+ eslint: ^8.57.0 || ^9.0.0
550
+ typescript: '>=4.8.4 <6.0.0'
551
+
552
+ '@typescript-eslint/project-service@8.39.0':
553
+ resolution: {integrity: sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==}
554
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
555
+ peerDependencies:
556
+ typescript: '>=4.8.4 <6.0.0'
557
+
558
+ '@typescript-eslint/scope-manager@8.39.0':
559
+ resolution: {integrity: sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==}
560
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
561
+
562
+ '@typescript-eslint/tsconfig-utils@8.39.0':
563
+ resolution: {integrity: sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==}
564
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
565
+ peerDependencies:
566
+ typescript: '>=4.8.4 <6.0.0'
567
+
568
+ '@typescript-eslint/type-utils@8.39.0':
569
+ resolution: {integrity: sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==}
570
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
571
+ peerDependencies:
572
+ eslint: ^8.57.0 || ^9.0.0
573
+ typescript: '>=4.8.4 <6.0.0'
574
+
575
+ '@typescript-eslint/types@8.39.0':
576
+ resolution: {integrity: sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==}
577
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
578
+
579
+ '@typescript-eslint/typescript-estree@8.39.0':
580
+ resolution: {integrity: sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==}
581
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
582
+ peerDependencies:
583
+ typescript: '>=4.8.4 <6.0.0'
584
+
585
+ '@typescript-eslint/utils@8.39.0':
586
+ resolution: {integrity: sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==}
587
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
588
+ peerDependencies:
589
+ eslint: ^8.57.0 || ^9.0.0
590
+ typescript: '>=4.8.4 <6.0.0'
591
+
592
+ '@typescript-eslint/visitor-keys@8.39.0':
593
+ resolution: {integrity: sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==}
594
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
595
+
596
+ '@vitejs/plugin-react@4.7.0':
597
+ resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
598
+ engines: {node: ^14.18.0 || >=16.0.0}
599
+ peerDependencies:
600
+ vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
601
+
602
+ acorn-jsx@5.3.2:
603
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
604
+ peerDependencies:
605
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
606
+
607
+ acorn@8.15.0:
608
+ resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
609
+ engines: {node: '>=0.4.0'}
610
+ hasBin: true
611
+
612
+ ajv@6.12.6:
613
+ resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
614
+
615
+ ansi-regex@5.0.1:
616
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
617
+ engines: {node: '>=8'}
618
+
619
+ ansi-regex@6.1.0:
620
+ resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
621
+ engines: {node: '>=12'}
622
+
623
+ ansi-styles@4.3.0:
624
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
625
+ engines: {node: '>=8'}
626
+
627
+ ansi-styles@6.2.1:
628
+ resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
629
+ engines: {node: '>=12'}
630
+
631
+ any-promise@1.3.0:
632
+ resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
633
+
634
+ anymatch@3.1.3:
635
+ resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
636
+ engines: {node: '>= 8'}
637
+
638
+ arg@5.0.2:
639
+ resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
640
+
641
+ argparse@2.0.1:
642
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
643
+
644
+ autoprefixer@10.4.21:
645
+ resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==}
646
+ engines: {node: ^10 || ^12 || >=14}
647
+ hasBin: true
648
+ peerDependencies:
649
+ postcss: ^8.1.0
650
+
651
+ balanced-match@1.0.2:
652
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
653
+
654
+ binary-extensions@2.3.0:
655
+ resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
656
+ engines: {node: '>=8'}
657
+
658
+ brace-expansion@1.1.12:
659
+ resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
660
+
661
+ brace-expansion@2.0.2:
662
+ resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
663
+
664
+ braces@3.0.3:
665
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
666
+ engines: {node: '>=8'}
667
+
668
+ browserslist@4.25.1:
669
+ resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==}
670
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
671
+ hasBin: true
672
+
673
+ callsites@3.1.0:
674
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
675
+ engines: {node: '>=6'}
676
+
677
+ camelcase-css@2.0.1:
678
+ resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
679
+ engines: {node: '>= 6'}
680
+
681
+ caniuse-lite@1.0.30001733:
682
+ resolution: {integrity: sha512-e4QKw/O2Kavj2VQTKZWrwzkt3IxOmIlU6ajRb6LP64LHpBo1J67k2Hi4Vu/TgJWsNtynurfS0uK3MaUTCPfu5Q==}
683
+
684
+ chalk@4.1.2:
685
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
686
+ engines: {node: '>=10'}
687
+
688
+ chokidar@3.6.0:
689
+ resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
690
+ engines: {node: '>= 8.10.0'}
691
+
692
+ classnames@2.5.1:
693
+ resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
694
+
695
+ color-convert@2.0.1:
696
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
697
+ engines: {node: '>=7.0.0'}
698
+
699
+ color-name@1.1.4:
700
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
701
+
702
+ commander@4.1.1:
703
+ resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
704
+ engines: {node: '>= 6'}
705
+
706
+ concat-map@0.0.1:
707
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
708
+
709
+ convert-source-map@2.0.0:
710
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
711
+
712
+ cross-spawn@7.0.6:
713
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
714
+ engines: {node: '>= 8'}
715
+
716
+ cssesc@3.0.0:
717
+ resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
718
+ engines: {node: '>=4'}
719
+ hasBin: true
720
+
721
+ csstype@3.1.3:
722
+ resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
723
+
724
+ debug@4.4.1:
725
+ resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
726
+ engines: {node: '>=6.0'}
727
+ peerDependencies:
728
+ supports-color: '*'
729
+ peerDependenciesMeta:
730
+ supports-color:
731
+ optional: true
732
+
733
+ deep-is@0.1.4:
734
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
735
+
736
+ detect-libc@2.0.4:
737
+ resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
738
+ engines: {node: '>=8'}
739
+
740
+ didyoumean@1.2.2:
741
+ resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
742
+
743
+ dlv@1.1.3:
744
+ resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
745
+
746
+ eastasianwidth@0.2.0:
747
+ resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
748
+
749
+ electron-to-chromium@1.5.199:
750
+ resolution: {integrity: sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ==}
751
+
752
+ emoji-regex@8.0.0:
753
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
754
+
755
+ emoji-regex@9.2.2:
756
+ resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
757
+
758
+ esbuild@0.25.8:
759
+ resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==}
760
+ engines: {node: '>=18'}
761
+ hasBin: true
762
+
763
+ escalade@3.2.0:
764
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
765
+ engines: {node: '>=6'}
766
+
767
+ escape-string-regexp@4.0.0:
768
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
769
+ engines: {node: '>=10'}
770
+
771
+ eslint-plugin-react-hooks@5.2.0:
772
+ resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==}
773
+ engines: {node: '>=10'}
774
+ peerDependencies:
775
+ eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
776
+
777
+ eslint-plugin-react-refresh@0.4.20:
778
+ resolution: {integrity: sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==}
779
+ peerDependencies:
780
+ eslint: '>=8.40'
781
+
782
+ eslint-scope@8.4.0:
783
+ resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
784
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
785
+
786
+ eslint-visitor-keys@3.4.3:
787
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
788
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
789
+
790
+ eslint-visitor-keys@4.2.1:
791
+ resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
792
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
793
+
794
+ eslint@9.33.0:
795
+ resolution: {integrity: sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==}
796
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
797
+ hasBin: true
798
+ peerDependencies:
799
+ jiti: '*'
800
+ peerDependenciesMeta:
801
+ jiti:
802
+ optional: true
803
+
804
+ espree@10.4.0:
805
+ resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
806
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
807
+
808
+ esquery@1.6.0:
809
+ resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
810
+ engines: {node: '>=0.10'}
811
+
812
+ esrecurse@4.3.0:
813
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
814
+ engines: {node: '>=4.0'}
815
+
816
+ estraverse@5.3.0:
817
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
818
+ engines: {node: '>=4.0'}
819
+
820
+ esutils@2.0.3:
821
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
822
+ engines: {node: '>=0.10.0'}
823
+
824
+ fast-deep-equal@3.1.3:
825
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
826
+
827
+ fast-glob@3.3.3:
828
+ resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
829
+ engines: {node: '>=8.6.0'}
830
+
831
+ fast-json-stable-stringify@2.1.0:
832
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
833
+
834
+ fast-levenshtein@2.0.6:
835
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
836
+
837
+ fastq@1.19.1:
838
+ resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
839
+
840
+ fdir@6.4.6:
841
+ resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==}
842
+ peerDependencies:
843
+ picomatch: ^3 || ^4
844
+ peerDependenciesMeta:
845
+ picomatch:
846
+ optional: true
847
+
848
+ file-entry-cache@8.0.0:
849
+ resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
850
+ engines: {node: '>=16.0.0'}
851
+
852
+ fill-range@7.1.1:
853
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
854
+ engines: {node: '>=8'}
855
+
856
+ find-up@5.0.0:
857
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
858
+ engines: {node: '>=10'}
859
+
860
+ flat-cache@4.0.1:
861
+ resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
862
+ engines: {node: '>=16'}
863
+
864
+ flatted@3.3.3:
865
+ resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
866
+
867
+ foreground-child@3.3.1:
868
+ resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
869
+ engines: {node: '>=14'}
870
+
871
+ fraction.js@4.3.7:
872
+ resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
873
+
874
+ fsevents@2.3.3:
875
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
876
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
877
+ os: [darwin]
878
+
879
+ function-bind@1.1.2:
880
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
881
+
882
+ gensync@1.0.0-beta.2:
883
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
884
+ engines: {node: '>=6.9.0'}
885
+
886
+ glob-parent@5.1.2:
887
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
888
+ engines: {node: '>= 6'}
889
+
890
+ glob-parent@6.0.2:
891
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
892
+ engines: {node: '>=10.13.0'}
893
+
894
+ glob@10.4.5:
895
+ resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
896
+ hasBin: true
897
+
898
+ globals@14.0.0:
899
+ resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
900
+ engines: {node: '>=18'}
901
+
902
+ globals@16.3.0:
903
+ resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==}
904
+ engines: {node: '>=18'}
905
+
906
+ graphemer@1.4.0:
907
+ resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
908
+
909
+ has-flag@4.0.0:
910
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
911
+ engines: {node: '>=8'}
912
+
913
+ hasown@2.0.2:
914
+ resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
915
+ engines: {node: '>= 0.4'}
916
+
917
+ ignore@5.3.2:
918
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
919
+ engines: {node: '>= 4'}
920
+
921
+ ignore@7.0.5:
922
+ resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
923
+ engines: {node: '>= 4'}
924
+
925
+ import-fresh@3.3.1:
926
+ resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
927
+ engines: {node: '>=6'}
928
+
929
+ imurmurhash@0.1.4:
930
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
931
+ engines: {node: '>=0.8.19'}
932
+
933
+ is-binary-path@2.1.0:
934
+ resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
935
+ engines: {node: '>=8'}
936
+
937
+ is-core-module@2.16.1:
938
+ resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
939
+ engines: {node: '>= 0.4'}
940
+
941
+ is-extglob@2.1.1:
942
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
943
+ engines: {node: '>=0.10.0'}
944
+
945
+ is-fullwidth-code-point@3.0.0:
946
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
947
+ engines: {node: '>=8'}
948
+
949
+ is-glob@4.0.3:
950
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
951
+ engines: {node: '>=0.10.0'}
952
+
953
+ is-number@7.0.0:
954
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
955
+ engines: {node: '>=0.12.0'}
956
+
957
+ isexe@2.0.0:
958
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
959
+
960
+ jackspeak@3.4.3:
961
+ resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
962
+
963
+ jiti@1.21.7:
964
+ resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
965
+ hasBin: true
966
+
967
+ jiti@2.5.1:
968
+ resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
969
+ hasBin: true
970
+
971
+ js-tokens@4.0.0:
972
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
973
+
974
+ js-yaml@4.1.0:
975
+ resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
976
+ hasBin: true
977
+
978
+ jsesc@3.1.0:
979
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
980
+ engines: {node: '>=6'}
981
+ hasBin: true
982
+
983
+ json-buffer@3.0.1:
984
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
985
+
986
+ json-schema-traverse@0.4.1:
987
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
988
+
989
+ json-stable-stringify-without-jsonify@1.0.1:
990
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
991
+
992
+ json5@2.2.3:
993
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
994
+ engines: {node: '>=6'}
995
+ hasBin: true
996
+
997
+ keyv@4.5.4:
998
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
999
+
1000
+ levn@0.4.1:
1001
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
1002
+ engines: {node: '>= 0.8.0'}
1003
+
1004
+ lightningcss-darwin-arm64@1.30.1:
1005
+ resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
1006
+ engines: {node: '>= 12.0.0'}
1007
+ cpu: [arm64]
1008
+ os: [darwin]
1009
+
1010
+ lightningcss-darwin-x64@1.30.1:
1011
+ resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==}
1012
+ engines: {node: '>= 12.0.0'}
1013
+ cpu: [x64]
1014
+ os: [darwin]
1015
+
1016
+ lightningcss-freebsd-x64@1.30.1:
1017
+ resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==}
1018
+ engines: {node: '>= 12.0.0'}
1019
+ cpu: [x64]
1020
+ os: [freebsd]
1021
+
1022
+ lightningcss-linux-arm-gnueabihf@1.30.1:
1023
+ resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==}
1024
+ engines: {node: '>= 12.0.0'}
1025
+ cpu: [arm]
1026
+ os: [linux]
1027
+
1028
+ lightningcss-linux-arm64-gnu@1.30.1:
1029
+ resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==}
1030
+ engines: {node: '>= 12.0.0'}
1031
+ cpu: [arm64]
1032
+ os: [linux]
1033
+
1034
+ lightningcss-linux-arm64-musl@1.30.1:
1035
+ resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
1036
+ engines: {node: '>= 12.0.0'}
1037
+ cpu: [arm64]
1038
+ os: [linux]
1039
+
1040
+ lightningcss-linux-x64-gnu@1.30.1:
1041
+ resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
1042
+ engines: {node: '>= 12.0.0'}
1043
+ cpu: [x64]
1044
+ os: [linux]
1045
+
1046
+ lightningcss-linux-x64-musl@1.30.1:
1047
+ resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
1048
+ engines: {node: '>= 12.0.0'}
1049
+ cpu: [x64]
1050
+ os: [linux]
1051
+
1052
+ lightningcss-win32-arm64-msvc@1.30.1:
1053
+ resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
1054
+ engines: {node: '>= 12.0.0'}
1055
+ cpu: [arm64]
1056
+ os: [win32]
1057
+
1058
+ lightningcss-win32-x64-msvc@1.30.1:
1059
+ resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==}
1060
+ engines: {node: '>= 12.0.0'}
1061
+ cpu: [x64]
1062
+ os: [win32]
1063
+
1064
+ lightningcss@1.30.1:
1065
+ resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
1066
+ engines: {node: '>= 12.0.0'}
1067
+
1068
+ lilconfig@3.1.3:
1069
+ resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
1070
+ engines: {node: '>=14'}
1071
+
1072
+ lines-and-columns@1.2.4:
1073
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
1074
+
1075
+ locate-path@6.0.0:
1076
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
1077
+ engines: {node: '>=10'}
1078
+
1079
+ lodash.merge@4.6.2:
1080
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
1081
+
1082
+ lru-cache@10.4.3:
1083
+ resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
1084
+
1085
+ lru-cache@5.1.1:
1086
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
1087
+
1088
+ merge2@1.4.1:
1089
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
1090
+ engines: {node: '>= 8'}
1091
+
1092
+ micromatch@4.0.8:
1093
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
1094
+ engines: {node: '>=8.6'}
1095
+
1096
+ minimatch@3.1.2:
1097
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
1098
+
1099
+ minimatch@9.0.5:
1100
+ resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
1101
+ engines: {node: '>=16 || 14 >=14.17'}
1102
+
1103
+ minipass@7.1.2:
1104
+ resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
1105
+ engines: {node: '>=16 || 14 >=14.17'}
1106
+
1107
+ ms@2.1.3:
1108
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
1109
+
1110
+ mz@2.7.0:
1111
+ resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
1112
+
1113
+ nanoid@3.3.11:
1114
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
1115
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
1116
+ hasBin: true
1117
+
1118
+ natural-compare@1.4.0:
1119
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
1120
+
1121
+ node-releases@2.0.19:
1122
+ resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
1123
+
1124
+ normalize-path@3.0.0:
1125
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
1126
+ engines: {node: '>=0.10.0'}
1127
+
1128
+ normalize-range@0.1.2:
1129
+ resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
1130
+ engines: {node: '>=0.10.0'}
1131
+
1132
+ object-assign@4.1.1:
1133
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
1134
+ engines: {node: '>=0.10.0'}
1135
+
1136
+ object-hash@3.0.0:
1137
+ resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
1138
+ engines: {node: '>= 6'}
1139
+
1140
+ optionator@0.9.4:
1141
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
1142
+ engines: {node: '>= 0.8.0'}
1143
+
1144
+ p-limit@3.1.0:
1145
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
1146
+ engines: {node: '>=10'}
1147
+
1148
+ p-locate@5.0.0:
1149
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
1150
+ engines: {node: '>=10'}
1151
+
1152
+ package-json-from-dist@1.0.1:
1153
+ resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
1154
+
1155
+ parent-module@1.0.1:
1156
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
1157
+ engines: {node: '>=6'}
1158
+
1159
+ path-exists@4.0.0:
1160
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
1161
+ engines: {node: '>=8'}
1162
+
1163
+ path-key@3.1.1:
1164
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
1165
+ engines: {node: '>=8'}
1166
+
1167
+ path-parse@1.0.7:
1168
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
1169
+
1170
+ path-scurry@1.11.1:
1171
+ resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
1172
+ engines: {node: '>=16 || 14 >=14.18'}
1173
+
1174
+ picocolors@1.1.1:
1175
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
1176
+
1177
+ picomatch@2.3.1:
1178
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
1179
+ engines: {node: '>=8.6'}
1180
+
1181
+ picomatch@4.0.3:
1182
+ resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
1183
+ engines: {node: '>=12'}
1184
+
1185
+ pify@2.3.0:
1186
+ resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
1187
+ engines: {node: '>=0.10.0'}
1188
+
1189
+ pirates@4.0.7:
1190
+ resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
1191
+ engines: {node: '>= 6'}
1192
+
1193
+ postcss-import@15.1.0:
1194
+ resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
1195
+ engines: {node: '>=14.0.0'}
1196
+ peerDependencies:
1197
+ postcss: ^8.0.0
1198
+
1199
+ postcss-js@4.0.1:
1200
+ resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
1201
+ engines: {node: ^12 || ^14 || >= 16}
1202
+ peerDependencies:
1203
+ postcss: ^8.4.21
1204
+
1205
+ postcss-load-config@4.0.2:
1206
+ resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==}
1207
+ engines: {node: '>= 14'}
1208
+ peerDependencies:
1209
+ postcss: '>=8.0.9'
1210
+ ts-node: '>=9.0.0'
1211
+ peerDependenciesMeta:
1212
+ postcss:
1213
+ optional: true
1214
+ ts-node:
1215
+ optional: true
1216
+
1217
+ postcss-nested@6.2.0:
1218
+ resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
1219
+ engines: {node: '>=12.0'}
1220
+ peerDependencies:
1221
+ postcss: ^8.2.14
1222
+
1223
+ postcss-selector-parser@6.1.2:
1224
+ resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
1225
+ engines: {node: '>=4'}
1226
+
1227
+ postcss-value-parser@4.2.0:
1228
+ resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
1229
+
1230
+ postcss@8.5.6:
1231
+ resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
1232
+ engines: {node: ^10 || ^12 || >=14}
1233
+
1234
+ prelude-ls@1.2.1:
1235
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
1236
+ engines: {node: '>= 0.8.0'}
1237
+
1238
+ punycode@2.3.1:
1239
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
1240
+ engines: {node: '>=6'}
1241
+
1242
+ queue-microtask@1.2.3:
1243
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
1244
+
1245
+ react-dom@19.1.1:
1246
+ resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==}
1247
+ peerDependencies:
1248
+ react: ^19.1.1
1249
+
1250
+ react-refresh@0.17.0:
1251
+ resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
1252
+ engines: {node: '>=0.10.0'}
1253
+
1254
+ react@19.1.1:
1255
+ resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==}
1256
+ engines: {node: '>=0.10.0'}
1257
+
1258
+ read-cache@1.0.0:
1259
+ resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
1260
+
1261
+ readdirp@3.6.0:
1262
+ resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
1263
+ engines: {node: '>=8.10.0'}
1264
+
1265
+ resolve-from@4.0.0:
1266
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
1267
+ engines: {node: '>=4'}
1268
+
1269
+ resolve@1.22.10:
1270
+ resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
1271
+ engines: {node: '>= 0.4'}
1272
+ hasBin: true
1273
+
1274
+ reusify@1.1.0:
1275
+ resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
1276
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
1277
+
1278
+ rollup@4.46.2:
1279
+ resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==}
1280
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
1281
+ hasBin: true
1282
+
1283
+ run-parallel@1.2.0:
1284
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
1285
+
1286
+ scheduler@0.26.0:
1287
+ resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
1288
+
1289
+ semver@6.3.1:
1290
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
1291
+ hasBin: true
1292
+
1293
+ semver@7.7.2:
1294
+ resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
1295
+ engines: {node: '>=10'}
1296
+ hasBin: true
1297
+
1298
+ shebang-command@2.0.0:
1299
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
1300
+ engines: {node: '>=8'}
1301
+
1302
+ shebang-regex@3.0.0:
1303
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
1304
+ engines: {node: '>=8'}
1305
+
1306
+ signal-exit@4.1.0:
1307
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
1308
+ engines: {node: '>=14'}
1309
+
1310
+ source-map-js@1.2.1:
1311
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
1312
+ engines: {node: '>=0.10.0'}
1313
+
1314
+ string-width@4.2.3:
1315
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
1316
+ engines: {node: '>=8'}
1317
+
1318
+ string-width@5.1.2:
1319
+ resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
1320
+ engines: {node: '>=12'}
1321
+
1322
+ strip-ansi@6.0.1:
1323
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
1324
+ engines: {node: '>=8'}
1325
+
1326
+ strip-ansi@7.1.0:
1327
+ resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
1328
+ engines: {node: '>=12'}
1329
+
1330
+ strip-json-comments@3.1.1:
1331
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
1332
+ engines: {node: '>=8'}
1333
+
1334
+ sucrase@3.35.0:
1335
+ resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
1336
+ engines: {node: '>=16 || 14 >=14.17'}
1337
+ hasBin: true
1338
+
1339
+ supports-color@7.2.0:
1340
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
1341
+ engines: {node: '>=8'}
1342
+
1343
+ supports-preserve-symlinks-flag@1.0.0:
1344
+ resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
1345
+ engines: {node: '>= 0.4'}
1346
+
1347
+ tailwindcss@3.4.17:
1348
+ resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==}
1349
+ engines: {node: '>=14.0.0'}
1350
+ hasBin: true
1351
+
1352
+ thenify-all@1.6.0:
1353
+ resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
1354
+ engines: {node: '>=0.8'}
1355
+
1356
+ thenify@3.3.1:
1357
+ resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
1358
+
1359
+ tinyglobby@0.2.14:
1360
+ resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
1361
+ engines: {node: '>=12.0.0'}
1362
+
1363
+ to-regex-range@5.0.1:
1364
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
1365
+ engines: {node: '>=8.0'}
1366
+
1367
+ ts-api-utils@2.1.0:
1368
+ resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
1369
+ engines: {node: '>=18.12'}
1370
+ peerDependencies:
1371
+ typescript: '>=4.8.4'
1372
+
1373
+ ts-interface-checker@0.1.13:
1374
+ resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
1375
+
1376
+ type-check@0.4.0:
1377
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
1378
+ engines: {node: '>= 0.8.0'}
1379
+
1380
+ typescript-eslint@8.39.0:
1381
+ resolution: {integrity: sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q==}
1382
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1383
+ peerDependencies:
1384
+ eslint: ^8.57.0 || ^9.0.0
1385
+ typescript: '>=4.8.4 <6.0.0'
1386
+
1387
+ typescript@5.8.3:
1388
+ resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
1389
+ engines: {node: '>=14.17'}
1390
+ hasBin: true
1391
+
1392
+ update-browserslist-db@1.1.3:
1393
+ resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
1394
+ hasBin: true
1395
+ peerDependencies:
1396
+ browserslist: '>= 4.21.0'
1397
+
1398
+ uri-js@4.4.1:
1399
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
1400
+
1401
+ util-deprecate@1.0.2:
1402
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
1403
+
1404
+ vite@7.1.1:
1405
+ resolution: {integrity: sha512-yJ+Mp7OyV+4S+afWo+QyoL9jFWD11QFH0i5i7JypnfTcA1rmgxCbiA8WwAICDEtZ1Z1hzrVhN8R8rGTqkTY8ZQ==}
1406
+ engines: {node: ^20.19.0 || >=22.12.0}
1407
+ hasBin: true
1408
+ peerDependencies:
1409
+ '@types/node': ^20.19.0 || >=22.12.0
1410
+ jiti: '>=1.21.0'
1411
+ less: ^4.0.0
1412
+ lightningcss: ^1.21.0
1413
+ sass: ^1.70.0
1414
+ sass-embedded: ^1.70.0
1415
+ stylus: '>=0.54.8'
1416
+ sugarss: ^5.0.0
1417
+ terser: ^5.16.0
1418
+ tsx: ^4.8.1
1419
+ yaml: ^2.4.2
1420
+ peerDependenciesMeta:
1421
+ '@types/node':
1422
+ optional: true
1423
+ jiti:
1424
+ optional: true
1425
+ less:
1426
+ optional: true
1427
+ lightningcss:
1428
+ optional: true
1429
+ sass:
1430
+ optional: true
1431
+ sass-embedded:
1432
+ optional: true
1433
+ stylus:
1434
+ optional: true
1435
+ sugarss:
1436
+ optional: true
1437
+ terser:
1438
+ optional: true
1439
+ tsx:
1440
+ optional: true
1441
+ yaml:
1442
+ optional: true
1443
+
1444
+ which@2.0.2:
1445
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
1446
+ engines: {node: '>= 8'}
1447
+ hasBin: true
1448
+
1449
+ word-wrap@1.2.5:
1450
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
1451
+ engines: {node: '>=0.10.0'}
1452
+
1453
+ wrap-ansi@7.0.0:
1454
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
1455
+ engines: {node: '>=10'}
1456
+
1457
+ wrap-ansi@8.1.0:
1458
+ resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
1459
+ engines: {node: '>=12'}
1460
+
1461
+ yallist@3.1.1:
1462
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
1463
+
1464
+ yaml@2.8.1:
1465
+ resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==}
1466
+ engines: {node: '>= 14.6'}
1467
+ hasBin: true
1468
+
1469
+ yocto-queue@0.1.0:
1470
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
1471
+ engines: {node: '>=10'}
1472
+
1473
+ snapshots:
1474
+
1475
+ '@alloc/quick-lru@5.2.0': {}
1476
+
1477
+ '@ampproject/remapping@2.3.0':
1478
+ dependencies:
1479
+ '@jridgewell/gen-mapping': 0.3.12
1480
+ '@jridgewell/trace-mapping': 0.3.29
1481
+
1482
+ '@babel/code-frame@7.27.1':
1483
+ dependencies:
1484
+ '@babel/helper-validator-identifier': 7.27.1
1485
+ js-tokens: 4.0.0
1486
+ picocolors: 1.1.1
1487
+
1488
+ '@babel/compat-data@7.28.0': {}
1489
+
1490
+ '@babel/core@7.28.0':
1491
+ dependencies:
1492
+ '@ampproject/remapping': 2.3.0
1493
+ '@babel/code-frame': 7.27.1
1494
+ '@babel/generator': 7.28.0
1495
+ '@babel/helper-compilation-targets': 7.27.2
1496
+ '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0)
1497
+ '@babel/helpers': 7.28.2
1498
+ '@babel/parser': 7.28.0
1499
+ '@babel/template': 7.27.2
1500
+ '@babel/traverse': 7.28.0
1501
+ '@babel/types': 7.28.2
1502
+ convert-source-map: 2.0.0
1503
+ debug: 4.4.1
1504
+ gensync: 1.0.0-beta.2
1505
+ json5: 2.2.3
1506
+ semver: 6.3.1
1507
+ transitivePeerDependencies:
1508
+ - supports-color
1509
+
1510
+ '@babel/generator@7.28.0':
1511
+ dependencies:
1512
+ '@babel/parser': 7.28.0
1513
+ '@babel/types': 7.28.2
1514
+ '@jridgewell/gen-mapping': 0.3.12
1515
+ '@jridgewell/trace-mapping': 0.3.29
1516
+ jsesc: 3.1.0
1517
+
1518
+ '@babel/helper-compilation-targets@7.27.2':
1519
+ dependencies:
1520
+ '@babel/compat-data': 7.28.0
1521
+ '@babel/helper-validator-option': 7.27.1
1522
+ browserslist: 4.25.1
1523
+ lru-cache: 5.1.1
1524
+ semver: 6.3.1
1525
+
1526
+ '@babel/helper-globals@7.28.0': {}
1527
+
1528
+ '@babel/helper-module-imports@7.27.1':
1529
+ dependencies:
1530
+ '@babel/traverse': 7.28.0
1531
+ '@babel/types': 7.28.2
1532
+ transitivePeerDependencies:
1533
+ - supports-color
1534
+
1535
+ '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)':
1536
+ dependencies:
1537
+ '@babel/core': 7.28.0
1538
+ '@babel/helper-module-imports': 7.27.1
1539
+ '@babel/helper-validator-identifier': 7.27.1
1540
+ '@babel/traverse': 7.28.0
1541
+ transitivePeerDependencies:
1542
+ - supports-color
1543
+
1544
+ '@babel/helper-plugin-utils@7.27.1': {}
1545
+
1546
+ '@babel/helper-string-parser@7.27.1': {}
1547
+
1548
+ '@babel/helper-validator-identifier@7.27.1': {}
1549
+
1550
+ '@babel/helper-validator-option@7.27.1': {}
1551
+
1552
+ '@babel/helpers@7.28.2':
1553
+ dependencies:
1554
+ '@babel/template': 7.27.2
1555
+ '@babel/types': 7.28.2
1556
+
1557
+ '@babel/parser@7.28.0':
1558
+ dependencies:
1559
+ '@babel/types': 7.28.2
1560
+
1561
+ '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.0)':
1562
+ dependencies:
1563
+ '@babel/core': 7.28.0
1564
+ '@babel/helper-plugin-utils': 7.27.1
1565
+
1566
+ '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.0)':
1567
+ dependencies:
1568
+ '@babel/core': 7.28.0
1569
+ '@babel/helper-plugin-utils': 7.27.1
1570
+
1571
+ '@babel/template@7.27.2':
1572
+ dependencies:
1573
+ '@babel/code-frame': 7.27.1
1574
+ '@babel/parser': 7.28.0
1575
+ '@babel/types': 7.28.2
1576
+
1577
+ '@babel/traverse@7.28.0':
1578
+ dependencies:
1579
+ '@babel/code-frame': 7.27.1
1580
+ '@babel/generator': 7.28.0
1581
+ '@babel/helper-globals': 7.28.0
1582
+ '@babel/parser': 7.28.0
1583
+ '@babel/template': 7.27.2
1584
+ '@babel/types': 7.28.2
1585
+ debug: 4.4.1
1586
+ transitivePeerDependencies:
1587
+ - supports-color
1588
+
1589
+ '@babel/types@7.28.2':
1590
+ dependencies:
1591
+ '@babel/helper-string-parser': 7.27.1
1592
+ '@babel/helper-validator-identifier': 7.27.1
1593
+
1594
+ '@esbuild/aix-ppc64@0.25.8':
1595
+ optional: true
1596
+
1597
+ '@esbuild/android-arm64@0.25.8':
1598
+ optional: true
1599
+
1600
+ '@esbuild/android-arm@0.25.8':
1601
+ optional: true
1602
+
1603
+ '@esbuild/android-x64@0.25.8':
1604
+ optional: true
1605
+
1606
+ '@esbuild/darwin-arm64@0.25.8':
1607
+ optional: true
1608
+
1609
+ '@esbuild/darwin-x64@0.25.8':
1610
+ optional: true
1611
+
1612
+ '@esbuild/freebsd-arm64@0.25.8':
1613
+ optional: true
1614
+
1615
+ '@esbuild/freebsd-x64@0.25.8':
1616
+ optional: true
1617
+
1618
+ '@esbuild/linux-arm64@0.25.8':
1619
+ optional: true
1620
+
1621
+ '@esbuild/linux-arm@0.25.8':
1622
+ optional: true
1623
+
1624
+ '@esbuild/linux-ia32@0.25.8':
1625
+ optional: true
1626
+
1627
+ '@esbuild/linux-loong64@0.25.8':
1628
+ optional: true
1629
+
1630
+ '@esbuild/linux-mips64el@0.25.8':
1631
+ optional: true
1632
+
1633
+ '@esbuild/linux-ppc64@0.25.8':
1634
+ optional: true
1635
+
1636
+ '@esbuild/linux-riscv64@0.25.8':
1637
+ optional: true
1638
+
1639
+ '@esbuild/linux-s390x@0.25.8':
1640
+ optional: true
1641
+
1642
+ '@esbuild/linux-x64@0.25.8':
1643
+ optional: true
1644
+
1645
+ '@esbuild/netbsd-arm64@0.25.8':
1646
+ optional: true
1647
+
1648
+ '@esbuild/netbsd-x64@0.25.8':
1649
+ optional: true
1650
+
1651
+ '@esbuild/openbsd-arm64@0.25.8':
1652
+ optional: true
1653
+
1654
+ '@esbuild/openbsd-x64@0.25.8':
1655
+ optional: true
1656
+
1657
+ '@esbuild/openharmony-arm64@0.25.8':
1658
+ optional: true
1659
+
1660
+ '@esbuild/sunos-x64@0.25.8':
1661
+ optional: true
1662
+
1663
+ '@esbuild/win32-arm64@0.25.8':
1664
+ optional: true
1665
+
1666
+ '@esbuild/win32-ia32@0.25.8':
1667
+ optional: true
1668
+
1669
+ '@esbuild/win32-x64@0.25.8':
1670
+ optional: true
1671
+
1672
+ '@eslint-community/eslint-utils@4.7.0(eslint@9.33.0(jiti@2.5.1))':
1673
+ dependencies:
1674
+ eslint: 9.33.0(jiti@2.5.1)
1675
+ eslint-visitor-keys: 3.4.3
1676
+
1677
+ '@eslint-community/regexpp@4.12.1': {}
1678
+
1679
+ '@eslint/config-array@0.21.0':
1680
+ dependencies:
1681
+ '@eslint/object-schema': 2.1.6
1682
+ debug: 4.4.1
1683
+ minimatch: 3.1.2
1684
+ transitivePeerDependencies:
1685
+ - supports-color
1686
+
1687
+ '@eslint/config-helpers@0.3.1': {}
1688
+
1689
+ '@eslint/core@0.15.2':
1690
+ dependencies:
1691
+ '@types/json-schema': 7.0.15
1692
+
1693
+ '@eslint/eslintrc@3.3.1':
1694
+ dependencies:
1695
+ ajv: 6.12.6
1696
+ debug: 4.4.1
1697
+ espree: 10.4.0
1698
+ globals: 14.0.0
1699
+ ignore: 5.3.2
1700
+ import-fresh: 3.3.1
1701
+ js-yaml: 4.1.0
1702
+ minimatch: 3.1.2
1703
+ strip-json-comments: 3.1.1
1704
+ transitivePeerDependencies:
1705
+ - supports-color
1706
+
1707
+ '@eslint/js@9.33.0': {}
1708
+
1709
+ '@eslint/object-schema@2.1.6': {}
1710
+
1711
+ '@eslint/plugin-kit@0.3.5':
1712
+ dependencies:
1713
+ '@eslint/core': 0.15.2
1714
+ levn: 0.4.1
1715
+
1716
+ '@humanfs/core@0.19.1': {}
1717
+
1718
+ '@humanfs/node@0.16.6':
1719
+ dependencies:
1720
+ '@humanfs/core': 0.19.1
1721
+ '@humanwhocodes/retry': 0.3.1
1722
+
1723
+ '@humanwhocodes/module-importer@1.0.1': {}
1724
+
1725
+ '@humanwhocodes/retry@0.3.1': {}
1726
+
1727
+ '@humanwhocodes/retry@0.4.3': {}
1728
+
1729
+ '@isaacs/cliui@8.0.2':
1730
+ dependencies:
1731
+ string-width: 5.1.2
1732
+ string-width-cjs: string-width@4.2.3
1733
+ strip-ansi: 7.1.0
1734
+ strip-ansi-cjs: strip-ansi@6.0.1
1735
+ wrap-ansi: 8.1.0
1736
+ wrap-ansi-cjs: wrap-ansi@7.0.0
1737
+
1738
+ '@jridgewell/gen-mapping@0.3.12':
1739
+ dependencies:
1740
+ '@jridgewell/sourcemap-codec': 1.5.4
1741
+ '@jridgewell/trace-mapping': 0.3.29
1742
+
1743
+ '@jridgewell/resolve-uri@3.1.2': {}
1744
+
1745
+ '@jridgewell/sourcemap-codec@1.5.4': {}
1746
+
1747
+ '@jridgewell/trace-mapping@0.3.29':
1748
+ dependencies:
1749
+ '@jridgewell/resolve-uri': 3.1.2
1750
+ '@jridgewell/sourcemap-codec': 1.5.4
1751
+
1752
+ '@nodelib/fs.scandir@2.1.5':
1753
+ dependencies:
1754
+ '@nodelib/fs.stat': 2.0.5
1755
+ run-parallel: 1.2.0
1756
+
1757
+ '@nodelib/fs.stat@2.0.5': {}
1758
+
1759
+ '@nodelib/fs.walk@1.2.8':
1760
+ dependencies:
1761
+ '@nodelib/fs.scandir': 2.1.5
1762
+ fastq: 1.19.1
1763
+
1764
+ '@pkgjs/parseargs@0.11.0':
1765
+ optional: true
1766
+
1767
+ '@rolldown/pluginutils@1.0.0-beta.27': {}
1768
+
1769
+ '@rollup/rollup-android-arm-eabi@4.46.2':
1770
+ optional: true
1771
+
1772
+ '@rollup/rollup-android-arm64@4.46.2':
1773
+ optional: true
1774
+
1775
+ '@rollup/rollup-darwin-arm64@4.46.2':
1776
+ optional: true
1777
+
1778
+ '@rollup/rollup-darwin-x64@4.46.2':
1779
+ optional: true
1780
+
1781
+ '@rollup/rollup-freebsd-arm64@4.46.2':
1782
+ optional: true
1783
+
1784
+ '@rollup/rollup-freebsd-x64@4.46.2':
1785
+ optional: true
1786
+
1787
+ '@rollup/rollup-linux-arm-gnueabihf@4.46.2':
1788
+ optional: true
1789
+
1790
+ '@rollup/rollup-linux-arm-musleabihf@4.46.2':
1791
+ optional: true
1792
+
1793
+ '@rollup/rollup-linux-arm64-gnu@4.46.2':
1794
+ optional: true
1795
+
1796
+ '@rollup/rollup-linux-arm64-musl@4.46.2':
1797
+ optional: true
1798
+
1799
+ '@rollup/rollup-linux-loongarch64-gnu@4.46.2':
1800
+ optional: true
1801
+
1802
+ '@rollup/rollup-linux-ppc64-gnu@4.46.2':
1803
+ optional: true
1804
+
1805
+ '@rollup/rollup-linux-riscv64-gnu@4.46.2':
1806
+ optional: true
1807
+
1808
+ '@rollup/rollup-linux-riscv64-musl@4.46.2':
1809
+ optional: true
1810
+
1811
+ '@rollup/rollup-linux-s390x-gnu@4.46.2':
1812
+ optional: true
1813
+
1814
+ '@rollup/rollup-linux-x64-gnu@4.46.2':
1815
+ optional: true
1816
+
1817
+ '@rollup/rollup-linux-x64-musl@4.46.2':
1818
+ optional: true
1819
+
1820
+ '@rollup/rollup-win32-arm64-msvc@4.46.2':
1821
+ optional: true
1822
+
1823
+ '@rollup/rollup-win32-ia32-msvc@4.46.2':
1824
+ optional: true
1825
+
1826
+ '@rollup/rollup-win32-x64-msvc@4.46.2':
1827
+ optional: true
1828
+
1829
+ '@types/babel__core@7.20.5':
1830
+ dependencies:
1831
+ '@babel/parser': 7.28.0
1832
+ '@babel/types': 7.28.2
1833
+ '@types/babel__generator': 7.27.0
1834
+ '@types/babel__template': 7.4.4
1835
+ '@types/babel__traverse': 7.28.0
1836
+
1837
+ '@types/babel__generator@7.27.0':
1838
+ dependencies:
1839
+ '@babel/types': 7.28.2
1840
+
1841
+ '@types/babel__template@7.4.4':
1842
+ dependencies:
1843
+ '@babel/parser': 7.28.0
1844
+ '@babel/types': 7.28.2
1845
+
1846
+ '@types/babel__traverse@7.28.0':
1847
+ dependencies:
1848
+ '@babel/types': 7.28.2
1849
+
1850
+ '@types/estree@1.0.8': {}
1851
+
1852
+ '@types/json-schema@7.0.15': {}
1853
+
1854
+ '@types/react-dom@19.1.7(@types/react@19.1.9(react@19.1.1))':
1855
+ dependencies:
1856
+ '@types/react': 19.1.9(react@19.1.1)
1857
+
1858
+ '@types/react@19.1.9(react@19.1.1)':
1859
+ dependencies:
1860
+ csstype: 3.1.3
1861
+ react: 19.1.1
1862
+
1863
+ '@typescript-eslint/eslint-plugin@8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)':
1864
+ dependencies:
1865
+ '@eslint-community/regexpp': 4.12.1
1866
+ '@typescript-eslint/parser': 8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)
1867
+ '@typescript-eslint/scope-manager': 8.39.0
1868
+ '@typescript-eslint/type-utils': 8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)
1869
+ '@typescript-eslint/utils': 8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)
1870
+ '@typescript-eslint/visitor-keys': 8.39.0
1871
+ eslint: 9.33.0(jiti@2.5.1)
1872
+ graphemer: 1.4.0
1873
+ ignore: 7.0.5
1874
+ natural-compare: 1.4.0
1875
+ ts-api-utils: 2.1.0(typescript@5.8.3)
1876
+ typescript: 5.8.3
1877
+ transitivePeerDependencies:
1878
+ - supports-color
1879
+
1880
+ '@typescript-eslint/parser@8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)':
1881
+ dependencies:
1882
+ '@typescript-eslint/scope-manager': 8.39.0
1883
+ '@typescript-eslint/types': 8.39.0
1884
+ '@typescript-eslint/typescript-estree': 8.39.0(typescript@5.8.3)
1885
+ '@typescript-eslint/visitor-keys': 8.39.0
1886
+ debug: 4.4.1
1887
+ eslint: 9.33.0(jiti@2.5.1)
1888
+ typescript: 5.8.3
1889
+ transitivePeerDependencies:
1890
+ - supports-color
1891
+
1892
+ '@typescript-eslint/project-service@8.39.0(typescript@5.8.3)':
1893
+ dependencies:
1894
+ '@typescript-eslint/tsconfig-utils': 8.39.0(typescript@5.8.3)
1895
+ '@typescript-eslint/types': 8.39.0
1896
+ debug: 4.4.1
1897
+ typescript: 5.8.3
1898
+ transitivePeerDependencies:
1899
+ - supports-color
1900
+
1901
+ '@typescript-eslint/scope-manager@8.39.0':
1902
+ dependencies:
1903
+ '@typescript-eslint/types': 8.39.0
1904
+ '@typescript-eslint/visitor-keys': 8.39.0
1905
+
1906
+ '@typescript-eslint/tsconfig-utils@8.39.0(typescript@5.8.3)':
1907
+ dependencies:
1908
+ typescript: 5.8.3
1909
+
1910
+ '@typescript-eslint/type-utils@8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)':
1911
+ dependencies:
1912
+ '@typescript-eslint/types': 8.39.0
1913
+ '@typescript-eslint/typescript-estree': 8.39.0(typescript@5.8.3)
1914
+ '@typescript-eslint/utils': 8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)
1915
+ debug: 4.4.1
1916
+ eslint: 9.33.0(jiti@2.5.1)
1917
+ ts-api-utils: 2.1.0(typescript@5.8.3)
1918
+ typescript: 5.8.3
1919
+ transitivePeerDependencies:
1920
+ - supports-color
1921
+
1922
+ '@typescript-eslint/types@8.39.0': {}
1923
+
1924
+ '@typescript-eslint/typescript-estree@8.39.0(typescript@5.8.3)':
1925
+ dependencies:
1926
+ '@typescript-eslint/project-service': 8.39.0(typescript@5.8.3)
1927
+ '@typescript-eslint/tsconfig-utils': 8.39.0(typescript@5.8.3)
1928
+ '@typescript-eslint/types': 8.39.0
1929
+ '@typescript-eslint/visitor-keys': 8.39.0
1930
+ debug: 4.4.1
1931
+ fast-glob: 3.3.3
1932
+ is-glob: 4.0.3
1933
+ minimatch: 9.0.5
1934
+ semver: 7.7.2
1935
+ ts-api-utils: 2.1.0(typescript@5.8.3)
1936
+ typescript: 5.8.3
1937
+ transitivePeerDependencies:
1938
+ - supports-color
1939
+
1940
+ '@typescript-eslint/utils@8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)':
1941
+ dependencies:
1942
+ '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1))
1943
+ '@typescript-eslint/scope-manager': 8.39.0
1944
+ '@typescript-eslint/types': 8.39.0
1945
+ '@typescript-eslint/typescript-estree': 8.39.0(typescript@5.8.3)
1946
+ eslint: 9.33.0(jiti@2.5.1)
1947
+ typescript: 5.8.3
1948
+ transitivePeerDependencies:
1949
+ - supports-color
1950
+
1951
+ '@typescript-eslint/visitor-keys@8.39.0':
1952
+ dependencies:
1953
+ '@typescript-eslint/types': 8.39.0
1954
+ eslint-visitor-keys: 4.2.1
1955
+
1956
+ '@vitejs/plugin-react@4.7.0(vite@7.1.1(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1))':
1957
+ dependencies:
1958
+ '@babel/core': 7.28.0
1959
+ '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0)
1960
+ '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0)
1961
+ '@rolldown/pluginutils': 1.0.0-beta.27
1962
+ '@types/babel__core': 7.20.5
1963
+ react-refresh: 0.17.0
1964
+ vite: 7.1.1(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1)
1965
+ transitivePeerDependencies:
1966
+ - supports-color
1967
+
1968
+ acorn-jsx@5.3.2(acorn@8.15.0):
1969
+ dependencies:
1970
+ acorn: 8.15.0
1971
+
1972
+ acorn@8.15.0: {}
1973
+
1974
+ ajv@6.12.6:
1975
+ dependencies:
1976
+ fast-deep-equal: 3.1.3
1977
+ fast-json-stable-stringify: 2.1.0
1978
+ json-schema-traverse: 0.4.1
1979
+ uri-js: 4.4.1
1980
+
1981
+ ansi-regex@5.0.1: {}
1982
+
1983
+ ansi-regex@6.1.0: {}
1984
+
1985
+ ansi-styles@4.3.0:
1986
+ dependencies:
1987
+ color-convert: 2.0.1
1988
+
1989
+ ansi-styles@6.2.1: {}
1990
+
1991
+ any-promise@1.3.0: {}
1992
+
1993
+ anymatch@3.1.3:
1994
+ dependencies:
1995
+ normalize-path: 3.0.0
1996
+ picomatch: 2.3.1
1997
+
1998
+ arg@5.0.2: {}
1999
+
2000
+ argparse@2.0.1: {}
2001
+
2002
+ autoprefixer@10.4.21(postcss@8.5.6):
2003
+ dependencies:
2004
+ browserslist: 4.25.1
2005
+ caniuse-lite: 1.0.30001733
2006
+ fraction.js: 4.3.7
2007
+ normalize-range: 0.1.2
2008
+ picocolors: 1.1.1
2009
+ postcss: 8.5.6
2010
+ postcss-value-parser: 4.2.0
2011
+
2012
+ balanced-match@1.0.2: {}
2013
+
2014
+ binary-extensions@2.3.0: {}
2015
+
2016
+ brace-expansion@1.1.12:
2017
+ dependencies:
2018
+ balanced-match: 1.0.2
2019
+ concat-map: 0.0.1
2020
+
2021
+ brace-expansion@2.0.2:
2022
+ dependencies:
2023
+ balanced-match: 1.0.2
2024
+
2025
+ braces@3.0.3:
2026
+ dependencies:
2027
+ fill-range: 7.1.1
2028
+
2029
+ browserslist@4.25.1:
2030
+ dependencies:
2031
+ caniuse-lite: 1.0.30001733
2032
+ electron-to-chromium: 1.5.199
2033
+ node-releases: 2.0.19
2034
+ update-browserslist-db: 1.1.3(browserslist@4.25.1)
2035
+
2036
+ callsites@3.1.0: {}
2037
+
2038
+ camelcase-css@2.0.1: {}
2039
+
2040
+ caniuse-lite@1.0.30001733: {}
2041
+
2042
+ chalk@4.1.2:
2043
+ dependencies:
2044
+ ansi-styles: 4.3.0
2045
+ supports-color: 7.2.0
2046
+
2047
+ chokidar@3.6.0:
2048
+ dependencies:
2049
+ anymatch: 3.1.3
2050
+ braces: 3.0.3
2051
+ glob-parent: 5.1.2
2052
+ is-binary-path: 2.1.0
2053
+ is-glob: 4.0.3
2054
+ normalize-path: 3.0.0
2055
+ readdirp: 3.6.0
2056
+ optionalDependencies:
2057
+ fsevents: 2.3.3
2058
+
2059
+ classnames@2.5.1: {}
2060
+
2061
+ color-convert@2.0.1:
2062
+ dependencies:
2063
+ color-name: 1.1.4
2064
+
2065
+ color-name@1.1.4: {}
2066
+
2067
+ commander@4.1.1: {}
2068
+
2069
+ concat-map@0.0.1: {}
2070
+
2071
+ convert-source-map@2.0.0: {}
2072
+
2073
+ cross-spawn@7.0.6:
2074
+ dependencies:
2075
+ path-key: 3.1.1
2076
+ shebang-command: 2.0.0
2077
+ which: 2.0.2
2078
+
2079
+ cssesc@3.0.0: {}
2080
+
2081
+ csstype@3.1.3: {}
2082
+
2083
+ debug@4.4.1:
2084
+ dependencies:
2085
+ ms: 2.1.3
2086
+
2087
+ deep-is@0.1.4: {}
2088
+
2089
+ detect-libc@2.0.4:
2090
+ optional: true
2091
+
2092
+ didyoumean@1.2.2: {}
2093
+
2094
+ dlv@1.1.3: {}
2095
+
2096
+ eastasianwidth@0.2.0: {}
2097
+
2098
+ electron-to-chromium@1.5.199: {}
2099
+
2100
+ emoji-regex@8.0.0: {}
2101
+
2102
+ emoji-regex@9.2.2: {}
2103
+
2104
+ esbuild@0.25.8:
2105
+ optionalDependencies:
2106
+ '@esbuild/aix-ppc64': 0.25.8
2107
+ '@esbuild/android-arm': 0.25.8
2108
+ '@esbuild/android-arm64': 0.25.8
2109
+ '@esbuild/android-x64': 0.25.8
2110
+ '@esbuild/darwin-arm64': 0.25.8
2111
+ '@esbuild/darwin-x64': 0.25.8
2112
+ '@esbuild/freebsd-arm64': 0.25.8
2113
+ '@esbuild/freebsd-x64': 0.25.8
2114
+ '@esbuild/linux-arm': 0.25.8
2115
+ '@esbuild/linux-arm64': 0.25.8
2116
+ '@esbuild/linux-ia32': 0.25.8
2117
+ '@esbuild/linux-loong64': 0.25.8
2118
+ '@esbuild/linux-mips64el': 0.25.8
2119
+ '@esbuild/linux-ppc64': 0.25.8
2120
+ '@esbuild/linux-riscv64': 0.25.8
2121
+ '@esbuild/linux-s390x': 0.25.8
2122
+ '@esbuild/linux-x64': 0.25.8
2123
+ '@esbuild/netbsd-arm64': 0.25.8
2124
+ '@esbuild/netbsd-x64': 0.25.8
2125
+ '@esbuild/openbsd-arm64': 0.25.8
2126
+ '@esbuild/openbsd-x64': 0.25.8
2127
+ '@esbuild/openharmony-arm64': 0.25.8
2128
+ '@esbuild/sunos-x64': 0.25.8
2129
+ '@esbuild/win32-arm64': 0.25.8
2130
+ '@esbuild/win32-ia32': 0.25.8
2131
+ '@esbuild/win32-x64': 0.25.8
2132
+
2133
+ escalade@3.2.0: {}
2134
+
2135
+ escape-string-regexp@4.0.0: {}
2136
+
2137
+ eslint-plugin-react-hooks@5.2.0(eslint@9.33.0(jiti@2.5.1)):
2138
+ dependencies:
2139
+ eslint: 9.33.0(jiti@2.5.1)
2140
+
2141
+ eslint-plugin-react-refresh@0.4.20(eslint@9.33.0(jiti@2.5.1)):
2142
+ dependencies:
2143
+ eslint: 9.33.0(jiti@2.5.1)
2144
+
2145
+ eslint-scope@8.4.0:
2146
+ dependencies:
2147
+ esrecurse: 4.3.0
2148
+ estraverse: 5.3.0
2149
+
2150
+ eslint-visitor-keys@3.4.3: {}
2151
+
2152
+ eslint-visitor-keys@4.2.1: {}
2153
+
2154
+ eslint@9.33.0(jiti@2.5.1):
2155
+ dependencies:
2156
+ '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1))
2157
+ '@eslint-community/regexpp': 4.12.1
2158
+ '@eslint/config-array': 0.21.0
2159
+ '@eslint/config-helpers': 0.3.1
2160
+ '@eslint/core': 0.15.2
2161
+ '@eslint/eslintrc': 3.3.1
2162
+ '@eslint/js': 9.33.0
2163
+ '@eslint/plugin-kit': 0.3.5
2164
+ '@humanfs/node': 0.16.6
2165
+ '@humanwhocodes/module-importer': 1.0.1
2166
+ '@humanwhocodes/retry': 0.4.3
2167
+ '@types/estree': 1.0.8
2168
+ '@types/json-schema': 7.0.15
2169
+ ajv: 6.12.6
2170
+ chalk: 4.1.2
2171
+ cross-spawn: 7.0.6
2172
+ debug: 4.4.1
2173
+ escape-string-regexp: 4.0.0
2174
+ eslint-scope: 8.4.0
2175
+ eslint-visitor-keys: 4.2.1
2176
+ espree: 10.4.0
2177
+ esquery: 1.6.0
2178
+ esutils: 2.0.3
2179
+ fast-deep-equal: 3.1.3
2180
+ file-entry-cache: 8.0.0
2181
+ find-up: 5.0.0
2182
+ glob-parent: 6.0.2
2183
+ ignore: 5.3.2
2184
+ imurmurhash: 0.1.4
2185
+ is-glob: 4.0.3
2186
+ json-stable-stringify-without-jsonify: 1.0.1
2187
+ lodash.merge: 4.6.2
2188
+ minimatch: 3.1.2
2189
+ natural-compare: 1.4.0
2190
+ optionator: 0.9.4
2191
+ optionalDependencies:
2192
+ jiti: 2.5.1
2193
+ transitivePeerDependencies:
2194
+ - supports-color
2195
+
2196
+ espree@10.4.0:
2197
+ dependencies:
2198
+ acorn: 8.15.0
2199
+ acorn-jsx: 5.3.2(acorn@8.15.0)
2200
+ eslint-visitor-keys: 4.2.1
2201
+
2202
+ esquery@1.6.0:
2203
+ dependencies:
2204
+ estraverse: 5.3.0
2205
+
2206
+ esrecurse@4.3.0:
2207
+ dependencies:
2208
+ estraverse: 5.3.0
2209
+
2210
+ estraverse@5.3.0: {}
2211
+
2212
+ esutils@2.0.3: {}
2213
+
2214
+ fast-deep-equal@3.1.3: {}
2215
+
2216
+ fast-glob@3.3.3:
2217
+ dependencies:
2218
+ '@nodelib/fs.stat': 2.0.5
2219
+ '@nodelib/fs.walk': 1.2.8
2220
+ glob-parent: 5.1.2
2221
+ merge2: 1.4.1
2222
+ micromatch: 4.0.8
2223
+
2224
+ fast-json-stable-stringify@2.1.0: {}
2225
+
2226
+ fast-levenshtein@2.0.6: {}
2227
+
2228
+ fastq@1.19.1:
2229
+ dependencies:
2230
+ reusify: 1.1.0
2231
+
2232
+ fdir@6.4.6(picomatch@4.0.3):
2233
+ optionalDependencies:
2234
+ picomatch: 4.0.3
2235
+
2236
+ file-entry-cache@8.0.0:
2237
+ dependencies:
2238
+ flat-cache: 4.0.1
2239
+
2240
+ fill-range@7.1.1:
2241
+ dependencies:
2242
+ to-regex-range: 5.0.1
2243
+
2244
+ find-up@5.0.0:
2245
+ dependencies:
2246
+ locate-path: 6.0.0
2247
+ path-exists: 4.0.0
2248
+
2249
+ flat-cache@4.0.1:
2250
+ dependencies:
2251
+ flatted: 3.3.3
2252
+ keyv: 4.5.4
2253
+
2254
+ flatted@3.3.3: {}
2255
+
2256
+ foreground-child@3.3.1:
2257
+ dependencies:
2258
+ cross-spawn: 7.0.6
2259
+ signal-exit: 4.1.0
2260
+
2261
+ fraction.js@4.3.7: {}
2262
+
2263
+ fsevents@2.3.3:
2264
+ optional: true
2265
+
2266
+ function-bind@1.1.2: {}
2267
+
2268
+ gensync@1.0.0-beta.2: {}
2269
+
2270
+ glob-parent@5.1.2:
2271
+ dependencies:
2272
+ is-glob: 4.0.3
2273
+
2274
+ glob-parent@6.0.2:
2275
+ dependencies:
2276
+ is-glob: 4.0.3
2277
+
2278
+ glob@10.4.5:
2279
+ dependencies:
2280
+ foreground-child: 3.3.1
2281
+ jackspeak: 3.4.3
2282
+ minimatch: 9.0.5
2283
+ minipass: 7.1.2
2284
+ package-json-from-dist: 1.0.1
2285
+ path-scurry: 1.11.1
2286
+
2287
+ globals@14.0.0: {}
2288
+
2289
+ globals@16.3.0: {}
2290
+
2291
+ graphemer@1.4.0: {}
2292
+
2293
+ has-flag@4.0.0: {}
2294
+
2295
+ hasown@2.0.2:
2296
+ dependencies:
2297
+ function-bind: 1.1.2
2298
+
2299
+ ignore@5.3.2: {}
2300
+
2301
+ ignore@7.0.5: {}
2302
+
2303
+ import-fresh@3.3.1:
2304
+ dependencies:
2305
+ parent-module: 1.0.1
2306
+ resolve-from: 4.0.0
2307
+
2308
+ imurmurhash@0.1.4: {}
2309
+
2310
+ is-binary-path@2.1.0:
2311
+ dependencies:
2312
+ binary-extensions: 2.3.0
2313
+
2314
+ is-core-module@2.16.1:
2315
+ dependencies:
2316
+ hasown: 2.0.2
2317
+
2318
+ is-extglob@2.1.1: {}
2319
+
2320
+ is-fullwidth-code-point@3.0.0: {}
2321
+
2322
+ is-glob@4.0.3:
2323
+ dependencies:
2324
+ is-extglob: 2.1.1
2325
+
2326
+ is-number@7.0.0: {}
2327
+
2328
+ isexe@2.0.0: {}
2329
+
2330
+ jackspeak@3.4.3:
2331
+ dependencies:
2332
+ '@isaacs/cliui': 8.0.2
2333
+ optionalDependencies:
2334
+ '@pkgjs/parseargs': 0.11.0
2335
+
2336
+ jiti@1.21.7: {}
2337
+
2338
+ jiti@2.5.1:
2339
+ optional: true
2340
+
2341
+ js-tokens@4.0.0: {}
2342
+
2343
+ js-yaml@4.1.0:
2344
+ dependencies:
2345
+ argparse: 2.0.1
2346
+
2347
+ jsesc@3.1.0: {}
2348
+
2349
+ json-buffer@3.0.1: {}
2350
+
2351
+ json-schema-traverse@0.4.1: {}
2352
+
2353
+ json-stable-stringify-without-jsonify@1.0.1: {}
2354
+
2355
+ json5@2.2.3: {}
2356
+
2357
+ keyv@4.5.4:
2358
+ dependencies:
2359
+ json-buffer: 3.0.1
2360
+
2361
+ levn@0.4.1:
2362
+ dependencies:
2363
+ prelude-ls: 1.2.1
2364
+ type-check: 0.4.0
2365
+
2366
+ lightningcss-darwin-arm64@1.30.1:
2367
+ optional: true
2368
+
2369
+ lightningcss-darwin-x64@1.30.1:
2370
+ optional: true
2371
+
2372
+ lightningcss-freebsd-x64@1.30.1:
2373
+ optional: true
2374
+
2375
+ lightningcss-linux-arm-gnueabihf@1.30.1:
2376
+ optional: true
2377
+
2378
+ lightningcss-linux-arm64-gnu@1.30.1:
2379
+ optional: true
2380
+
2381
+ lightningcss-linux-arm64-musl@1.30.1:
2382
+ optional: true
2383
+
2384
+ lightningcss-linux-x64-gnu@1.30.1:
2385
+ optional: true
2386
+
2387
+ lightningcss-linux-x64-musl@1.30.1:
2388
+ optional: true
2389
+
2390
+ lightningcss-win32-arm64-msvc@1.30.1:
2391
+ optional: true
2392
+
2393
+ lightningcss-win32-x64-msvc@1.30.1:
2394
+ optional: true
2395
+
2396
+ lightningcss@1.30.1:
2397
+ dependencies:
2398
+ detect-libc: 2.0.4
2399
+ optionalDependencies:
2400
+ lightningcss-darwin-arm64: 1.30.1
2401
+ lightningcss-darwin-x64: 1.30.1
2402
+ lightningcss-freebsd-x64: 1.30.1
2403
+ lightningcss-linux-arm-gnueabihf: 1.30.1
2404
+ lightningcss-linux-arm64-gnu: 1.30.1
2405
+ lightningcss-linux-arm64-musl: 1.30.1
2406
+ lightningcss-linux-x64-gnu: 1.30.1
2407
+ lightningcss-linux-x64-musl: 1.30.1
2408
+ lightningcss-win32-arm64-msvc: 1.30.1
2409
+ lightningcss-win32-x64-msvc: 1.30.1
2410
+ optional: true
2411
+
2412
+ lilconfig@3.1.3: {}
2413
+
2414
+ lines-and-columns@1.2.4: {}
2415
+
2416
+ locate-path@6.0.0:
2417
+ dependencies:
2418
+ p-locate: 5.0.0
2419
+
2420
+ lodash.merge@4.6.2: {}
2421
+
2422
+ lru-cache@10.4.3: {}
2423
+
2424
+ lru-cache@5.1.1:
2425
+ dependencies:
2426
+ yallist: 3.1.1
2427
+
2428
+ merge2@1.4.1: {}
2429
+
2430
+ micromatch@4.0.8:
2431
+ dependencies:
2432
+ braces: 3.0.3
2433
+ picomatch: 2.3.1
2434
+
2435
+ minimatch@3.1.2:
2436
+ dependencies:
2437
+ brace-expansion: 1.1.12
2438
+
2439
+ minimatch@9.0.5:
2440
+ dependencies:
2441
+ brace-expansion: 2.0.2
2442
+
2443
+ minipass@7.1.2: {}
2444
+
2445
+ ms@2.1.3: {}
2446
+
2447
+ mz@2.7.0:
2448
+ dependencies:
2449
+ any-promise: 1.3.0
2450
+ object-assign: 4.1.1
2451
+ thenify-all: 1.6.0
2452
+
2453
+ nanoid@3.3.11: {}
2454
+
2455
+ natural-compare@1.4.0: {}
2456
+
2457
+ node-releases@2.0.19: {}
2458
+
2459
+ normalize-path@3.0.0: {}
2460
+
2461
+ normalize-range@0.1.2: {}
2462
+
2463
+ object-assign@4.1.1: {}
2464
+
2465
+ object-hash@3.0.0: {}
2466
+
2467
+ optionator@0.9.4:
2468
+ dependencies:
2469
+ deep-is: 0.1.4
2470
+ fast-levenshtein: 2.0.6
2471
+ levn: 0.4.1
2472
+ prelude-ls: 1.2.1
2473
+ type-check: 0.4.0
2474
+ word-wrap: 1.2.5
2475
+
2476
+ p-limit@3.1.0:
2477
+ dependencies:
2478
+ yocto-queue: 0.1.0
2479
+
2480
+ p-locate@5.0.0:
2481
+ dependencies:
2482
+ p-limit: 3.1.0
2483
+
2484
+ package-json-from-dist@1.0.1: {}
2485
+
2486
+ parent-module@1.0.1:
2487
+ dependencies:
2488
+ callsites: 3.1.0
2489
+
2490
+ path-exists@4.0.0: {}
2491
+
2492
+ path-key@3.1.1: {}
2493
+
2494
+ path-parse@1.0.7: {}
2495
+
2496
+ path-scurry@1.11.1:
2497
+ dependencies:
2498
+ lru-cache: 10.4.3
2499
+ minipass: 7.1.2
2500
+
2501
+ picocolors@1.1.1: {}
2502
+
2503
+ picomatch@2.3.1: {}
2504
+
2505
+ picomatch@4.0.3: {}
2506
+
2507
+ pify@2.3.0: {}
2508
+
2509
+ pirates@4.0.7: {}
2510
+
2511
+ postcss-import@15.1.0(postcss@8.5.6):
2512
+ dependencies:
2513
+ postcss: 8.5.6
2514
+ postcss-value-parser: 4.2.0
2515
+ read-cache: 1.0.0
2516
+ resolve: 1.22.10
2517
+
2518
+ postcss-js@4.0.1(postcss@8.5.6):
2519
+ dependencies:
2520
+ camelcase-css: 2.0.1
2521
+ postcss: 8.5.6
2522
+
2523
+ postcss-load-config@4.0.2(postcss@8.5.6):
2524
+ dependencies:
2525
+ lilconfig: 3.1.3
2526
+ yaml: 2.8.1
2527
+ optionalDependencies:
2528
+ postcss: 8.5.6
2529
+
2530
+ postcss-nested@6.2.0(postcss@8.5.6):
2531
+ dependencies:
2532
+ postcss: 8.5.6
2533
+ postcss-selector-parser: 6.1.2
2534
+
2535
+ postcss-selector-parser@6.1.2:
2536
+ dependencies:
2537
+ cssesc: 3.0.0
2538
+ util-deprecate: 1.0.2
2539
+
2540
+ postcss-value-parser@4.2.0: {}
2541
+
2542
+ postcss@8.5.6:
2543
+ dependencies:
2544
+ nanoid: 3.3.11
2545
+ picocolors: 1.1.1
2546
+ source-map-js: 1.2.1
2547
+
2548
+ prelude-ls@1.2.1: {}
2549
+
2550
+ punycode@2.3.1: {}
2551
+
2552
+ queue-microtask@1.2.3: {}
2553
+
2554
+ react-dom@19.1.1(react@19.1.1):
2555
+ dependencies:
2556
+ react: 19.1.1
2557
+ scheduler: 0.26.0
2558
+
2559
+ react-refresh@0.17.0: {}
2560
+
2561
+ react@19.1.1: {}
2562
+
2563
+ read-cache@1.0.0:
2564
+ dependencies:
2565
+ pify: 2.3.0
2566
+
2567
+ readdirp@3.6.0:
2568
+ dependencies:
2569
+ picomatch: 2.3.1
2570
+
2571
+ resolve-from@4.0.0: {}
2572
+
2573
+ resolve@1.22.10:
2574
+ dependencies:
2575
+ is-core-module: 2.16.1
2576
+ path-parse: 1.0.7
2577
+ supports-preserve-symlinks-flag: 1.0.0
2578
+
2579
+ reusify@1.1.0: {}
2580
+
2581
+ rollup@4.46.2:
2582
+ dependencies:
2583
+ '@types/estree': 1.0.8
2584
+ optionalDependencies:
2585
+ '@rollup/rollup-android-arm-eabi': 4.46.2
2586
+ '@rollup/rollup-android-arm64': 4.46.2
2587
+ '@rollup/rollup-darwin-arm64': 4.46.2
2588
+ '@rollup/rollup-darwin-x64': 4.46.2
2589
+ '@rollup/rollup-freebsd-arm64': 4.46.2
2590
+ '@rollup/rollup-freebsd-x64': 4.46.2
2591
+ '@rollup/rollup-linux-arm-gnueabihf': 4.46.2
2592
+ '@rollup/rollup-linux-arm-musleabihf': 4.46.2
2593
+ '@rollup/rollup-linux-arm64-gnu': 4.46.2
2594
+ '@rollup/rollup-linux-arm64-musl': 4.46.2
2595
+ '@rollup/rollup-linux-loongarch64-gnu': 4.46.2
2596
+ '@rollup/rollup-linux-ppc64-gnu': 4.46.2
2597
+ '@rollup/rollup-linux-riscv64-gnu': 4.46.2
2598
+ '@rollup/rollup-linux-riscv64-musl': 4.46.2
2599
+ '@rollup/rollup-linux-s390x-gnu': 4.46.2
2600
+ '@rollup/rollup-linux-x64-gnu': 4.46.2
2601
+ '@rollup/rollup-linux-x64-musl': 4.46.2
2602
+ '@rollup/rollup-win32-arm64-msvc': 4.46.2
2603
+ '@rollup/rollup-win32-ia32-msvc': 4.46.2
2604
+ '@rollup/rollup-win32-x64-msvc': 4.46.2
2605
+ fsevents: 2.3.3
2606
+
2607
+ run-parallel@1.2.0:
2608
+ dependencies:
2609
+ queue-microtask: 1.2.3
2610
+
2611
+ scheduler@0.26.0: {}
2612
+
2613
+ semver@6.3.1: {}
2614
+
2615
+ semver@7.7.2: {}
2616
+
2617
+ shebang-command@2.0.0:
2618
+ dependencies:
2619
+ shebang-regex: 3.0.0
2620
+
2621
+ shebang-regex@3.0.0: {}
2622
+
2623
+ signal-exit@4.1.0: {}
2624
+
2625
+ source-map-js@1.2.1: {}
2626
+
2627
+ string-width@4.2.3:
2628
+ dependencies:
2629
+ emoji-regex: 8.0.0
2630
+ is-fullwidth-code-point: 3.0.0
2631
+ strip-ansi: 6.0.1
2632
+
2633
+ string-width@5.1.2:
2634
+ dependencies:
2635
+ eastasianwidth: 0.2.0
2636
+ emoji-regex: 9.2.2
2637
+ strip-ansi: 7.1.0
2638
+
2639
+ strip-ansi@6.0.1:
2640
+ dependencies:
2641
+ ansi-regex: 5.0.1
2642
+
2643
+ strip-ansi@7.1.0:
2644
+ dependencies:
2645
+ ansi-regex: 6.1.0
2646
+
2647
+ strip-json-comments@3.1.1: {}
2648
+
2649
+ sucrase@3.35.0:
2650
+ dependencies:
2651
+ '@jridgewell/gen-mapping': 0.3.12
2652
+ commander: 4.1.1
2653
+ glob: 10.4.5
2654
+ lines-and-columns: 1.2.4
2655
+ mz: 2.7.0
2656
+ pirates: 4.0.7
2657
+ ts-interface-checker: 0.1.13
2658
+
2659
+ supports-color@7.2.0:
2660
+ dependencies:
2661
+ has-flag: 4.0.0
2662
+
2663
+ supports-preserve-symlinks-flag@1.0.0: {}
2664
+
2665
+ tailwindcss@3.4.17:
2666
+ dependencies:
2667
+ '@alloc/quick-lru': 5.2.0
2668
+ arg: 5.0.2
2669
+ chokidar: 3.6.0
2670
+ didyoumean: 1.2.2
2671
+ dlv: 1.1.3
2672
+ fast-glob: 3.3.3
2673
+ glob-parent: 6.0.2
2674
+ is-glob: 4.0.3
2675
+ jiti: 1.21.7
2676
+ lilconfig: 3.1.3
2677
+ micromatch: 4.0.8
2678
+ normalize-path: 3.0.0
2679
+ object-hash: 3.0.0
2680
+ picocolors: 1.1.1
2681
+ postcss: 8.5.6
2682
+ postcss-import: 15.1.0(postcss@8.5.6)
2683
+ postcss-js: 4.0.1(postcss@8.5.6)
2684
+ postcss-load-config: 4.0.2(postcss@8.5.6)
2685
+ postcss-nested: 6.2.0(postcss@8.5.6)
2686
+ postcss-selector-parser: 6.1.2
2687
+ resolve: 1.22.10
2688
+ sucrase: 3.35.0
2689
+ transitivePeerDependencies:
2690
+ - ts-node
2691
+
2692
+ thenify-all@1.6.0:
2693
+ dependencies:
2694
+ thenify: 3.3.1
2695
+
2696
+ thenify@3.3.1:
2697
+ dependencies:
2698
+ any-promise: 1.3.0
2699
+
2700
+ tinyglobby@0.2.14:
2701
+ dependencies:
2702
+ fdir: 6.4.6(picomatch@4.0.3)
2703
+ picomatch: 4.0.3
2704
+
2705
+ to-regex-range@5.0.1:
2706
+ dependencies:
2707
+ is-number: 7.0.0
2708
+
2709
+ ts-api-utils@2.1.0(typescript@5.8.3):
2710
+ dependencies:
2711
+ typescript: 5.8.3
2712
+
2713
+ ts-interface-checker@0.1.13: {}
2714
+
2715
+ type-check@0.4.0:
2716
+ dependencies:
2717
+ prelude-ls: 1.2.1
2718
+
2719
+ typescript-eslint@8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3):
2720
+ dependencies:
2721
+ '@typescript-eslint/eslint-plugin': 8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)
2722
+ '@typescript-eslint/parser': 8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)
2723
+ '@typescript-eslint/typescript-estree': 8.39.0(typescript@5.8.3)
2724
+ '@typescript-eslint/utils': 8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)
2725
+ eslint: 9.33.0(jiti@2.5.1)
2726
+ typescript: 5.8.3
2727
+ transitivePeerDependencies:
2728
+ - supports-color
2729
+
2730
+ typescript@5.8.3: {}
2731
+
2732
+ update-browserslist-db@1.1.3(browserslist@4.25.1):
2733
+ dependencies:
2734
+ browserslist: 4.25.1
2735
+ escalade: 3.2.0
2736
+ picocolors: 1.1.1
2737
+
2738
+ uri-js@4.4.1:
2739
+ dependencies:
2740
+ punycode: 2.3.1
2741
+
2742
+ util-deprecate@1.0.2: {}
2743
+
2744
+ vite@7.1.1(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1):
2745
+ dependencies:
2746
+ esbuild: 0.25.8
2747
+ fdir: 6.4.6(picomatch@4.0.3)
2748
+ picomatch: 4.0.3
2749
+ postcss: 8.5.6
2750
+ rollup: 4.46.2
2751
+ tinyglobby: 0.2.14
2752
+ optionalDependencies:
2753
+ fsevents: 2.3.3
2754
+ jiti: 2.5.1
2755
+ lightningcss: 1.30.1
2756
+ yaml: 2.8.1
2757
+
2758
+ which@2.0.2:
2759
+ dependencies:
2760
+ isexe: 2.0.0
2761
+
2762
+ word-wrap@1.2.5: {}
2763
+
2764
+ wrap-ansi@7.0.0:
2765
+ dependencies:
2766
+ ansi-styles: 4.3.0
2767
+ string-width: 4.2.3
2768
+ strip-ansi: 6.0.1
2769
+
2770
+ wrap-ansi@8.1.0:
2771
+ dependencies:
2772
+ ansi-styles: 6.2.1
2773
+ string-width: 5.1.2
2774
+ strip-ansi: 7.1.0
2775
+
2776
+ yallist@3.1.1: {}
2777
+
2778
+ yaml@2.8.1: {}
2779
+
2780
+ yocto-queue@0.1.0: {}
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
frontend/public/logo.svg ADDED
frontend/public/vite.svg ADDED
frontend/src/App.css ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Main app styles - matches original Next.js design */
2
+
3
+ /* Card styling to match original */
4
+ .card {
5
+ @apply bg-white rounded-xl border border-gray-200 shadow-sm transition-shadow duration-200;
6
+ }
7
+
8
+ .card:hover {
9
+ @apply shadow-md;
10
+ }
11
+
12
+ /* Button styling to match original */
13
+ .btn {
14
+ @apply inline-flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium shadow disabled:opacity-50 disabled:cursor-not-allowed;
15
+ background-image: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
16
+ color: white;
17
+ }
18
+
19
+ /* Input styling with animated blue glow */
20
+ .input {
21
+ @apply rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2;
22
+ transition: box-shadow .2s ease;
23
+ box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.5);
24
+ }
25
+ .input:focus {
26
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.35);
27
+ }
28
+
29
+ /* Badge styling for potential use */
30
+ .badge {
31
+ @apply inline-flex items-center rounded-full bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700;
32
+ }
33
+
34
+ /* Custom spinner animation */
35
+ @keyframes spin {
36
+ to {
37
+ transform: rotate(360deg);
38
+ }
39
+ }
40
+
41
+ .animate-spin {
42
+ animation: spin 1s linear infinite;
43
+ }
44
+
45
+ /* Line clamp utility for older browsers */
46
+ .line-clamp-2 {
47
+ display: -webkit-box;
48
+ -webkit-line-clamp: 2;
49
+ -webkit-box-orient: vertical;
50
+ overflow: hidden;
51
+ }
52
+
53
+ /* Prose styling for newsletter content */
54
+ .prose {
55
+ max-width: 65ch;
56
+ }
57
+
58
+ .prose-sm {
59
+ font-size: 0.875rem;
60
+ line-height: 1.5;
61
+ }
62
+
63
+ /* Ensure the app fills the viewport */
64
+ #root {
65
+ min-height: 100vh;
66
+ }
frontend/src/App.tsx ADDED
@@ -0,0 +1,594 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useMemo, useState } from 'react'
2
+ import FeedPicker from './components/FeedPicker'
3
+ import EditorModal from './components/EditorModal'
4
+ import TweetCards from './components/TweetCards'
5
+ import './App.css'
6
+
7
+ type Article = { title: string, link: string, summary?: string, published?: string, source?: string }
8
+
9
+ // Simple spinner component
10
+ function Spinner() {
11
+ return (
12
+ <div className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]" role="status">
13
+ <span className="!absolute !-m-px !h-px !w-px !overflow-hidden !whitespace-nowrap !border-0 !p-0 ![clip:rect(0,0,0,0)]">Loading...</span>
14
+ </div>
15
+ )
16
+ }
17
+
18
+ function App() {
19
+ const [sessionId] = useState(() => Math.random().toString(36).slice(2))
20
+ const [apiBase] = useState('/api')
21
+ const [sources, setSources] = useState<string[]>([])
22
+ const [articles, setArticles] = useState<Article[]>([])
23
+ const [summary, setSummary] = useState('')
24
+ const [tweets, setTweets] = useState<Array<{id: string, content: string, summary_title: string, summary_link: string, summary_source: string}>>([])
25
+
26
+ const [newsletterHtml, setNewsletterHtml] = useState('')
27
+ const [loadingHighlights, setLoadingHighlights] = useState(false)
28
+ const [loadingSummaries, setLoadingSummaries] = useState(false)
29
+ const [loadingTweets, setLoadingTweets] = useState(false)
30
+ const [loadingNewsletter, setLoadingNewsletter] = useState(false)
31
+ const [summariesMode, setSummariesMode] = useState(false)
32
+ const [pageIndex, setPageIndex] = useState(0)
33
+ const [highlights, setHighlights] = useState<Array<{ title: string, link: string, source?: string, summary: string }>>([])
34
+
35
+ const [selectedArticles, setSelectedArticles] = useState<string[]>([]) // Array of article URLs
36
+ const [isPaginated, setIsPaginated] = useState(true) // Default to paginated view
37
+ const [currentEditingTweetId, setCurrentEditingTweetId] = useState<string | null>(null)
38
+ const [tweetConversations, setTweetConversations] = useState<Record<string, Array<{role: string, content: string}>>>({})
39
+
40
+ const [pendingTweetUpdate, setPendingTweetUpdate] = useState<string | null>(null)
41
+ const hasHighlights = useMemo(() => !!summary, [summary])
42
+
43
+ const [editorOpen, setEditorOpen] = useState(false)
44
+ const [editorTitle, setEditorTitle] = useState('Editor')
45
+ const [editorText, setEditorText] = useState('')
46
+ const [editTarget, setEditTarget] = useState<'summary' | `tweet-${number}` | 'newsletter'>('summary')
47
+
48
+ async function fetchAndSummarize() {
49
+ setLoadingHighlights(true)
50
+ try {
51
+ // 1) Aggregate articles (uses selected sources or backend defaults)
52
+ const resAgg = await fetch(`${apiBase}/aggregate`, {
53
+ method: 'POST',
54
+ headers: { 'Content-Type': 'application/json' },
55
+ body: JSON.stringify({ sources })
56
+ })
57
+ const dataAgg = await resAgg.json()
58
+
59
+ // Auto-select all articles by default
60
+ const articleUrls = dataAgg.articles.map((a: any) => a.link)
61
+
62
+ // 2) Try to summarize using the freshly fetched articles
63
+ let summaryText = ''
64
+ try {
65
+ const resSum = await fetch(`${apiBase}/highlights`, {
66
+ method: 'POST',
67
+ headers: { 'Content-Type': 'application/json' },
68
+ body: JSON.stringify({ session_id: sessionId, articles: dataAgg.articles })
69
+ })
70
+ const dataSum = await resSum.json()
71
+
72
+ // Set appropriate message based on whether articles were found
73
+ if (dataAgg.articles && dataAgg.articles.length > 0) {
74
+ summaryText = dataSum.summary_markdown || 'Highlights generated successfully.'
75
+ } else {
76
+ summaryText = 'No articles found for the selected sources in the past 7 days.'
77
+ }
78
+ } catch (summaryError) {
79
+ // If summary fails (e.g., no OpenAI key), still show articles and allow progression
80
+ console.error('Summary generation failed:', summaryError)
81
+ if (dataAgg.articles && dataAgg.articles.length > 0) {
82
+ summaryText = 'Articles fetched successfully. Click "Get Summaries" to process selected articles.'
83
+ } else {
84
+ summaryText = 'No articles found for the selected sources in the past 7 days.'
85
+ }
86
+ }
87
+
88
+ // Update all state together after loading is complete
89
+ setLoadingHighlights(false)
90
+ setArticles(dataAgg.articles)
91
+ setSelectedArticles(articleUrls)
92
+ setSummariesMode(false)
93
+ setSummary(summaryText)
94
+ } catch (error) {
95
+ setLoadingHighlights(false)
96
+ console.error('Failed to fetch articles:', error)
97
+ setSummary('Failed to fetch articles. Please try again.')
98
+ }
99
+ }
100
+
101
+ async function getHighlights() {
102
+ // Validate selection limit before making API call
103
+ if (selectedArticles.length > 5) {
104
+ alert('Please select 5 or fewer articles for summarization. You currently have ' + selectedArticles.length + ' articles selected.')
105
+ return
106
+ }
107
+
108
+ setLoadingSummaries(true)
109
+ try {
110
+ // Only scrape selected articles
111
+ const selectedArticleData = articles.filter(a => selectedArticles.includes(a.link))
112
+
113
+ const res = await fetch(`${apiBase}/summaries_selected`, {
114
+ method: 'POST',
115
+ headers: { 'Content-Type': 'application/json' },
116
+ body: JSON.stringify({ articles: selectedArticleData })
117
+ })
118
+ const data = await res.json()
119
+ const items: { title: string, link: string, source?: string, summary: string }[] = data.items || []
120
+
121
+ // Batch all state updates after loading is complete
122
+ setLoadingSummaries(false)
123
+ setHighlights(items)
124
+ setSummariesMode(true)
125
+ setPageIndex(0)
126
+ } catch (error) {
127
+ setLoadingSummaries(false)
128
+ throw error
129
+ }
130
+ }
131
+
132
+ function resetSummaries() {
133
+ setSummariesMode(false)
134
+ setSummary('')
135
+ setArticles([])
136
+ setTweets([])
137
+ setNewsletterHtml('')
138
+ setPageIndex(0)
139
+ setHighlights([])
140
+ setSelectedArticles([])
141
+ setIsPaginated(true) // Reset to default paginated view
142
+ }
143
+
144
+ function toggleArticleSelection(articleUrl: string) {
145
+ setSelectedArticles(prev =>
146
+ prev.includes(articleUrl)
147
+ ? prev.filter(url => url !== articleUrl)
148
+ : [...prev, articleUrl]
149
+ )
150
+ }
151
+
152
+ function selectAllArticles() {
153
+ setSelectedArticles(articles.map(a => a.link))
154
+ }
155
+
156
+ function deselectAllArticles() {
157
+ setSelectedArticles([])
158
+ }
159
+
160
+ async function makeTweets() {
161
+ setLoadingTweets(true)
162
+ try {
163
+ const res = await fetch(`${apiBase}/tweets`, {
164
+ method: 'POST',
165
+ headers: { 'Content-Type': 'application/json' },
166
+ body: JSON.stringify({
167
+ session_id: sessionId,
168
+ summaries: highlights // Send highlights instead of summary_markdown
169
+ })
170
+ })
171
+ const data = await res.json()
172
+
173
+ // Batch state updates after loading is complete
174
+ setLoadingTweets(false)
175
+ setTweets(data.tweets)
176
+ } catch (error) {
177
+ setLoadingTweets(false)
178
+ throw error
179
+ }
180
+ }
181
+
182
+ async function makeNewsletter() {
183
+ setLoadingNewsletter(true)
184
+ try {
185
+ const res = await fetch(`${apiBase}/newsletter`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: sessionId, summary_markdown: summary, articles }) })
186
+ const data = await res.json()
187
+
188
+ // Batch state updates after loading is complete
189
+ setLoadingNewsletter(false)
190
+ setNewsletterHtml(data.html)
191
+ } catch (error) {
192
+ setLoadingNewsletter(false)
193
+ throw error
194
+ }
195
+ }
196
+
197
+ function openEditor(target: 'summary' | `tweet-${number}` | 'newsletter') {
198
+ setEditTarget(target)
199
+ if (target === 'summary') {
200
+ setEditorTitle('Edit Summary')
201
+ setEditorText(summary)
202
+ } else if (target.startsWith('tweet-')) {
203
+ const idx = Number(target.split('-')[1])
204
+ setEditorTitle(`Edit Tweet ${idx + 1}`)
205
+ setEditorText(tweets[idx]?.content || '')
206
+ } else {
207
+ setEditorTitle('Edit Newsletter (HTML)')
208
+ setEditorText(newsletterHtml)
209
+ }
210
+ setEditorOpen(true)
211
+ }
212
+
213
+ function openTweetEditor(tweet: {id: string, content: string, summary_title: string, summary_link: string, summary_source: string}) {
214
+ // Close any existing editor first, then open the new one
215
+ if (currentEditingTweetId === tweet.id) {
216
+ setCurrentEditingTweetId(null)
217
+ setPendingTweetUpdate(null)
218
+ } else {
219
+ setCurrentEditingTweetId(tweet.id)
220
+ setPendingTweetUpdate(null)
221
+ }
222
+ }
223
+
224
+ async function sendTweetMessage(message: string) {
225
+ if (!currentEditingTweetId) return
226
+
227
+ const currentTweet = tweets.find(t => t.id === currentEditingTweetId)
228
+ if (!currentTweet) return
229
+
230
+ try {
231
+ const res = await fetch(`${apiBase}/edit_tweet`, {
232
+ method: 'POST',
233
+ headers: { 'Content-Type': 'application/json' },
234
+ body: JSON.stringify({
235
+ session_id: sessionId,
236
+ tweet_id: currentTweet.id,
237
+ current_tweet: currentTweet.content,
238
+ original_summary: highlights.find(h => h.link === currentTweet.summary_link)?.summary || '',
239
+ user_message: message,
240
+ conversation_history: tweetConversations[currentTweet.id] || []
241
+ })
242
+ })
243
+
244
+ const data = await res.json()
245
+
246
+ // Store pending update instead of immediately applying it
247
+ setPendingTweetUpdate(data.new_tweet)
248
+
249
+ // Update conversation history
250
+ setTweetConversations(prev => ({
251
+ ...prev,
252
+ [currentTweet.id]: data.conversation_history
253
+ }))
254
+
255
+ return data.ai_response
256
+ } catch (error) {
257
+ console.error('Error editing tweet:', error)
258
+ return 'Sorry, I encountered an error while processing your request.'
259
+ }
260
+ }
261
+
262
+ function acceptTweetUpdate() {
263
+ if (!currentEditingTweetId || !pendingTweetUpdate) return
264
+
265
+ // Update tweet content in main list
266
+ const updatedTweets = tweets.map(tweet =>
267
+ tweet.id === currentEditingTweetId
268
+ ? { ...tweet, content: pendingTweetUpdate }
269
+ : tweet
270
+ )
271
+ setTweets(updatedTweets)
272
+ setPendingTweetUpdate(null)
273
+ }
274
+
275
+ function rejectTweetUpdate() {
276
+ setPendingTweetUpdate(null)
277
+ }
278
+
279
+ function onEditorDone(newText: string) {
280
+ if (editTarget === 'summary') setSummary(newText)
281
+ else if (editTarget.startsWith('tweet-')) {
282
+ const idx = Number(editTarget.split('-')[1])
283
+ const next = tweets.slice()
284
+ if (next[idx]) {
285
+ next[idx] = { ...next[idx], content: newText }
286
+ setTweets(next)
287
+ }
288
+ } else setNewsletterHtml(newText)
289
+ }
290
+
291
+ async function download() {
292
+ const res = await fetch(`${apiBase}/download_html`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: sessionId, html: newsletterHtml }) })
293
+ const blob = await res.blob()
294
+ const url = URL.createObjectURL(blob)
295
+ const a = document.createElement('a')
296
+ a.href = url
297
+ a.download = 'ai_weekly.html'
298
+ document.body.appendChild(a)
299
+ a.click()
300
+ URL.revokeObjectURL(url)
301
+ a.remove()
302
+ }
303
+
304
+ return (
305
+ <div className="min-h-screen bg-gradient-to-b from-indigo-50 to-purple-50 text-gray-900">
306
+ <div className="mx-auto max-w-5xl px-4 py-6">
307
+ <header className="mb-6">
308
+ <div className="flex items-center gap-3">
309
+ <img src="/logo.svg" alt="AI Newsletter" className="h-10 w-10" />
310
+ <div>
311
+ <h1 className="text-xl font-semibold bg-gradient-to-r from-indigo-500 to-purple-500 bg-clip-text text-transparent">
312
+ AI Newsletter Generator
313
+ </h1>
314
+ <p className="text-xs text-gray-500">Curate, summarize, and publish—fast.</p>
315
+ </div>
316
+ </div>
317
+ </header>
318
+ <main className="space-y-4">
319
+
320
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-3">
321
+ <div className="md:col-span-1">
322
+ <FeedPicker selected={sources} setSelected={setSources} />
323
+ </div>
324
+ <div className="md:col-span-2 space-y-3">
325
+ <div className="card p-4">
326
+ <div className="mb-2 flex items-center justify-between">
327
+ <h3 className="text-sm font-semibold">Weekly Highlights</h3>
328
+ <div className="flex flex-wrap items-center gap-2">
329
+ {!hasHighlights && !summariesMode && (
330
+ <button className="btn flex items-center gap-2" onClick={fetchAndSummarize} disabled={loadingHighlights}>
331
+ {loadingHighlights && <Spinner />}
332
+ Get Highlights
333
+ </button>
334
+ )}
335
+ {hasHighlights && !summariesMode && (
336
+ <>
337
+ <button className="btn" onClick={resetSummaries}>Reset</button>
338
+ <button
339
+ className="btn flex items-center gap-2"
340
+ onClick={async () => { await getHighlights(); /* switches to summariesMode */ }}
341
+ disabled={loadingSummaries || selectedArticles.length === 0}
342
+ title={selectedArticles.length === 0 ? "Please select at least one article" : `Process ${selectedArticles.length} selected articles`}
343
+ >
344
+ {loadingSummaries && <Spinner />}
345
+ Get Summaries ({selectedArticles.length})
346
+ </button>
347
+ </>
348
+ )}
349
+ {summariesMode && (
350
+ <>
351
+ <button className="btn" onClick={resetSummaries}>Reset</button>
352
+ <label className="flex items-center gap-2 text-sm">
353
+ <input
354
+ type="checkbox"
355
+ checked={isPaginated}
356
+ onChange={(e) => {
357
+ setIsPaginated(e.target.checked)
358
+ setPageIndex(0) // Reset to first page when toggling
359
+ }}
360
+ className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
361
+ />
362
+ Paginated View
363
+ </label>
364
+ </>
365
+ )}
366
+ </div>
367
+ </div>
368
+ {!summariesMode ? (
369
+ <>
370
+ {!hasHighlights && (
371
+ summary ? (
372
+ <div className="max-h-96 overflow-y-auto">
373
+ <pre className="whitespace-pre-wrap text-sm text-gray-900">{summary}</pre>
374
+ </div>
375
+ ) : (
376
+ <div className="text-sm text-gray-500">No highlights yet.</div>
377
+ )
378
+ )}
379
+
380
+ {hasHighlights && articles.length === 0 && (
381
+ <div className="mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
382
+ <div className="text-sm text-yellow-800">
383
+ No articles found for the selected sources in the past 7 days.
384
+ </div>
385
+ </div>
386
+ )}
387
+
388
+ {articles.length > 0 && (
389
+ <div className="mt-4 border-t pt-4">
390
+ <div className="flex items-center justify-between mb-3">
391
+ <h4 className="text-sm font-medium text-gray-700">
392
+ Articles Found ({articles.length})
393
+ </h4>
394
+ <div className="flex gap-2">
395
+ <button
396
+ className="text-xs text-blue-600 hover:text-blue-800"
397
+ onClick={selectAllArticles}
398
+ >
399
+ Select All
400
+ </button>
401
+ <button
402
+ className="text-xs text-gray-600 hover:text-gray-800"
403
+ onClick={deselectAllArticles}
404
+ >
405
+ Deselect All
406
+ </button>
407
+ </div>
408
+ </div>
409
+ <div className="max-h-64 overflow-y-auto space-y-2">
410
+ {articles.map((article, i) => (
411
+ <div key={`${article.link}-${i}`} className="flex items-start gap-3 p-3 border rounded-lg hover:bg-gray-50">
412
+ <input
413
+ type="checkbox"
414
+ checked={selectedArticles.includes(article.link)}
415
+ onChange={() => toggleArticleSelection(article.link)}
416
+ className="mt-1 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
417
+ />
418
+ <div className="flex-1 min-w-0">
419
+ <a
420
+ href={article.link}
421
+ target="_blank"
422
+ rel="noopener noreferrer"
423
+ className="text-sm font-medium text-blue-600 hover:text-blue-800 block truncate"
424
+ >
425
+ {article.title}
426
+ </a>
427
+ <div className="text-xs text-gray-500 mt-1">
428
+ {article.source} {article.published && `• ${article.published}`}
429
+ </div>
430
+ {article.summary && (
431
+ <div className="text-xs text-gray-600 mt-1 line-clamp-2">
432
+ {article.summary}
433
+ </div>
434
+ )}
435
+ </div>
436
+ </div>
437
+ ))}
438
+ </div>
439
+ <div className="mt-3 text-xs text-gray-600">
440
+ {selectedArticles.length} of {articles.length} articles selected
441
+ </div>
442
+ </div>
443
+ )}
444
+ </>
445
+ ) : (
446
+ <>
447
+ {isPaginated ? (
448
+ // Paginated view - one summary per page
449
+ <div className="space-y-4">
450
+ {highlights.length > 0 && (
451
+ <div className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
452
+ <div className="flex items-start justify-between mb-2">
453
+ <a
454
+ href={highlights[pageIndex]?.link}
455
+ target="_blank"
456
+ rel="noopener noreferrer"
457
+ className="text-lg font-semibold text-blue-600 hover:text-blue-800 leading-tight"
458
+ >
459
+ {highlights[pageIndex]?.title}
460
+ </a>
461
+ </div>
462
+ <div className="text-sm text-gray-500 mb-3">
463
+ {highlights[pageIndex]?.source}
464
+ </div>
465
+ <div className="prose prose-sm max-w-none">
466
+ <div className="text-gray-700 whitespace-pre-wrap">{highlights[pageIndex]?.summary}</div>
467
+ </div>
468
+ </div>
469
+ )}
470
+
471
+ {/* Pagination controls */}
472
+ {highlights.length > 1 && (
473
+ <div className="flex items-center justify-center gap-4 mt-4">
474
+ <button
475
+ className="px-3 py-1 border rounded text-sm disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
476
+ onClick={() => setPageIndex(Math.max(0, pageIndex - 1))}
477
+ disabled={pageIndex === 0}
478
+ >
479
+ ← Previous
480
+ </button>
481
+ <span className="text-sm text-gray-600">
482
+ {pageIndex + 1} of {highlights.length}
483
+ </span>
484
+ <button
485
+ className="px-3 py-1 border rounded text-sm disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
486
+ onClick={() => setPageIndex(Math.min(highlights.length - 1, pageIndex + 1))}
487
+ disabled={pageIndex === highlights.length - 1}
488
+ >
489
+ Next →
490
+ </button>
491
+ </div>
492
+ )}
493
+ </div>
494
+ ) : (
495
+ // List view - all summaries at once (original behavior)
496
+ <div className="max-h-96 overflow-y-auto space-y-4">
497
+ {highlights.map((it, i) => (
498
+ <div key={`${it.link}-${i}`} className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
499
+ <div className="flex items-start justify-between mb-2">
500
+ <a
501
+ href={it.link}
502
+ target="_blank"
503
+ rel="noopener noreferrer"
504
+ className="text-lg font-semibold text-blue-600 hover:text-blue-800 leading-tight"
505
+ >
506
+ {it.title}
507
+ </a>
508
+ </div>
509
+ <div className="text-sm text-gray-500 mb-3">
510
+ {it.source}
511
+ </div>
512
+ <div className="prose prose-sm max-w-none">
513
+ <div className="text-gray-700 whitespace-pre-wrap">{it.summary}</div>
514
+ </div>
515
+ </div>
516
+ ))}
517
+ <div className="text-center text-sm text-gray-600 pt-2">
518
+ {highlights.length} summaries generated
519
+ </div>
520
+ </div>
521
+ )}
522
+ </>
523
+ )}
524
+ </div>
525
+ <div className="card p-4">
526
+ <div className="mb-2 flex items-center justify-between">
527
+ <h3 className="text-sm font-semibold">X/Tweets</h3>
528
+ <div className="flex gap-2">
529
+ <button className="btn flex items-center gap-2" onClick={makeTweets} disabled={highlights.length === 0 || loadingTweets}>
530
+ {loadingTweets && <Spinner />}
531
+ Generate X/Tweets
532
+ </button>
533
+ </div>
534
+ </div>
535
+ {tweets.length > 0 ? (
536
+ <TweetCards
537
+ tweets={tweets}
538
+ onEdit={(tweet) => openTweetEditor(tweet)}
539
+ currentEditingTweetId={currentEditingTweetId}
540
+ tweetConversations={tweetConversations}
541
+ pendingTweetUpdate={pendingTweetUpdate}
542
+ highlights={highlights}
543
+ onSendMessage={sendTweetMessage}
544
+ onAcceptUpdate={acceptTweetUpdate}
545
+ onRejectUpdate={rejectTweetUpdate}
546
+ onCloseEditor={() => {
547
+ setCurrentEditingTweetId(null)
548
+ setPendingTweetUpdate(null)
549
+ }}
550
+ />
551
+ ) : (
552
+ <div className="text-sm text-gray-500">No tweets yet.</div>
553
+ )}
554
+ </div>
555
+ <div className="card p-4">
556
+ <div className="mb-2 flex items-center justify-between">
557
+ <h3 className="text-sm font-semibold">Newsletter</h3>
558
+ <div className="flex gap-2">
559
+ <button
560
+ className="btn flex items-center gap-2"
561
+ onClick={makeNewsletter}
562
+ disabled={highlights.length === 0 || loadingNewsletter}
563
+ title={highlights.length === 0 ? "Please create summaries first by clicking 'Get Summaries'" : "Generate newsletter from summaries"}
564
+ >
565
+ {loadingNewsletter && <Spinner />}
566
+ Generate
567
+ </button>
568
+ <button className="btn" onClick={() => openEditor('newsletter')} disabled={!newsletterHtml}>Edit with AI</button>
569
+ <button className="btn" onClick={download} disabled={!newsletterHtml}>Download</button>
570
+ </div>
571
+ </div>
572
+ <div className="overflow-hidden rounded-lg border">
573
+ {newsletterHtml ? <iframe srcDoc={newsletterHtml} className="h-[500px] w-full" /> : <div className="p-4 text-sm text-gray-500">No newsletter yet.</div>}
574
+ </div>
575
+ </div>
576
+ </div>
577
+ </div>
578
+
579
+ <EditorModal
580
+ open={editorOpen}
581
+ onClose={() => setEditorOpen(false)}
582
+ onDone={onEditorDone}
583
+ initialText={editorText}
584
+ title={editorTitle}
585
+ sessionId={sessionId}
586
+ apiBase={apiBase}
587
+ />
588
+ </main>
589
+ </div>
590
+ </div>
591
+ )
592
+ }
593
+
594
+ export default App
frontend/src/assets/react.svg ADDED
frontend/src/components/EditorModal.tsx ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react'
2
+
3
+ interface EditorModalProps {
4
+ open: boolean
5
+ onClose: () => void
6
+ onDone: (text: string) => void
7
+ initialText: string
8
+ title: string
9
+ sessionId: string
10
+ apiBase: string
11
+ }
12
+
13
+ export default function EditorModal({
14
+ open,
15
+ onClose,
16
+ onDone,
17
+ initialText,
18
+ title,
19
+ sessionId: _,
20
+ apiBase: __
21
+ }: EditorModalProps) {
22
+ const [text, setText] = useState(initialText)
23
+
24
+ if (!open) return null
25
+
26
+ return (
27
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
28
+ <div className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] flex flex-col">
29
+ <div className="flex items-center justify-between p-4 border-b">
30
+ <h2 className="text-lg font-semibold">{title}</h2>
31
+ <button
32
+ onClick={onClose}
33
+ className="text-gray-500 hover:text-gray-700"
34
+ >
35
+
36
+ </button>
37
+ </div>
38
+
39
+ <div className="flex-1 p-4 overflow-hidden">
40
+ <textarea
41
+ value={text}
42
+ onChange={(e) => setText(e.target.value)}
43
+ className="w-full h-full resize-none border rounded p-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
44
+ placeholder="Enter your text here..."
45
+ />
46
+ </div>
47
+
48
+ <div className="flex justify-end gap-2 p-4 border-t">
49
+ <button
50
+ onClick={onClose}
51
+ className="px-4 py-2 text-gray-600 border rounded hover:bg-gray-50"
52
+ >
53
+ Cancel
54
+ </button>
55
+ <button
56
+ onClick={() => {
57
+ onDone(text)
58
+ onClose()
59
+ }}
60
+ className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
61
+ >
62
+ Save
63
+ </button>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ )
68
+ }
frontend/src/components/FeedPicker.tsx ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from 'react'
2
+
3
+ export default function FeedPicker({
4
+ selected,
5
+ setSelected,
6
+ apiBase = '/api',
7
+ }: {
8
+ selected: string[]
9
+ setSelected: (s: string[]) => void
10
+ apiBase?: string
11
+ }) {
12
+ const [defaults, setDefaults] = useState<Record<string, string>>({})
13
+ // const [custom, setCustom] = useState('') // Temporarily disabled
14
+
15
+ useEffect(() => {
16
+ let cancelled = false
17
+ fetch(`${apiBase}/defaults`).then(r => r.json()).then((data: Record<string, string>) => {
18
+ if (cancelled) return
19
+ setDefaults(data)
20
+ // Default select all if none selected yet
21
+ if (selected.length === 0) {
22
+ const all = Object.values(data)
23
+ setSelected(all)
24
+ }
25
+ }).catch(console.error)
26
+ return () => { cancelled = true }
27
+ }, [apiBase])
28
+
29
+ function toggle(url: string) {
30
+ setSelected(selected.includes(url) ? selected.filter(u => u !== url) : [...selected, url])
31
+ }
32
+
33
+ // function addCustom() { // Temporarily disabled
34
+ // try {
35
+ // const url = new URL(custom).toString()
36
+ // if (!selected.includes(url)) setSelected([...selected, url])
37
+ // setCustom('')
38
+ // } catch { /* ignore invalid */ }
39
+ // }
40
+
41
+ return (
42
+ <div className="card p-4">
43
+ <h3 className="mb-3 text-sm font-semibold">Sources</h3>
44
+ <div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
45
+ {Object.entries(defaults).map(([name, url]) => (
46
+ <label key={url} className="flex items-center gap-2">
47
+ <input type="checkbox" checked={selected.includes(url)} onChange={() => toggle(url)} />
48
+ <span className="text-sm">{name}</span>
49
+ </label>
50
+ ))}
51
+ </div>
52
+ {/* Custom RSS URL input - temporarily disabled */}
53
+ {/*
54
+ <div className="mt-4 flex gap-2">
55
+ <input
56
+ type="text"
57
+ placeholder="Custom RSS URL"
58
+ className="input flex-1"
59
+ value={custom}
60
+ onChange={e => setCustom(e.target.value)}
61
+ />
62
+ <button className="btn" onClick={addCustom}>Add</button>
63
+ </div>
64
+ */}
65
+ </div>
66
+ )
67
+ }
frontend/src/components/TweetCards.tsx ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ type Tweet = {
2
+ id: string
3
+ content: string
4
+ summary_title: string
5
+ summary_link: string
6
+ summary_source: string
7
+ }
8
+
9
+ type Highlight = {
10
+ title: string
11
+ link: string
12
+ source?: string
13
+ summary: string
14
+ }
15
+
16
+ export default function TweetCards({
17
+ tweets,
18
+ onEdit,
19
+ currentEditingTweetId,
20
+ tweetConversations,
21
+ pendingTweetUpdate,
22
+ highlights: _highlights,
23
+ onSendMessage,
24
+ onAcceptUpdate,
25
+ onRejectUpdate,
26
+ onCloseEditor
27
+ }: {
28
+ tweets: Tweet[],
29
+ onEdit: (tweet: Tweet) => void,
30
+ currentEditingTweetId: string | null,
31
+ tweetConversations: Record<string, Array<{role: string, content: string}>>,
32
+ pendingTweetUpdate: string | null,
33
+ highlights: Highlight[],
34
+ onSendMessage: (message: string) => Promise<string | undefined>,
35
+ onAcceptUpdate: () => void,
36
+ onRejectUpdate: () => void,
37
+ onCloseEditor: () => void
38
+ }) {
39
+ const XLogo = () => (
40
+ <svg viewBox="0 0 24 24" className="h-5 w-5 fill-current" aria-hidden="true">
41
+ <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
42
+ </svg>
43
+ )
44
+
45
+ return (
46
+ <div className="space-y-4">
47
+ {tweets.map((tweet) => (
48
+ <div key={tweet.id} className="bg-white border border-gray-200 rounded-xl p-4 hover:bg-gray-50 transition-colors">
49
+ {/* Header */}
50
+ <div className="flex items-start space-x-3">
51
+ <div className="flex-shrink-0">
52
+ <div className="w-10 h-10 bg-black rounded-full flex items-center justify-center">
53
+ <XLogo />
54
+ </div>
55
+ </div>
56
+ <div className="flex-1 min-w-0">
57
+ <div className="flex items-center space-x-2">
58
+ <div className="text-sm font-bold text-gray-900">AI Newsletter</div>
59
+ <div className="text-sm text-gray-500">@AI_Newsletter</div>
60
+ <div className="text-sm text-gray-500">·</div>
61
+ <div className="text-sm text-gray-500">now</div>
62
+ </div>
63
+
64
+ {/* Tweet Content */}
65
+ <div className="mt-2">
66
+ <div className="text-gray-900 whitespace-pre-wrap break-words">
67
+ {tweet.content}
68
+ </div>
69
+ </div>
70
+
71
+ {/* Source Link */}
72
+ <div className="mt-3 p-3 border border-gray-200 rounded-lg bg-gray-50">
73
+ <div className="text-xs text-gray-500 mb-1">{tweet.summary_source}</div>
74
+ <a
75
+ href={tweet.summary_link}
76
+ target="_blank"
77
+ rel="noopener noreferrer"
78
+ className="text-sm font-medium text-blue-600 hover:text-blue-800 line-clamp-2"
79
+ >
80
+ {tweet.summary_title}
81
+ </a>
82
+ </div>
83
+
84
+ {/* Actions */}
85
+ <div className="mt-4 flex items-center justify-between">
86
+ <div className="flex items-center space-x-6 text-gray-500">
87
+ <button className="flex items-center space-x-2 hover:text-blue-500 transition-colors">
88
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
89
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
90
+ </svg>
91
+ <span className="text-sm">0</span>
92
+ </button>
93
+ <button className="flex items-center space-x-2 hover:text-green-500 transition-colors">
94
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
95
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
96
+ </svg>
97
+ <span className="text-sm">0</span>
98
+ </button>
99
+ <button className="flex items-center space-x-2 hover:text-red-500 transition-colors">
100
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
101
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
102
+ </svg>
103
+ <span className="text-sm">0</span>
104
+ </button>
105
+ <button className="flex items-center space-x-2 hover:text-blue-500 transition-colors">
106
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
107
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z" />
108
+ </svg>
109
+ <span className="text-sm">0</span>
110
+ </button>
111
+ </div>
112
+ {currentEditingTweetId !== tweet.id && (
113
+ <button
114
+ onClick={() => onEdit(tweet)}
115
+ className="px-3 py-1 text-sm bg-blue-500 text-white rounded-full hover:bg-blue-600 transition-colors"
116
+ >
117
+ Edit with AI
118
+ </button>
119
+ )}
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ {/* Inline Chatbot - Show only for the currently editing tweet */}
125
+ {currentEditingTweetId === tweet.id && (
126
+ <div className="mt-4 border-t border-gray-200 pt-4">
127
+ <div className="bg-gray-50 rounded-lg p-4">
128
+ <div className="flex items-center justify-between mb-3">
129
+ <h4 className="text-sm font-medium text-gray-700">Edit Tweet with AI</h4>
130
+ <button
131
+ onClick={onCloseEditor}
132
+ className="text-gray-500 hover:text-gray-700"
133
+ >
134
+
135
+ </button>
136
+ </div>
137
+
138
+ {/* Current Tweet Preview */}
139
+ <div className="mb-4 p-3 bg-white rounded border">
140
+ <div className="text-xs font-medium text-gray-600 mb-1">Current Tweet:</div>
141
+ <div className="text-sm text-gray-900">{tweet.content}</div>
142
+ <div className="text-xs text-gray-500 mt-1">
143
+ About: {tweet.summary_title}
144
+ </div>
145
+ </div>
146
+
147
+ {/* Conversation History */}
148
+ <div className="max-h-64 overflow-y-auto mb-4">
149
+ <div className="space-y-2">
150
+ {(tweetConversations[tweet.id] || []).map((msg, i) => {
151
+ const isLastMessage = i === (tweetConversations[tweet.id] || []).length - 1
152
+ const isAIMessage = msg.role === 'assistant'
153
+ const hasUpdateInMessage = pendingTweetUpdate && isLastMessage && isAIMessage
154
+
155
+ return (
156
+ <div key={i}>
157
+ <div className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
158
+ <div className={`max-w-[80%] p-2 rounded text-sm ${
159
+ msg.role === 'user'
160
+ ? 'bg-blue-500 text-white'
161
+ : 'bg-white border text-gray-900'
162
+ }`}>
163
+ {msg.content}
164
+ </div>
165
+ </div>
166
+
167
+ {/* Show accept/reject buttons after the last AI message with an update */}
168
+ {hasUpdateInMessage && (
169
+ <div className="mt-2 ml-auto max-w-[80%]">
170
+ <div className="p-3 bg-blue-50 border border-blue-200 rounded">
171
+ <div className="flex items-center justify-between mb-1">
172
+ <div className="text-xs font-medium text-blue-700">Suggested Tweet:</div>
173
+ <div className={`text-xs font-mono ${
174
+ pendingTweetUpdate.length > 280 ? 'text-red-600' :
175
+ pendingTweetUpdate.length > 260 ? 'text-yellow-600' : 'text-green-600'
176
+ }`}>
177
+ {pendingTweetUpdate.length}/280
178
+ </div>
179
+ </div>
180
+ <div className="text-sm text-gray-900 mb-2 p-2 bg-white rounded border">
181
+ {pendingTweetUpdate}
182
+ </div>
183
+ <div className="flex gap-2">
184
+ <button
185
+ onClick={onAcceptUpdate}
186
+ className="px-2 py-1 bg-green-500 text-white text-xs rounded hover:bg-green-600 transition-colors"
187
+ disabled={pendingTweetUpdate.length > 280}
188
+ >
189
+ ✓ Accept
190
+ </button>
191
+ <button
192
+ onClick={onRejectUpdate}
193
+ className="px-2 py-1 bg-red-500 text-white text-xs rounded hover:bg-red-600 transition-colors"
194
+ >
195
+ ✗ Reject
196
+ </button>
197
+ </div>
198
+ {pendingTweetUpdate.length > 280 && (
199
+ <div className="mt-1 text-xs text-red-600">
200
+ Tweet is too long! Ask the AI to shorten it.
201
+ </div>
202
+ )}
203
+ </div>
204
+ </div>
205
+ )}
206
+ </div>
207
+ )
208
+ })}
209
+ </div>
210
+ </div>
211
+
212
+ {/* Message Input */}
213
+ <form onSubmit={async (e) => {
214
+ e.preventDefault()
215
+ const formData = new FormData(e.target as HTMLFormElement)
216
+ const message = formData.get('message') as string
217
+ if (!message.trim()) return
218
+
219
+ // Clear input
220
+ const form = e.target as HTMLFormElement
221
+ form.reset()
222
+
223
+ // Send message and get AI response
224
+ await onSendMessage(message)
225
+ }}>
226
+ <div className="flex gap-2">
227
+ <input
228
+ name="message"
229
+ type="text"
230
+ placeholder="Tell me how to improve this tweet..."
231
+ className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
232
+ />
233
+ <button
234
+ type="submit"
235
+ className="px-3 py-2 bg-blue-500 text-white text-sm rounded hover:bg-blue-600 transition-colors"
236
+ >
237
+ Send
238
+ </button>
239
+ </div>
240
+ </form>
241
+ </div>
242
+ </div>
243
+ )}
244
+ </div>
245
+ ))}
246
+ </div>
247
+ )
248
+ }
frontend/src/index.css ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
frontend/src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.tsx'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
frontend/src/vite-env.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ /// <reference types="vite/client" />
frontend/tailwind.config.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{js,ts,jsx,tsx}",
6
+ ],
7
+ theme: {
8
+ extend: {},
9
+ },
10
+ plugins: [],
11
+ }
frontend/tsconfig.app.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+
18
+ /* Linting */
19
+ "strict": true,
20
+ "noUnusedLocals": true,
21
+ "noUnusedParameters": true,
22
+ "erasableSyntaxOnly": true,
23
+ "noFallthroughCasesInSwitch": true,
24
+ "noUncheckedSideEffectImports": true
25
+ },
26
+ "include": ["src"]
27
+ }
frontend/tsconfig.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
frontend/tsconfig.node.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "verbatimModuleSyntax": true,
13
+ "moduleDetection": "force",
14
+ "noEmit": true,
15
+
16
+ /* Linting */
17
+ "strict": true,
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "erasableSyntaxOnly": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "noUncheckedSideEffectImports": true
23
+ },
24
+ "include": ["vite.config.ts"]
25
+ }
frontend/vite.config.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ })
pyproject.toml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "ai-newsletter"
3
+ version = "0.1.0"
4
+ description = "AI Newsletter Generator - Full Stack Application"
5
+ requires-python = ">=3.12"
6
+ dependencies = [
7
+ "fastapi>=0.116.0",
8
+ "uvicorn>=0.35.0",
9
+ "openai>=1.99.0",
10
+ "feedparser>=6.0.11",
11
+ "pydantic>=2.11.0",
12
+ "httpx>=0.28.0",
13
+ "python-dateutil>=2.9.0",
14
+ "python-dotenv>=1.1.0",
15
+ ]
16
+
17
+ [tool.uv]
18
+ dev-dependencies = []
19
+
20
+
requirements.txt ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file was autogenerated by uv via the following command:
2
+ # uv export --format requirements-txt --no-hashes
3
+ annotated-types==0.7.0
4
+ # via pydantic
5
+ anyio==4.10.0
6
+ # via
7
+ # httpx
8
+ # openai
9
+ # starlette
10
+ certifi==2025.8.3
11
+ # via
12
+ # httpcore
13
+ # httpx
14
+ click==8.2.1
15
+ # via uvicorn
16
+ colorama==0.4.6 ; sys_platform == 'win32'
17
+ # via
18
+ # click
19
+ # tqdm
20
+ distro==1.9.0
21
+ # via openai
22
+ fastapi==0.116.1
23
+ # via ai-newsletter
24
+ feedparser==6.0.11
25
+ # via ai-newsletter
26
+ h11==0.16.0
27
+ # via
28
+ # httpcore
29
+ # uvicorn
30
+ httpcore==1.0.9
31
+ # via httpx
32
+ httpx==0.28.1
33
+ # via
34
+ # ai-newsletter
35
+ # openai
36
+ idna==3.10
37
+ # via
38
+ # anyio
39
+ # httpx
40
+ jiter==0.10.0
41
+ # via openai
42
+ openai==1.99.5
43
+ # via ai-newsletter
44
+ pydantic==2.11.7
45
+ # via
46
+ # ai-newsletter
47
+ # fastapi
48
+ # openai
49
+ pydantic-core==2.33.2
50
+ # via pydantic
51
+ python-dateutil==2.9.0.post0
52
+ # via ai-newsletter
53
+ python-dotenv==1.1.1
54
+ # via ai-newsletter
55
+ sgmllib3k==1.0.0
56
+ # via feedparser
57
+ six==1.17.0
58
+ # via python-dateutil
59
+ sniffio==1.3.1
60
+ # via
61
+ # anyio
62
+ # openai
63
+ starlette==0.47.2
64
+ # via fastapi
65
+ tqdm==4.67.1
66
+ # via openai
67
+ typing-extensions==4.14.1
68
+ # via
69
+ # anyio
70
+ # fastapi
71
+ # openai
72
+ # pydantic
73
+ # pydantic-core
74
+ # starlette
75
+ # typing-inspection
76
+ typing-inspection==0.4.1
77
+ # via pydantic
78
+ uvicorn==0.35.0
79
+ # via ai-newsletter
uv.lock ADDED
@@ -0,0 +1,399 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version = 1
2
+ revision = 1
3
+ requires-python = ">=3.12"
4
+ resolution-markers = [
5
+ "python_full_version >= '3.13'",
6
+ "python_full_version < '3.13'",
7
+ ]
8
+
9
+ [[package]]
10
+ name = "ai-newsletter"
11
+ version = "0.1.0"
12
+ source = { virtual = "." }
13
+ dependencies = [
14
+ { name = "fastapi" },
15
+ { name = "feedparser" },
16
+ { name = "httpx" },
17
+ { name = "openai" },
18
+ { name = "pydantic" },
19
+ { name = "python-dateutil" },
20
+ { name = "python-dotenv" },
21
+ { name = "uvicorn" },
22
+ ]
23
+
24
+ [package.metadata]
25
+ requires-dist = [
26
+ { name = "fastapi", specifier = ">=0.116.0" },
27
+ { name = "feedparser", specifier = ">=6.0.11" },
28
+ { name = "httpx", specifier = ">=0.28.0" },
29
+ { name = "openai", specifier = ">=1.99.0" },
30
+ { name = "pydantic", specifier = ">=2.11.0" },
31
+ { name = "python-dateutil", specifier = ">=2.9.0" },
32
+ { name = "python-dotenv", specifier = ">=1.1.0" },
33
+ { name = "uvicorn", specifier = ">=0.35.0" },
34
+ ]
35
+
36
+ [package.metadata.requires-dev]
37
+ dev = []
38
+
39
+ [[package]]
40
+ name = "annotated-types"
41
+ version = "0.7.0"
42
+ source = { registry = "https://pypi.org/simple" }
43
+ sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
44
+ wheels = [
45
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
46
+ ]
47
+
48
+ [[package]]
49
+ name = "anyio"
50
+ version = "4.10.0"
51
+ source = { registry = "https://pypi.org/simple" }
52
+ dependencies = [
53
+ { name = "idna" },
54
+ { name = "sniffio" },
55
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
56
+ ]
57
+ sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252 }
58
+ wheels = [
59
+ { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213 },
60
+ ]
61
+
62
+ [[package]]
63
+ name = "certifi"
64
+ version = "2025.8.3"
65
+ source = { registry = "https://pypi.org/simple" }
66
+ sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386 }
67
+ wheels = [
68
+ { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216 },
69
+ ]
70
+
71
+ [[package]]
72
+ name = "click"
73
+ version = "8.2.1"
74
+ source = { registry = "https://pypi.org/simple" }
75
+ dependencies = [
76
+ { name = "colorama", marker = "sys_platform == 'win32'" },
77
+ ]
78
+ sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 }
79
+ wheels = [
80
+ { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 },
81
+ ]
82
+
83
+ [[package]]
84
+ name = "colorama"
85
+ version = "0.4.6"
86
+ source = { registry = "https://pypi.org/simple" }
87
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
88
+ wheels = [
89
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
90
+ ]
91
+
92
+ [[package]]
93
+ name = "distro"
94
+ version = "1.9.0"
95
+ source = { registry = "https://pypi.org/simple" }
96
+ sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 }
97
+ wheels = [
98
+ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 },
99
+ ]
100
+
101
+ [[package]]
102
+ name = "fastapi"
103
+ version = "0.116.1"
104
+ source = { registry = "https://pypi.org/simple" }
105
+ dependencies = [
106
+ { name = "pydantic" },
107
+ { name = "starlette" },
108
+ { name = "typing-extensions" },
109
+ ]
110
+ sdist = { url = "https://files.pythonhosted.org/packages/78/d7/6c8b3bfe33eeffa208183ec037fee0cce9f7f024089ab1c5d12ef04bd27c/fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143", size = 296485 }
111
+ wheels = [
112
+ { url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631 },
113
+ ]
114
+
115
+ [[package]]
116
+ name = "feedparser"
117
+ version = "6.0.11"
118
+ source = { registry = "https://pypi.org/simple" }
119
+ dependencies = [
120
+ { name = "sgmllib3k" },
121
+ ]
122
+ sdist = { url = "https://files.pythonhosted.org/packages/ff/aa/7af346ebeb42a76bf108027fe7f3328bb4e57a3a96e53e21fd9ef9dd6dd0/feedparser-6.0.11.tar.gz", hash = "sha256:c9d0407b64c6f2a065d0ebb292c2b35c01050cc0dc33757461aaabdc4c4184d5", size = 286197 }
123
+ wheels = [
124
+ { url = "https://files.pythonhosted.org/packages/7c/d4/8c31aad9cc18f451c49f7f9cfb5799dadffc88177f7917bc90a66459b1d7/feedparser-6.0.11-py3-none-any.whl", hash = "sha256:0be7ee7b395572b19ebeb1d6aafb0028dee11169f1c934e0ed67d54992f4ad45", size = 81343 },
125
+ ]
126
+
127
+ [[package]]
128
+ name = "h11"
129
+ version = "0.16.0"
130
+ source = { registry = "https://pypi.org/simple" }
131
+ sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 }
132
+ wheels = [
133
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
134
+ ]
135
+
136
+ [[package]]
137
+ name = "httpcore"
138
+ version = "1.0.9"
139
+ source = { registry = "https://pypi.org/simple" }
140
+ dependencies = [
141
+ { name = "certifi" },
142
+ { name = "h11" },
143
+ ]
144
+ sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 }
145
+ wheels = [
146
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 },
147
+ ]
148
+
149
+ [[package]]
150
+ name = "httpx"
151
+ version = "0.28.1"
152
+ source = { registry = "https://pypi.org/simple" }
153
+ dependencies = [
154
+ { name = "anyio" },
155
+ { name = "certifi" },
156
+ { name = "httpcore" },
157
+ { name = "idna" },
158
+ ]
159
+ sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
160
+ wheels = [
161
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
162
+ ]
163
+
164
+ [[package]]
165
+ name = "idna"
166
+ version = "3.10"
167
+ source = { registry = "https://pypi.org/simple" }
168
+ sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
169
+ wheels = [
170
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
171
+ ]
172
+
173
+ [[package]]
174
+ name = "jiter"
175
+ version = "0.10.0"
176
+ source = { registry = "https://pypi.org/simple" }
177
+ sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759 }
178
+ wheels = [
179
+ { url = "https://files.pythonhosted.org/packages/6d/b5/348b3313c58f5fbfb2194eb4d07e46a35748ba6e5b3b3046143f3040bafa/jiter-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1e274728e4a5345a6dde2d343c8da018b9d4bd4350f5a472fa91f66fda44911b", size = 312262 },
180
+ { url = "https://files.pythonhosted.org/packages/9c/4a/6a2397096162b21645162825f058d1709a02965606e537e3304b02742e9b/jiter-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7202ae396446c988cb2a5feb33a543ab2165b786ac97f53b59aafb803fef0744", size = 320124 },
181
+ { url = "https://files.pythonhosted.org/packages/2a/85/1ce02cade7516b726dd88f59a4ee46914bf79d1676d1228ef2002ed2f1c9/jiter-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2", size = 345330 },
182
+ { url = "https://files.pythonhosted.org/packages/75/d0/bb6b4f209a77190ce10ea8d7e50bf3725fc16d3372d0a9f11985a2b23eff/jiter-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026", size = 369670 },
183
+ { url = "https://files.pythonhosted.org/packages/a0/f5/a61787da9b8847a601e6827fbc42ecb12be2c925ced3252c8ffcb56afcaf/jiter-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c", size = 489057 },
184
+ { url = "https://files.pythonhosted.org/packages/12/e4/6f906272810a7b21406c760a53aadbe52e99ee070fc5c0cb191e316de30b/jiter-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959", size = 389372 },
185
+ { url = "https://files.pythonhosted.org/packages/e2/ba/77013b0b8ba904bf3762f11e0129b8928bff7f978a81838dfcc958ad5728/jiter-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a", size = 352038 },
186
+ { url = "https://files.pythonhosted.org/packages/67/27/c62568e3ccb03368dbcc44a1ef3a423cb86778a4389e995125d3d1aaa0a4/jiter-0.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6842184aed5cdb07e0c7e20e5bdcfafe33515ee1741a6835353bb45fe5d1bd95", size = 391538 },
187
+ { url = "https://files.pythonhosted.org/packages/c0/72/0d6b7e31fc17a8fdce76164884edef0698ba556b8eb0af9546ae1a06b91d/jiter-0.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea", size = 523557 },
188
+ { url = "https://files.pythonhosted.org/packages/2f/09/bc1661fbbcbeb6244bd2904ff3a06f340aa77a2b94e5a7373fd165960ea3/jiter-0.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b", size = 514202 },
189
+ { url = "https://files.pythonhosted.org/packages/1b/84/5a5d5400e9d4d54b8004c9673bbe4403928a00d28529ff35b19e9d176b19/jiter-0.10.0-cp312-cp312-win32.whl", hash = "sha256:8be921f0cadd245e981b964dfbcd6fd4bc4e254cdc069490416dd7a2632ecc01", size = 211781 },
190
+ { url = "https://files.pythonhosted.org/packages/9b/52/7ec47455e26f2d6e5f2ea4951a0652c06e5b995c291f723973ae9e724a65/jiter-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7c7d785ae9dda68c2678532a5a1581347e9c15362ae9f6e68f3fdbfb64f2e49", size = 206176 },
191
+ { url = "https://files.pythonhosted.org/packages/2e/b0/279597e7a270e8d22623fea6c5d4eeac328e7d95c236ed51a2b884c54f70/jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644", size = 311617 },
192
+ { url = "https://files.pythonhosted.org/packages/91/e3/0916334936f356d605f54cc164af4060e3e7094364add445a3bc79335d46/jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a", size = 318947 },
193
+ { url = "https://files.pythonhosted.org/packages/6a/8e/fd94e8c02d0e94539b7d669a7ebbd2776e51f329bb2c84d4385e8063a2ad/jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6", size = 344618 },
194
+ { url = "https://files.pythonhosted.org/packages/6f/b0/f9f0a2ec42c6e9c2e61c327824687f1e2415b767e1089c1d9135f43816bd/jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3", size = 368829 },
195
+ { url = "https://files.pythonhosted.org/packages/e8/57/5bbcd5331910595ad53b9fd0c610392ac68692176f05ae48d6ce5c852967/jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2", size = 491034 },
196
+ { url = "https://files.pythonhosted.org/packages/9b/be/c393df00e6e6e9e623a73551774449f2f23b6ec6a502a3297aeeece2c65a/jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25", size = 388529 },
197
+ { url = "https://files.pythonhosted.org/packages/42/3e/df2235c54d365434c7f150b986a6e35f41ebdc2f95acea3036d99613025d/jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041", size = 350671 },
198
+ { url = "https://files.pythonhosted.org/packages/c6/77/71b0b24cbcc28f55ab4dbfe029f9a5b73aeadaba677843fc6dc9ed2b1d0a/jiter-0.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15acb267ea5e2c64515574b06a8bf393fbfee6a50eb1673614aa45f4613c0cca", size = 390864 },
199
+ { url = "https://files.pythonhosted.org/packages/6a/d3/ef774b6969b9b6178e1d1e7a89a3bd37d241f3d3ec5f8deb37bbd203714a/jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4", size = 522989 },
200
+ { url = "https://files.pythonhosted.org/packages/0c/41/9becdb1d8dd5d854142f45a9d71949ed7e87a8e312b0bede2de849388cb9/jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e", size = 513495 },
201
+ { url = "https://files.pythonhosted.org/packages/9c/36/3468e5a18238bdedae7c4d19461265b5e9b8e288d3f86cd89d00cbb48686/jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d", size = 211289 },
202
+ { url = "https://files.pythonhosted.org/packages/7e/07/1c96b623128bcb913706e294adb5f768fb7baf8db5e1338ce7b4ee8c78ef/jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4", size = 205074 },
203
+ { url = "https://files.pythonhosted.org/packages/54/46/caa2c1342655f57d8f0f2519774c6d67132205909c65e9aa8255e1d7b4f4/jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca", size = 318225 },
204
+ { url = "https://files.pythonhosted.org/packages/43/84/c7d44c75767e18946219ba2d703a5a32ab37b0bc21886a97bc6062e4da42/jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070", size = 350235 },
205
+ { url = "https://files.pythonhosted.org/packages/01/16/f5a0135ccd968b480daad0e6ab34b0c7c5ba3bc447e5088152696140dcb3/jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca", size = 207278 },
206
+ { url = "https://files.pythonhosted.org/packages/1c/9b/1d646da42c3de6c2188fdaa15bce8ecb22b635904fc68be025e21249ba44/jiter-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5e9251a5e83fab8d87799d3e1a46cb4b7f2919b895c6f4483629ed2446f66522", size = 310866 },
207
+ { url = "https://files.pythonhosted.org/packages/ad/0e/26538b158e8a7c7987e94e7aeb2999e2e82b1f9d2e1f6e9874ddf71ebda0/jiter-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:023aa0204126fe5b87ccbcd75c8a0d0261b9abdbbf46d55e7ae9f8e22424eeb8", size = 318772 },
208
+ { url = "https://files.pythonhosted.org/packages/7b/fb/d302893151caa1c2636d6574d213e4b34e31fd077af6050a9c5cbb42f6fb/jiter-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c189c4f1779c05f75fc17c0c1267594ed918996a231593a21a5ca5438445216", size = 344534 },
209
+ { url = "https://files.pythonhosted.org/packages/01/d8/5780b64a149d74e347c5128d82176eb1e3241b1391ac07935693466d6219/jiter-0.10.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15720084d90d1098ca0229352607cd68256c76991f6b374af96f36920eae13c4", size = 369087 },
210
+ { url = "https://files.pythonhosted.org/packages/e8/5b/f235a1437445160e777544f3ade57544daf96ba7e96c1a5b24a6f7ac7004/jiter-0.10.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f2fb68e5f1cfee30e2b2a09549a00683e0fde4c6a2ab88c94072fc33cb7426", size = 490694 },
211
+ { url = "https://files.pythonhosted.org/packages/85/a9/9c3d4617caa2ff89cf61b41e83820c27ebb3f7b5fae8a72901e8cd6ff9be/jiter-0.10.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce541693355fc6da424c08b7edf39a2895f58d6ea17d92cc2b168d20907dee12", size = 388992 },
212
+ { url = "https://files.pythonhosted.org/packages/68/b1/344fd14049ba5c94526540af7eb661871f9c54d5f5601ff41a959b9a0bbd/jiter-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31c50c40272e189d50006ad5c73883caabb73d4e9748a688b216e85a9a9ca3b9", size = 351723 },
213
+ { url = "https://files.pythonhosted.org/packages/41/89/4c0e345041186f82a31aee7b9d4219a910df672b9fef26f129f0cda07a29/jiter-0.10.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa3402a2ff9815960e0372a47b75c76979d74402448509ccd49a275fa983ef8a", size = 392215 },
214
+ { url = "https://files.pythonhosted.org/packages/55/58/ee607863e18d3f895feb802154a2177d7e823a7103f000df182e0f718b38/jiter-0.10.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:1956f934dca32d7bb647ea21d06d93ca40868b505c228556d3373cbd255ce853", size = 522762 },
215
+ { url = "https://files.pythonhosted.org/packages/15/d0/9123fb41825490d16929e73c212de9a42913d68324a8ce3c8476cae7ac9d/jiter-0.10.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:fcedb049bdfc555e261d6f65a6abe1d5ad68825b7202ccb9692636c70fcced86", size = 513427 },
216
+ { url = "https://files.pythonhosted.org/packages/d8/b3/2bd02071c5a2430d0b70403a34411fc519c2f227da7b03da9ba6a956f931/jiter-0.10.0-cp314-cp314-win32.whl", hash = "sha256:ac509f7eccca54b2a29daeb516fb95b6f0bd0d0d8084efaf8ed5dfc7b9f0b357", size = 210127 },
217
+ { url = "https://files.pythonhosted.org/packages/03/0c/5fe86614ea050c3ecd728ab4035534387cd41e7c1855ef6c031f1ca93e3f/jiter-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ed975b83a2b8639356151cef5c0d597c68376fc4922b45d0eb384ac058cfa00", size = 318527 },
218
+ { url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213 },
219
+ ]
220
+
221
+ [[package]]
222
+ name = "openai"
223
+ version = "1.99.5"
224
+ source = { registry = "https://pypi.org/simple" }
225
+ dependencies = [
226
+ { name = "anyio" },
227
+ { name = "distro" },
228
+ { name = "httpx" },
229
+ { name = "jiter" },
230
+ { name = "pydantic" },
231
+ { name = "sniffio" },
232
+ { name = "tqdm" },
233
+ { name = "typing-extensions" },
234
+ ]
235
+ sdist = { url = "https://files.pythonhosted.org/packages/2f/4a/16b1b6ee8a62cbfb59057f97f6d9b7bb5ce529047d80bc0b406f65dfdc48/openai-1.99.5.tar.gz", hash = "sha256:aa97ac3326cac7949c5e4ac0274c454c1d19c939760107ae0d3948fc26a924ca", size = 505144 }
236
+ wheels = [
237
+ { url = "https://files.pythonhosted.org/packages/e6/f2/2472ae020f5156a994710bf926a76915c71bc7b5debf7b81a11506ec8414/openai-1.99.5-py3-none-any.whl", hash = "sha256:4e870f9501b7c36132e2be13313ce3c4d6915a837e7a299c483aab6a6d4412e9", size = 786246 },
238
+ ]
239
+
240
+ [[package]]
241
+ name = "pydantic"
242
+ version = "2.11.7"
243
+ source = { registry = "https://pypi.org/simple" }
244
+ dependencies = [
245
+ { name = "annotated-types" },
246
+ { name = "pydantic-core" },
247
+ { name = "typing-extensions" },
248
+ { name = "typing-inspection" },
249
+ ]
250
+ sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350 }
251
+ wheels = [
252
+ { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782 },
253
+ ]
254
+
255
+ [[package]]
256
+ name = "pydantic-core"
257
+ version = "2.33.2"
258
+ source = { registry = "https://pypi.org/simple" }
259
+ dependencies = [
260
+ { name = "typing-extensions" },
261
+ ]
262
+ sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 }
263
+ wheels = [
264
+ { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000 },
265
+ { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996 },
266
+ { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957 },
267
+ { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199 },
268
+ { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296 },
269
+ { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109 },
270
+ { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028 },
271
+ { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044 },
272
+ { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881 },
273
+ { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034 },
274
+ { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187 },
275
+ { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628 },
276
+ { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866 },
277
+ { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894 },
278
+ { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 },
279
+ { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 },
280
+ { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 },
281
+ { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859 },
282
+ { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810 },
283
+ { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498 },
284
+ { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611 },
285
+ { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924 },
286
+ { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196 },
287
+ { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389 },
288
+ { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223 },
289
+ { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 },
290
+ { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 },
291
+ { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 },
292
+ { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 },
293
+ { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 },
294
+ { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 },
295
+ ]
296
+
297
+ [[package]]
298
+ name = "python-dateutil"
299
+ version = "2.9.0.post0"
300
+ source = { registry = "https://pypi.org/simple" }
301
+ dependencies = [
302
+ { name = "six" },
303
+ ]
304
+ sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
305
+ wheels = [
306
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
307
+ ]
308
+
309
+ [[package]]
310
+ name = "python-dotenv"
311
+ version = "1.1.1"
312
+ source = { registry = "https://pypi.org/simple" }
313
+ sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978 }
314
+ wheels = [
315
+ { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556 },
316
+ ]
317
+
318
+ [[package]]
319
+ name = "sgmllib3k"
320
+ version = "1.0.0"
321
+ source = { registry = "https://pypi.org/simple" }
322
+ sdist = { url = "https://files.pythonhosted.org/packages/9e/bd/3704a8c3e0942d711c1299ebf7b9091930adae6675d7c8f476a7ce48653c/sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9", size = 5750 }
323
+
324
+ [[package]]
325
+ name = "six"
326
+ version = "1.17.0"
327
+ source = { registry = "https://pypi.org/simple" }
328
+ sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
329
+ wheels = [
330
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
331
+ ]
332
+
333
+ [[package]]
334
+ name = "sniffio"
335
+ version = "1.3.1"
336
+ source = { registry = "https://pypi.org/simple" }
337
+ sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
338
+ wheels = [
339
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
340
+ ]
341
+
342
+ [[package]]
343
+ name = "starlette"
344
+ version = "0.47.2"
345
+ source = { registry = "https://pypi.org/simple" }
346
+ dependencies = [
347
+ { name = "anyio" },
348
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
349
+ ]
350
+ sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948 }
351
+ wheels = [
352
+ { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984 },
353
+ ]
354
+
355
+ [[package]]
356
+ name = "tqdm"
357
+ version = "4.67.1"
358
+ source = { registry = "https://pypi.org/simple" }
359
+ dependencies = [
360
+ { name = "colorama", marker = "sys_platform == 'win32'" },
361
+ ]
362
+ sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 }
363
+ wheels = [
364
+ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 },
365
+ ]
366
+
367
+ [[package]]
368
+ name = "typing-extensions"
369
+ version = "4.14.1"
370
+ source = { registry = "https://pypi.org/simple" }
371
+ sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673 }
372
+ wheels = [
373
+ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906 },
374
+ ]
375
+
376
+ [[package]]
377
+ name = "typing-inspection"
378
+ version = "0.4.1"
379
+ source = { registry = "https://pypi.org/simple" }
380
+ dependencies = [
381
+ { name = "typing-extensions" },
382
+ ]
383
+ sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 }
384
+ wheels = [
385
+ { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 },
386
+ ]
387
+
388
+ [[package]]
389
+ name = "uvicorn"
390
+ version = "0.35.0"
391
+ source = { registry = "https://pypi.org/simple" }
392
+ dependencies = [
393
+ { name = "click" },
394
+ { name = "h11" },
395
+ ]
396
+ sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473 }
397
+ wheels = [
398
+ { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406 },
399
+ ]