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": "[email protected]",
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([email protected])
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([email protected])
26
+ '@types/react-dom':
27
+ specifier: ^19.1.7
28
+ version: 19.1.7(@types/[email protected]([email protected]))
29
+ '@vitejs/plugin-react':
30
+ specifier: ^4.7.0
31
32
+ autoprefixer:
33
+ specifier: ^10.4.21
34
+ version: 10.4.21([email protected])
35
+ classnames:
36
+ specifier: ^2.5.1
37
+ version: 2.5.1
38
+ eslint:
39
+ specifier: ^9.32.0
40
+ version: 9.33.0([email protected])
41
+ eslint-plugin-react-hooks:
42
+ specifier: ^5.2.0
43
44
+ eslint-plugin-react-refresh:
45
+ specifier: ^0.4.20
46
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
62
+ vite:
63
+ specifier: ^7.1.0
64
65
+
66
+ packages:
67
+
68
+ '@alloc/[email protected]':
69
+ resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
70
+ engines: {node: '>=10'}
71
+
72
+ '@ampproject/[email protected]':
73
+ resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
74
+ engines: {node: '>=6.0.0'}
75
+
76
+ '@babel/[email protected]':
77
+ resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
78
+ engines: {node: '>=6.9.0'}
79
+
80
+ '@babel/[email protected]':
81
+ resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==}
82
+ engines: {node: '>=6.9.0'}
83
+
84
+ '@babel/[email protected]':
85
+ resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==}
86
+ engines: {node: '>=6.9.0'}
87
+
88
+ '@babel/[email protected]':
89
+ resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==}
90
+ engines: {node: '>=6.9.0'}
91
+
92
+ '@babel/[email protected]':
93
+ resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
94
+ engines: {node: '>=6.9.0'}
95
+
96
+ '@babel/[email protected]':
97
+ resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
98
+ engines: {node: '>=6.9.0'}
99
+
100
+ '@babel/[email protected]':
101
+ resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
102
+ engines: {node: '>=6.9.0'}
103
+
104
+ '@babel/[email protected]':
105
+ resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==}
106
+ engines: {node: '>=6.9.0'}
107
+ peerDependencies:
108
+ '@babel/core': ^7.0.0
109
+
110
+ '@babel/[email protected]':
111
+ resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
112
+ engines: {node: '>=6.9.0'}
113
+
114
+ '@babel/[email protected]':
115
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
116
+ engines: {node: '>=6.9.0'}
117
+
118
+ '@babel/[email protected]':
119
+ resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
120
+ engines: {node: '>=6.9.0'}
121
+
122
+ '@babel/[email protected]':
123
+ resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
124
+ engines: {node: '>=6.9.0'}
125
+
126
+ '@babel/[email protected]':
127
+ resolution: {integrity: sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==}
128
+ engines: {node: '>=6.9.0'}
129
+
130
+ '@babel/[email protected]':
131
+ resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==}
132
+ engines: {node: '>=6.0.0'}
133
+ hasBin: true
134
+
135
+ '@babel/[email protected]':
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/[email protected]':
142
+ resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
143
+ engines: {node: '>=6.9.0'}
144
+ peerDependencies:
145
+ '@babel/core': ^7.0.0-0
146
+
147
+ '@babel/[email protected]':
148
+ resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
149
+ engines: {node: '>=6.9.0'}
150
+
151
+ '@babel/[email protected]':
152
+ resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==}
153
+ engines: {node: '>=6.9.0'}
154
+
155
+ '@babel/[email protected]':
156
+ resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
157
+ engines: {node: '>=6.9.0'}
158
+
159
+ '@esbuild/[email protected]':
160
+ resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==}
161
+ engines: {node: '>=18'}
162
+ cpu: [ppc64]
163
+ os: [aix]
164
+
165
+ '@esbuild/[email protected]':
166
+ resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==}
167
+ engines: {node: '>=18'}
168
+ cpu: [arm64]
169
+ os: [android]
170
+
171
+ '@esbuild/[email protected]':
172
+ resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==}
173
+ engines: {node: '>=18'}
174
+ cpu: [arm]
175
+ os: [android]
176
+
177
+ '@esbuild/[email protected]':
178
+ resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==}
179
+ engines: {node: '>=18'}
180
+ cpu: [x64]
181
+ os: [android]
182
+
183
+ '@esbuild/[email protected]':
184
+ resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==}
185
+ engines: {node: '>=18'}
186
+ cpu: [arm64]
187
+ os: [darwin]
188
+
189
+ '@esbuild/[email protected]':
190
+ resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==}
191
+ engines: {node: '>=18'}
192
+ cpu: [x64]
193
+ os: [darwin]
194
+
195
+ '@esbuild/[email protected]':
196
+ resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==}
197
+ engines: {node: '>=18'}
198
+ cpu: [arm64]
199
+ os: [freebsd]
200
+
201
+ '@esbuild/[email protected]':
202
+ resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==}
203
+ engines: {node: '>=18'}
204
+ cpu: [x64]
205
+ os: [freebsd]
206
+
207
+ '@esbuild/[email protected]':
208
+ resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==}
209
+ engines: {node: '>=18'}
210
+ cpu: [arm64]
211
+ os: [linux]
212
+
213
+ '@esbuild/[email protected]':
214
+ resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==}
215
+ engines: {node: '>=18'}
216
+ cpu: [arm]
217
+ os: [linux]
218
+
219
+ '@esbuild/[email protected]':
220
+ resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==}
221
+ engines: {node: '>=18'}
222
+ cpu: [ia32]
223
+ os: [linux]
224
+
225
+ '@esbuild/[email protected]':
226
+ resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==}
227
+ engines: {node: '>=18'}
228
+ cpu: [loong64]
229
+ os: [linux]
230
+
231
+ '@esbuild/[email protected]':
232
+ resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==}
233
+ engines: {node: '>=18'}
234
+ cpu: [mips64el]
235
+ os: [linux]
236
+
237
+ '@esbuild/[email protected]':
238
+ resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==}
239
+ engines: {node: '>=18'}
240
+ cpu: [ppc64]
241
+ os: [linux]
242
+
243
+ '@esbuild/[email protected]':
244
+ resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==}
245
+ engines: {node: '>=18'}
246
+ cpu: [riscv64]
247
+ os: [linux]
248
+
249
+ '@esbuild/[email protected]':
250
+ resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==}
251
+ engines: {node: '>=18'}
252
+ cpu: [s390x]
253
+ os: [linux]
254
+
255
+ '@esbuild/[email protected]':
256
+ resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==}
257
+ engines: {node: '>=18'}
258
+ cpu: [x64]
259
+ os: [linux]
260
+
261
+ '@esbuild/[email protected]':
262
+ resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==}
263
+ engines: {node: '>=18'}
264
+ cpu: [arm64]
265
+ os: [netbsd]
266
+
267
+ '@esbuild/[email protected]':
268
+ resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==}
269
+ engines: {node: '>=18'}
270
+ cpu: [x64]
271
+ os: [netbsd]
272
+
273
+ '@esbuild/[email protected]':
274
+ resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==}
275
+ engines: {node: '>=18'}
276
+ cpu: [arm64]
277
+ os: [openbsd]
278
+
279
+ '@esbuild/[email protected]':
280
+ resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==}
281
+ engines: {node: '>=18'}
282
+ cpu: [x64]
283
+ os: [openbsd]
284
+
285
+ '@esbuild/[email protected]':
286
+ resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==}
287
+ engines: {node: '>=18'}
288
+ cpu: [arm64]
289
+ os: [openharmony]
290
+
291
+ '@esbuild/[email protected]':
292
+ resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==}
293
+ engines: {node: '>=18'}
294
+ cpu: [x64]
295
+ os: [sunos]
296
+
297
+ '@esbuild/[email protected]':
298
+ resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==}
299
+ engines: {node: '>=18'}
300
+ cpu: [arm64]
301
+ os: [win32]
302
+
303
+ '@esbuild/[email protected]':
304
+ resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==}
305
+ engines: {node: '>=18'}
306
+ cpu: [ia32]
307
+ os: [win32]
308
+
309
+ '@esbuild/[email protected]':
310
+ resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==}
311
+ engines: {node: '>=18'}
312
+ cpu: [x64]
313
+ os: [win32]
314
+
315
+ '@eslint-community/[email protected]':
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/[email protected]':
322
+ resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
323
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
324
+
325
+ '@eslint/[email protected]':
326
+ resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==}
327
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
328
+
329
+ '@eslint/[email protected]':
330
+ resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==}
331
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
332
+
333
+ '@eslint/[email protected]':
334
+ resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==}
335
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
336
+
337
+ '@eslint/[email protected]':
338
+ resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
339
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
340
+
341
+ '@eslint/[email protected]':
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/[email protected]':
346
+ resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
347
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
348
+
349
+ '@eslint/[email protected]':
350
+ resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==}
351
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
352
+
353
+ '@humanfs/[email protected]':
354
+ resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
355
+ engines: {node: '>=18.18.0'}
356
+
357
+ '@humanfs/[email protected]':
358
+ resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==}
359
+ engines: {node: '>=18.18.0'}
360
+
361
+ '@humanwhocodes/[email protected]':
362
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
363
+ engines: {node: '>=12.22'}
364
+
365
+ '@humanwhocodes/[email protected]':
366
+ resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
367
+ engines: {node: '>=18.18'}
368
+
369
+ '@humanwhocodes/[email protected]':
370
+ resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
371
+ engines: {node: '>=18.18'}
372
+
373
+ '@isaacs/[email protected]':
374
+ resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
375
+ engines: {node: '>=12'}
376
+
377
+ '@jridgewell/[email protected]':
378
+ resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
379
+
380
+ '@jridgewell/[email protected]':
381
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
382
+ engines: {node: '>=6.0.0'}
383
+
384
+ '@jridgewell/[email protected]':
385
+ resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
386
+
387
+ '@jridgewell/[email protected]':
388
+ resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
389
+
390
+ '@nodelib/[email protected]':
391
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
392
+ engines: {node: '>= 8'}
393
+
394
+ '@nodelib/[email protected]':
395
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
396
+ engines: {node: '>= 8'}
397
+
398
+ '@nodelib/[email protected]':
399
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
400
+ engines: {node: '>= 8'}
401
+
402
+ '@pkgjs/[email protected]':
403
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
404
+ engines: {node: '>=14'}
405
+
406
+ '@rolldown/[email protected]':
407
+ resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
408
+
409
+ '@rollup/[email protected]':
410
+ resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==}
411
+ cpu: [arm]
412
+ os: [android]
413
+
414
+ '@rollup/[email protected]':
415
+ resolution: {integrity: sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==}
416
+ cpu: [arm64]
417
+ os: [android]
418
+
419
+ '@rollup/[email protected]':
420
+ resolution: {integrity: sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==}
421
+ cpu: [arm64]
422
+ os: [darwin]
423
+
424
+ '@rollup/[email protected]':
425
+ resolution: {integrity: sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==}
426
+ cpu: [x64]
427
+ os: [darwin]
428
+
429
+ '@rollup/[email protected]':
430
+ resolution: {integrity: sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==}
431
+ cpu: [arm64]
432
+ os: [freebsd]
433
+
434
+ '@rollup/[email protected]':
435
+ resolution: {integrity: sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==}
436
+ cpu: [x64]
437
+ os: [freebsd]
438
+
439
+ '@rollup/[email protected]':
440
+ resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==}
441
+ cpu: [arm]
442
+ os: [linux]
443
+
444
+ '@rollup/[email protected]':
445
+ resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==}
446
+ cpu: [arm]
447
+ os: [linux]
448
+
449
+ '@rollup/[email protected]':
450
+ resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==}
451
+ cpu: [arm64]
452
+ os: [linux]
453
+
454
+ '@rollup/[email protected]':
455
+ resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==}
456
+ cpu: [arm64]
457
+ os: [linux]
458
+
459
+ '@rollup/[email protected]':
460
+ resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==}
461
+ cpu: [loong64]
462
+ os: [linux]
463
+
464
+ '@rollup/[email protected]':
465
+ resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==}
466
+ cpu: [ppc64]
467
+ os: [linux]
468
+
469
+ '@rollup/[email protected]':
470
+ resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==}
471
+ cpu: [riscv64]
472
+ os: [linux]
473
+
474
+ '@rollup/[email protected]':
475
+ resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==}
476
+ cpu: [riscv64]
477
+ os: [linux]
478
+
479
+ '@rollup/[email protected]':
480
+ resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==}
481
+ cpu: [s390x]
482
+ os: [linux]
483
+
484
+ '@rollup/[email protected]':
485
+ resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==}
486
+ cpu: [x64]
487
+ os: [linux]
488
+
489
+ '@rollup/[email protected]':
490
+ resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==}
491
+ cpu: [x64]
492
+ os: [linux]
493
+
494
+ '@rollup/[email protected]':
495
+ resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==}
496
+ cpu: [arm64]
497
+ os: [win32]
498
+
499
+ '@rollup/[email protected]':
500
+ resolution: {integrity: sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==}
501
+ cpu: [ia32]
502
+ os: [win32]
503
+
504
+ '@rollup/[email protected]':
505
+ resolution: {integrity: sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==}
506
+ cpu: [x64]
507
+ os: [win32]
508
+
509
+ '@types/[email protected]':
510
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
511
+
512
+ '@types/[email protected]':
513
+ resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
514
+
515
+ '@types/[email protected]':
516
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
517
+
518
+ '@types/[email protected]':
519
+ resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
520
+
521
+ '@types/[email protected]':
522
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
523
+
524
+ '@types/[email protected]':
525
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
526
+
527
+ '@types/[email protected]':
528
+ resolution: {integrity: sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==}
529
+ peerDependencies:
530
+ '@types/react': ^19.0.0
531
+
532
+ '@types/[email protected]':
533
+ resolution: {integrity: sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==}
534
+ peerDependencies:
535
+ react: '*'
536
+
537
+ '@typescript-eslint/[email protected]':
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/[email protected]':
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/[email protected]':
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/[email protected]':
559
+ resolution: {integrity: sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==}
560
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
561
+
562
+ '@typescript-eslint/[email protected]':
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/[email protected]':
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/[email protected]':
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/[email protected]':
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/[email protected]':
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/[email protected]':
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/[email protected]':
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
603
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
604
+ peerDependencies:
605
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
606
+
607
608
+ resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
609
+ engines: {node: '>=0.4.0'}
610
+ hasBin: true
611
+
612
613
+ resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
614
+
615
616
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
617
+ engines: {node: '>=8'}
618
+
619
620
+ resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
621
+ engines: {node: '>=12'}
622
+
623
624
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
625
+ engines: {node: '>=8'}
626
+
627
628
+ resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
629
+ engines: {node: '>=12'}
630
+
631
632
+ resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
633
+
634
635
+ resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
636
+ engines: {node: '>= 8'}
637
+
638
639
+ resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
640
+
641
642
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
643
+
644
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
652
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
653
+
654
655
+ resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
656
+ engines: {node: '>=8'}
657
+
658
659
+ resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
660
+
661
662
+ resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
663
+
664
665
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
666
+ engines: {node: '>=8'}
667
+
668
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
674
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
675
+ engines: {node: '>=6'}
676
+
677
678
+ resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
679
+ engines: {node: '>= 6'}
680
+
681
682
+ resolution: {integrity: sha512-e4QKw/O2Kavj2VQTKZWrwzkt3IxOmIlU6ajRb6LP64LHpBo1J67k2Hi4Vu/TgJWsNtynurfS0uK3MaUTCPfu5Q==}
683
+
684
685
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
686
+ engines: {node: '>=10'}
687
+
688
689
+ resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
690
+ engines: {node: '>= 8.10.0'}
691
+
692
693
+ resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
694
+
695
696
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
697
+ engines: {node: '>=7.0.0'}
698
+
699
700
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
701
+
702
703
+ resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
704
+ engines: {node: '>= 6'}
705
+
706
707
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
708
+
709
710
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
711
+
712
713
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
714
+ engines: {node: '>= 8'}
715
+
716
717
+ resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
718
+ engines: {node: '>=4'}
719
+ hasBin: true
720
+
721
722
+ resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
723
+
724
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
734
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
735
+
736
737
+ resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
738
+ engines: {node: '>=8'}
739
+
740
741
+ resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
742
+
743
744
+ resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
745
+
746
747
+ resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
748
+
749
750
+ resolution: {integrity: sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ==}
751
+
752
753
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
754
+
755
756
+ resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
757
+
758
759
+ resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==}
760
+ engines: {node: '>=18'}
761
+ hasBin: true
762
+
763
764
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
765
+ engines: {node: '>=6'}
766
+
767
768
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
769
+ engines: {node: '>=10'}
770
+
771
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
778
+ resolution: {integrity: sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==}
779
+ peerDependencies:
780
+ eslint: '>=8.40'
781
+
782
783
+ resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
784
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
785
+
786
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
791
+ resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
792
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
793
+
794
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
805
+ resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
806
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
807
+
808
809
+ resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
810
+ engines: {node: '>=0.10'}
811
+
812
813
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
814
+ engines: {node: '>=4.0'}
815
+
816
817
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
818
+ engines: {node: '>=4.0'}
819
+
820
821
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
822
+ engines: {node: '>=0.10.0'}
823
+
824
825
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
826
+
827
828
+ resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
829
+ engines: {node: '>=8.6.0'}
830
+
831
832
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
833
+
834
835
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
836
+
837
838
+ resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
839
+
840
841
+ resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==}
842
+ peerDependencies:
843
+ picomatch: ^3 || ^4
844
+ peerDependenciesMeta:
845
+ picomatch:
846
+ optional: true
847
+
848
849
+ resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
850
+ engines: {node: '>=16.0.0'}
851
+
852
853
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
854
+ engines: {node: '>=8'}
855
+
856
857
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
858
+ engines: {node: '>=10'}
859
+
860
861
+ resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
862
+ engines: {node: '>=16'}
863
+
864
865
+ resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
866
+
867
868
+ resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
869
+ engines: {node: '>=14'}
870
+
871
872
+ resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
873
+
874
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
880
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
881
+
882
883
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
884
+ engines: {node: '>=6.9.0'}
885
+
886
887
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
888
+ engines: {node: '>= 6'}
889
+
890
891
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
892
+ engines: {node: '>=10.13.0'}
893
+
894
895
+ resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
896
+ hasBin: true
897
+
898
899
+ resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
900
+ engines: {node: '>=18'}
901
+
902
903
+ resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==}
904
+ engines: {node: '>=18'}
905
+
906
907
+ resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
908
+
909
910
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
911
+ engines: {node: '>=8'}
912
+
913
914
+ resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
915
+ engines: {node: '>= 0.4'}
916
+
917
918
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
919
+ engines: {node: '>= 4'}
920
+
921
922
+ resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
923
+ engines: {node: '>= 4'}
924
+
925
926
+ resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
927
+ engines: {node: '>=6'}
928
+
929
930
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
931
+ engines: {node: '>=0.8.19'}
932
+
933
934
+ resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
935
+ engines: {node: '>=8'}
936
+
937
938
+ resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
939
+ engines: {node: '>= 0.4'}
940
+
941
942
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
943
+ engines: {node: '>=0.10.0'}
944
+
945
946
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
947
+ engines: {node: '>=8'}
948
+
949
950
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
951
+ engines: {node: '>=0.10.0'}
952
+
953
954
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
955
+ engines: {node: '>=0.12.0'}
956
+
957
958
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
959
+
960
961
+ resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
962
+
963
964
+ resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
965
+ hasBin: true
966
+
967
968
+ resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
969
+ hasBin: true
970
+
971
972
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
973
+
974
975
+ resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
976
+ hasBin: true
977
+
978
979
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
980
+ engines: {node: '>=6'}
981
+ hasBin: true
982
+
983
984
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
985
+
986
987
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
988
+
989
990
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
991
+
992
993
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
994
+ engines: {node: '>=6'}
995
+ hasBin: true
996
+
997
998
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
999
+
1000
1001
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
1002
+ engines: {node: '>= 0.8.0'}
1003
+
1004
1005
+ resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
1006
+ engines: {node: '>= 12.0.0'}
1007
+ cpu: [arm64]
1008
+ os: [darwin]
1009
+
1010
1011
+ resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==}
1012
+ engines: {node: '>= 12.0.0'}
1013
+ cpu: [x64]
1014
+ os: [darwin]
1015
+
1016
1017
+ resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==}
1018
+ engines: {node: '>= 12.0.0'}
1019
+ cpu: [x64]
1020
+ os: [freebsd]
1021
+
1022
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
1029
+ resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==}
1030
+ engines: {node: '>= 12.0.0'}
1031
+ cpu: [arm64]
1032
+ os: [linux]
1033
+
1034
1035
+ resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
1036
+ engines: {node: '>= 12.0.0'}
1037
+ cpu: [arm64]
1038
+ os: [linux]
1039
+
1040
1041
+ resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
1042
+ engines: {node: '>= 12.0.0'}
1043
+ cpu: [x64]
1044
+ os: [linux]
1045
+
1046
1047
+ resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
1048
+ engines: {node: '>= 12.0.0'}
1049
+ cpu: [x64]
1050
+ os: [linux]
1051
+
1052
1053
+ resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
1054
+ engines: {node: '>= 12.0.0'}
1055
+ cpu: [arm64]
1056
+ os: [win32]
1057
+
1058
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
1065
+ resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
1066
+ engines: {node: '>= 12.0.0'}
1067
+
1068
1069
+ resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
1070
+ engines: {node: '>=14'}
1071
+
1072
1073
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
1074
+
1075
1076
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
1077
+ engines: {node: '>=10'}
1078
+
1079
1080
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
1081
+
1082
1083
+ resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
1084
+
1085
1086
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
1087
+
1088
1089
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
1090
+ engines: {node: '>= 8'}
1091
+
1092
1093
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
1094
+ engines: {node: '>=8.6'}
1095
+
1096
1097
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
1098
+
1099
1100
+ resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
1101
+ engines: {node: '>=16 || 14 >=14.17'}
1102
+
1103
1104
+ resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
1105
+ engines: {node: '>=16 || 14 >=14.17'}
1106
+
1107
1108
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
1109
+
1110
1111
+ resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
1112
+
1113
1114
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
1115
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
1116
+ hasBin: true
1117
+
1118
1119
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
1120
+
1121
1122
+ resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
1123
+
1124
1125
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
1126
+ engines: {node: '>=0.10.0'}
1127
+
1128
1129
+ resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
1130
+ engines: {node: '>=0.10.0'}
1131
+
1132
1133
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
1134
+ engines: {node: '>=0.10.0'}
1135
+
1136
1137
+ resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
1138
+ engines: {node: '>= 6'}
1139
+
1140
1141
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
1142
+ engines: {node: '>= 0.8.0'}
1143
+
1144
1145
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
1146
+ engines: {node: '>=10'}
1147
+
1148
1149
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
1150
+ engines: {node: '>=10'}
1151
+
1152
1153
+ resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
1154
+
1155
1156
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
1157
+ engines: {node: '>=6'}
1158
+
1159
1160
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
1161
+ engines: {node: '>=8'}
1162
+
1163
1164
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
1165
+ engines: {node: '>=8'}
1166
+
1167
1168
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
1169
+
1170
1171
+ resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
1172
+ engines: {node: '>=16 || 14 >=14.18'}
1173
+
1174
1175
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
1176
+
1177
1178
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
1179
+ engines: {node: '>=8.6'}
1180
+
1181
1182
+ resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
1183
+ engines: {node: '>=12'}
1184
+
1185
1186
+ resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
1187
+ engines: {node: '>=0.10.0'}
1188
+
1189
1190
+ resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
1191
+ engines: {node: '>= 6'}
1192
+
1193
1194
+ resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
1195
+ engines: {node: '>=14.0.0'}
1196
+ peerDependencies:
1197
+ postcss: ^8.0.0
1198
+
1199
1200
+ resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
1201
+ engines: {node: ^12 || ^14 || >= 16}
1202
+ peerDependencies:
1203
+ postcss: ^8.4.21
1204
+
1205
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
1218
+ resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
1219
+ engines: {node: '>=12.0'}
1220
+ peerDependencies:
1221
+ postcss: ^8.2.14
1222
+
1223
1224
+ resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
1225
+ engines: {node: '>=4'}
1226
+
1227
1228
+ resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
1229
+
1230
1231
+ resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
1232
+ engines: {node: ^10 || ^12 || >=14}
1233
+
1234
1235
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
1236
+ engines: {node: '>= 0.8.0'}
1237
+
1238
1239
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
1240
+ engines: {node: '>=6'}
1241
+
1242
1243
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
1244
+
1245
1246
+ resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==}
1247
+ peerDependencies:
1248
+ react: ^19.1.1
1249
+
1250
1251
+ resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
1252
+ engines: {node: '>=0.10.0'}
1253
+
1254
1255
+ resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==}
1256
+ engines: {node: '>=0.10.0'}
1257
+
1258
1259
+ resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
1260
+
1261
1262
+ resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
1263
+ engines: {node: '>=8.10.0'}
1264
+
1265
1266
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
1267
+ engines: {node: '>=4'}
1268
+
1269
1270
+ resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
1271
+ engines: {node: '>= 0.4'}
1272
+ hasBin: true
1273
+
1274
1275
+ resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
1276
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
1277
+
1278
1279
+ resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==}
1280
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
1281
+ hasBin: true
1282
+
1283
1284
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
1285
+
1286
1287
+ resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
1288
+
1289
1290
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
1291
+ hasBin: true
1292
+
1293
1294
+ resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
1295
+ engines: {node: '>=10'}
1296
+ hasBin: true
1297
+
1298
1299
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
1300
+ engines: {node: '>=8'}
1301
+
1302
1303
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
1304
+ engines: {node: '>=8'}
1305
+
1306
1307
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
1308
+ engines: {node: '>=14'}
1309
+
1310
1311
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
1312
+ engines: {node: '>=0.10.0'}
1313
+
1314
1315
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
1316
+ engines: {node: '>=8'}
1317
+
1318
1319
+ resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
1320
+ engines: {node: '>=12'}
1321
+
1322
1323
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
1324
+ engines: {node: '>=8'}
1325
+
1326
1327
+ resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
1328
+ engines: {node: '>=12'}
1329
+
1330
1331
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
1332
+ engines: {node: '>=8'}
1333
+
1334
1335
+ resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
1336
+ engines: {node: '>=16 || 14 >=14.17'}
1337
+ hasBin: true
1338
+
1339
1340
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
1341
+ engines: {node: '>=8'}
1342
+
1343
1344
+ resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
1345
+ engines: {node: '>= 0.4'}
1346
+
1347
1348
+ resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==}
1349
+ engines: {node: '>=14.0.0'}
1350
+ hasBin: true
1351
+
1352
1353
+ resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
1354
+ engines: {node: '>=0.8'}
1355
+
1356
1357
+ resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
1358
+
1359
1360
+ resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
1361
+ engines: {node: '>=12.0.0'}
1362
+
1363
1364
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
1365
+ engines: {node: '>=8.0'}
1366
+
1367
1368
+ resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
1369
+ engines: {node: '>=18.12'}
1370
+ peerDependencies:
1371
+ typescript: '>=4.8.4'
1372
+
1373
1374
+ resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
1375
+
1376
1377
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
1378
+ engines: {node: '>= 0.8.0'}
1379
+
1380
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
1388
+ resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
1389
+ engines: {node: '>=14.17'}
1390
+ hasBin: true
1391
+
1392
1393
+ resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
1394
+ hasBin: true
1395
+ peerDependencies:
1396
+ browserslist: '>= 4.21.0'
1397
+
1398
1399
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
1400
+
1401
1402
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
1403
+
1404
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
1445
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
1446
+ engines: {node: '>= 8'}
1447
+ hasBin: true
1448
+
1449
1450
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
1451
+ engines: {node: '>=0.10.0'}
1452
+
1453
1454
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
1455
+ engines: {node: '>=10'}
1456
+
1457
1458
+ resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
1459
+ engines: {node: '>=12'}
1460
+
1461
1462
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
1463
+
1464
1465
+ resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==}
1466
+ engines: {node: '>= 14.6'}
1467
+ hasBin: true
1468
+
1469
1470
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
1471
+ engines: {node: '>=10'}
1472
+
1473
+ snapshots:
1474
+
1475
+ '@alloc/[email protected]': {}
1476
+
1477
+ '@ampproject/[email protected]':
1478
+ dependencies:
1479
+ '@jridgewell/gen-mapping': 0.3.12
1480
+ '@jridgewell/trace-mapping': 0.3.29
1481
+
1482
+ '@babel/[email protected]':
1483
+ dependencies:
1484
+ '@babel/helper-validator-identifier': 7.27.1
1485
+ js-tokens: 4.0.0
1486
+ picocolors: 1.1.1
1487
+
1488
+ '@babel/[email protected]': {}
1489
+
1490
+ '@babel/[email protected]':
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/[email protected])
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/[email protected]':
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/[email protected]':
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/[email protected]': {}
1527
+
1528
+ '@babel/[email protected]':
1529
+ dependencies:
1530
+ '@babel/traverse': 7.28.0
1531
+ '@babel/types': 7.28.2
1532
+ transitivePeerDependencies:
1533
+ - supports-color
1534
+
1535
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/[email protected]': {}
1545
+
1546
+ '@babel/[email protected]': {}
1547
+
1548
+ '@babel/[email protected]': {}
1549
+
1550
+ '@babel/[email protected]': {}
1551
+
1552
+ '@babel/[email protected]':
1553
+ dependencies:
1554
+ '@babel/template': 7.27.2
1555
+ '@babel/types': 7.28.2
1556
+
1557
+ '@babel/[email protected]':
1558
+ dependencies:
1559
+ '@babel/types': 7.28.2
1560
+
1561
1562
+ dependencies:
1563
+ '@babel/core': 7.28.0
1564
+ '@babel/helper-plugin-utils': 7.27.1
1565
+
1566
1567
+ dependencies:
1568
+ '@babel/core': 7.28.0
1569
+ '@babel/helper-plugin-utils': 7.27.1
1570
+
1571
+ '@babel/[email protected]':
1572
+ dependencies:
1573
+ '@babel/code-frame': 7.27.1
1574
+ '@babel/parser': 7.28.0
1575
+ '@babel/types': 7.28.2
1576
+
1577
+ '@babel/[email protected]':
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/[email protected]':
1590
+ dependencies:
1591
+ '@babel/helper-string-parser': 7.27.1
1592
+ '@babel/helper-validator-identifier': 7.27.1
1593
+
1594
+ '@esbuild/[email protected]':
1595
+ optional: true
1596
+
1597
+ '@esbuild/[email protected]':
1598
+ optional: true
1599
+
1600
+ '@esbuild/[email protected]':
1601
+ optional: true
1602
+
1603
+ '@esbuild/[email protected]':
1604
+ optional: true
1605
+
1606
+ '@esbuild/[email protected]':
1607
+ optional: true
1608
+
1609
+ '@esbuild/[email protected]':
1610
+ optional: true
1611
+
1612
+ '@esbuild/[email protected]':
1613
+ optional: true
1614
+
1615
+ '@esbuild/[email protected]':
1616
+ optional: true
1617
+
1618
+ '@esbuild/[email protected]':
1619
+ optional: true
1620
+
1621
+ '@esbuild/[email protected]':
1622
+ optional: true
1623
+
1624
+ '@esbuild/[email protected]':
1625
+ optional: true
1626
+
1627
+ '@esbuild/[email protected]':
1628
+ optional: true
1629
+
1630
+ '@esbuild/[email protected]':
1631
+ optional: true
1632
+
1633
+ '@esbuild/[email protected]':
1634
+ optional: true
1635
+
1636
+ '@esbuild/[email protected]':
1637
+ optional: true
1638
+
1639
+ '@esbuild/[email protected]':
1640
+ optional: true
1641
+
1642
+ '@esbuild/[email protected]':
1643
+ optional: true
1644
+
1645
+ '@esbuild/[email protected]':
1646
+ optional: true
1647
+
1648
+ '@esbuild/[email protected]':
1649
+ optional: true
1650
+
1651
+ '@esbuild/[email protected]':
1652
+ optional: true
1653
+
1654
+ '@esbuild/[email protected]':
1655
+ optional: true
1656
+
1657
+ '@esbuild/[email protected]':
1658
+ optional: true
1659
+
1660
+ '@esbuild/[email protected]':
1661
+ optional: true
1662
+
1663
+ '@esbuild/[email protected]':
1664
+ optional: true
1665
+
1666
+ '@esbuild/[email protected]':
1667
+ optional: true
1668
+
1669
+ '@esbuild/[email protected]':
1670
+ optional: true
1671
+
1672
1673
+ dependencies:
1674
+ eslint: 9.33.0([email protected])
1675
+ eslint-visitor-keys: 3.4.3
1676
+
1677
+ '@eslint-community/[email protected]': {}
1678
+
1679
+ '@eslint/[email protected]':
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/[email protected]': {}
1688
+
1689
+ '@eslint/[email protected]':
1690
+ dependencies:
1691
+ '@types/json-schema': 7.0.15
1692
+
1693
+ '@eslint/[email protected]':
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/[email protected]': {}
1708
+
1709
+ '@eslint/[email protected]': {}
1710
+
1711
+ '@eslint/[email protected]':
1712
+ dependencies:
1713
+ '@eslint/core': 0.15.2
1714
+ levn: 0.4.1
1715
+
1716
+ '@humanfs/[email protected]': {}
1717
+
1718
+ '@humanfs/[email protected]':
1719
+ dependencies:
1720
+ '@humanfs/core': 0.19.1
1721
+ '@humanwhocodes/retry': 0.3.1
1722
+
1723
+ '@humanwhocodes/[email protected]': {}
1724
+
1725
+ '@humanwhocodes/[email protected]': {}
1726
+
1727
+ '@humanwhocodes/[email protected]': {}
1728
+
1729
+ '@isaacs/[email protected]':
1730
+ dependencies:
1731
+ string-width: 5.1.2
1732
+ string-width-cjs: [email protected]
1733
+ strip-ansi: 7.1.0
1734
+ strip-ansi-cjs: [email protected]
1735
+ wrap-ansi: 8.1.0
1736
+ wrap-ansi-cjs: [email protected]
1737
+
1738
+ '@jridgewell/[email protected]':
1739
+ dependencies:
1740
+ '@jridgewell/sourcemap-codec': 1.5.4
1741
+ '@jridgewell/trace-mapping': 0.3.29
1742
+
1743
+ '@jridgewell/[email protected]': {}
1744
+
1745
+ '@jridgewell/[email protected]': {}
1746
+
1747
+ '@jridgewell/[email protected]':
1748
+ dependencies:
1749
+ '@jridgewell/resolve-uri': 3.1.2
1750
+ '@jridgewell/sourcemap-codec': 1.5.4
1751
+
1752
+ '@nodelib/[email protected]':
1753
+ dependencies:
1754
+ '@nodelib/fs.stat': 2.0.5
1755
+ run-parallel: 1.2.0
1756
+
1757
+ '@nodelib/[email protected]': {}
1758
+
1759
+ '@nodelib/[email protected]':
1760
+ dependencies:
1761
+ '@nodelib/fs.scandir': 2.1.5
1762
+ fastq: 1.19.1
1763
+
1764
+ '@pkgjs/[email protected]':
1765
+ optional: true
1766
+
1767
+ '@rolldown/[email protected]': {}
1768
+
1769
+ '@rollup/[email protected]':
1770
+ optional: true
1771
+
1772
+ '@rollup/[email protected]':
1773
+ optional: true
1774
+
1775
+ '@rollup/[email protected]':
1776
+ optional: true
1777
+
1778
+ '@rollup/[email protected]':
1779
+ optional: true
1780
+
1781
+ '@rollup/[email protected]':
1782
+ optional: true
1783
+
1784
+ '@rollup/[email protected]':
1785
+ optional: true
1786
+
1787
+ '@rollup/[email protected]':
1788
+ optional: true
1789
+
1790
+ '@rollup/[email protected]':
1791
+ optional: true
1792
+
1793
+ '@rollup/[email protected]':
1794
+ optional: true
1795
+
1796
+ '@rollup/[email protected]':
1797
+ optional: true
1798
+
1799
+ '@rollup/[email protected]':
1800
+ optional: true
1801
+
1802
+ '@rollup/[email protected]':
1803
+ optional: true
1804
+
1805
+ '@rollup/[email protected]':
1806
+ optional: true
1807
+
1808
+ '@rollup/[email protected]':
1809
+ optional: true
1810
+
1811
+ '@rollup/[email protected]':
1812
+ optional: true
1813
+
1814
+ '@rollup/[email protected]':
1815
+ optional: true
1816
+
1817
+ '@rollup/[email protected]':
1818
+ optional: true
1819
+
1820
+ '@rollup/[email protected]':
1821
+ optional: true
1822
+
1823
+ '@rollup/[email protected]':
1824
+ optional: true
1825
+
1826
+ '@rollup/[email protected]':
1827
+ optional: true
1828
+
1829
+ '@types/[email protected]':
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/[email protected]':
1838
+ dependencies:
1839
+ '@babel/types': 7.28.2
1840
+
1841
+ '@types/[email protected]':
1842
+ dependencies:
1843
+ '@babel/parser': 7.28.0
1844
+ '@babel/types': 7.28.2
1845
+
1846
+ '@types/[email protected]':
1847
+ dependencies:
1848
+ '@babel/types': 7.28.2
1849
+
1850
+ '@types/[email protected]': {}
1851
+
1852
+ '@types/[email protected]': {}
1853
+
1854
1855
+ dependencies:
1856
+ '@types/react': 19.1.9([email protected])
1857
+
1858
1859
+ dependencies:
1860
+ csstype: 3.1.3
1861
+ react: 19.1.1
1862
+
1863
1864
+ dependencies:
1865
+ '@eslint-community/regexpp': 4.12.1
1866
+ '@typescript-eslint/parser': 8.39.0([email protected]([email protected]))([email protected])
1867
+ '@typescript-eslint/scope-manager': 8.39.0
1868
+ '@typescript-eslint/type-utils': 8.39.0([email protected]([email protected]))([email protected])
1869
+ '@typescript-eslint/utils': 8.39.0([email protected]([email protected]))([email protected])
1870
+ '@typescript-eslint/visitor-keys': 8.39.0
1871
+ eslint: 9.33.0([email protected])
1872
+ graphemer: 1.4.0
1873
+ ignore: 7.0.5
1874
+ natural-compare: 1.4.0
1875
+ ts-api-utils: 2.1.0([email protected])
1876
+ typescript: 5.8.3
1877
+ transitivePeerDependencies:
1878
+ - supports-color
1879
+
1880
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([email protected])
1885
+ '@typescript-eslint/visitor-keys': 8.39.0
1886
+ debug: 4.4.1
1887
+ eslint: 9.33.0([email protected])
1888
+ typescript: 5.8.3
1889
+ transitivePeerDependencies:
1890
+ - supports-color
1891
+
1892
+ '@typescript-eslint/[email protected]([email protected])':
1893
+ dependencies:
1894
+ '@typescript-eslint/tsconfig-utils': 8.39.0([email protected])
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/[email protected]':
1902
+ dependencies:
1903
+ '@typescript-eslint/types': 8.39.0
1904
+ '@typescript-eslint/visitor-keys': 8.39.0
1905
+
1906
+ '@typescript-eslint/[email protected]([email protected])':
1907
+ dependencies:
1908
+ typescript: 5.8.3
1909
+
1910
1911
+ dependencies:
1912
+ '@typescript-eslint/types': 8.39.0
1913
+ '@typescript-eslint/typescript-estree': 8.39.0([email protected])
1914
+ '@typescript-eslint/utils': 8.39.0([email protected]([email protected]))([email protected])
1915
+ debug: 4.4.1
1916
+ eslint: 9.33.0([email protected])
1917
+ ts-api-utils: 2.1.0([email protected])
1918
+ typescript: 5.8.3
1919
+ transitivePeerDependencies:
1920
+ - supports-color
1921
+
1922
+ '@typescript-eslint/[email protected]': {}
1923
+
1924
+ '@typescript-eslint/[email protected]([email protected])':
1925
+ dependencies:
1926
+ '@typescript-eslint/project-service': 8.39.0([email protected])
1927
+ '@typescript-eslint/tsconfig-utils': 8.39.0([email protected])
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([email protected])
1936
+ typescript: 5.8.3
1937
+ transitivePeerDependencies:
1938
+ - supports-color
1939
+
1940
1941
+ dependencies:
1942
+ '@eslint-community/eslint-utils': 4.7.0([email protected]([email protected]))
1943
+ '@typescript-eslint/scope-manager': 8.39.0
1944
+ '@typescript-eslint/types': 8.39.0
1945
+ '@typescript-eslint/typescript-estree': 8.39.0([email protected])
1946
+ eslint: 9.33.0([email protected])
1947
+ typescript: 5.8.3
1948
+ transitivePeerDependencies:
1949
+ - supports-color
1950
+
1951
+ '@typescript-eslint/[email protected]':
1952
+ dependencies:
1953
+ '@typescript-eslint/types': 8.39.0
1954
+ eslint-visitor-keys: 4.2.1
1955
+
1956
1957
+ dependencies:
1958
+ '@babel/core': 7.28.0
1959
+ '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/[email protected])
1960
+ '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/[email protected])
1961
+ '@rolldown/pluginutils': 1.0.0-beta.27
1962
+ '@types/babel__core': 7.20.5
1963
+ react-refresh: 0.17.0
1964
1965
+ transitivePeerDependencies:
1966
+ - supports-color
1967
+
1968
1969
+ dependencies:
1970
+ acorn: 8.15.0
1971
+
1972
1973
+
1974
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
1982
+
1983
1984
+
1985
1986
+ dependencies:
1987
+ color-convert: 2.0.1
1988
+
1989
1990
+
1991
1992
+
1993
1994
+ dependencies:
1995
+ normalize-path: 3.0.0
1996
+ picomatch: 2.3.1
1997
+
1998
1999
+
2000
2001
+
2002
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
2013
+
2014
2015
+
2016
2017
+ dependencies:
2018
+ balanced-match: 1.0.2
2019
+ concat-map: 0.0.1
2020
+
2021
2022
+ dependencies:
2023
+ balanced-match: 1.0.2
2024
+
2025
2026
+ dependencies:
2027
+ fill-range: 7.1.1
2028
+
2029
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([email protected])
2035
+
2036
2037
+
2038
2039
+
2040
2041
+
2042
2043
+ dependencies:
2044
+ ansi-styles: 4.3.0
2045
+ supports-color: 7.2.0
2046
+
2047
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
2060
+
2061
2062
+ dependencies:
2063
+ color-name: 1.1.4
2064
+
2065
2066
+
2067
2068
+
2069
2070
+
2071
2072
+
2073
2074
+ dependencies:
2075
+ path-key: 3.1.1
2076
+ shebang-command: 2.0.0
2077
+ which: 2.0.2
2078
+
2079
2080
+
2081
2082
+
2083
2084
+ dependencies:
2085
+ ms: 2.1.3
2086
+
2087
2088
+
2089
2090
+ optional: true
2091
+
2092
2093
+
2094
2095
+
2096
2097
+
2098
2099
+
2100
2101
+
2102
2103
+
2104
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
2134
+
2135
2136
+
2137
2138
+ dependencies:
2139
+ eslint: 9.33.0([email protected])
2140
+
2141
2142
+ dependencies:
2143
+ eslint: 9.33.0([email protected])
2144
+
2145
2146
+ dependencies:
2147
+ esrecurse: 4.3.0
2148
+ estraverse: 5.3.0
2149
+
2150
2151
+
2152
2153
+
2154
2155
+ dependencies:
2156
+ '@eslint-community/eslint-utils': 4.7.0([email protected]([email protected]))
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
2197
+ dependencies:
2198
+ acorn: 8.15.0
2199
+ acorn-jsx: 5.3.2([email protected])
2200
+ eslint-visitor-keys: 4.2.1
2201
+
2202
2203
+ dependencies:
2204
+ estraverse: 5.3.0
2205
+
2206
2207
+ dependencies:
2208
+ estraverse: 5.3.0
2209
+
2210
2211
+
2212
2213
+
2214
2215
+
2216
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
2225
+
2226
2227
+
2228
2229
+ dependencies:
2230
+ reusify: 1.1.0
2231
+
2232
2233
+ optionalDependencies:
2234
+ picomatch: 4.0.3
2235
+
2236
2237
+ dependencies:
2238
+ flat-cache: 4.0.1
2239
+
2240
2241
+ dependencies:
2242
+ to-regex-range: 5.0.1
2243
+
2244
2245
+ dependencies:
2246
+ locate-path: 6.0.0
2247
+ path-exists: 4.0.0
2248
+
2249
2250
+ dependencies:
2251
+ flatted: 3.3.3
2252
+ keyv: 4.5.4
2253
+
2254
2255
+
2256
2257
+ dependencies:
2258
+ cross-spawn: 7.0.6
2259
+ signal-exit: 4.1.0
2260
+
2261
2262
+
2263
2264
+ optional: true
2265
+
2266
2267
+
2268
2269
+
2270
2271
+ dependencies:
2272
+ is-glob: 4.0.3
2273
+
2274
2275
+ dependencies:
2276
+ is-glob: 4.0.3
2277
+
2278
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
2288
+
2289
2290
+
2291
2292
+
2293
2294
+
2295
2296
+ dependencies:
2297
+ function-bind: 1.1.2
2298
+
2299
2300
+
2301
2302
+
2303
2304
+ dependencies:
2305
+ parent-module: 1.0.1
2306
+ resolve-from: 4.0.0
2307
+
2308
2309
+
2310
2311
+ dependencies:
2312
+ binary-extensions: 2.3.0
2313
+
2314
2315
+ dependencies:
2316
+ hasown: 2.0.2
2317
+
2318
2319
+
2320
2321
+
2322
2323
+ dependencies:
2324
+ is-extglob: 2.1.1
2325
+
2326
2327
+
2328
2329
+
2330
2331
+ dependencies:
2332
+ '@isaacs/cliui': 8.0.2
2333
+ optionalDependencies:
2334
+ '@pkgjs/parseargs': 0.11.0
2335
+
2336
2337
+
2338
2339
+ optional: true
2340
+
2341
2342
+
2343
2344
+ dependencies:
2345
+ argparse: 2.0.1
2346
+
2347
2348
+
2349
2350
+
2351
2352
+
2353
2354
+
2355
2356
+
2357
2358
+ dependencies:
2359
+ json-buffer: 3.0.1
2360
+
2361
2362
+ dependencies:
2363
+ prelude-ls: 1.2.1
2364
+ type-check: 0.4.0
2365
+
2366
2367
+ optional: true
2368
+
2369
2370
+ optional: true
2371
+
2372
2373
+ optional: true
2374
+
2375
2376
+ optional: true
2377
+
2378
2379
+ optional: true
2380
+
2381
2382
+ optional: true
2383
+
2384
2385
+ optional: true
2386
+
2387
2388
+ optional: true
2389
+
2390
2391
+ optional: true
2392
+
2393
2394
+ optional: true
2395
+
2396
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
2413
+
2414
2415
+
2416
2417
+ dependencies:
2418
+ p-locate: 5.0.0
2419
+
2420
2421
+
2422
2423
+
2424
2425
+ dependencies:
2426
+ yallist: 3.1.1
2427
+
2428
2429
+
2430
2431
+ dependencies:
2432
+ braces: 3.0.3
2433
+ picomatch: 2.3.1
2434
+
2435
2436
+ dependencies:
2437
+ brace-expansion: 1.1.12
2438
+
2439
2440
+ dependencies:
2441
+ brace-expansion: 2.0.2
2442
+
2443
2444
+
2445
2446
+
2447
2448
+ dependencies:
2449
+ any-promise: 1.3.0
2450
+ object-assign: 4.1.1
2451
+ thenify-all: 1.6.0
2452
+
2453
2454
+
2455
2456
+
2457
2458
+
2459
2460
+
2461
2462
+
2463
2464
+
2465
2466
+
2467
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
2477
+ dependencies:
2478
+ yocto-queue: 0.1.0
2479
+
2480
2481
+ dependencies:
2482
+ p-limit: 3.1.0
2483
+
2484
2485
+
2486
2487
+ dependencies:
2488
+ callsites: 3.1.0
2489
+
2490
2491
+
2492
2493
+
2494
2495
+
2496
2497
+ dependencies:
2498
+ lru-cache: 10.4.3
2499
+ minipass: 7.1.2
2500
+
2501
2502
+
2503
2504
+
2505
2506
+
2507
2508
+
2509
2510
+
2511
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
2519
+ dependencies:
2520
+ camelcase-css: 2.0.1
2521
+ postcss: 8.5.6
2522
+
2523
2524
+ dependencies:
2525
+ lilconfig: 3.1.3
2526
+ yaml: 2.8.1
2527
+ optionalDependencies:
2528
+ postcss: 8.5.6
2529
+
2530
2531
+ dependencies:
2532
+ postcss: 8.5.6
2533
+ postcss-selector-parser: 6.1.2
2534
+
2535
2536
+ dependencies:
2537
+ cssesc: 3.0.0
2538
+ util-deprecate: 1.0.2
2539
+
2540
2541
+
2542
2543
+ dependencies:
2544
+ nanoid: 3.3.11
2545
+ picocolors: 1.1.1
2546
+ source-map-js: 1.2.1
2547
+
2548
2549
+
2550
2551
+
2552
2553
+
2554
2555
+ dependencies:
2556
+ react: 19.1.1
2557
+ scheduler: 0.26.0
2558
+
2559
2560
+
2561
2562
+
2563
2564
+ dependencies:
2565
+ pify: 2.3.0
2566
+
2567
2568
+ dependencies:
2569
+ picomatch: 2.3.1
2570
+
2571
2572
+
2573
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
2580
+
2581
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
2608
+ dependencies:
2609
+ queue-microtask: 1.2.3
2610
+
2611
2612
+
2613
2614
+
2615
2616
+
2617
2618
+ dependencies:
2619
+ shebang-regex: 3.0.0
2620
+
2621
2622
+
2623
2624
+
2625
2626
+
2627
2628
+ dependencies:
2629
+ emoji-regex: 8.0.0
2630
+ is-fullwidth-code-point: 3.0.0
2631
+ strip-ansi: 6.0.1
2632
+
2633
2634
+ dependencies:
2635
+ eastasianwidth: 0.2.0
2636
+ emoji-regex: 9.2.2
2637
+ strip-ansi: 7.1.0
2638
+
2639
2640
+ dependencies:
2641
+ ansi-regex: 5.0.1
2642
+
2643
2644
+ dependencies:
2645
+ ansi-regex: 6.1.0
2646
+
2647
2648
+
2649
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
2660
+ dependencies:
2661
+ has-flag: 4.0.0
2662
+
2663
2664
+
2665
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([email protected])
2683
+ postcss-js: 4.0.1([email protected])
2684
+ postcss-load-config: 4.0.2([email protected])
2685
+ postcss-nested: 6.2.0([email protected])
2686
+ postcss-selector-parser: 6.1.2
2687
+ resolve: 1.22.10
2688
+ sucrase: 3.35.0
2689
+ transitivePeerDependencies:
2690
+ - ts-node
2691
+
2692
2693
+ dependencies:
2694
+ thenify: 3.3.1
2695
+
2696
2697
+ dependencies:
2698
+ any-promise: 1.3.0
2699
+
2700
2701
+ dependencies:
2702
+ fdir: 6.4.6([email protected])
2703
+ picomatch: 4.0.3
2704
+
2705
2706
+ dependencies:
2707
+ is-number: 7.0.0
2708
+
2709
2710
+ dependencies:
2711
+ typescript: 5.8.3
2712
+
2713
2714
+
2715
2716
+ dependencies:
2717
+ prelude-ls: 1.2.1
2718
+
2719
2720
+ dependencies:
2721
2722
+ '@typescript-eslint/parser': 8.39.0([email protected]([email protected]))([email protected])
2723
+ '@typescript-eslint/typescript-estree': 8.39.0([email protected])
2724
+ '@typescript-eslint/utils': 8.39.0([email protected]([email protected]))([email protected])
2725
+ eslint: 9.33.0([email protected])
2726
+ typescript: 5.8.3
2727
+ transitivePeerDependencies:
2728
+ - supports-color
2729
+
2730
2731
+
2732
2733
+ dependencies:
2734
+ browserslist: 4.25.1
2735
+ escalade: 3.2.0
2736
+ picocolors: 1.1.1
2737
+
2738
2739
+ dependencies:
2740
+ punycode: 2.3.1
2741
+
2742
2743
+
2744
2745
+ dependencies:
2746
+ esbuild: 0.25.8
2747
+ fdir: 6.4.6([email protected])
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
2759
+ dependencies:
2760
+ isexe: 2.0.0
2761
+
2762
2763
+
2764
2765
+ dependencies:
2766
+ ansi-styles: 4.3.0
2767
+ string-width: 4.2.3
2768
+ strip-ansi: 6.0.1
2769
+
2770
2771
+ dependencies:
2772
+ ansi-styles: 6.2.1
2773
+ string-width: 5.1.2
2774
+ strip-ansi: 7.1.0
2775
+
2776
2777
+
2778
2779
+
2780
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
+ ]